Сорок секунд: первый EXPLAIN ANALYZE

Хронометраж: EXPLAIN ANALYZE

12 min
O que você vai aprender
  • запускать хронометраж: EXPLAIN ANALYZE выполняет запрос по-настоящему и дописывает к прогнозу факт
  • читать вторую скобку плана: actual time, rows, loops
  • помнить главное правило скобки: её числа — среднее на ОДИН запуск узла, полный вклад считается умножением на loops
  • знать, что произойдёт, если скормить ANALYZE команду UPDATE, — и правило безопасности на этот случай

Чертежи запросов уже не кажутся тебе тайнописью: cost, rows, width — ты читаешь их, как штурманскую прокладку. Но по дороге вниз, к машинному отделению, тебя догоняет неприятная мысль: всё это — прогнозы. Штурман ОБЕЩАЕТ маршрут за столько-то попугаев. А как рейс пройдёт на самом деле?

КВЕРИ ждёт тебя у испытательного стенда — плиты с зажимами, над которой на цепочке висит древний секундомер с механической стрелкой.

КВЕРИ: Чертёж говорит, как штурман собирался ехать. Хронометраж — как съездилось. Инженер, который верит одним чертежам, рано или поздно строит мост из бумаги. Заводи секундомер.

The same machines now running — belts turning, steam venting, the chalk forecast still ghosting over them; a brass stopwatch swings from the cadet's hand beside a clicking counter wheel and chalk tally marks for repeat passes, the robotic cat on a warm pipe above and the parrot perched on the stopwatch crown.
The same route, but actually travelled: next to the forecast come the fact, the row count and the number of passes.

Вторая скобка: факт рядом с прогнозом

К знакомому EXPLAIN добавляется одно слово:

EXPLAIN ANALYZE <твой запрос>;

С ANALYZE машина не ограничивается прогнозом — она ВЫПОЛНЯЕТ запрос по-настоящему, с секундомером на каждом узле, и печатает тот же чертёж с дописанными показаниями. Эту команду ты уже встречал в Хранилище; теперь разберём её вывод по цифрам. У каждого узла появляется вторая скобка:

(cost=… rows=… width=…)                ← прогноз штурмана, попугаи
(actual time=старт..итог rows=… loops=…) ← показания секундомера, факт
  • actual time=старт..итог — устроено как у cost (прошлый урок), только единицы честные: МИЛЛИСЕКУНДЫ. Первая цифра — через сколько узел отдал первую строку, вторая — когда закончил.
  • rows — сколько строк узел вернул НА САМОМ ДЕЛЕ. Рядом с прогнозным rows из первой скобки это главная пара чисел плана — ей мы посвятим отдельный урок.
  • loops — сколько раз узел запускался за рейс. Пока держи в голове «обычно 1»; чуть ниже увидишь, как это число взрывается.

Внизу плана добавляются две служебные строки: Planning Time — сколько думал штурман, и Execution Time — сколько ехал исполнитель.

ANALYZE — это настоящий запуск. EXPLAIN ANALYZE UPDATE … не «посмотрит план обновления» — он ОБНОВИТ строки, а потом покажет план содеянного. С DELETE то же самое. В Хранилище ты осваивал защитную обёртку: BEGIN; EXPLAIN ANALYZE UPDATE …; ROLLBACK; — план получен, изменения откатились. В песочнице станции отключены, но и бояться нечего: архив пересобирается заново перед каждым запуском ячейки. На боевой базе правило железное: под ANALYZE — только внутри транзакции с ROLLBACK.

Оценка встречает факт

Испытуемый — термодатчик №388: он висит прямо над люком машинного отделения, на палубе J. Запрос собирает всю его ленту показаний по времени: полный проход по журналу с фильтром, над ним сортировка.

Сначала — чертёж без рейса, как ты уже умеешь:

