Гайды
Оптимизация запросов через 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 в проде удобно оборачивать в BEGIN … ROLLBACK, если нужен только план без побочных эффектов.
| Команда | Что даёт |
|---|---|
EXPLAIN запрос | План и оценки без выполнения |
EXPLAIN ANALYZE запрос | План + реальные actual time, rows, loops |
EXPLAIN (ANALYZE, BUFFERS) … | Плюс чтение из буферов/диска по узлам |
EXPLAIN (ANALYZE, VERBOSE, BUFFERS, SETTINGS) … | Расширенная картина для отладки |
Важно: на продакшене для тяжёлых запросов
ANALYZEсоздаёт реальную нагрузку. Запускайте в непик, с лимитом по данным (LIMITв подзапросе) или на копии/стенде.
2. Как читать строку узла плана
Типичный фрагмент:
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. Практический чек-лист оптимизации
- Соберите план:
EXPLAIN (ANALYZE, BUFFERS, VERBOSE) <ваш_запрос>; - Найдите узлы с наибольшим
actual time(с учётомloops). - Сравните оценочные и фактические
rows— большой разрыв часто значит устаревшую статистику или сложные предикаты. Тогда:ANALYZE,CREATE STATISTICS, переписывание условия. - Проверьте буферы: много
readпри маломhitна горячих запросах — память/кэш или объём данных. - Ищите
Seq Scanпри узкой выборке — кандидат на индекс или пересмотрWHERE. - Спилл во временные файлы — точечно
work_memв сессии для тяжёлого отчёта, не «на всегда» глобально без измерений. - После изменения — снова полный
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. Мини-пример
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 из‑за селективности.