open atlas
← Все проекты

backend · intermediate · 5d

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

Вставь EXPLAIN (ANALYZE, FORMAT JSON) и отрисуй дерево плана с таймингом по узлам и ошибкой оценки строк, чтобы плохой join был виден сразу.

Читать сырой EXPLAIN JSON — это аналог чтения минифицированного бандла: информация вся тут, но структура враждебна. Этот проект заставляет разобраться в формате плана достаточно глубоко, чтобы отрисовать его — а значит, нужно правильно учесть число циклов (фактические строки в дочернем узле nested loop указаны за цикл, а не суммарно), вычислить self-time как разницу между суммарным временем узла и временем его потомков, и выбрать осмысленное определение «худшего узла», которое не сводится ни просто к самому дорогому, ни просто к самому неожиданному. Практический выигрыш очевиден: каждый сеньор, оптимизирующий запросы, рано или поздно строит или тянется именно к такому инструменту — а его самостоятельная сборка делает семантику плана незабываемой.

Результат

Веб-приложение, превращающее EXPLAIN JSON в аннотированное сворачиваемое дерево плана с подсветкой худшего узла.

Этапы

0/3 · 0%
  1. 01Распарси EXPLAIN JSON в дерево

    Распарси EXPLAIN JSON в дерево узлов.

    Критерии готовности
    • EXPLAIN (ANALYZE, FORMAT JSON) парсится в дерево узлов с сохранением структуры родитель/потомок и типов узлов.
    • Некорректный или не-JSON ввод падает с понятным сообщением, а не крашем.
  2. 02По узлам: факт против плана

    Отрисуй дерево с actual vs planned по каждому узлу.

    Критерии готовности
    • Каждый узел показывает факт против плана по строкам и собственное время; дерево сворачивается.
    • Число циклов учтено, чтобы не путать строки за цикл и суммарные.
  3. 03Подсвети худший узел

    Подсветь узел с самой большой ошибкой оценки и самым большим self-time.

    Критерии готовности
    • Узел с наибольшей ошибкой оценки (факт/план) и наибольшим собственным временем визуально помечены.
    • Корректный план без большой ошибки не даёт ложной тревоги.

Рубрика

Джуниор Миддл Сеньор
Корректность парсинга плана Парсит узел верхнего уровня и отображает тип узла, оценочную стоимость и плановые строки для простых однорузловых планов. Рекурсивно обходит Plans/children, сохраняет отношения родитель-потомок для всех типов узлов скана и join, и обрабатывает планы с несколькими массивами потомков (например, стороны build и probe hash join) без их смешения. Корректно учитывает число циклов при вычислении фактических итогов по узлу: потомок внутри nested loop с 1000 итерациями сообщает строки за цикл, а не суммарные — смешение даёт ошибку оценки ровно в 1000 раз. Умеет объяснить, какие поля JSON используются и почему Loops необходимо учитывать.
Атрибуция стоимости и тайминга Отображает Total Cost планировщика и Actual Total Time из JSON как есть для каждого узла. Вычисляет self-time каждого узла как разность Total Time и суммы Total Time потомков, отображает его рядом с накопленным временем и показывает плановые против фактических строк, чтобы ошибка оценки была сразу видна. Помечает спиллующие узлы: hash или sort с Batches > 1 означают спилл work_mem на диск, что типично даёт замедление в 10–100 раз. Предлагает нижнюю границу work_mem из поля Peak Memory Usage или оценивает её по числу партий hash-таблицы при отсутствии поля.
Обнаружение худшего узла Подсвечивает узел с наибольшей Total Cost как узкое место. Вычисляет два отдельных сигнала — наибольшую ошибку оценки (фактические/плановые строки с учётом циклов) и наибольший self-time — и показывает каждый независимо, чтобы пользователь видел одновременно «куда ушло время» и «где планировщик ошибся сильнее всего». Избегает ложных тревог на тривиально дешёвых узлах: ошибка оценки в 100 раз на узле длительностью 0,01 мс — это шум; оценка тяжести взвешивает величину ошибки по self-time. Формулирует, почему корректный план без большого разрыва в оценке не должен давать флаги, и явно тестирует этот случай.
Эталонный разбор (спойлер)

Структура EXPLAIN JSON: массив верхнего уровня содержит один объект плана; каждый узел имеет Node Type, массив потомков Plans, Startup Cost, Total Cost, Plan Rows, Actual Startup Time, Actual Total Time, Actual Rows и Loops. Loops — это множитель: Actual Rows в JSON указан за цикл, поэтому суммарные фактические строки = Actual Rows × Loops. Пропустить это значит получить катастрофически неверные вычисления ошибки оценки для любого плана с nested loop.

Self-time против накопленного времени: Total Time в узле включает время всех потомков. Стоимость самого узла — это Total Time минус сумма Total Time его потомков. Для листовых узлов (seq scan, index scan) self-time равен Total Time. Отображать только Total Time вводит в заблуждение — корень всегда выглядит самым дорогим узлом.

Seq scan как диагностический сигнал: seq scan по большой таблице не всегда ошибочен — если запрос возвращает значительную долю строк, seq scan выигрывает у index scan, потому что случайные обращения к куче дороже последовательного чтения. Флаг стоит поднимать при сочетании seq scan с большим разрывом в оценке — обычно это означает устаревшую статистику или отсутствующий индекс.

Спиллы work_mem: hash join и сортировки используют хеш-таблицы в памяти, ограниченные work_mem (по умолчанию 4 МБ на операцию). Когда данные превышают лимит, Postgres сбрасывает их на диск партиями (Batches > 1). Спилл на 10 партий означает примерно в 10 раз больше I/O. Глобально устанавливать высокий work_mem опасно — каждый одновременный запрос может его использовать: 100 соединений × 256 МБ = 25 ГБ только на сортировки.

Сделай по-сеньорски

  • Определи проливающиеся hash/sort узлы (Batches > 1) и предложи целевой work_mem.
  • Сравни два плана (до/после фикса) рядом.

Навыки

EXPLAIN JSON parsingtree layoutestimate-vs-actual diffing