EXPLAIN SQL: шпаргалка для аналитика
Зачем аналитику EXPLAIN
На senior-позициях вас спросят: «почему этот запрос медленный?». Правильный ответ — «давайте посмотрим EXPLAIN». Без этого вы можете угадывать, но не доказывать.
EXPLAIN vs EXPLAIN ANALYZE
-- План без выполнения (быстро)
EXPLAIN SELECT * FROM orders WHERE user_id = 1;
-- План + реальное выполнение + время
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 1;
-- С буферами и timing
EXPLAIN (ANALYZE, BUFFERS, TIMING) SELECT * FROM orders WHERE user_id = 1;
-- В формате JSON
EXPLAIN (ANALYZE, FORMAT JSON) SELECT ...;ANALYZE действительно выполняет запрос — осторожно с UPDATE/DELETE/INSERT!
Структура плана
Sort (cost=123.45..125.67 rows=100 width=32) (actual time=5.1..5.8 rows=100 loops=1)
Sort Key: created_at
-> Index Scan using idx_orders_user on orders (cost=0.42..100.00 rows=100 ...)
Index Cond: (user_id = 1)Читается снизу вверх и от внутренних операций к внешним:
- Сначала Index Scan — достаёт 100 строк.
- Потом Sort — сортирует их по created_at.
Если хочется сразу закрепить тему на практике — открой тренажёр в Telegram. 10 минут в день — и синтаксис в пальцах.
Виды сканов
Seq Scan (Sequential Scan)
Полный перебор таблицы:
Seq Scan on orders (cost=0..1000 rows=10000)
Filter: (user_id = 1)Когда норма: маленькие таблицы, или фильтр выбирает >10-20% строк.
Когда плохо: большая таблица + маленькая доля строк + есть индекс — значит, планировщик его не использует. Проверяйте почему.
Index Scan
Использует индекс:
Index Scan using idx_orders_user on orders (cost=0.42..10)
Index Cond: (user_id = 1)Идёт по B-tree индексу, потом читает сами строки из таблицы.
Index Only Scan
Индекс покрывает все нужные столбцы — в таблицу не ходим:
Index Only Scan using idx_orders_user on orders
Index Cond: (user_id = 1)
Heap Fetches: 0Самый быстрый вариант. Достигается covering index с INCLUDE.
Bitmap Heap Scan
Гибрид: собирает битмап матчей из индекса, потом читает из таблицы блоками.
Bitmap Heap Scan on orders
Recheck Cond: (status = 'paid')
-> Bitmap Index Scan on idx_orders_statusЭффективен, когда индекс даёт много совпадений, но не всю таблицу.
Виды JOIN
Nested Loop
Для каждой строки левой — ищет совпадения в правой:
Nested Loop (cost=... rows=100)
-> Seq Scan on users u
-> Index Scan on orders o (user_id = u.user_id)Быстрый для маленьких левых таблиц + индекса на правой.
Hash Join
Строит hash-таблицу на меньшей стороне, сканит большую:
Hash Join
Hash Cond: (o.user_id = u.user_id)
-> Seq Scan on orders
-> Hash
-> Seq Scan on usersХорош для равных по размеру таблиц без индексов.
Merge Join
Обе стороны уже отсортированы, идём «в параллель»:
Merge Join
Merge Cond: (o.user_id = u.user_id)
-> Index Scan on orders (sorted by user_id)
-> Index Scan on users (sorted by id)Редко, но эффективен для больших JOIN-ов с индексами-сортировками.
Cost и rows
(cost=1000.00..1500.00 rows=100 width=32)- 1000.00..1500.00 — начальная..финальная стоимость (условные единицы).
- rows=100 — ожидаемое количество строк.
- width=32 — средний размер строки в байтах.
На собесе: «estimated rows очень далеки от actual rows — это проблема statistics». Решение: ANALYZE orders; для обновления статистики.
Типовые проблемы и решения
1. Seq Scan на большой таблице
Seq Scan on orders (rows=10_000_000)
Filter: (user_id = 1)Нет индекса или он не используется. Добавьте CREATE INDEX idx_orders_user ON orders(user_id);.
2. Неиспользуемый индекс из-за функции
-- Индекс на email не используется
WHERE LOWER(email) = 'x'Решение: функциональный индекс:
CREATE INDEX ON users (LOWER(email));3. Slow Sort
Sort Method: external merge Disk: 256MBСортировка в памяти не поместилась, пошло на диск. Решения:
- Добавить индекс, соответствующий ORDER BY.
- Увеличить work_mem (DBA).
4. Misestimated rows
Seq Scan on orders (cost=... rows=100) (actual ... rows=1000000)Огромная разница → плохие статистики. ANALYZE таблицу.
Чтобы не только читать теорию, но и решать реальные задачи — загляните в бот Карьерника. Там по каждой теме подборка вопросов с разборами.
10 практических ситуаций
1. Запрос тормозит
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 1;Смотрим: Seq Scan или Index Scan, actual time.
2. JOIN замедлился
Смотрим тип JOIN (Nested / Hash / Merge). Если Nested Loop на больших таблицах — проверяем индексы.
3. GROUP BY медленный
Смотрим, есть ли HashAggregate vs GroupAggregate. Hash — быстрее для маленьких групп.
4. ORDER BY + LIMIT
Проверяем, использует ли index для сортировки (нет Sort в плане).
5. DISTINCT
DISTINCT → Unique (после сортировки) или HashAggregate. Если видим Sort — можно заменить на GROUP BY + индекс.
6. Subquery медленный
Смотрим, материализуется он или inline. Explain покажет CTE Scan или подзапрос как часть плана.
7. Multi-column filter
WHERE user_id = 1 AND status = 'paid'Composite index (user_id, status) даст Index Cond на обоих. Два отдельных — только один.
8. COUNT(*)
COUNT(*) в PostgreSQL — seq scan. Альтернатива — приближённый count из pg_stats.
9. JOIN 5+ таблиц
Оптимизатор может выбрать неоптимальный порядок. Иногда помогает SET join_collapse_limit = 1 или hint.
10. CTE против subquery
EXPLAIN покажет, материализуется ли CTE. В PostgreSQL 13+ обычно inline.
Как тренироваться
EXPLAIN — скилл, который учится только через реальные запросы. Запустите тяжёлый SELECT, посмотрите план, добавьте индекс, посмотрите ещё раз.
Тренажёр Карьерник содержит блок на оптимизацию: EXPLAIN, индексы, как ускорять запросы.
Совет: на собесе, если спрашивают «как бы вы диагностировали медленный запрос», первые слова — «EXPLAIN ANALYZE». Это сразу +2 балла за практичность.
Читайте также
FAQ
EXPLAIN или EXPLAIN ANALYZE?
EXPLAIN — оценка плана, не выполняет запрос. ANALYZE — выполняет и показывает реальное время. Для SELECT можно ANALYZE. Для UPDATE/DELETE/INSERT — ОПАСНО, он выполнит запрос. Используйте EXPLAIN (ANALYZE FALSE) для них.
Как понять, что индекс нужен?
Seq Scan на большой таблице с узким фильтром + actual rows << total rows. Если ваш запрос читает 100 строк, а Seq Scan перебирает миллион — индекс ускорит в 10 000 раз.
Что такое cost в EXPLAIN?
Условные единицы планировщика. Учитывает seq page reads, random page reads, CPU. Абсолютное значение не важно — важно сравнение разных планов одного запроса.
Почему планировщик не использует мой индекс?
Частые причины: (1) функция поверх столбца — WHERE LOWER(col), (2) implicit cast, (3) старая статистика — ANALYZE поможет, (4) маленькая таблица — Seq Scan быстрее.