EXPLAIN в SQL — как читать план выполнения запроса

Коротко

EXPLAIN показывает план выполнения запроса — то, как PostgreSQL собирается получить данные. Это основной инструмент для диагностики медленных запросов. На собеседованиях аналитиков тему спрашивают на middle+ позициях, обычно в связке с индексами и оптимизацией. Если вы можете прочитать план и предложить, как ускорить запрос, — это серьёзный плюс.

EXPLAIN vs EXPLAIN ANALYZE

EXPLAIN показывает план — что СУБД собирается делать. Запрос при этом не выполняется.

EXPLAIN
SELECT * FROM orders WHERE user_id = 42;

EXPLAIN ANALYZE выполняет запрос и сравнивает план с реальными цифрами: сколько строк вернулось, сколько времени заняла каждая операция.

EXPLAIN ANALYZE
SELECT * FROM orders WHERE user_id = 42;

Важный момент: EXPLAIN ANALYZE реально выполняет запрос. Если это DELETE или UPDATE — данные изменятся. Безопасный способ — обернуть в транзакцию:

BEGIN;
EXPLAIN ANALYZE DELETE FROM orders WHERE id = 1;
ROLLBACK;

Для аналитических SELECT-запросов это некритично, но помнить стоит.

Как читать вывод

Seq Scan on orders  (cost=0.00..1250.00 rows=50 width=72)
                     (actual time=0.015..12.340 rows=48 loops=1)
  Filter: (user_id = 42)
  Rows Removed by Filter: 99952

Разберём по частям:

  • Seq Scan on orders — тип операции (узел плана). Здесь — последовательное чтение всей таблицы.
  • cost=0.00..1250.00 — оценка стоимости. Первое число — стоимость получения первой строки, второе — всех строк. Единицы условные, сравнивать можно только между планами одного запроса.
  • rows=50 — оценка количества строк, которые вернёт этот узел.
  • actual time=0.015..12.340 — реальное время в миллисекундах (только в EXPLAIN ANALYZE).
  • rows=48 — реальное количество строк.
  • Rows Removed by Filter: 99952 — сколько строк было прочитано и отброшено. Если это число большое — запрос неэффективен.

План — это дерево. Вложенные узлы выполняются первыми, результат передаётся вверх. Читайте снизу вверх, от самых вложенных операций к корню.

Типы сканирования

Тип Что делает Когда используется
Seq Scan Читает всю таблицу целиком Нет подходящего индекса, маленькая таблица, низкая селективность
Index Scan Ищет по индексу, затем читает строки из таблицы Есть индекс, высокая селективность
Index Only Scan Берёт все данные из индекса, таблицу не трогает Все нужные столбцы есть в индексе
Bitmap Index Scan Строит битовую карту по индексу, затем читает данные Условия с OR, диапазоны, средняя селективность

Seq Scan — не всегда плохо. На таблице в 100 строк Index Scan будет медленнее, потому что обращение к индексу добавляет накладные расходы. Проблема — когда Seq Scan на таблице с миллионами строк фильтрует 99% данных.

Типы соединений (JOIN)

Тип Как работает Когда выбирается
Nested Loop Для каждой строки левой таблицы сканирует правую Маленькая левая таблица, есть индекс на правой
Hash Join Строит хеш-таблицу из меньшей таблицы, сканирует большую Большие таблицы без подходящего индекса, равенство в условии
Merge Join Сортирует обе таблицы и сливает Обе таблицы отсортированы (или есть индексы), большие объёмы

На практике для аналитических запросов с JOIN чаще всего встречаются Hash Join и Nested Loop. Merge Join — реже, обычно когда обе стороны уже отсортированы.

Как находить проблемы

Большое расхождение между rows (оценка) и actual rows. Если оптимизатор ожидал 10 строк, а получил 100 000 — он выбрал неоптимальный план. Решение: выполнить ANALYZE для обновления статистики таблицы.

Seq Scan на большой таблице с фильтром. Если Rows Removed by Filter составляет 95%+ от прочитанных строк — нужен индекс.

Высокий cost в Sort-узле. Если в плане есть Sort Method: external merge Disk — данные не помещаются в память и сортируются на диске. Можно увеличить work_mem или добавить индекс, покрывающий ORDER BY.

Nested Loop на больших таблицах без индекса. Вложенный цикл без индекса — это O(n*m). Если обе таблицы большие, Hash Join будет эффективнее. Обычно оптимизатор справляется сам, но при неточной статистике может ошибиться.

Практический пример

Допустим, аналитический запрос считает количество ответов по дням:

EXPLAIN ANALYZE
SELECT
    DATE_TRUNC('day', created_at) AS dt,
    COUNT(*) AS answers
FROM training_events
WHERE user_id = 42
  AND created_at >= '2025-01-01'
GROUP BY dt
ORDER BY dt;

Без индекса план выглядит так:

Sort  (cost=2500.00..2500.05 rows=20 width=16)
  Sort Key: (date_trunc('day', created_at))
  ->  HashAggregate  (cost=2490.00..2495.00 rows=20 width=16)
        ->  Seq Scan on training_events  (cost=0.00..2480.00 rows=50 width=8)
              Filter: (user_id = 42 AND created_at >= '2025-01-01')
              Rows Removed by Filter: 1999950

