open atlas
↑ К треку
SQL и PostgreSQL вглубь SQL · 08 · 01

Как читать EXPLAIN ANALYZE

EXPLAIN ANALYZE печатает дерево плана с пометками оценка против факта. Зазор между оценочными и фактическими строками — это сигнал, что планировщику соврали.

SQL Middle ◷ 15 min
Уровень
ОсновыJuniorMiddleSenior
Уже знаешь этот юнит? Пройди быструю проверку за минуту →

Отчётный запрос упирается в таймаут 30 с на проде. Джуниор трижды переписывает SQL — не помогает. Ты один раз запускаешь EXPLAIN ANALYZE, и ответ лежит на третьей строке: узел говорит rows=1 оценка, rows=2 300 000 факт. Планировщик думал, что тронет одну строку, и построил весь join вокруг этой лжи. План никто не прочитал. Прочитать его — навык на пять минут, который предотвращает пятичасовые инциденты.

Что на самом деле в выводе

EXPLAIN ANALYZE <query> выполняет твой запрос и печатает дерево плана, которое его породило, по одному узлу на строку, с отступами по вложенности. Глава об execution-plans трека databases выводит, почему планировщик строит дерево сканов и join’ов; здесь ты учишься только читать его как runbook (пошаговый рабочий справочник, к которому тянешься при инциденте). Каждая строка-узел несёт две группы чисел:

  • Оценки (с планирования): cost=0.00..431.00 (startup..total в условных единицах стоимости) и rows=1000 — что планировщик угадал до запуска.
  • Факты (с выполнения): actual time=0.012..4.318 (мс) и rows=998 loops=1 — что реально произошло.
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT o.id, o.total
FROM orders o
JOIN line_items li ON li.order_id = o.id
WHERE o.status = 'shipped';

Реальный (урезанный) результат:

Hash Join  (cost=33.50..812.40 rows=1200 width=20)
            (actual time=1.2..58.7 rows=1180 loops=1)
  Hash Cond: (li.order_id = o.id)
  Buffers: shared hit=512 read=140
  ->  Seq Scan on line_items li
        (cost=0..512 rows=20000 width=12)
        (actual time=0.01..21.3 rows=20000 loops=1)
  ->  Hash  (cost=20..20 rows=300 width=12)
              (actual time=1.1..1.1 rows=305 loops=1)
        ->  Index Scan on orders o
              (cost=0.29..20 rows=300 width=12)
              (actual time=0.03..0.8 rows=305 loops=1)
              Index Cond: (status = 'shipped')
Planning Time: 0.4 ms
Execution Time: 59.4 ms

Читай изнутри наружу, следи за зазором

Дерево выполняется от самых глубоких, внутренних узлов наружу — листья первыми, корень последним. Читай так же. Для каждого узла задай один вопрос: совпадает ли rows= оценка с rows= факт?

  • Оценка 300, факт 305 → планировщик откалиброван хорошо. Этому поддереву можно доверять.
  • Оценка 1, факт 2 300 000 → планировщик промахнулся в оценке. Каждое решение выше этого узла (какой алгоритм join, какой порядок join’ов) принималось на неверном числе строк, поэтому весь план над ним под подозрением. Это самый ценный сигнал во всём выводе, и весь урок 03 — про то, как это чинить.

Зазор в 10× — жёлтый флаг; зазор в 1000× — это баг. Фикс почти никогда не «переписать запрос» — это обновить статистику, добавить индекс, которому планировщик поверит, или поправить оценки коррелированных колонок.

Три числа, которые читают неверно

Loops — actual time это на одну итерацию, не итого. На внутренней стороне nested loop ты увидишь actual time=0.05..0.06 rows=1 loops=50000. Этот узел отработал 50 000 раз. Итоговое время на нём — примерно время на итерацию × loops0.06 мс × 50000 = 3 секунды, а не 0.06 мс. Самая частая ошибка новичка — «этот узел быстрый», когда он тайком и есть всё время выполнения. Так же rows на узле в цикле — среднее на итерацию; умножай на loops для реального итого.

