backend · intermediate · 5d
Визуализатор планов запросов
Вставь EXPLAIN (ANALYZE, FORMAT JSON) и отрисуй дерево плана с таймингом по узлам и ошибкой оценки строк, чтобы плохой join был виден сразу.
Результат
Веб-приложение, превращающее EXPLAIN JSON в аннотированное сворачиваемое дерево плана с подсветкой худшего узла.
Этапы
0/3 · 0%- 01Распарси EXPLAIN JSON в дерево
Распарси EXPLAIN JSON в дерево узлов.
Критерии готовности- EXPLAIN (ANALYZE, FORMAT JSON) парсится в дерево узлов с сохранением структуры родитель/потомок и типов узлов.
- Некорректный или не-JSON ввод падает с понятным сообщением, а не крашем.
- 02По узлам: факт против плана
Отрисуй дерево с actual vs planned по каждому узлу.
Критерии готовности- Каждый узел показывает факт против плана по строкам и собственное время; дерево сворачивается.
- Число циклов учтено, чтобы не путать строки за цикл и суммарные.
- 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.
- Сравни два плана (до/после фикса) рядом.