После обновления отчёт в PostgreSQL стал выполняться заметно дольше. В pg_stat_io выросло число чтений клиентских процессов, а на графике накопителя появилась активность. Команда быстро сформулировала два решения: добавить RAM, чтобы больше данных оставалось в кэше, или заменить NVMe. Оба решения могут оказаться ошибочными. Новый план способен прочитать больше страниц при прежней памяти и накопителе. Полный проход даст похожую картину, а выгрузка сортировки во временный файл нагрузит тот же storage, но может не появиться в pg_stat_io. pg_stat_io разделяет часть путей relation/WAL I/O, но не превращает счётчик чтений в диагноз, не показывает spill и не называет медленный SQL. Для руководителя это граница между обоснованной инвестицией и попыткой лечить не тот слой.

Карта работы кластера, а не список запросов

pg_stat_io — накопленная статистика ввода-вывода по всему кластеру PostgreSQL. Строки сгруппированы по типу серверного процесса, контексту работы и объекту, к которому шло обращение. Простыми словами, представление отвечает: кто выполнял I/O, в каком режиме и с каким классом данных. Тип процесса отделяет клиентские подключения от checkpointer, фонового писателя, autovacuum и других внутренних работников. Контекст различает обычную работу, крупное последовательное чтение, массовую запись и обслуживание. Объект показывает, шла ли речь о постоянных отношениях, временных отношениях или журнале предзаписи WAL.

Одна строка объединяет работу множества процессов одного типа с момента сброса статистики, а не одно выполнение запроса. Поэтому рост normal reads у клиентских процессов говорит, что обычная нагрузка чаще обращалась за страницами, но не сообщает, какой текст SQL это вызвал. Большое итоговое число может быть следствием долгого периода нормальной работы, поэтому важнее изменение за сопоставимое окно.

Чтение PostgreSQL ещё не означает чтение с NVMe

Самое важное предупреждение находится в официальной документации PostgreSQL. Статистика I/O фиксирует большинство случаев, когда сервер обращался к ядру для выполнения ввода-вывода, но не различает данные, которые пришлось физически получить с накопителя, и данные, уже находившиеся в page cache ядра. Page cache — файловый кэш Linux. Операционная система держит в RAM недавно использованные части файлов, поэтому запрос PostgreSQL к файлу может завершиться из памяти, не доходя до NVMe. Значит, большой reads или read_bytes не является прямым измерением физической работы накопителя.

Из этого счётчика нельзя вывести, что shared_buffers слишком мал. Страница могла отсутствовать во внутреннем буфере PostgreSQL, но находиться в кэше ОС; запрос мог впервые затронуть новый диапазон; неудачный план мог прочитать ненужные бизнес-задаче страницы. Поэтому одинаковый рост чтений допускает разные решения. При широком, но неизбежном рабочем наборе может быть полезна дополнительная память. При лишнем полном проходе важнее запрос, индекс или статистика планировщика. При физически медленном I/O нужен разговор о накопителе и конкуренции за него. pg_stat_io лишь открывает развилку.

Контекст меняет смысл одинакового счётчика

Обычные чтения клиентских процессов часто отражают работу приложений. Если они растут вместе с числом запросов и обработанных данных, это может быть естественным масштабированием. Если полезная работа не изменилась, причиной может стать новый план, расширившийся диапазон или вытеснение повторно нужных страниц из кэшей. Контекст bulkread PostgreSQL использует для некоторых крупных операций чтения вне обычного пути shared buffers, например последовательного просмотра большой таблицы. Его появление не означает, что кэш «сломался». Система намеренно обрабатывает большой поток иначе, чтобы единичный проход меньше вытеснял обычный рабочий набор.

У временных данных есть важная граница. Объект temp relation в pg_stat_io означает ввод-вывод временных таблиц и индексов. Он не обозначает рабочие файлы, куда сортировка или хэширование выгружает промежуточные данные. Такой spill не отражается в pg_stat_io — это существенная слепая зона представления. Spill может быть связан с work_mem и планом и нагружать тот же накопитель. Его устанавливают другими источниками: pg_stat_statements накапливает временные блоки по нормализованным типам SQL, а план с фактическим исполнением показывает temp blocks конкретного запуска. Повышение work_mem не является автоматическим ответом, потому что память задаётся операциям и несколько запросов могут выполнять их одновременно.

