Гайды

Оптимизация запросов через EXPLAIN ANALYZE в PostgreSQL

Чтение плана выполнения, BUFFERS и VERBOSE, Seq Scan vs Index, Nested Loop и work_mem, связка с индексами и pg_stat_statements.

~12 мин чтения

Оптимизация запросов через EXPLAIN ANALYZE

Этот материал логично продолжает гайд «Индексы в PostgreSQL: типы, мониторинг и типичные ошибки»: здесь вы научитесь читать план выполнения и использовать EXPLAIN ANALYZE как основной инструмент диагностики медленных запросов — без догадок и «магических» настроек.


1. Зачем нужен EXPLAIN

PostgreSQL строит план — дерево операций (сканирование таблиц, соединения, сортировки). Команда EXPLAIN показывает оценку планировщика: ожидаемые строки, стоимость, порядок узлов.

EXPLAIN ANALYZE выполняет запрос и добавляет фактические времена и число прочитанных строк. Для INSERT/UPDATE/DELETE в проде удобно оборачивать в BEGINROLLBACK, если нужен только план без побочных эффектов.

КомандаЧто даёт
EXPLAIN запросПлан и оценки без выполнения
EXPLAIN ANALYZE запросПлан + реальные actual time, rows, loops
EXPLAIN (ANALYZE, BUFFERS) …Плюс чтение из буферов/диска по узлам
EXPLAIN (ANALYZE, VERBOSE, BUFFERS, SETTINGS) …Расширенная картина для отладки

Важно: на продакшене для тяжёлых запросов ANALYZE создаёт реальную нагрузку. Запускайте в непик, с лимитом по данным (LIMIT в подзапросе) или на копии/стенде.


2. Как читать строку узла плана

Типичный фрагмент:

text
Index Scan using idx_orders_user_id on orders  (cost=0.42..8.44 rows=1 width=36) (actual time=0.015..0.016 rows=1 loops=1)
  • УзелIndex Scan, имя индекса, таблица.
  • cost=startup..total — условные единицы стоимости (не секунды). Сравнивать полезно между вариантами одного запроса.
  • rows — ожидаемое число строк на один проход узла.
  • width — оценка среднего размера строки в байтах.
  • actual time=первый_ряд..все_ряды — реальное время в миллисекундах.
  • loops — сколько раз узел выполнялся (важно для вложенных циклов: в выводе rows часто на один loop, итог ≈ rows * loops).

С BUFFERS появятся shared hit/read, temp read/written — признаки кэша, диска и спилла во временные файлы.


3. Основные типы узлов (кратко)

УзелСмысл
Seq ScanПолное сканирование таблицы. Нормально для малой доли данных или широких выборок без подходящего индекса.
Index Scan / Index Only ScanДоступ по индексу; Index Only — если все нужные столбцы в индексе (и visibility map позволяет).
Bitmap Index Scan + Bitmap Heap ScanСначала набор TID из индекса, потом чтение страниц таблицы пачками.
Nested LoopДля каждой строки внешнего — внутренний узел. Дорого при больших внешних множествах.
Hash JoinХеш-таблица по одной стороне. Хорош для средних/больших наборов.
Merge JoinДва отсортированных потока.
Sort / HashAggregate / GroupAggregateСортировка и агрегации; следите за external merge и temp в BUFFERS.

Понимание узлов напрямую связано с индексами и статистикой — см. снова гайд по индексам.


4. Практический чек-лист оптимизации

  1. Соберите план: EXPLAIN (ANALYZE, BUFFERS, VERBOSE) <ваш_запрос>;
  2. Найдите узлы с наибольшим actual time (с учётом loops).
  3. Сравните оценочные и фактические rows — большой разрыв часто значит устаревшую статистику или сложные предикаты. Тогда: ANALYZE, CREATE STATISTICS, переписывание условия.
  4. Проверьте буферы: много read при малом hit на горячих запросах — память/кэш или объём данных.
  5. Ищите Seq Scan при узкой выборке — кандидат на индекс или пересмотр WHERE.
  6. Спилл во временные файлы — точечно work_mem в сессии для тяжёлого отчёта, не «на всегда» глобально без измерений.
  7. После изменения — снова полный EXPLAIN ANALYZE и сравнение.

5. Полезные варианты EXPLAIN

  • EXPLAIN (FORMAT JSON) — для визуализаторов и скриптов.
  • auto_explain в postgresql.conf — логировать планы дольше порога (следите за объёмом логов).
  • pg_stat_statements — найти топ по суммарному времени, затем копать конкретный текст через EXPLAIN.

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

ОшибкаПоследствия
Смотреть только на cost без ANALYZEПлан может сильно отличаться от реальности
Игнорировать loopsЗанижение вклада Nested Loop
Поднимать work_mem «на всякий случай»Риск OOM при множестве параллельных сортировок/хешей
Бороться с каждым Seq Scan без контекстаНа большой доле таблицы Seq Scan может быть оптимален
Не обновлять статистику после крупных загрузокПлохие оценки rows → неверный план

7. Мини-пример

sql
EXPLAIN (ANALYZE, BUFFERS)
SELECT o.id, o.created_at
FROM orders o
WHERE o.user_id = 42
  AND o.created_at > now() - interval '30 days';

Проверьте: есть ли индекс по user_id / (user_id, created_at), насколько rows совпадают с реальностью, нет ли лишнего Sort или тяжёлого Bitmap Heap Scan из‑за селективности.


См. также