В предыдущих уроках мы видели, как Postgres строит план. Теперь смотрим на нечто более экзотическое: сам план превращается в код. С версии 11 в Postgres появился JIT (Just-In-Time compilation) — механизм, который через LLVM на лету компилирует выражения запроса в нативный machine code.
Это не маркетинг, а реальный прирост скорости на тяжёлых аналитических запросах. Но только на них — на коротких JIT часто тормозит запрос. Разберёмся, как это работает и когда его включать/отключать.
Что именно компилируется
Не весь запрос целиком, а конкретные «горячие» фрагменты:
- Expression evaluation — выражения в WHERE, SELECT, ORDER BY. Например,
WHERE total_cents > 50000 AND status = 'paid'— это набор инструкций, и Postgres «интерпретирует» их виртуально для каждой строки. JIT компилирует это в одну функцию. - Tuple deforming — извлечение значений колонок из физического tuple (учитывая null bitmap, alignment, varlena). Это вызывается миллионы раз на больших запросах; компиляция убирает интерпретатор.
- Inlining built-in functions — функции
=,>,+,int8sumвшиваются прямо в тело сгенерированной функции, без call overhead.
Plan tree содержит выражения. Без JIT executor интерпретирует их «виртуальной машиной». С JIT — генерирует LLVM IR → ассемблер → x86-64.
То есть тебе придётся «заплатить» 50-200 ms на компиляцию, чтобы потом каждая из миллиона строк обрабатывалась в 5-10 раз быстрее. Math простая: окупается, когда суммарное время выполнения без JIT больше, чем overhead компиляции.
Cost-based включение
Postgres сам решает, включать JIT для конкретного запроса или нет, через несколько порогов:
jit(default on) — глобальный переключатель.jit_above_cost(default 100000) — если оценочная стоимость плана выше этого, включить JIT в принципе.jit_inline_above_cost(default 500000) — если выше, включить ещё и inlining функций (более агрессивная оптимизация).jit_optimize_above_cost(default 500000) — включить дополнительные LLVM-оптимизации (loop unrolling, SCEV).
То есть JIT включается только для тяжёлых планов, по дефолту с cost > 100K. Это разумно: не стоит компилировать SELECT * WHERE id = 5.
Параметры JIT. В pglite JIT обычно собран без LLVM (или с заглушкой), но GUC видны и SHOW работает.
Анатомия EXPLAIN с JIT
В реальной БД при включённом JIT и cost выше порога EXPLAIN (ANALYZE) покажет дополнительную секцию:
JIT:
Functions: 12
Options: Inlining true, Optimization true, Expressions true, Deforming true
Timing: Generation 1.234 ms, Inlining 5.678 ms, Optimization 15.432 ms, Emission 8.765 ms, Total 31.109 ms
Что здесь смотреть:
- Functions: сколько функций было сгенерировано. На простом запросе — 1-3, на сложном с агрегациями — 10-20.
- Generation: время на построение LLVM IR (промежуточное представление).
- Inlining: время на подстановку builtin’ов.
- Optimization: LLVM-оптимизации над IR.
- Emission: финальная генерация машинного кода.
Если Total = 31 ms, а сам запрос выполнился за 200 ms — JIT окупился, потому что без него было бы, скажем, 280 ms. Если запрос выполнился за 25 ms — overhead JIT (тоже 31 ms) удвоил время.
Когда JIT мешает
Самая частая засада: запросы со сложным планом (много join’ов), но маленьким количеством строк на выходе. Cost планировщик оценивает по числу row’ов и страниц, и для большого join’а это число может быть много больше 100K — JIT включится. Но если фактически join возвращает 10 строк (например, селективные фильтры сильно режут), JIT не успевает окупиться.
Окупаемость зависит от того, сколько строк обрабатывает executor. На малых выборках overhead больше выигрыша.
Это feedback loop: cost-модель работает по оценкам, JIT включается по оценкам, и обе могут ошибаться. Если EXPLAIN ANALYZE показывает большой JIT.Timing при малом фактическом времени — это симптом misalignment.
Как контролировать
Несколько типичных стратегий:
Поднять порог глобально:
ALTER SYSTEM SET jit_above_cost = 500000;
ALTER SYSTEM SET jit_inline_above_cost = 1000000;
ALTER SYSTEM SET jit_optimize_above_cost = 1000000;
Это сделает JIT включаемым только для действительно тяжёлых запросов. Хорошо для смешанного workload.
Отключить локально для короткой сессии:
SET jit = off;
SELECT ... (точечный запрос);
Отключить для роли — частая практика для OLTP-сервисов:
ALTER ROLE oltp_app SET jit = off;
Полностью отключить, если JIT-провайдер не установлен (например, контейнерный image без llvmjit.so):
ALTER SYSTEM SET jit = off;
Посмотрим стоимости
Запрос с агрегацией на 500K строк — cost планируется обычно >100K, и в реальной БД с JIT enabled будет включён JIT.
Тот же запрос с пониженным jit_above_cost — в реальной БД увидишь JIT block в EXPLAIN ANALYZE.
В pglite ANALYZE вернёт реальные числа исполнения, но без JIT-блока (LLVM не подключён в WebAssembly-сборке). В реальной БД с включённым JIT на этом запросе ты увидишь нечто вроде:
HashAggregate (cost=10000..15000 rows=6) (actual time=120..125 rows=6)
Group Key: status
-> Seq Scan on orders (cost=0..7500 rows=500000)
Planning Time: 0.5 ms
JIT:
Functions: 5
Timing: Generation 2 ms, Inlining 8 ms, Optimization 15 ms, Emission 5 ms, Total 30 ms
Execution Time: 155 ms
Без JIT тот же запрос мог бы быть ~180 ms — экономия 25 ms, окупаемость есть. На запросе же с cost 120K и фактическим временем 5 ms ты бы получил 5 → 35 ms — потеря.
Эволюция и совместимость
- PG 11 — JIT появился, но был опасным; многие отключали.
- PG 12-13 — стал стабильнее, дефолтные пороги пересмотрены.
- PG 14+ — partition-aware, лучше работает на partitioned tables.
- PG 16+ — улучшения inlining и работы с extensions.
Важно: JIT требует, чтобы Postgres был собран с LLVM. Многие облачные провайдеры (RDS, Cloud SQL) собирают с JIT по умолчанию, но container-distributions (alpine slim images) — часто без. Проверить: SHOW jit_provider — обычно llvmjit. Если функция не найдена при SET jit = on, значит JIT-провайдер не установлен.
Краткий decision tree
- OLTP, короткие запросы →
jit = offдля роли. Не нужно. - Reporting / OLAP →
jit = on, дефолтные пороги. Скорее всего окупается. - Сложные запросы с малой выборкой → подними
jit_above_costдо 1M, или используйSET jit = offлокально. - Видишь
JIT.Timing > 30%execution time в EXPLAIN ANALYZE — запрос «не созрел» для JIT, поднимай порог или отключай.
Чек-лист
- JIT с PG 11+ через LLVM: компилирует выражения, tuple deforming, builtin’ы в нативный код.
- Включается по cost:
jit_above_cost = 100K,jit_inline_above_cost = 500K,jit_optimize_above_cost = 500K. - Помогает: long-running OLAP-запросы с большим числом строк через filter/aggregate.
- Вредит: запросы с высокой оценкой cost, но малым actual rows (overhead не окупается).
EXPLAIN (ANALYZE)показывает блокJIT:с Generation/Inlining/Optimization/Emission timings.- Для OLTP — отключай через
ALTER ROLE app SET jit = off. - Требует Postgres, собранный с LLVM (
SHOW jit_provider). - Партиционированные таблицы могут генерировать много JIT-функций — старые версии страдают.