Хронометраж: EXPLAIN ANALYZE
O que você vai aprender
- запускать хронометраж:
EXPLAIN ANALYZEвыполняет запрос по-настоящему и дописывает к прогнозу факт - читать вторую скобку плана:
actual time,rows,loops - помнить главное правило скобки: её числа — среднее на ОДИН запуск узла, полный вклад считается умножением на
loops - знать, что произойдёт, если скормить
ANALYZEкомандуUPDATE, — и правило безопасности на этот случай
Чертежи запросов уже не кажутся тебе тайнописью: cost, rows, width — ты читаешь их, как штурманскую прокладку. Но по дороге вниз, к машинному отделению, тебя догоняет неприятная мысль: всё это — прогнозы. Штурман ОБЕЩАЕТ маршрут за столько-то попугаев. А как рейс пройдёт на самом деле?
КВЕРИ ждёт тебя у испытательного стенда — плиты с зажимами, над которой на цепочке висит древний секундомер с механической стрелкой.
КВЕРИ: Чертёж говорит, как штурман собирался ехать. Хронометраж — как съездилось. Инженер, который верит одним чертежам, рано или поздно строит мост из бумаги. Заводи секундомер.

Вторая скобка: факт рядом с прогнозом
К знакомому 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. Запрос собирает всю его ленту показаний по времени: полный проход по журналу с фильтром, над ним сортировка.
Сначала — чертёж без рейса, как ты уже умеешь:
rows=361), отбросив 179 639 (Rows Removed by Filter), и весь рейс уложился в считаные миллисекунды — Execution Time внизу.Читаем показания секундомера
Три наблюдения по свежему хронометражу:
- Штурман почти угадал. Прогноз обещал около 360 строк — факт дал ровно 361. Когда оценка и факт сходятся, штурман ведёт по свежим картам; расхождение на порядок — сигнал тревоги, и ему посвящён отдельный урок этой главы.
- actual time показывает характер узла. У старт около нуля, а итог — почти всё время рейса: первую строку он отдаёт сразу и дальше струится ровно — узел-ручей из прошлого урока. У Sort старт и итог почти совпадают: сортировка не может отдать НИ ОДНОЙ строки, пока не увидит все, — та самая плотина: копит и отдаёт залпом. Две цифры — и ты уже знаешь, кто перед тобой.
- Миллисекунды плавают. Запусти ячейку ещё раз — времена чуть изменятся: кеши, соседние процессы, настроение станции. А вот
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 и посчитаем ввод-вывод.
(actual … rows=1 loops=40). Сколько строк этот узел вернул за весь запрос?