Какие знаешь проблемы составных индексов в БД?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Проблемы составных индексов в БД
Составные (composite/compound) индексы — это мощный инструмент оптимизации, но их неправильное использование может привести к деградации производительности вместо улучшения. Это одна из самых частых ошибок в проектировании БД.
Проблема 1: Неправильный порядок колонок
Правило: Column Order matters (leftmost prefix rule)
Множественный индекс (col1, col2, col3) может быть использован ТОЛЬКО для запросов, которые фильтруют по leftmost префиксу.
-- Индекс: (user_id, created_at, status)
CREATE INDEX idx_users ON orders(user_id, created_at, status);
-- ХОРОШО: используется leftmost prefix
SELECT * FROM orders WHERE user_id = 1;
-- Индекс используется по (user_id)
SELECT * FROM orders WHERE user_id = 1 AND created_at > '2024-01-01';
-- Индекс используется по (user_id, created_at)
SELECT * FROM orders WHERE user_id = 1 AND created_at > '2024-01-01' AND status = 'completed';
-- Индекс используется полностью
-- ПЛОХО: прропускаем leftmost колонку
SELECT * FROM orders WHERE created_at > '2024-01-01';
-- Индекс НЕ используется! (Требуется FULL TABLE SCAN)
SELECT * FROM orders WHERE status = 'completed';
-- Индекс НЕ используется!
SELECT * FROM orders WHERE created_at > '2024-01-01' AND status = 'completed';
-- Индекс НЕ используется!
Правильный порядок колонок (ESR rule):
1. Equality (=) колонки — вперед
2. Sort (ORDER BY) колонки — в середину
3. Range (<, >, BETWEEN) колонки — в конец
Пример:
Часто используемый запрос:
SELECT * FROM users
WHERE country = 'USA' AND age > 18
ORDER BY created_at DESC LIMIT 10;
Правильный индекс: (country, created_at, age)
-- NOT (country, age, created_at) — created_at используется для ORDER BY
-- Пример неправильного порядка
CREATE INDEX idx_bad ON orders(status, user_id, created_at);
-- Запрос ищет по (user_id, created_at)
SELECT * FROM orders WHERE user_id = 1 AND created_at > '2024-01-01';
-- Индекс не оптимален! Начинает со status, но не фильтруем по нему
-- Правильный индекс для этого запроса
CREATE INDEX idx_good ON orders(user_id, created_at);
Проблема 2: Слишком много составных индексов
Каждый индекс имеет стоимость:
- Замедляет INSERT, UPDATE, DELETE (нужно обновлять индекс)
- Занимает память / дисковое пространство
- Optimizer тратит время на выбор лучшего индекса
// Плохо: 5 похожих индексов
@Entity
public class Order {
@Index(name = "idx_user_id")
private Long userId;
@Index(name = "idx_user_id_status")
private String status;
@Index(name = "idx_user_id_created_at")
private Instant createdAt;
@Index(name = "idx_user_id_status_created_at")
private Instant updatedAt;
@Index(name = "idx_user_id_amount")
private BigDecimal amount;
}
-- Все эти индексы начинаются с user_id!
-- ФАКТИЧЕСКИЙ размер индексов: вы платите за 5 индексов вместо одного
-- INSERT/UPDATE: обновляется 5 индексов вместо 1
Правильный подход:
-- Один составной индекс вместо пяти
CREATE INDEX idx_orders ON orders(user_id, status, created_at, amount);
-- Этот индекс поддерживает ВСЕ запросы:
SELECT * FROM orders WHERE user_id = 1; -- Использует (user_id)
SELECT * FROM orders WHERE user_id = 1 AND status = 'pending'; -- Использует (user_id, status)
SELECT * FROM orders WHERE user_id = 1 AND status = 'pending' AND created_at > '2024-01-01'; -- Полный префикс
-- Один индекс = меньше памяти = быстрее INSERT/UPDATE
Проблема 3: "Fat indices" (жирные индексы)
Индекс с слишком многими колонками теряет эффективность:
-- Плохо: индекс с 10 колонками
CREATE INDEX idx_fat ON users(
id, username, email, status, created_at,
country, age, role, verified, updated_at
);
-- Проблемы:
-- 1. Индекс почти такой же большой, как сама таблица
-- 2. Selectivity падает с каждой добавленной колонкой
-- 3. Для большинства запросов используется только первые 2-3 колонки
Правильный подход:
-- Выделите 3-4 самые важные колонки
CREATE INDEX idx_lean ON users(id, username, status, created_at);
-- Остальные колонки достаточно часто используются, но добавляют мало value
-- Рассмотри covering index или include для них (в PostgreSQL 11+)
Проблема 4: Covering Index используется неправильно
Covering Index: Индекс содержит все колонки, нужные для query, поэтому БД не нужно обращаться к таблице.
-- Хорошо: covering index
CREATE INDEX idx_orders ON orders(user_id, created_at) INCLUDE (amount, status);
-- Может быть использован для этого запроса без access к таблице:
SELECT amount, status FROM orders WHERE user_id = 1 AND created_at > '2024-01-01';
-- Плохо: слишком много INCLUDE колонок
CREATE INDEX idx_orders_fat ON orders(user_id) INCLUDE (
amount, status, shipping_address, billing_address,
customer_notes, payment_details, tracking_info
);
-- Индекс становится очень большим и может быть медленнее, чем простой доступ к таблице
Правило INCLUDE:
- Используй INCLUDE только для часто используемых colонок
- Максимум 5-10 колонок в INCLUDE
- Экономит место, чем просто добавлять колонки в индекс
Проблема 5: Индекс не используется из-за типов данных
-- Индекс определён на INTEGER
CREATE INDEX idx_user_id ON orders(user_id);
-- Но запрос использует STRING (неявная конвертация)
SELECT * FROM orders WHERE user_id = '123'; -- Индекс не используется!
-- Объяснение: конвертация может быть дорогая, поэтому optimizer
-- выбирает FULL SCAN вместо использования индекса
Проблема 6: NULL значения в индексах
-- Индекс может не включать NULL значения в некоторых БД
CREATE INDEX idx_optional_field ON orders(optional_field);
SELECT * FROM orders WHERE optional_field IS NULL;
-- В некоторых БД этот запрос НЕ использует индекс!
-- (В PostgreSQL использует, в MySQL зависит от версии)
-- Решение: явный индекс для NULL
CREATE INDEX idx_optional_field_null ON orders(optional_field) WHERE optional_field IS NOT NULL;
Проблема 7: Partial Index неправильно
-- Плохо: индекс на все статусы
CREATE INDEX idx_orders_status ON orders(status);
-- Хорошо: индекс только на активные заказы (95% запросов)
CREATE INDEX idx_orders_active ON orders(status, created_at)
WHERE status IN ('pending', 'processing');
-- Сокращает размер индекса на 50%+ и улучшает cache efficiency
Проблема 8: Query плана выбирает неправильный индекс
CREATE INDEX idx_1 ON orders(user_id);
CREATE INDEX idx_2 ON orders(user_id, created_at);
-- Оптимизатор может выбрать idx_1 вместо idx_2!
SELECT * FROM orders WHERE user_id = 1 AND created_at > '2024-01-01';
-- EXPLAIN ANALYZE для проверки
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 1 AND created_at > '2024-01-01';
-- Если выбран неправильный индекс, используй FORCE INDEX (MySQL) или SET enable_seqscan = off
Проблема 9: Неправильный EXPLAIN анализ
-- Часто разработчики не читают EXPLAIN внимательно
EXPLAIN ANALYZE SELECT * FROM users WHERE email LIKE '%@gmail.com%';
-- Результат показывает Seq Scan, но индекс есть!
Index on email НЕ используется для LIKE '%pattern%'
-- Решение:
-- 1. Используй Full Text Search для текстового поиска
-- 2. Или используй индекс с LIKE 'pattern%' (без ведущего %)
SELECT * FROM users WHERE email LIKE 'john%'; -- Может использовать индекс
Проблема 10: Неправильная selectivity
-- Плохо: индекс на колонке с низкой selectivity
CREATE INDEX idx_gender ON users(gender); -- Только 2 значения!
-- Optimizer может выбрать Seq Scan вместо использования индекса
-- потому что результат всё равно будет полусоставляющей таблицы
-- Хорошо: использовать составной индекс
CREATE INDEX idx_gender_country ON users(gender, country);
-- gender + country имеет лучшую selectivity
Диагностирование проблем
-- PostgreSQL: найди неиспользуемые индексы
SELECT schemaname, tablename, indexname, idx_scan, idx_tup_read, idx_tup_fetch
FROM pg_stat_user_indexes
WHERE idx_scan = 0 OR (idx_tup_read - idx_tup_fetch > 1000)
ORDER BY idx_scan DESC;
-- MySQL: дублирующиеся индексы
SELECT * FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = 'mydb'
GROUP BY TABLE_NAME, SEQ_IN_INDEX
HAVING COUNT(*) > 1;
Рекомендации по составным индексам
1. НИКОГДА не создавай индекс без понимания, какие запросы его будут использовать
2. ESR правило: Equality, Sort, Range
CREATE INDEX idx ON table(eq_col, sort_col, range_col);
3. Не создавай множество похожих индексов
ОДИН составной индекс лучше, чем пять отдельных
4. Используй EXPLAIN ANALYZE для проверки
Не полагайся на интуицию
5. Partial индексы для больших таблиц
WHERE status = 'active' — сокращает размер индекса
6. Мониторь unused индексы
Удаляй, если idx_scan = 0 за месяц
7. В Java: явно указывай индексы в JPA
@Index(name = "idx_user_created", columnList = "user_id,created_at")
Заключение
Составные индексы — это не серебряная пуля. Они требуют:
- Понимания ESR правила (Equality-Sort-Range)
- Анализа query patterns вашего приложения
- Регулярного мониторинга и очистки неиспользуемых индексов
- EXPLAIN анализа для проверки
Неправильный индекс может быть МЕДЛЕННЕЕ, чем отсутствие индекса, потому что:
- Замедляет INSERT/UPDATE
- Увеличивает используемую память
- Может выбрать optimizer неправильно