PostgreSQL: EXPLAIN ANALYZE — как найти медленный запрос
Как включить и читать план выполнения запроса: EXPLAIN ANALYZE, BUFFERS, Seq Scan vs Index Scan. По официальной документации PostgreSQL.
Как использовать
- Перед своим запросом выполни: EXPLAIN (ANALYZE, BUFFERS) SELECT ...
- Seq Scan по большой таблице — добавь индекс по полям в WHERE/ORDER BY.
- Если rows в плане сильно расходится с actual rows — выполни ANALYZE table_name;
Медленный SELECT в PostgreSQL проще всего разбирать по плану выполнения: видно, по каким таблицам идёт полное сканирование, используются ли индексы, сколько строк реально обработано. EXPLAIN показывает план без выполнения; EXPLAIN ANALYZE выполняет запрос и добавляет фактические время и число строк. Проблема: без плана непонятно, почему запрос тормозит — полное сканирование большой таблицы (Seq Scan) или устаревшая статистика. Симптомы: долгий ответ на выборке, высокая нагрузка на БД. Ниже — как вызывать EXPLAIN (ANALYZE, BUFFERS), как читать вывод (Seq Scan vs Index Scan) и что делать по результату по документации Using EXPLAIN.
Решение
Поиск узких мест через план выполнения. EXPLAIN — только план; EXPLAIN ANALYZE — запрос выполняется, в выводе actual time, actual rows. BUFFERS — использование кэша (shared/hit read).
Базовый вызов
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE created_at > now() - interval '7 days';
Форматы вывода: TEXT (по умолчанию), JSON, YAML, XML. Для консоли достаточно TEXT.
Как читать вывод
Плохо — последовательное сканирование большой таблицы:
Seq Scan on orders (cost=0.00..1234.00 rows=5000 width=80) (actual time=0.05..89.22 rows=4823 loops=1)
Filter: (created_at > (now() - '7 days'::interval))
Rows Removed by Filter: 95177
Planning Time: 0.12 ms
Execution Time: 91.45 ms
- Seq Scan — чтение всей таблицы.
- Rows Removed by Filter — сколько строк отброшено; при большом числе запрос тяжёлый.
Хорошо — использование индекса:
Index Scan using idx_orders_created_at on orders (cost=0.42..312.18 rows=5000 width=80) (actual time=0.03..8.11 rows=4823 loops=1)
Index Cond: (created_at > (now() - '7 days'::interval))
Planning Time: 0.08 ms
Execution Time: 9.02 ms
- Index Scan — планёр использует индекс; время выполнения обычно сильно меньше.
Что делать по результату
| Ситуация | Действие |
|---|---|
| Seq Scan по большой таблице | Добавить индекс по полям в WHERE / ORDER BY |
| rows в плане сильно ≠ actual rows | Выполнить ANALYZE table_name; |
| Нужна только статистика, без выполнения | Использовать EXPLAIN без ANALYZE |
Пример индекса по дате:
CREATE INDEX idx_orders_created_at ON orders(created_at);
После создания индекса снова выполните EXPLAIN (ANALYZE, BUFFERS) SELECT ... и сравните Execution Time.
Минимальный пример
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, total FROM orders WHERE status = 'paid' ORDER BY created_at DESC LIMIT 100;
-- Если Seq Scan по orders — добавь индекс, например:
-- CREATE INDEX idx_orders_status_created ON orders(status, created_at DESC);
-- Затем снова EXPLAIN (ANALYZE, BUFFERS) ...
Проверка
-
Запуск EXPLAIN — выполните ваш медленный запрос с префиксом EXPLAIN (ANALYZE, BUFFERS) в psql или клиенте. Убедитесь, что запрос реально выполняется (ANALYZE) и в выводе есть actual time и actual rows.
-
Сравнение до и после индекса — сохраните вывод EXPLAIN до создания индекса, создайте индекс, выполните EXPLAIN снова. Execution Time и тип сканирования (Seq Scan → Index Scan) должны измениться в пользу индекса.
-
Статистика — если rows в плане сильно отличается от actual rows, обновите статистику:
ANALYZE orders;(или ваша таблица), затем снова EXPLAIN. -
Без выполнения запроса — для тяжёлых запросов, которые не хотите выполнять, используйте только
EXPLAIN(без ANALYZE). План будет оценочным, без actual time/rows.
Типичные ошибки
- Не смотреть на Execution Time — главный показатель «медленности» в выводе ANALYZE — Execution Time. Сравнивайте его до и после изменений (индекс, ANALYZE, переписывание запроса).
- Индекс по полю, не участвующему в WHERE/ORDER BY — индекс должен покрывать условия и сортировку. Иначе планёр может не использовать его. Для составного условия создайте составной индекс (например status, created_at).
- Забыть BUFFERS — BUFFERS показывает, сколько блоков прочитано из кэша и с диска. Помогает понять, упирается ли запрос в I/O. Для первичной оценки достаточно ANALYZE; BUFFERS добавляйте при углублённой оптимизации.
Где применять
- Prod / dev: любой медленный SELECT в PostgreSQL. Запускайте EXPLAIN (ANALYZE, BUFFERS) в psql, из приложения (например node-pg) или из админки БД.
- После добавления индекса — всегда перепроверяйте план и Execution Time; при необходимости обновите статистику (ANALYZE).
Связанные сниппеты: JSONB и GIN-индекс, Запросы через node-pg.
Документация: Using EXPLAIN, EXPLAIN.