Learning Platform
Урок 07.05 · 23 мин
Продвинутый
PlannerExecutorJITLLVMPerformance

В предыдущих уроках мы видели, как 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.
Что делает JIT под капотом

Plan tree содержит выражения. Без JIT executor интерпретирует их «виртуальной машиной». С JIT — генерирует LLVM IR → ассемблер → x86-64.

executor видитWHERE total > 50000 AND status = 'paid'
без JITExecQual → 50 instr на строку (виртуальные)
с JITскомпилированная функция → 5 instr на строку
overheadвремя компиляции LLVM ≈ 50-200 ms на запрос

То есть тебе придётся «заплатить» 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 работает.

PostgreSQL

Анатомия 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 не успевает окупиться.

Когда JIT помогает vs вредит

Окупаемость зависит от того, сколько строк обрабатывает executor. На малых выборках overhead больше выигрыша.

Помогаетreporting, OLAP, ETL
10M+ строк через filterкаждая строка экономит 10 cycles
результат 5-40% быстрее
Вредитhigh cost, low actual rows
селективный JOINplanner ошибся в оценке
overhead 50-200 ms на ничего

Это 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.

PostgreSQL

Тот же запрос с пониженным jit_above_cost — в реальной БД увидишь JIT block в EXPLAIN ANALYZE.

PostgreSQL

В 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 / OLAPjit = on, дефолтные пороги. Скорее всего окупается.
  • Сложные запросы с малой выборкой → подними jit_above_cost до 1M, или используй SET jit = off локально.
  • Видишь JIT.Timing > 30% execution time в EXPLAIN ANALYZE — запрос «не созрел» для JIT, поднимай порог или отключай.
Проверка знанийKnowledge check
У тебя aналитический запрос на partitioned таблице, EXPLAIN ANALYZE показывает: Planning 5 ms, JIT.Total 250 ms, Execution 180 ms. Что это означает и что делать?
ОтветAnswer
JIT компилировался 250 ms, а сам запрос потом выполнился за 180 ms. Чистая потеря 250 ms — без JIT было бы быстрее. Причины: (1) запрос на partitioned таблице может иметь много partitions, и для каждой генерируется отдельный набор функций (поэтому JIT.Functions велик). (2) Cost planner оценил высоко (благодаря большому числу partitions), а реальных строк мало. Решение: либо SET jit_inline_above_cost / jit_optimize_above_cost существенно выше — выключая самые дорогие LLVM-оптимизации (inline и optimize дают основную долю overhead), либо SET jit = off для этой роли/запроса. На PG 14+ есть улучшения partition-aware 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-функций — старые версии страдают.
Векторизованное выполнение в ClickHouse

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что именно JIT в PostgreSQL компилирует в нативный код через LLVM?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5