Query processing
JIT compilation
For queries the planner thinks are expensive, PostgreSQL can compile parts of the plan to machine code with LLVM. It speeds up long, CPU-heavy queries, but the compile time is wasted on short ones.
In plain words
PostgreSQL normally interprets expressions such as WHERE amt > 3 AND id % 7 = 1: a general-purpose routine walks through the expression for every row. JIT (just-in-time compilation) instead writes a small program specialised for this exact query and compiles it to machine code with the LLVM compiler.
The compiled code is faster per row, but compiling takes time, from a few milliseconds to around a second. That pays off when a query processes hundreds of millions of rows, and is pure loss for a query that would have finished in 20 ms.
PostgreSQL decides by the planner's estimated cost, not real time. When the estimate is wrong, a fast query can suddenly take a second. Choose a query below and compare the timelines.
For DBAs and operators
What gets compiled
- Expressions (
jit_expressions): WHERE clauses, target lists, aggregates and projections. - Tuple deforming (
jit_tuple_deforming): extracting columns from the on-disk tuple format, specialised for the table's column layout.
The three thresholds
| if total plan cost > | default | then |
|---|---|---|
jit_above_cost | 100000 | JIT-compile (cheap code generation) |
jit_optimize_above_cost | 500000 | also run LLVM optimization passes |
jit_inline_above_cost | 500000 | also inline functions and operators into the code (most expensive) |
-1 disables a stage. jit = on is the default since PostgreSQL 12, but it only works if the server package includes the LLVM provider (jit_provider = llvmjit; on Debian/Ubuntu (PGDG) the postgresql-18-jit package, recommended by postgresql-18; on RHEL a separate postgresql18-llvmjit package). SELECT pg_jit_available(); tells you. Each parallel worker compiles its own copy.
Reading EXPLAIN ANALYZE (real PostgreSQL 18 output)
A 27 ms GROUP BY query, with the thresholds forced to 0 to show the worst case:
JIT:
Functions: 13
Options: Inlining true, Optimization true, Expressions true, Deforming true
Timing: Generation 0.633 ms (Deform 0.202 ms), Inlining 914.591 ms, Optimization 68.357 ms, Emission 35.302 ms, Total 1018.884 ms
Execution Time: 1071.523 msThe same query with jit = off: Execution Time: 26.928 ms. Inlining dominates; the first JIT compile in a session is the slowest because LLVM and its bitcode summaries must be loaded first. Plain EXPLAIN (without ANALYZE) already shows a JIT: section with Functions and Options, so you can see whether a plan will JIT.
When it hurts
- OLTP or API queries whose cost is overestimated (stale statistics, many partitions, big joins that return few rows) cross 100000 and pay compile time on every execution. Classic ticket: "query went from 30 ms to 900 ms after upgrade", with
JIT:in the plan. - Prepared statements keep the decision of their plan.
- Fixes: raise
jit_above_cost(e.g. 1000000 or more) and the optimize/inline thresholds, setjit = offfor the application role (ALTER ROLE app SET jit = off), or fix the estimate.
SELECT query, calls, mean_exec_time, jit_functions,
jit_generation_time + jit_inlining_time +
jit_optimization_time + jit_emission_time AS jit_ms
FROM pg_stat_statements ORDER BY jit_ms DESC LIMIT 10;JIT helps analytical queries that are CPU-bound over many rows (aggregates, complex expressions). It does nothing for I/O-bound queries.
For developers: inside the source
Where it lives
jit/README: design notes (why LLVM, inlining, the type-sync trick).jit.c: provider-independent glue.jit_compile_expr()is called byExecReadyExpr()(execExpr.c) before it falls back toExecReadyInterpretedExpr().provider_init()loads the library named byjit_providerand calls its_PG_jit_provider_init(), which fillsJitProviderCallbacks.src/backend/jit/llvm/:llvmjit.c(llvm_create_context(),llvm_mutable_module(),llvm_compile_module(),llvm_get_function()),llvmjit_expr.c(llvm_compile_expr()turnsExprEvalStepopcodes such asEEOP_DONE_RETURNinto LLVM IR),llvmjit_deform.c(slot_compile_deform()),llvmjit_inline.cpp(llvm_inline(), reads bitcode from$pkglibdir/bitcode),llvmjit_types.c(compiled tollvmjit_types.bcso the IR uses the same struct layouts as the C code).
How the decision is made
The planner, not the executor, decides. At the end of standard_planner() (planner.c):
result->jitFlags = PGJIT_NONE;
if (jit_enabled && jit_above_cost >= 0 &&
top_plan->total_cost > jit_above_cost)
{
result->jitFlags |= PGJIT_PERFORM;
/* + PGJIT_OPT3, PGJIT_INLINE, PGJIT_EXPR, PGJIT_DEFORM */standard_ExecutorStart() copies the flags into estate->es_jit_flags. The first expression compiled creates the JitContext (es_jit). Code is generated per expression but emitted lazily: llvm_get_function() calls llvm_compile_module() only when a function is first needed, running the pass pipeline default<O3> with PGJIT_OPT3, otherwise default<O0>,mem2reg (plus inline when inlining is on).
Instrumentation
JitInstrumentation (jit.h) collects the function count and the generation, inlining, optimization and emission times that EXPLAIN prints. Workers ship theirs back under PARALLEL_KEY_JIT_INSTRUMENTATION and InstrJitAgg() sums them; pg_stat_statements stores them in its jit_* columns.
Debugging
jit_dump_bitcodewrites<pid>.<n>.bcand.optimized.bcfiles into the data directory (inspect withllvm-dis).jit_debugging_supportandjit_profiling_supportregister generated code with GDB / perf; both are developer options and need an LLVM build.- Force JIT in tests with
SET jit_above_cost = 0and checkpg_jit_available();src/test/regress/sql/explain.sqlexercises the EXPLAIN JIT output.
Parameters that shape this
jit jit_above_cost jit_optimize_above_cost jit_inline_above_cost jit_provider jit_expressions jit_tuple_deforming
Related topics
Planner cost model Life of a query Parallel query ANALYZE and statistics