bitlvl
RU

Визуализатор планов EXPLAIN

Выполните EXPLAIN (ANALYZE, BUFFERS) (или FORMAT JSON) в PostgreSQL либо EXPLAIN ANALYZE в MySQL 8 и вставьте вывод. Каждый узел показывает собственное время, долю от общего, оценочное и фактическое число строк и буферы; список замечаний говорит, что медленно и что обычно это исправляет. План остаётся в вашем браузере.

Примеры:

Результаты и сообщения этого инструмента выводятся на английском языке.

Как читать план

План — это дерево операций; корень возвращает результат, а каждый узел забирает строки у своих потомков. EXPLAIN сам по себе показывает оценки планировщика (единицы стоимости и ожидаемые строки); EXPLAIN ANALYZE реально выполняет запрос и добавляет настоящее время и число строк — именно это делает план читаемым. BUFFERS добавляет, сколько страниц по 8 КБ пришло из кеша (hit) или с диска (read). Обычный путь по плану: найти узел с наибольшим исключительным временем, затем понять, почему он там: неверная оценка строк выше по дереву, отсутствующий индекс или настройка памяти.

Числа на узле

ПолеЗначение
cost=0.29..8.31Оценка планировщика в условных единицах: стоимость получения первой строки .. стоимость получения всех строк. Сопоставимо только внутри одного плана.
rows=Оценочное число строк на один цикл. Фактическое значение тоже на цикл; умножьте на loops для итога.
actual time=0.05..12.3Миллисекунды до первой строки .. до последней строки, на цикл, включая потомков. Инструмент вычитает потомков, чтобы показать собственное время каждого узла.
loops=Сколько раз выполнялся узел. Внутренняя сторона nested loop выполняется по разу на каждую внешнюю строку; под Gather это число равно количеству воркеров.
Buffers: shared hit=.. read=..Страницы, найденные в shared_buffers, против прочитанных из ОС/с диска. Чтения — это то, куда уходит время I/O; track_io_timing показывает его как I/O Timings.
Rows Removed by FilterСтроки, которые узел прочитал и затем отбросил: самый явный признак отсутствующего индекса.

Пять типичных проблем

  1. Неверные оценки. Если узел ожидает 10 строк, а получает 100 000, планировщик выбрал nested loop или сортировку, имевшие смысл только для 10. Выполните ANALYZE; для предикатов по коррелированным столбцам создайте расширенную статистику; для CTE и функций планировщик ничего не знает и использует значения по умолчанию.
  2. Последовательное сканирование с селективным фильтром. Нормально на маленьких таблицах. На больших решение — индекс по фильтруемым столбцам, в идеале соответствующий WHERE и ORDER BY.
  3. Сбросы на диск. Sort external merge, Hash Batches > 1, HashAggregate на диске, lossy-битмапы: операция не уместилась в work_mem. Повысить его для сессии или запроса дёшево; повышение глобально умножается на число соединений.
  4. Nested loop над сканированием. Внутренняя сторона выполняется по разу на каждую внешнюю строку; если это Seq Scan или Materialize большого набора, время взрывается. Внутренней стороне нужен индекс, либо планировщику нужно разрешить hash join.
  5. Index-only scan, которые таковыми не являются. Heap Fetches, близкие к числу строк, означают, что карта видимости устарела: выполните VACUUM таблицы.

Как получить план

-- PostgreSQL: самая полезная форма
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS, FORMAT JSON) SELECT …;
-- для записи — внутри транзакции, которую вы откатите
BEGIN; EXPLAIN (ANALYZE, BUFFERS) UPDATE …; ROLLBACK;
-- автоматически логировать медленные планы: auto_explain
LOAD 'auto_explain'; SET auto_explain.log_min_duration = '200ms'; SET auto_explain.log_analyze = on;

-- MySQL 8.0.18+
EXPLAIN ANALYZE SELECT …;          -- дерево с фактическим временем
EXPLAIN FORMAT=JSON SELECT …;      -- оценки с деталями стоимости

Вопросы

Почему время узлов не складывается в общее?

Фактическое время каждого узла включает его потомков, поэтому инструмент показывает исключительное время (узел минус потомки). Параллельные воркеры работают одновременно, поэтому узел под Gather с loops=3 считается один раз, а не три; внутренние стороны nested loop умножаются на loops. Время планирования и время триггеров указаны отдельно.

Какой план считается хорошим?

Тот, в котором оценки отличаются от фактических значений не более чем в десять раз, ни один узел не сбрасывается на диск, самый дорогой узел — тот, которого вы ожидаете (большая таблица, которую действительно нужно прочитать), а буферов, прочитанных с диска, мало по сравнению с попаданиями в кеш. Для такого плана список замечаний пуст.

Поддерживаются ли секционированные таблицы и параллельные планы?

Да: Append / Merge Append по секциям, Gather и Gather Merge с Workers Planned/Launched, агрегаты Partial и Finalize. Разбивка по воркерам (VERBOSE) разбирается, но показывается как часть узла.

Можно ли вставить план из pgAdmin, DataGrip или psql с рамками таблицы?

Да. Строки очищаются от символов рамки psql, заголовка QUERY PLAN и подвала «(n rows)». JSON, скопированный из GUI как одна строка в кавычках, нужно сначала освободить от кавычек.

Насколько полная поддержка MySQL?

Дерево EXPLAIN ANALYZE в MySQL 8 имеет ту же форму, что и текстовый план PostgreSQL, и поддерживается полностью; EXPLAIN FORMAT=JSON (только оценки) отображается на то же дерево: типы доступа становятся видами сканирования, nested_loop — узлом соединения, а флаги filesort / временной таблицы — замечаниями. ANALYZE FORMAT=JSON из MariaDB пока не поддерживается.