Seq Scan прочитал 2 миллиона строк и отбросил 99.99%. Создаём составной индекс:

CREATE INDEX idx_events_user_date ON training_events (user_id, created_at);

Повторяем EXPLAIN ANALYZE:

Sort  (cost=15.20..15.25 rows=20 width=16)
  Sort Key: (date_trunc('day', created_at))
  ->  HashAggregate  (cost=14.00..14.50 rows=20 width=16)
        ->  Index Scan using idx_events_user_date on training_events
              (cost=0.43..13.50 rows=48 width=8)
              Index Cond: (user_id = 42 AND created_at >= '2025-01-01')

Cost упал с 2500 до 15. Вместо чтения всей таблицы — точечное обращение по индексу.

Типичные ошибки

Преждевременная оптимизация. Не нужно добавлять индексы «на всякий случай». Сначала найдите реально медленный запрос, потом смотрите EXPLAIN и решайте.

Забывать про ANALYZE. После массовой загрузки данных, создания таблицы или удаления большого объёма строк статистика устаревает. Оптимизатор принимает решения на основе старых данных и выбирает плохие планы. ANALYZE table_name; обновляет статистику.

Читать только верхний узел. Проблема может быть не в корне плана, а во вложенном узле. Если видите Sort с cost=50000, посмотрите, что внутри — возможно, там Seq Scan, который и создаёт проблему.

Вопросы с собеседований

Что такое EXPLAIN и зачем он нужен? — EXPLAIN показывает план выполнения запроса: какие операции СУБД выполнит, в каком порядке, с какой оценочной стоимостью. Нужен для диагностики медленных запросов и проверки, используются ли индексы.

Чем EXPLAIN отличается от EXPLAIN ANALYZE? — EXPLAIN показывает только план без выполнения запроса. EXPLAIN ANALYZE выполняет запрос и добавляет реальные метрики: actual time, actual rows, loops. Это позволяет сравнить оценку оптимизатора с реальностью.

Что означает cost в плане выполнения? — Cost — условная единица стоимости. Первое число — стоимость получения первой строки (startup cost), второе — всех строк (total cost). Абсолютное значение само по себе малоинформативно, но полезно для сравнения вариантов одного запроса. Высокий cost во вложенном узле указывает на узкое место.

Когда Seq Scan — это нормально? — На маленьких таблицах (до нескольких тысяч строк), при низкой селективности фильтра (запрос возвращает большую часть таблицы), и при отсутствии условия WHERE. В этих случаях Seq Scan дешевле, чем обращение к индексу.

Как понять, что оптимизатор ошибся в оценке? — Сравнить rows (оценка) и actual rows (реальность) в EXPLAIN ANALYZE. Если расхождение в разы — статистика устарела, нужен ANALYZE. Если расхождение сохраняется после ANALYZE — возможно, данные распределены неравномерно и стоит рассмотреть расширенную статистику (CREATE STATISTICS).


Потренируйтесь решать задачи — откройте тренажёр с 1500+ вопросами для подготовки к собеседованиям аналитиков.

FAQ

Безопасно ли запускать EXPLAIN ANALYZE на проде?

Для SELECT — да, запрос просто выполняется. Но на тяжёлых запросах он может нагрузить базу, потому что выполняется целиком. Для INSERT, UPDATE и DELETE — оборачивайте в BEGIN ... ROLLBACK. Голый EXPLAIN (без ANALYZE) безопасен всегда — он только строит план.

Как часто нужно запускать ANALYZE?

PostgreSQL запускает autovacuum, который периодически обновляет статистику. Но после массовой загрузки данных (COPY, bulk INSERT) или удаления стоит запустить ANALYZE вручную. На практике проблемы с устаревшей статистикой — одна из самых частых причин плохих планов.

Нужно ли аналитику уметь читать EXPLAIN?

На junior — скорее бонус. На middle+ — ожидают, что вы можете посмотреть план медленного запроса, определить узкое место и предложить решение (добавить индекс, переписать подзапрос, обновить статистику). Это показывает, что вы понимаете, как СУБД работает «под капотом».

Можно ли использовать EXPLAIN в ClickHouse, MySQL, BigQuery?

Да, но синтаксис и вывод отличаются. MySQL использует EXPLAIN с табличным выводом. ClickHouse — EXPLAIN с pipeline-нотацией. BigQuery показывает план в веб-интерфейсе (Execution Details). Принципы те же — ищите полные сканирования и узкие места — но детали специфичны для каждой СУБД.

Как тренироваться

Лучший способ разобраться в EXPLAIN — запустить несколько запросов с индексом и без, сравнить планы. Создайте таблицу с миллионом строк, попробуйте разные фильтры и посмотрите, как меняется план.

В тренажёре Карьерник есть вопросы на планы выполнения, индексы и оптимизацию запросов. Больше материалов по SQL — в гайде для аналитиков. Примеры вопросов по всем темам — в разделе с примерами.