Кеш планов, настройка константных стоимостей и внутренности планировщика
Подготовленные операторы переключаются на общий план после 5 выполнений — ловушка для косых параметров. SSD требует random_page_cost = 1.1. GEQO обрабатывает большие соединения. JIT помогает CPU-bound запросам. Каждый механизм имеет режим отказа и исправление.
Параметризованный запрос работал быстро месяцами. Однажды P99 задержка возрастает до 4 секунд. Среднее по-прежнему 3 мс. Тот же запрос, другие параметры — распределение косое. После 5 выполнений Postgres переключился на общий план, не знающий значений параметров. Одна строка ALTER ROLE исправляет это.
Внутренняя структура планировщика
Прежде чем объяснять, почему планировщик выбрал неожиданный план для сложного запроса, нужно понять, что именно он сравнивает — пути, а не готовые планы.
Для каждого запроса планировщик строит пути — лёгкие, частичные планы для фрагментов запроса (сканирование одной таблицы, соединения небольших наборов отношений). Пути сравниваются по стоимости; только дешевейший путь за «набор отношений + порядок сортировки» сохраняется (остальные отбрасываются). Для многоотношённых соединений рассматриваются все комбинации с помощью динамического программирования — исчерпывающим образом до geqo_threshold (умолчание 12) отношений, затем переключается на Genetic Query Optimizer (эвристический поиск) выше этого порога.
Ментальная модель для старших: когда видите неожиданный план, спрашивайте «какие ещё пути рассматривал планировщик и почему они были отброшены?». Ответ почти всегда — разница в стоимости, обусловленная оценками строк.
Общие vs кастомные планы (подготовленные операторы)
Каждый параметризованный запрос от драйвера ($1, $2, …) и каждая PL/pgSQL-функция с параметрами проходит через кеш планов. Жизненный цикл:
- Первые 5 выполнений: Postgres планирует каждое с реальными значениями параметров — кастомные планы, оптимальные для конкретного параметра.
- После 5-го выполнения: Postgres вычисляет среднюю стоимость 5 кастомных планов и сравнивает её со стоимостью общего плана (без привязки к параметрам).
- Если стоимость общего плана в пределах ~10% от средней стоимости кастомных: Postgres навсегда переключается на общий план на время жизни подготовленного оператора.
Все три шага вместе означают, что переключение — автоматическое, невидимое и постоянное до закрытия соединения или явного вмешательства. Переключение невидимо для приложения. Для равномерно распределённых параметров общий план обычно подходит. Для косых распределений (workspace_id, где у некоторых workspace 8% строк, а у других 0.01%), общий план оптимизируется под «типичный» параметр и катастрофически неверен для выбросов.
Симптомы: высокое stddev_exec_time в pg_stat_statements (бимодальная задержка — быстро для распространённых параметров, медленно для выбросов).
Диагностика (PG 16+): EXPLAIN (GENERIC_PLAN) на подготовленной форме — показывает точно какой план получают все выполнения после переключения.
Исправление:
ALTER ROLE app SET plan_cache_mode = 'force_custom_plan';Каждое выполнение перепланируется с реальным параметром. Стоимость планирования: ~0.4–2 мс — ничтожно по сравнению с 4-секундной хвостовой задержкой.
Подготовленный оператор, который был быстрым, теперь медленный — диагностика
# Строка pg_stat_statements для проблемного запроса:
query: "SELECT * FROM orders WHERE workspace_id = $1 AND status = $2 ORDER BY created_at DESC LIMIT 50"
calls: 482,910
total_exec_time: 1,852,400 ms
mean_exec_time: 3.84 ms
stddev_exec_time: 412.6 ms # ОГРОМНОЕ stddev -- симптом переключения плана
min_exec_time: 0.8 ms
max_exec_time: 4,290 ms
# EXPLAIN (ANALYZE, BUFFERS) на ТОМ ЖЕ запросе с workspace_id=42 (большой объём):
Limit (cost=0..14.2 rows=50) (actual time=2.1..2.3 rows=50 loops=1)
-> Index Scan using idx_orders_workspace_status_created on orders
(cost=0..14000 rows=50000) (actual time=2.1..2.3 rows=50 loops=1)
Index Cond: ((workspace_id=42) AND (status='pending'::text))
Buffers: shared hit=12
Planning Time: 0.4 ms
Execution Time: 2.4 ms
# EXPLAIN (GENERIC_PLAN, BUFFERS) на подготовленной форме (PG 16+):
Limit (cost=0.43..28.5 rows=50)
-> Index Scan using idx_orders_created on orders
(cost=0.43..15000.0 rows=27000)
Filter: ((workspace_id = $1) AND (status = $2))
Planning Time: 0.2 ms
# Контекст:
# Распределение workspace_id: workspace 1 занимает 0.01% строк; workspace 42 — 8%.
# Распределение status: 'pending' 15%, 'shipped' 80%, 'cancelled' 5%. Почему тот же подготовленный оператор показывает mean 3.84мс, но stddev 412мс? Каково немедленное исправление?
Настройка константных стоимостей для SSD
Postgres поставляется с random_page_cost = 4.0 и seq_page_cost = 1.0 — откалиброваными для вращающихся HDD, где случайные чтения в 4× медленнее последовательных. На NVMe SSD отношение 1.5–2×; на системах, где рабочий набор помещается в RAM, приближается к 1.0.
-- В postgresql.conf для SSD-систем:
random_page_cost = 1.1
seq_page_cost = 1.0
-- Сообщите планировщику, сколько данных кешировано ОС:
effective_cache_size = '24GB' -- ~75% общей RAMЭффект: планировщик становится более склонен использовать Index Scan и Index Only Scan вместо Seq Scan — что является правильным смещением для современного железа. Это единственная наиболее эффективная настройка планировщика на любом SSD-обеспеченном Postgres.
effective_cache_size — не выделение памяти, а подсказка планировщику о доступном объёме OS page-cache + shared_buffers. Правильная установка делает разницу стоимости между кешированными страницами индекса и некешированными страницами кучи более точной.
GEQO и большие соединения
Для запросов, соединяющих более geqo_threshold (умолчание 12) отношений, планировщик переключается с исчерпывающего динамического программирования на Genetic Query Optimizer — эвристику, которая быстра, но не гарантирует оптимальность. Эвристика рандомизирована — одинаковый запрос может производить разные планы при разных запусках.
Для аналитических запросов с 15–30 соединёнными таблицами GEQO может производить нестабильные планы. Обходные пути:
-- Принудительный исчерпывающий поиск (медленнее планирование, стабильные планы):
SET geqo = off;
-- Зафиксировать порядок JOIN в явном порядке SQL:
SET from_collapse_limit = 1;
SET join_collapse_limit = 1;Трюк с from_collapse_limit = 1 полезен, когда вы вручную настроили порядок JOIN сложного запроса и хотите, чтобы он оставался настроенным при изменениях данных.
JIT-компиляция
PG 11+ поддерживает JIT-компиляцию вычисления выражений (предикаты фильтрации, проекции, агрегация). JIT включается когда общая стоимость плана превышает jit_above_cost (умолчание 100,000).
JIT помогает CPU-bound запросам на больших сканированиях (миллионы строк, сложные выражения) на 10–30%. Для коротких OLTP-запросов добавляет накладные расходы — время компиляции превышает экономию времени выполнения.
-- Для OLTP-нагрузок, где JIT вредит больше, чем помогает:
SET jit_above_cost = 500000;
-- Или полностью отключить для OLTP-ролей:
SET jit = off;Используйте EXPLAIN (ANALYZE, JIT) для просмотра тайминов JIT по фазам и принятия решения о настройке порогов.
- Кастомных планов до переключения на общий
- 5
- Допустимая разница стоимости общего плана vs кастомного
- ~10%
- geqo_threshold (умолчание)
- 12 отношений
- jit_above_cost (умолчание)
- 100,000
- jit_inline_above_cost (умолчание)
- 500,000
- from_collapse_limit / join_collapse_limit
- 8 / 8
- Время планирования на выполнение (типичный OLTP)
- 0.4-2 мс
- random_page_cost (настройка для SSD)
- 1.1
- Рекомендация effective_cache_size
- ~75% общей RAM
Подготовленный оператор работал быстро 5 выполнений. На 6-м становится медленным. Какова наиболее вероятная причина?
Какая возможность PostgreSQL 16 позволяет инспектировать какой план получат все выполнения подготовленного оператора после 6-го, без реального выполнения 5 раз?
Вы установили random_page_cost = 1.1 на SSD-обеспеченном Postgres. Что происходит с планами для запросов с селективными предикатами?
- 01Опишите полный жизненный цикл того, как Postgres выбирает план для параметризованного запроса — от первого выполнения до переключения на общий план.
- 02Почему effective_cache_size важен для планировщика, даже если он не выделяет никакой памяти?
Кеширование планов в Postgres имеет специфический режим отказа: после 5 выполнений подготовленного оператора Postgres сравнивает среднюю стоимость кастомного плана с общим планом и может навсегда переключиться на общий. При косых распределениях параметров общий план оптимален для «типичного» параметра и катастрофически медленен для выбросов — производя бимодальную задержку (низкое среднее, высокое stddev). Диагностируйте через stddev_exec_time в pg_stat_statements и EXPLAIN (GENERIC_PLAN) (PG 16+). Исправляйте через plan_cache_mode = 'force_custom_plan' для роли. Для SSD-систем устанавливайте random_page_cost = 1.1 — планировщик будет правильно предпочитать Index Scan над Seq Scan для селективных предикатов. Теперь, когда увидишь подготовленный оператор, который быстр большую часть времени, но с диким хвостом задержки — первым делом посмотри на stddev_exec_time. Эта асимметрия — подпись ловушки общего плана.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
встречается в177
- Путь запроса: семь остановок от сокета до ответаjunior
- Accept и парсинг: от очереди ядра до типизированного запросаmiddle
- Маршрутизация и middleware: что выполняется и в каком порядкеmiddle
- Обработчик и ответ: от бизнес-логики до байтов на проводеmiddle
- Стриминг и backpressure: когда клиент читает медленнее, чем вы пишетеsenior
- Таймауты и хвостовая задержка: бюджеты, дедлайны и ловушка fan-outsenior
- Middleware и DI: два паттерна, формирующие любой backendjunior
- Пишем middleware: сигнатуры, next() и три модели фреймворковmiddle
- Инверсия управления: как зависимости добираются до классаmiddle
- Скоупы и время жизни DI: singleton, request, transientmiddle
- DI как шов для тестов: фейки, моки и граница, которая важнаsenior
- DI-контейнеры в продакшене: графы разрешения, циклы и когда не стоитsenior
- Блокирующий vs неблокирующий I/O: два способа ждатьjunior
- Event loop: один поток, упорядоченные фазыmiddle
- Что блокирует цикл: CPU-работа и синхронные вызовыmiddle
- Вынос CPU-работы: worker threads и пул libuvmiddle
- Backpressure и ограниченная конкурентностьsenior
- Пропускная способность под нагрузкой: хвостовая задержка и насыщениеsenior
- Зачем пул: цена создания соединенияjunior
- Размер пула: почему больше не значит быстрееmiddle
- Взятие и таймауты: очередь ожидания — настоящий дроссель задержкиmiddle
- Стратегии retry: backoff, jitter и thundering herdmiddle
- Наблюдаемость, production-инциденты и дизайн для глобального масштабаsenior
- Задачи, микрозадачи и scheduler.yield()middle
- Точность таймеров, троттлинг и фоновая работаmiddle
- Event loop Node.js: фазы, nextTick и задержка циклаsenior
- Стратегии рендеринга: SSG, SSR, ISR, streaming и гидратацияjunior
- SSG, SSR, ISR, streaming и RSC — как работает каждая стратегияmiddle
- Цена гидратации: selective, progressive, острова, resumabilitymiddle
- Core Web Vitals: что измеряют LCP, INP и CLSjunior
- LCP: четыре фазы, одна доминирующая стоимостьmiddle
- INP: input delay, processing, presentationmiddle
- Lab vs field: почему они расходятся и как использовать каждыйmiddle
- Трейдоффы метрик, RUM-атрибуция и цикл CI+полеsenior
- Общая картина: от URL до LCP до INP как эстафетаjunior
- Восемь слоёв трассировки: от service worker до второй навигацииmiddle
- Пять канонических поломок: где производство стабильно ломаетсяsenior
- Метод трёх треков: чтение трасс и построение системы мониторингаsenior
- Поколенческий GC и Scavengermiddle
- Major GC: mark, sweep, compactsenior
- Write barriers: цена инкрементального и поколенческого GCsenior
- Биты в проводеjunior
- Математика задержкиmiddle
- Bufferbloat и перегрузкаsenior
- Граница физического уровняsenior
- Номера последовательности и состояние соединенияmiddle
- Управление потоком и перегрузкойmiddle
- BBR, производственная наблюдаемость и за пределами TCPsenior
- CDN: контент по соседствуjunior
- Anycast и GeoDNS: маршрутизация к ближайшему edgemiddle
- Многоуровневый кеш и Cache-Controlmiddle
- Заголовок Vary и cache keysmiddle
- Stale-while-revalidate и cache stampedesenior
- Edge workers и edge-side compositionsenior
- CDN: операции и observabilitysenior
- WebSocket: HTTP-апгрейд до постоянного соединенияjunior
- WebSocket vs SSE vs long-polling: выбор правильного транспортаmiddle
- Backpressure в WebSocket: когда клиенты не успеваютmiddle
- Реконнект: jittered backoff, thundering herd, восстановление сообщенийsenior
- WebSocket в масштабе: HTTP/2 мультиплексирование, permessage-deflate, C10Msenior
- WebSocket в production: прокси, безопасность и распределённая архитектураsenior
- Что делают обратные проксиjunior
- Алгоритмы балансировки: от round-robin до power-of-two-choicesmiddle
- L4 vs L7 балансировка и сохранение IP клиентаmiddle
- Health checks, connection draining и slow startmiddle
- Retry-бури, circuit breakers и load sheddingsenior
- Устойчивая архитектура LB: anycast, zone-aware маршрутизация и observabilitysenior
- Почему QUIC, а не TCP+TLSjunior
- QUIC-потоки и head-of-line blockingjunior
- Объединённое рукопожатие и 1-RTTmiddle
- Connection ID и миграция сетиmiddle
- Обнаружение потерь и управление перегрузкойmiddle
- Возобновление 0-RTT и шифрование пакетовsenior
- Развёртывание и стоимость CPUsenior
- DDoS: что это и почему работаетjunior
- Атаки усиления и истощение состоянияmiddle
- Ограничение скорости: алгоритмы и архитектураmiddle
- WAF, межсетевые экраны, mTLS и HSTSmiddle
- Отравление DNS-кэша и BGP-перехватsenior
- Эшелонированная защита и экономика атакsenior
- Двенадцать слоёв: один URL, семь действующих лицjunior
- DNS, TCP, TLS по очереди: куда уходят миллисекундыmiddle
- Критический путь рендеринга и Core Web Vitalsmiddle
- Перехват прокси и шлюзы безопасности: rate limiter, WAF, mTLSmiddle
- Альтернативные пути: QUIC 0-RTT, WebSocket upgrade, миграция соединенияmiddle
- Наблюдаемость: распределённые трейсы, USE/RED и семплированиеsenior
- Устойчивость: каскадные повторы, circuit breakers и error budgetsenior
- Что такое три сигнала: метрики, логи, трейсыjunior
- Метрики и cardinality: cost-модель time-series databasemiddle
- Логи и объём: cost-модель структурного логированияmiddle
- Трейсы и сэмплирование: cost-модель distributed tracingmiddle
- Join-ключи и exemplar''''ы: как три сигнала становятся компонуемымиmiddle
- Observability 2.0: широкие события и сдвиг стоимостиsenior
- Режимы сбоя и инженерная практика: cardinality budget''''ы, PII и сэмплированиеsenior
- Зачем нужны структурные логи: дневник против таблицыjunior
- Схема продакшн-лога: поля, которые несёт каждая строкаmiddle
- Log levels и маршрутизация алертовmiddle
- Стратегии sampling и стоимость логовmiddle
- PII-редакция и log injectionsenior
- Propagation trace-контекста в логахsenior
- OTel Logs Data Model и audit-логи как подсистемаsenior
- Сигналы OTel, Semantic Conventions и проводной формат OTLPmiddle
- Авто-инструментирование и ручные спаны: правило 80/20 в OTelmiddle
- Collector OTel: receivers, processors, exporters и паттерны развёртыванияmiddle
- Стратегии сэмплирования: head, tail и parent-basedmiddle
- Vendor-нейтральность, eBPF-инструментирование, Operator и OTel в браузере и serverlesssenior
- Эксплуатация OTel Collector: надёжность, version skew, режимы отказа и управлениеsenior
- RED и USE: два чек-листа, одна дисциплина триажаjunior
- Инструментация RED в Prometheus: счётчики, гистограммы и дисциплина cardinalitymiddle
- USE на Linux: CPU, память, диск, сеть и PSImiddle
- Golden signals, структура дашборда и auto-RED в service meshmiddle
- Cardinality как драйвер затрат: label, PII, exemplars и семплированиеmiddle
- Native histograms, SLO и паттерны production-сбоевmiddle
- Выбор SLI и SLO-целей: отношения, не ощущенияmiddle
- Multi-window multi-burn-rate-алертинг: почему AND лучше ORmiddle
- Error budget policy, latency SLO и составные journeysmiddle
- Iceberg SLI, математика составного SLO и SLA vs SLOsenior
- Flame graph: читаем картинку, которая показывает, куда ушло времяjunior
- Sampling vs instrumentation profiling: почему 99 Гц побеждает в productionmiddle
- Типы профилей: CPU, память, off-CPU, mutex — какой когда братьmiddle
- Continuous profiling: always-on flame graphs с eBPF и корреляцией trace-idmiddle
- Как flame graph строится из сэмплов и как использовать его в productionmiddle
- Linux perf, внутренности eBPF, PGO и ограничения sampling''''аsenior
- Profiling в production: безопасность, war stories, OTel profiles и дизайн инфраструктурыsenior
- Debugging-воронка: SLO → RED → trace → profilejunior
- Архитектура OTel: один SDK, четыре сигнала, один wire-форматmiddle
- Экономия на observability: удерживаем затраты в пределах 5% inframiddle
- Масштаб, безопасность и ROI наблюдаемых системsenior
- Сначала профиль: измерь куда реально уходит времяjunior
- Закон Амдала и self-time: потолок любого ускорения, которое ты можешь выпуститьmiddle
- Измерительный цикл: микробенч, макробенч, prod-профиль, эффект наблюдателяmiddle
- Чтение флейм-графов: формы, профайлеры по языкам и 60-секундный сканmiddle
- Статистические baseline''''ы: почему один запуск — не измерениеmiddle
- История профайлеров и ловушки микробенчей: от Кнута до GWPsenior
- Hardware counters, профили холодного старта и безопасность профилейsenior
- Непрерывное профилирование в масштабе: затраты, CI-гейты, корреляция с трейсами и антипаттерныsenior
- Что делает путь горячим: симптом против причиныjunior
- Пять форм hotspot''''а: CPU, аллокации, кэш, лок, syscallmiddle
- Чтение parent и child chains: где применять правкуmiddle
- JIT deopt, цикл fix-and-verify и PR-time профилированиеmiddle
- Аппаратные счётчики и Intel TMA: диагностика подкатегорийsenior
- False sharing и горячие пути нативных мостовsenior
- Горячие пути в production: безопасность, хвостовая латентность и происхождение инструментовsenior
- Иерархия памяти: почему расстояние важнее числа операцийjunior
- Row-major vs column-major: порядок доступа и разрыв в 9xjunior
- Branch prediction: 10–30 циклов штрафа за неожиданный ifmiddle
- Hardware prefetcher, TLB и memory-level parallelismsenior
- Основы GC: за что рантайм берёт налогjunior
- Алгоритмы GC: поколенческая гипотеза, concurrent marking и write barriermiddle
- GC tradeoffs: пауза, throughput, память и давление аллокацийmiddle
- Настройка GC: пейсинг, форма кучи и наблюдаемость аллокацийmiddle
- Внутреннее устройство GC: tri-color инвариант, write barriers и глубокое погружение в рантаймыsenior
- GC в production: наблюдаемость, безопасность, edge cases и управление флотомsenior
- N+1: одна логическая операция, много round-trip''''овjunior
- Семейства фиксов: JOIN, IN, preload и DataLoadermiddle
- Обнаружение N+1: query logs, APM traces и CI gatesmiddle
- DataLoader: батчинг по дереву резолверовmiddle
- Кросс-протокольный N+1: HTTP fan-out и Redis MGETmiddle
- N+1 в масштабе: исчерпание пула, изменения планов и денормализацияsenior
- Batching: амортизируй фиксированную цену каждой операцииjunior
- Окно батчинга: размер и время ожиданияmiddle
- Batching в Kafka и Postgresmiddle
- io_uring и наблюдаемость пакетированияmiddle
- От Nagle до io_uring: эволюция пакетированияmiddle
- Backpressure, изоляция сбоев и безопасность батчей в продакшенеsenior
- Что на самом деле стоит bundle: download, parse, compile, executejunior
- Core Web Vitals: LCP, INP и CLSmiddle
- Code splitting: route-level, component-level, vendor splittingmiddle
- Tree shaking и compression: удаляем то, что не используемmiddle
- Third-party scripts: тихий убийца бюджетаmiddle
- CI enforcement и RUM: делаем бюджеты рабочимиmiddle
- V8 JIT-пайплайн, HTTP-приоритеты и безопасность bundlesenior
- Цикл performance: дисциплина, а не проектjunior
- Классификация и исправление: сопоставление family bottleneck с методамиmiddle
- Observability-стек и CI gates: ловить регрессии до выпускаmiddle
- От инцидента к enforcement: SLO burn до верифицированного исправления за 35 минутmiddle
- Культура, экономика и масштаб performancesenior
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.