Куда ушло время: BUFFERS
Чему научишься
- мерить работу запроса страницами, а не секундами:
EXPLAIN (ANALYZE, BUFFERS) - отличать
shared hit(страница из кеша) отread(страница с диска) - находить «прочитано зря» в строке
Rows Removed by Filter - видеть контраст маршрутов: полный проход — мегабайты, точечный запрос по ключу — килобайты
Хронометраж показал тебе, сколько стоит каждый узел маршрута. Но одна загадка осталась: тот же запрос по тому же плану идёт то быстро, то заметно медленнее — как будто у машины бывает настроение.
КВЕРИ ведёт тебя дальше по машинному отделению, туда, где гул ровнее и воздух холоднее. Вдоль стен — стеллажи с одинаковыми контейнерами. Весь архив станции нарезан на такие блоки: страницы по восемь килобайт.
КВЕРИ: У машины не бывает настроения. Бывает кеш. Секундомер меряет погоду: тёплая страница — миллисекунды, холодная — жди диск. Работу меряют не секундами. Работу меряют страницами.
Сегодня ты добавишь к хронометражу третье измерение — ввод-вывод: сколько страниц запрос перетаскал, где он их взял и сколько привёз зря.

Страницы: валюта ввода-вывода
Postgres никогда не читает «одну строку». Таблица хранится страницами по 8 КБ, и страница — минимальная единица чтения: нужна тебе из неё одна строка или все сто двадцать — со стеллажа поднимается весь контейнер. Журнал ls_readings на сто восемьдесят тысяч строк — это 1513 страниц, примерно по сто двадцать строк в каждой.
Между диском и исполнителем стоит буферный кеш — область памяти, где живут недавно тронутые страницы. Отсюда два тарифа на одну и ту же страницу:
shared hit— страница нашлась в кеше. Быстро: это просто память.read— страницы в кеше не было, пришлось поднимать с диска. Медленнее — иногда на порядки.
Флаг BUFFERS дописывает эти счётчики к каждому узлу плана:
EXPLAIN (ANALYZE, BUFFERS) <твой запрос>;
План превращается в карту ввода-вывода: видно не только, где узел потратил время, но и сколько страниц он для этого перетаскал.
Открой такую карту для знакомого маршрута — полного прохода по журналу. Условие нарочно узкое — показания со статусом warn, их в журнале всего два процента, — но индекса по status в журнале нет, и у машины один маршрут: поднять со стеллажей каждую страницу.
Rows Removed by Filter: 176400 — столько строк прочитано и выброшено; Buffers: shared hit=1513 — журнал поднят со стеллажей целиком, страница за страницей.Читаем накладную
Разбери три строки под Seq Scan — накладную этого рейса:
Filter: (status = 'warn')— условие, которое исполнитель проверял на каждой строке.Rows Removed by Filter: 176400— строки, которые прочитали и выбросили. Запросу нужны 3600 строк, а прочитан весь журнал: девяносто восемь из каждых ста поднятых строк уехали в мусор. Это и есть «прочитано зря» — главный счётчик расточительности маршрута.Buffers: shared hit=1513— все 1513 страниц журнала. Умножь на восемь килобайт: запрос перелопатил почти двенадцать мегабайт, чтобы отдать тебе несколько десятков килобайт результата. Заодно разгадка ценника из позапрошлого урока: 3763 попугая скана — это 1513 страниц по цене страницы плюс построчная работа процессора над ста восемьюдесятью тысячами строк.
Заметь: счётчика read в выводе нет вовсе. Журнал только что посеян песочницей и целиком горяч — каждая страница нашлась в кеше, поэтому весь проход уложился в считаные миллисекунды. На так везёт не всегда: первый утренний запрос застаёт кеш холодным, те же 1513 страниц превращаются из hit в read — и время подскакивает в разы. План тот же, работа та же — погода другая. Вот почему инженер сравнивает маршруты по страницам, а не по секундомеру.
Мелкая деталь: у блока Planning: есть собственные Buffers — это страницы, которые тронул сам , пока сверялся с каталогом. К маршруту они не относятся.
Точечный рейс
Теперь противоположный полюс. Единственный , который в журнале уже есть, — индекс reading_id. Спроси одну конкретную строку — и штурман проложит маршрут не по стеллажам, а по лестнице индекса: с этажа на этаж, каждый шаг — одна страница, и в конце — единственная страница журнала, где лежит сама строка.
В ячейке две команды — приём, который ты будешь видеть в этом курсе постоянно: первая выполняет сам запрос — и штурман, готовя его, заранее пролистывает свои страницы каталога, — вторая снимает карту с того же запроса. Собственные Buffers узла от прогрева не меняются, но без него под картой вырос бы блок Planning: с тремя десятками служебных страниц. Ты уже знаешь, что к маршруту они не относятся, — а чистая накладная, где четыре страницы и ничего лишнего, красноречивее.
Index Scan — поиск по индексу вместо полного прохода — и Buffers: shared hit=4: три шага по лестнице индекса и одна страница журнала. Четыре страницы вместо 1513.Мегабайты против килобайт
Положи две накладные рядом. Полный проход: 1513 страниц — почти двенадцать мегабайт перелопаченного архива. Точечный рейс: четыре страницы — тридцать два килобайта. Маршруты к одной и той же таблице различаются по вводу-выводу почти в четыреста раз.
И вот что важно: на глаз оба запроса «мгновенные» — миллисекунды там, доли миллисекунды тут; горячий кеш прячет разницу. Секундомер здесь почти слеп. А карта ввода-вывода — нет: она показывает работу, которую машина совершает при любой погоде. Запрос, который таскает мегабайты ради килобайт, — это счёт, который однажды придёт: холодным утром, на выросшем журнале, в толчее соседних запросов.
КВЕРИ: Плохой маршрут на тёплом кеше — как течь в трюме при штиле. Незаметно. Ровно до первого шторма.
Одна деталь напоследок. В первой строке карты полного прохода штурман обещал несколько тысяч строк — и почти угадал: приехало ровно 3600. Пока оценка и факт дружат. В следующем уроке ты увидишь, что случается с маршрутами, когда оценка с фактом ссорятся, — и почему эта пара — главная на всём чертеже.
Buffers: shared hit=1513. Значит ли это, что запрос прочитал с диска 1513 страниц?