Cost против time. cost безразмерен и имеет смысл только для сравнения кандидатных планов внутри планировщика (урок 02). Это не миллисекунды и не сравнимо между машинами. Для диагностики реального запроса игнорируй cost и читай actual time и rows.

BUFFERS — куда ушёл I/O. BUFFERS добавляет shared hit=N (страницы по 8 КБ из буферного кеша) и read=N (страницы с диска/ОС). Узел с shared read=180000 упёрт в I/O: он вытащил 180k страниц (~1.4 ГБ) с диска. Это, а не CPU, и есть твоя медлительность — фикс это более селективный индекс или меньше сканируемых данных. BUFFERS — разница между догадкой «может, это I/O» и доказательством этого.

Почему это работает

Почему EXPLAIN ANALYZE реально выполняет запрос (и потому опасен на записях)? Потому что половина «ANALYZE» значит «инструментируй и выполни», а не «только спланируй». Запуск EXPLAIN ANALYZE на UPDATE или DELETE выполняет запись. Чтобы безопасно осмотреть изменяющий запрос, оберни его: BEGIN; EXPLAIN ANALYZE UPDATE ...; ROLLBACK;. Сама инструментация тоже добавляет накладные расходы (чтение часов на каждом узле), поэтому Execution Time здесь слегка завышен относительно голого запроса — используй его для сравнения, а не как SLA-число. FORMAT TEXT (по умолчанию) — для чтения глазами; FORMAT JSON — когда вывод будет парсить инструмент.

Расставь шаги по порядку

Расставь, как сеньор читает план EXPLAIN ANALYZE, чтобы найти проблему:

  1. 1 Запусти EXPLAIN (ANALYZE, BUFFERS) на медленном запросе
  2. 2 Найди самые внутренние (глубже отступ) узлы — они выполняются первыми
  3. 3 На каждом узле сравни оценочные строки с фактическими
  4. 4 Найди узел с самым большим зазором оценка-против-факта
  5. 5 Умножь время на итерацию на loops, чтобы получить реальную цену узла
  6. 6 Проверь BUFFERS: цена это чтения с диска или попадания в кеш
  7. 7 Выбери фикс: обновить статистику, индекс или переписать — по тому, что показали числа
Викторина

Внутренний узел nested loop показывает: actual time=0.04..0.05 rows=1 loops=80000. Сколько реального времени стоит этот узел?

Викторина

Узел Seq Scan читает rows=1 оценка, но rows=2 300 000 факт. О чём это говорит в первую очередь?

↻ задержка
Вспомните перед уходом
  1. 01
    Какое самое полезное число читать в выводе EXPLAIN ANALYZE и почему?
  2. 02
    Почему 'actual time=0.05 rows=1' на узле в цикле обманчиво и как получить реальную цену?
  3. 03
    Что добавляет BUFFERS и что говорят shared hit против read?
Итог

EXPLAIN ANALYZE выполняет запрос и печатает выбранное дерево плана, по одному узлу с отступом на строку, и каждый несёт оценку планировщика (cost, rows) рядом с фактом исполнителя (time, rows, loops). Читай изнутри наружу — глубочайшие узлы выполняются первыми — и на каждом узле сравнивай оценочные строки с фактическими: самый большой зазор — где планировщика обманули и где живёт реальный фикс (статистика, индекс, оценки коррелированных колонок — уроки 02 и 03), а не переписывание запроса. Следи за двумя ловушками: на узлах в цикле actual time и rowsна итерацию, поэтому умножай на loops для реальной цены; а cost — безразмерная мерка планирования, не миллисекунды. Добавь BUFFERS, чтобы превратить «похоже на I/O» в доказательство — shared hit это кеш, read это диск. Теперь, когда видишь медленный запрос, твой первый ход — EXPLAIN (ANALYZE, BUFFERS), а первое, что ищешь — зазор оценка-против-факта по строкам на каждом узле, а не новый индекс и не переписывание.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 7 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.