← Назад к вопросам

Какие знаешь проблемы составных индексов в БД?

1.7 Middle🔥 91 комментариев
#Базы данных и SQL

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Проблемы составных индексов в БД

Составные (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")

Заключение

Составные индексы — это не серебряная пуля. Они требуют:

  1. Понимания ESR правила (Equality-Sort-Range)
  2. Анализа query patterns вашего приложения
  3. Регулярного мониторинга и очистки неиспользуемых индексов
  4. EXPLAIN анализа для проверки

Неправильный индекс может быть МЕДЛЕННЕЕ, чем отсутствие индекса, потому что:

  • Замедляет INSERT/UPDATE
  • Увеличивает используемую память
  • Может выбрать optimizer неправильно