Что такое EXPLAIN ANALYZE и как читать BUFFERS в PostgreSQL?

SeniorPostgreSQL · Backend·Обновлено 1 сентября 2026
Коротко
EXPLAIN ANALYZE реально выполняет запрос и показывает фактическое время и число строк на каждом узле плана, а опция BUFFERS добавляет статистику по обращениям к буферному кэшу (hit/read/dirtied/written), по которой видно, читает ли запрос данные из памяти или идёт на диск.

EXPLAIN vs EXPLAIN ANALYZE

EXPLAIN показывает только план выполнения запроса и оценки планировщика (estimated rows, cost) — сам запрос не выполняется. EXPLAIN ANALYZE реально выполняет запрос (включая INSERT/UPDATE/DELETE!) и добавляет к плану фактические метрики: actual time, actual rows, loops. Это позволяет сравнить ожидания планировщика с реальностью — большое расхождение (например, ожидали 10 строк, получили 100000) обычно означает устаревшую статистику или неудачный запрос.

Важно: так как запрос выполняется по-настоящему, для модифицирующих запросов на проде нужно оборачивать в транзакцию с откатом:

BEGIN;
EXPLAIN (ANALYZE, BUFFERS) UPDATE orders SET status = 'paid' WHERE id = 42;
ROLLBACK;

Опция BUFFERS

BUFFERS добавляет к каждому узлу плана статистику обращений к буферному кэшу PostgreSQL:

  • shared hit — блоки найдены в shared buffers (кэш в памяти), быстро;
  • shared read — блоки пришлось прочитать с диска (или из кэша ОС) — потенциально дорого;
  • shared dirtied — блоки помечены как изменённые;
  • shared written — блоки вытеснены из кэша на диск во время выполнения;
  • temp read/written — использование временных файлов на диске, когда операции (сортировка, hash join, group by) не помещаются в work_mem.

Пример вызова:

EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders
WHERE customer_id = 123
ORDER BY created_at DESC
LIMIT 20;

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

Index Scan using idx_orders_customer on orders
  (cost=0.43..120.50 rows=50 width=200)
  (actual time=0.05..1.20 rows=48 loops=1)
  Buffers: shared hit=40 read=5
  • cost=0.43..120.50 — оценка планировщика (условные единицы, не мс);
  • actual time=0.05..1.20 — реальное время старта первой строки и полного выполнения узла, в мс;
  • rows=48 vs rows=50 — фактическое число строк близко к оценке, статистика актуальна;
  • Buffers: shared hit=40 read=5 — 40 блоков нашлись в кэше, 5 пришлось читать с диска. Cache hit ratio здесь = 40/(40+5) ≈ 89%.

На что смотреть в первую очередь

  1. Высокий read при частом выполнении запроса — данные не помещаются в shared_buffers или ОС-кэш вымывается другими запросами.
  2. temp read/written у Sort или HashAggregate — сигнал увеличить work_mem или переписать запрос.
  3. Большое число loops у вложенных узлов (Nested Loop) — стоимость нужно умножать на loops, реальная суммарная стоимость может быть скрыта.
  4. Расхождение estimated vs actual rows в разы — устаревшая статистика (нужен ANALYZE на таблице) или проблема с селективностью условия.
  5. Sequential Scan с большим read на крупной таблице — возможно, отсутствует нужный индекс.

Дополнительно

Полезно комбинировать с FORMAT JSON для программного анализа, и с расширением pg_stat_statements/визуализаторами планов (explain.dalibo.com) для сложных запросов с множеством join.

Что хочет услышать интервьюер

Чётко объясняет разницу между EXPLAIN и EXPLAIN ANALYZE — оценка плана vs реальное выполнение

Знает, что BUFFERS показывает shared hit/read/dirtied/written и temp read/written

Умеет посчитать cache hit ratio из hit и read и объяснить, что означает низкий ratio

Понимает риск запуска EXPLAIN ANALYZE на изменяющих данные запросах и знает про обёртку в транзакцию с ROLLBACK

Может по расхождению estimated/actual rows и наличию temp-файлов диагностировать проблему (устаревшая статистика, нехватка work_mem, отсутствие индекса)

Пример: Безопасный запуск EXPLAIN ANALYZE для изменяющего запроса

BEGIN;

-- реально выполняет запрос, поэтому оборачиваем в транзакцию
EXPLAIN (ANALYZE, BUFFERS)
UPDATE orders
SET status = 'paid'
WHERE id = 42;

-- откатываем, чтобы не изменить данные по-настоящему
ROLLBACK;

Пример: Пример чтения buffers в плане

EXPLAIN (ANALYZE, BUFFERS)
SELECT o.id, c.name
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.created_at > now() - interval '7 days'
ORDER BY o.created_at DESC
LIMIT 50;

-- в выводе смотрим на каждый узел:
-- Buffers: shared hit=120 read=30 -- 30 блоков читались с диска, кэш прогрет не полностью
-- Buffers: temp read=200 written=200 -- сортировка не поместилась в work_mem, ушла на диск

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

Запускают EXPLAIN ANALYZE на UPDATE/DELETE в проде без транзакции и отката, реально изменяя данные

Путают cost (условные единицы планировщика) с actual time (реальные миллисекунды)

Игнорируют секцию Buffers и делают выводы только по времени выполнения, упуская проблемы с диском

Не учитывают эффект холодного/тёплого кэша — сравнивают время первого и повторного запуска без пометки

Не понимают, что loops нужно учитывать при оценке суммарной стоимости вложенных узлов (Nested Loop)

Лучшие курсы по теме

Иконка молнииНовый
изображение курса

Продвинутый Python

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга5.0
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Golang - Templ Fiber HTMX

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Основы C#

Антон Ларичев
AI-тренажерыAI-тренажеры
Гарантия
Бонусы
иконка звёздочки рейтинга4.3
3 999 ₽ 6 990 ₽
Подробнее