Tämä artikkeli on tällä hetkellä venäjäksi — englanninkielinen käännös on työn alla.
EXPLAIN без ANALYZE показывает только план и оценки планировщика. EXPLAIN (ANALYZE, BUFFERS) реально выполняет запрос и докладывает фактические строки, время и обращения к страницам. Именно расхождение оценки с фактом обычно и указывает на проблему.
Для инженера это не академический разбор дерева, а быстрый способ перестать спорить на глаз. План показывает, где запрос читает миллионы строк ради девяти, где вложенный цикл незаметно повторился тысячи раз и где кэш скрывает настоящую цену ввода-вывода.
Реальные и оценочные значения
Каждый узел плана показывает две пары чисел. Оценочные — cost и rows (что планировщик предполагал до запуска). Фактические — actual time и rows (что вышло на самом деле).
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE user_id = 5;
Типичная строка узла выглядит так:
Seq Scan on orders (cost=0.00..1934.00 rows=12 width=40)
(actual time=0.40..18.7 rows=9 loops=1)
cost=0.00..1934.00 — оценка в условных единицах: запуск и полный проход.
rows=12 — ожидалось 12 строк, actual ... rows=9 — реально вернулось 9.
actual time=0.40..18.7 — миллисекунды до первой и до последней строки.
Читайте план снизу вверх и изнутри наружу: листовые узлы выполняются первыми.
Loops: умножайте на число проходов
В nested loop внутренний узел запускается много раз, и actual time/rows в нём указаны на один проход. Умножайте на loops, чтобы получить итог.
EXPLAIN (ANALYZE, BUFFERS)
SELECT u.email, o.amount
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.country = 'DE';
Если внутренний Index Scan показывает actual time=0.02 rows=3 loops=5000, то это не 0.02 мс, а около 100 мс суммарно и 15000 строк. Грабли: маленький per-loop тайминг при больших loops — самый частый способ недооценить дорогой узел.
Разрыв оценки и факта
Большой разрыв между rows оценочным и фактическим — главный сигнал. Планировщик выбирает стратегию по оценке; если она врёт, он берёт неоптимальный план.
ANALYZE orders;
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE status = 'paid' AND created_at >= '2026-01-01';
- Оценка занижена в десятки раз — обычно устаревшая статистика: запустите
ANALYZE.
- Разрыв не уходит после
ANALYZE — вероятно, коррелированные условия; помогут расширенная статистика (CREATE STATISTICS) или индекс.
- Завышенная оценка заставляет планировщик зря брать Seq Scan вместо Index Scan.
BUFFERS: shared hit и read
BUFFERS показывает работу с памятью на каждом узле. shared hit — страницы, найденные в кэше; shared read — прочитанные с диска (медленно).
Seq Scan on orders (actual time=0.4..210 rows=2000000 loops=1)
Buffers: shared hit=128 read=16234
shared hit — попадание в shared_buffers, дёшево.
shared read — промах кэша, физическое чтение; много read = узел тяжёлый по вводу-выводу.
Buffers помогает понять, был ли запрос медленным из-за диска или из-за плохого плана.
Seq Scan, который должен быть Index Scan
Главный практический сценарий: запрос фильтрует по селективному столбцу, но видите Seq Scan с огромным read и фактическим временем.
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE user_id = 5;
CREATE INDEX idx_orders_user ON orders (user_id);
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE user_id = 5;
После индекса Seq Scan сменяется на Index Scan, а Buffers: shared read падает с тысяч до единиц.
- MySQL:
EXPLAIN ANALYZE (8.0+) даёт actual time и loops; буферов в стиле PostgreSQL нет, смотрите Handler_read* и профайлер.
- ClickHouse: вместо плана с фактами используют
EXPLAIN для структуры и системные логи (query_log) для реальных метрик чтения.
Грабли: ANALYZE действительно запускает запрос. Для обычного SELECT это безопасно, но для модифицирующих команд используйте транзакцию с откатом или анализируйте эквивалентный читающий запрос. Сначала найдите узел с максимальным временем, чтениями или ошибкой оценки, затем уже добавляйте индекс или меняйте SQL.
Итог: читайте план как протокол выполнения, а не как набор названий узлов. Большие расхождения строк, крупные shared read и высокий множитель loops почти всегда быстрее приведут к причине, чем перебор индексов наугад.
EXPLAINбезANALYZEпоказывает только план и оценки планировщика.EXPLAIN (ANALYZE, BUFFERS)реально выполняет запрос и докладывает фактические строки, время и обращения к страницам. Именно расхождение оценки с фактом обычно и указывает на проблему.Для инженера это не академический разбор дерева, а быстрый способ перестать спорить на глаз. План показывает, где запрос читает миллионы строк ради девяти, где вложенный цикл незаметно повторился тысячи раз и где кэш скрывает настоящую цену ввода-вывода.
Реальные и оценочные значения
Каждый узел плана показывает две пары чисел. Оценочные —
costиrows(что планировщик предполагал до запуска). Фактические —actual timeиrows(что вышло на самом деле).EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE user_id = 5;Типичная строка узла выглядит так:
cost=0.00..1934.00— оценка в условных единицах: запуск и полный проход.rows=12— ожидалось 12 строк,actual ... rows=9— реально вернулось 9.actual time=0.40..18.7— миллисекунды до первой и до последней строки.Читайте план снизу вверх и изнутри наружу: листовые узлы выполняются первыми.
Loops: умножайте на число проходов
В nested loop внутренний узел запускается много раз, и
actual time/rowsв нём указаны на один проход. Умножайте наloops, чтобы получить итог.EXPLAIN (ANALYZE, BUFFERS) SELECT u.email, o.amount FROM users u JOIN orders o ON o.user_id = u.id WHERE u.country = 'DE';Если внутренний
Index Scanпоказываетactual time=0.02 rows=3 loops=5000, то это не 0.02 мс, а около 100 мс суммарно и 15000 строк. Грабли: маленький per-loop тайминг при большихloops— самый частый способ недооценить дорогой узел.Разрыв оценки и факта
Большой разрыв между
rowsоценочным и фактическим — главный сигнал. Планировщик выбирает стратегию по оценке; если она врёт, он берёт неоптимальный план.ANALYZE orders; -- rebuild statistics EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE status = 'paid' AND created_at >= '2026-01-01';ANALYZE.ANALYZE— вероятно, коррелированные условия; помогут расширенная статистика (CREATE STATISTICS) или индекс.BUFFERS: shared hit и read
BUFFERSпоказывает работу с памятью на каждом узле.shared hit— страницы, найденные в кэше;shared read— прочитанные с диска (медленно).shared hit— попадание вshared_buffers, дёшево.shared read— промах кэша, физическое чтение; многоread= узел тяжёлый по вводу-выводу.Buffersпомогает понять, был ли запрос медленным из-за диска или из-за плохого плана.Seq Scan, который должен быть Index Scan
Главный практический сценарий: запрос фильтрует по селективному столбцу, но видите
Seq Scanс огромнымreadи фактическим временем.-- before: Seq Scan, reads the whole table for 9 rows EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE user_id = 5; CREATE INDEX idx_orders_user ON orders (user_id); -- after: Index Scan, pinpoint read EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE user_id = 5;После индекса
Seq Scanсменяется наIndex Scan, аBuffers: shared readпадает с тысяч до единиц.EXPLAIN ANALYZE(8.0+) даётactual timeиloops; буферов в стиле PostgreSQL нет, смотритеHandler_read*и профайлер.EXPLAINдля структуры и системные логи (query_log) для реальных метрик чтения.Грабли: ANALYZE действительно запускает запрос. Для обычного SELECT это безопасно, но для модифицирующих команд используйте транзакцию с откатом или анализируйте эквивалентный читающий запрос. Сначала найдите узел с максимальным временем, чтениями или ошибкой оценки, затем уже добавляйте индекс или меняйте SQL.
Итог: читайте план как протокол выполнения, а не как набор названий узлов. Большие расхождения строк, крупные shared read и высокий множитель loops почти всегда быстрее приведут к причине, чем перебор индексов наугад.