Записи и принудительная фиксация данных тоже нельзя объяснить одной нехваткой RAM. Checkpointer, фоновый писатель и путь WAL выполняют разные обязанности по сохранности изменений. Их активность может совпасть с медленным чтением клиента и создать очередь на общем устройстве, но это конкуренция разных путей, а не доказательство, что каждому из них нужен больший shared_buffers.

Медленный запрос может почти не создавать I/O

Обратная ошибка — считать небольшой I/O доказательством здоровья запроса. PostgreSQL может долго работать на процессоре, ждать блокировку, сеть или клиента. pg_stat_io не объясняет эти задержки. Даже время I/O требует аккуратности. Если track_io_timing выключен, соответствующие поля времени для обычных объектов показывают ноль. Для WAL действует отдельная настройка track_wal_io_timing. Ноль в такой ситуации означает отсутствие измерения, а не мгновенный накопитель.

Накопленное время и задержка запроса имеют разные масштабы: параллельные процессы суммируют ожидания, а один запрос может попасть в очередь после небольшого чтения. Для бизнеса важны p99 — граница для подавляющего большинства ответов — и завершённая полезная работа. pg_stat_io помогает проверить I/O-гипотезу, но не назначает виновника.

Три уровня отвечают на три разных вопроса

Первый уровень — pg_stat_io. Он показывает I/O отношений, включая временные таблицы и индексы, и WAL по классам процессов и контекстам. Здесь видны клиентские чтения, крупные проходы и фоновая запись, но не временные файлы сортировок и не конкретный запрос. Второй уровень — расширение pg_stat_statements, если оно включено. Оно объединяет нормализованные запросы и накапливает для них время, общие и локальные блоки, а также temp blocks временных файлов. Так находят тип SQL со spill, но не точную историю каждого вызова.

Третий уровень — план конкретного запроса. Обычный EXPLAIN показывает гипотезу без исполнения. Вариант с ANALYZE и BUFFERS действительно исполняет запрос и показывает общие, локальные и временные блоки этого запуска, поэтому его нельзя бездумно применять к тяжёлой или изменяющей данные операции. Связь уровней превращает корреляцию в объяснение: карта замечает класс relation/WAL I/O, история запросов обнаруживает temp blocks, а план показывает причину работы конкретного SQL.

Покупка следует за причиной, а не за счётчиком

Неправильное чтение pg_stat_io ведёт к трём дорогим решениям. Компания покупает DRAM для запроса с плохим планом, меняет NVMe при массовой выгрузке промежуточных данных или оптимизирует отдельный отчёт, когда несколько независимых потоков насыщают общий путь I/O. Технически каждый шаг выглядит правдоподобно, но причинная цепочка не доказана. Если рабочий набор шире доступного кэша и страницы регулярно нужны снова, DRAM может сократить физические чтения. Если лишнюю работу создаёт план, выгоднее исправить запрос, индекс или статистику. Если накопитель насыщается общим потоком, решение относится к расписанию, конкуренции или storage.

Прозрачный страничный уровень класса gigaRAM имеет смысл обсуждать только после подтверждения, что у нагрузки есть подходящие менее активные страницы, горячий набор остаётся в DRAM, а цена обращений к NVMe приемлема. pg_stat_io в одиночку этого не доказывает; такой слой не является кэшем PostgreSQL, движком хранения или лекарством от плохого плана, а публичные расчётные числа нельзя выдавать за результат для конкретной базы.

Руководитель получает от pg_stat_io не готовый ответ, а правильную постановку вопроса. Представление отделяет клиентские чтения отношений, bulkread, I/O временных таблиц и индексов, фоновые процессы и путь WAL, но spill остаётся за его пределами. После этого можно связать изменение с типом SQL и планом и только затем выбирать память, запрос, настройку или накопитель.

Источники. PostgreSQL 18 — pg_stat_io определяет relation/WAL I/O, temp relation и границу page cache ядра.

Run-time Statistics задаёт смысл track_io_timing и track_wal_io_timing.

pg_stat_statements описывает temp blocks нормализованных запросов.

EXPLAIN разделяет общие, локальные и временные блоки и объясняет ANALYZE.

Resource Consumption используется только для границы work_mem.

Meta Engineering — TMO подтверждает лишь общую механику страничного переноса, а продуктовая страница — позиционирование класса решения.