Один тяжёлый отчёт в PostgreSQL работал медленно из-за временных файлов. После увеличения work_mem он стал завершаться быстрее, и команда решила, что нашла безопасную настройку. В начале рабочего дня несколько подразделений одновременно запустили тот же отчёт, планировщик подключил параллельные процессы, и запас RAM исчез. Каждый отчёт по отдельности помещался. Вместе они создали давление, вырос хвост задержки и риск аварийного завершения процессов. Ошибка была не в желании уменьшить временные файлы, а в представлении о work_mem как о едином лимите запроса или резерве одного соединения. work_mem устроен иначе. Это базовая граница для отдельных операций плана, и общий расход зависит от того, какие из них активны одновременно. Поэтому удачный одиночный запуск ничего не говорит о стоимости нескольких копий отчёта в реальном расписании.

Один параметр относится к одной операции

План запроса — это последовательность и дерево внутренних действий, с помощью которых PostgreSQL читает, соединяет, сортирует и группирует данные. Отдельная сортировка или хэш-таблица получает право использовать память до базовой границы work_mem, прежде чем перейти к временным файлам. Это не означает, что вся сумма выделяется заранее. Открытое соединение также не получает постоянный блок размером work_mem. Память появляется по фактической потребности активной операции. Бездействующее соединение и сессия, которая прямо сейчас сортирует большой результат, имеют разную стоимость для сервера.

Сложный запрос может содержать несколько сортировок и хэш-операций. Некоторые способны быть активны в одно время, другие выполняются последовательно или уже освобождают рабочую область, когда начинается следующая стадия. Поэтому work_mem нельзя назвать ни лимитом всего SQL, ни точным прогнозом его пика. Настройка определяет пространство манёвра одной операции, а счёт за память выставляет их совпадение. Расчёт по числу подключений или одному удачному запросу не видит этой временной структуры.

Узлы плана не складываются механически

На схеме плана можно увидеть несколько мест, которым потенциально нужна рабочая память. Но их количество не является готовым множителем. Один узел может выполниться один раз, другой — повторяться внутри цикла, третий — начать работу только после завершения дочерней части. Память тоже живёт разное время. Сортировка может удерживать результат для следующей стадии, а другая рабочая область — освободиться раньше. При изменении плана после обновления статистики или объёма данных пересечения меняются, хотя текст запроса остаётся прежним.

Хэш-операции имеют отдельную особенность. Параметр hash_mem_multiplier разрешает им использовать больше базового work_mem, поскольку хэш-таблицы особенно чувствительны к нехватке памяти. Это ещё одна причина не строить бюджет по одному числу, но не готовая формула общего расхода сервера. Даже сведения о фактической памяти узла плана относятся к конкретному исполнению и конкретным данным. Они помогают понять поведение операции, но не обещают тот же пик при другой конкуренции. План — карта возможной работы, а не счёт на заранее зарезервированную RAM.

Параллельность добавляет отдельные процессы

Параллельный запрос PostgreSQL делит часть плана между worker-процессами. Для обычного parallel query ограничения ресурсов вроде work_mem применяются отдельно к каждому worker. Поэтому один запрос способен создать несколько рабочих областей одного типа одновременно. Процесс-лидер пользовательской сессии тоже может выполнять параллельную часть, но считать его постоянным дополнительным worker неправильно. Если worker-процессы производят немного строк, лидер способен активно помогать. Если данных много, он может быть занят приёмом их результатов и обработкой верхней части плана.

Фактическое число worker-процессов также может оказаться меньше запланированного: общий пул процессов занят другими запросами. Это меняет и скорость, и профиль памяти. Механическое умножение work_mem на число узлов, подключений и worker-процессов выглядит точным только на бумаге. Значение max_connections не исправляет неопределённость. Оно ограничивает число возможных соединений, но не показывает, сколько сессий одновременно дошли до памятьёмкой стадии. Несколько сотен спокойных подключений могут стоить дешевле десятка синхронных аналитических запросов.

Мало памяти и много памяти стоят по-разному

