Многоколоночные индексы, порядок колонок, правило левой границы.
Порядок колонок в составном индексе — это не деталь реализации, а фундаментальное решение, определяющее эффективность запросов.
Составной индекс (composite index) — это индекс по нескольким колонкам одновременно.
-- Индекс по двум колонкам
CREATE INDEX idx_users_name ON users(last_name, first_name);
-- Индекс по трём колонкам
CREATE INDEX idx_orders ON orders(user_id, status, created_at);| Сценарий | Пример |
|---|---|
| Частые комбинации условий | WHERE last_name = ? AND first_name = ? |
| Сортировка по нескольким колонкам | ORDER BY status, created_at |
| Покрытие частых запросов | WHERE user_id = ? AND status = ? |
Самое важное правило составных индексов!
Составной индекс по колонкам (A, B, C) может использоваться для запросов по:
AA, BA, B, CНо НЕ для:
BCB, CA, C (пропуск B)B-дерево сортирует данные лексикографически — сначала по первой колонке, затем внутри каждого значения первой колонки — по второй, и так далее.
Индекс (last_name, first_name):
ivanov ivan TID=1
ivanov ivan TID=2 ← Дубликаты last_name сортируются по first_name
ivanov petr TID=3
petrov alex TID=4
petrov boris TID=5
sidorov alex TID=6
last_name = 'ivanov':ivanovlast_name = 'ivanov'first_name = 'ivan':first_name = 'ivan' разбросаны по всему деревуCREATE INDEX idx_name ON users(last_name, first_name);
-- ✅ Использует индекс
SELECT * FROM users WHERE last_name = 'Ivanov';
-- ✅ Использует индекс
SELECT * FROM users WHERE last_name = 'Ivanov' AND first_name = 'Ivan';
-- ❌ НЕ использует индекс (или использует плохо)
SELECT * FROM users WHERE first_name = 'Ivan';
-- ✅ Использует только первую колонку
SELECT * FROM users
WHERE last_name = 'Ivanov' AND first_name = 'Ivan' AND email = 'test@example.com';CREATE INDEX idx_orders ON orders(user_id, status, created_at);
-- ✅ Использует индекс
SELECT * FROM orders WHERE user_id = 123;
-- ✅ Использует индекс
SELECT * FROM orders WHERE user_id = 123 AND status = 'pending';
-- ✅ Использует индекс полностью
SELECT * FROM orders
WHERE user_id = 123 AND status = 'pending' AND created_at > '2026-01-01';
-- ❌ НЕ использует индекс
SELECT * FROM orders WHERE status = 'pending';
-- ⚠️ Использует только первую колонку (user_id)
SELECT * FROM orders
WHERE user_id = 123 AND created_at > '2026-01-01';
-- status пропущен, но created_at может использоваться для фильтрацииПорядок колонок критически важен. Рассмотрим таблицу заказов:
CREATE TABLE orders (
id BIGSERIAL,
user_id BIGINT,
status VARCHAR(20),
amount DECIMAL,
created_at TIMESTAMP
);| Запрос | Частота |
|---|---|
WHERE user_id = ? | 50% |
WHERE user_id = ? AND status = ? | 30% |
WHERE status = ? | 10% |
WHERE status = ? AND created_at > ? | 10% |
(user_id, status)CREATE INDEX idx_orders_v1 ON orders(user_id, status);| Запрос | Работает? |
|---|---|
WHERE user_id = ? | ✅ Да |
WHERE user_id = ? AND status = ? | ✅ Да |
WHERE status = ? | ❌ Нет |
WHERE status = ? AND created_at > ? | ❌ Нет |
Охват: 80% запросов
(status, user_id)CREATE INDEX idx_orders_v2 ON orders(status, user_id);| Запрос | Работает? |
|---|---|
WHERE user_id = ? | ❌ Нет |
WHERE user_id = ? AND status = ? | ⚠️ Частично (только status) |
WHERE status = ? | ✅ Да |
WHERE status = ? AND created_at > ? | ⚠️ Частично (только status) |
Охват: 20% запросов эффективно
Вариант 1 лучше, потому что:
user_id имеет высокую селективность (один пользователь = мало заказов)status имеет низкую селективность (несколько статусов = много заказов)Составной индекс может устранить сортировку для ORDER BY.
CREATE INDEX idx_orders_sort ON orders(user_id, created_at DESC);
-- ✅ Использует индекс для сортировки
SELECT * FROM orders
WHERE user_id = 123
ORDER BY created_at DESC;Индекс можно сканировать в обратном направлении:
-- ✅ Тоже использует индекс (сканирование справа налево)
SELECT * FROM orders
WHERE user_id = 123
ORDER BY created_at ASC;CREATE INDEX idx_mixed ON orders(status ASC, created_at DESC);
-- ✅ Использует индекс
SELECT * FROM orders
ORDER BY status ASC, created_at DESC;
-- ❌ НЕ использует индекс для сортировки
SELECT * FROM orders
ORDER BY status DESC, created_at DESC;
-- Направления не совпадаютCREATE INDEX idx_orders_optimal ON orders(user_id, status, created_at DESC);
-- ✅ Идеальное совпадение
SELECT * FROM orders
WHERE user_id = 123 AND status = 'pending'
ORDER BY created_at DESC
LIMIT 10;PostgreSQL:
user_id = 123status = 'pending'created_at DESC (уже отсортировано!)Сортировка не требуется!
CREATE INDEX idx_covering ON orders(user_id, status) INCLUDE (amount, created_at);
-- ✅ Index Only Scan — все данные в индексе
SELECT user_id, status, amount, created_at
FROM orders
WHERE user_id = 123 AND status = 'pending';Ключевые колонки (user_id, status) используются для поиска, INCLUDE-колонки хранятся в листовых узлах.
Комбинация составного индекса с частичным (WHERE-условием):
CREATE INDEX idx_active_users
ON users(last_name, first_name)
WHERE active = true;
-- ✅ Использует индекс
SELECT * FROM users
WHERE active = true
AND last_name = 'Ivanov'
AND first_name = 'Ivan';SELECT
indexrelname,
idx_scan as scans,
idx_tup_read as tuples_read
FROM pg_stat_user_indexes
WHERE indexrelname = 'idx_users_name';EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE user_id = 123 AND status = 'pending';Ищите:
Index Scan — используется ли индексIndex Cond — какие условия примененыRows — сколько строк фактически прочитаноSELECT
indexrelname,
idx_scan,
pg_size_pretty(pg_relation_size(indexrelid)) as size
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
AND idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;-- ❌ Плохо: status имеет низкую селективность
CREATE INDEX idx_bad ON orders(status, user_id);
-- ✅ Хорошо: user_id более селективен
CREATE INDEX idx_good ON orders(user_id, status);CREATE INDEX idx_v1 ON orders(user_id);
CREATE INDEX idx_v2 ON orders(user_id, status); -- ❌ idx_v1 избыточен!Индекс (user_id, status) может использоваться как (user_id). Удалите idx_v1.
-- ❌ Плохо: 10 колонок — огромный индекс, медленная вставка
CREATE INDEX idx_huge ON orders(
user_id, status, amount, created_at,
updated_at, region, category, priority,
assigned_to, completed_at
);
-- ✅ Хорошо: 2-4 ключевые колонки
CREATE INDEX idx_optimal ON orders(user_id, status, created_at);-- Запросы только по first_name
SELECT * FROM users WHERE first_name = 'Ivan';
-- ❌ Бесполезный индекс
CREATE INDEX idx_wrong ON users(last_name, first_name);
-- ✅ Правильный индекс
CREATE INDEX idx_right ON users(first_name, last_name);(A, B), то (A) не нужен)ORDER BY-- Частый запрос в отчёте
SELECT user_id, status, COUNT(*), SUM(amount)
FROM orders
WHERE created_at BETWEEN '2026-01-01' AND '2026-01-31'
GROUP BY user_id, status;created_at BETWEEN ...user_id, statusCREATE INDEX idx_orders_report ON orders(created_at, user_id, status);Почему такой порядок:
created_at — используется в WHERE (фильтр по диапазону)user_id, status — для GROUP BY (данные уже сгруппированы в индексе)-- До: Seq Scan, 1500 ms
-- После: Index Only Scan, 50 ms
(A, B, C) работает для A, A+B, A+B+CORDER BY(A, B), то (A) не нуженВопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.