Прогноз: Seq Scan листает журнал, Sort строит ленту по времени. Запомни rows в скобке у Seq Scan — штурман предсказывает около 360 строк. Проверим секундомером?
Тот же запрос — но теперь с рейсом. Дерево не изменилось, а у каждого узла выросла вторая скобка. Факт: Seq Scan вернул ровно 361 строку (rows=361), отбросив 179 639 (Rows Removed by Filter), и весь рейс уложился в считаные миллисекунды — Execution Time внизу.

Читаем показания секундомера

Три наблюдения по свежему хронометражу:

  1. Штурман почти угадал. Прогноз обещал около 360 строк — факт дал ровно 361. Когда оценка и факт сходятся, штурман ведёт по свежим картам; расхождение на порядок — сигнал тревоги, и ему посвящён отдельный урок этой главы.
  2. actual time показывает характер узла. У старт около нуля, а итог — почти всё время рейса: первую строку он отдаёт сразу и дальше струится ровно — узел-ручей из прошлого урока. У Sort старт и итог почти совпадают: сортировка не может отдать НИ ОДНОЙ строки, пока не увидит все, — та самая плотина: копит и отдаёт залпом. Две цифры — и ты уже знаешь, кто перед тобой.
  3. Миллисекунды плавают. Запусти ячейку ещё раз — времена чуть изменятся: кеши, соседние процессы, настроение станции. А вот rows=361 и Rows Removed by Filter: 179639 не сдвинутся ни на строку. Факт рейса детерминирован — дрожат лишь стрелка секундомера да, как ты помнишь с прошлого урока, прогноз штурмана. Поэтому инженер цитирует в отчётах строки и узлы, а времена — только порядками.

Узел, который запускался сорок раз

В планах выше у каждого узла стояло loops=1: узел отработал за рейс один раз. Так бывает не всегда. Вспомни отчёт, который завис: -счётчик машина запускала заново для КАЖДОГО датчика. Сейчас ты увидишь, как это выглядит на секундомере.

Берём сорок датчиков палубы J — той самой, через чей люк ты сюда спустился, — и для каждого спрашиваем: когда он в последний раз выходил на связь?

Найди SubPlan 1. У узла Aggregate — это твой max()rows=1 loops=40: он запускался сорок раз и каждый раз возвращал одну строку. Под ним Seq Scan с теми же loops=40 — сорок полных проходов по журналу, и рейс вышел в десятки раз дольше, чем у одиночного датчика. Служебный блок JIT внизу пока пропусти: станция просто скомпилировала часть выражений в машинный код.

Главное правило скобки: умножай на loops

Числа в actual-скобке — это СРЕДНЕЕ НА ОДИН запуск узла (округлённое), а не сумма. Полный вклад узла считается умножением:

  • Aggregate: rows=1 × loops=40 → сорок строк за весь рейс, по одному «последнему сеансу связи» на датчик.
  • on ls_readings: rows=361 × loops=40 ≈ 14 440 возвращённых строк. А ПРОЛИСТАНО куда больше: каждый запуск перебирает журнал целиком, 40 × 180 000 = 7 200 000 строк — семь миллионов ради сорока ответов.
  • actual time — тоже среднее на запуск: полное время узла ≈ итоговая цифра × loops.

Вот почему, прежде чем судить узел по его миллисекундам, инженер смотрит на loops: скромный узел на несколько миллисекунд, запущенный тысячи раз, съедает минуты — сорок секунд утреннего отчёта сделаны ровно из этого теста, только датчиков там было больше.

КВЕРИ: Секундомер не обвиняет — он свидетельствует. Узел на считаные миллисекунды невиновен. Присмотрись к тому, кто запустил его сорок раз.

Секундомер сказал, СКОЛЬКО времени ушло. Следующий вопрос инженера — КУДА: что именно машина листала все эти миллисекунды? Ответ измеряется не строками, а страницами — в следующем уроке откроем BUFFERS и посчитаем ввод-вывод.

Check yourself
В хронометраже у узла стоит (actual … rows=1 loops=40). Сколько строк этот узел вернул за весь запрос?
Practice: solve the tasks
Solved 0 of 1