Слишком маленький work_mem повышает вероятность, что поддерживаемая сортировка или хэш-операция не поместится в память и продолжит работу через временные файлы. Запрос делает дополнительные чтения и записи, дольше занимает процессор и соединение, а несколько таких запросов конкурируют за один накопитель. Слишком большое глобальное значение создаёт противоположный риск. Оно не резервирует этот объём для всех подключений при запуске, но разрешает каждой активной операции взять больше. Пока тяжёлый отчёт один, сервер выглядит стабильным; когда такие работы совпадают, разрешённые области быстро съедают запас.

Последствие видно не только по объёму RAM. Давление может привести к подкачке, замедлению соседних запросов или аварийному завершению процесса операционной системой. Средняя загрузка за день при этом остаётся спокойной и скрывает короткий интервал, который нарушает p99 — время, быстрее которого завершается подавляющее большинство запросов. Компромисс нельзя свести к правилу «избегать временных файлов любой ценой». Иногда spill дешевле, чем постоянный риск общего пика, особенно для редкого фонового отчёта. В другом случае задержка важного запроса оправдывает больше памяти, но только в рамках управляемой конкуренции.

Безопасный отчёт становится опасным в расписании

Вернёмся к отчёту с сортировкой и hash join — соединением данных через хэш-таблицу. Ночью он получает свободный сервер, его рабочие области растут, а часть плана выполняется параллельно. Тест показывает хорошее время и приемлемый пик. Утром запускаются несколько копий. Каждая проходит собственные сортировки и хэширование, несколько worker-процессов делают это параллельно, а обычные транзакции продолжают пользоваться памятью. Ни одна отдельная операция не обязана нарушить свой предел, чтобы сумма стала опасной для хоста.

В такой ситуации дополнительная DRAM — только один вариант. Если отчёт читает лишние данные, выгоднее изменить запрос или индекс. Если бизнес допускает разные сроки, можно развести работы по времени или ограничить их одновременность. Для интерактивных и пакетных классов допустимы разные профили настройки, потому что цена задержки и цена пика у них различаются. Это не конфигурационный рецепт, а смена единицы планирования. Решение принимают не по формальному числу соединений и не по максимальному размеру одного отчёта, а по обязательному сочетанию классов работы. Память должна выдерживать то, что бизнес действительно требует выполнять одновременно.

work_mem не заменяет управление нагрузкой

Рост временных блоков в pg_stat_statements помогает увидеть типы запросов, которые пишут и читают рабочие файлы. План конкретного исполнения объясняет, какая сортировка или хэш-операция потребовала память и перешла на диск. Но эти данные не измеряют универсальный объём RAM для будущего запуска: меняются входные данные, план, число повторов и конкуренция. Хорошее решение сохраняет полезную пропускную способность и задержку без опасного общего пика. Иногда для этого повышают доступную память отдельному классу отчётов, иногда уменьшают объём обрабатываемых данных, меняют план или расписание. Глобальное повышение удобно, но переносит риск на все подходящие операции сервера.

Прозрачный страничный уровень класса gigaRAM не отменяет эту кратность. Рабочие области сортировки и хэширования обычно активно используются во время операции и не являются очевидными кандидатами для переноса на NVMe. Страничный слой может обслуживать другие подходящие менее активные страницы, оставляя горячие в DRAM, но ограничивать work_mem и одновременность запросов всё равно должен PostgreSQL. Управленческий вывод прост: work_mem задают по классу запросов и реальной одновременности, а не как долю всей RAM и не как резерв соединения. Малое значение покупает безопасность ценой временного I/O, большое — возможность удержать отдельную операцию в памяти ценой более высокого совместного пика. Экономически верен тот баланс, который выдерживает обязательное расписание, а не одиночный показательный запуск.

Источники. PostgreSQL 18 — Resource Consumption определяет work_mem как базовый максимум операции, описывает одновременные sort/hash, hash_mem_multiplier и отдельные лимиты worker-процессов.

How Parallel Query Works объясняет роль worker-процессов и переменное участие лидера.

EXPLAIN используется для границ фактического плана, памяти и повторов, а pg_stat_statements — только для накопленных временных блоков по типам запросов.

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