DB Internals AtlasPostgreSQL 18.6 Home About

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 >defaultthen
jit_above_cost100000JIT-compile (cheap code generation)
jit_optimize_above_cost500000also run LLVM optimization passes
jit_inline_above_cost500000also 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 ms

The 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, set jit = off for 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 by ExecReadyExpr() (execExpr.c) before it falls back to ExecReadyInterpretedExpr(). provider_init() loads the library named by jit_provider and calls its _PG_jit_provider_init(), which fills JitProviderCallbacks.
  • src/backend/jit/llvm/: llvmjit.c (llvm_create_context(), llvm_mutable_module(), llvm_compile_module(), llvm_get_function()), llvmjit_expr.c (llvm_compile_expr() turns ExprEvalStep opcodes such as EEOP_DONE_RETURN into LLVM IR), llvmjit_deform.c (slot_compile_deform()), llvmjit_inline.cpp (llvm_inline(), reads bitcode from $pkglibdir/bitcode), llvmjit_types.c (compiled to llvmjit_types.bc so 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_bitcode writes <pid>.<n>.bc and .optimized.bc files into the data directory (inspect with llvm-dis).
  • jit_debugging_support and jit_profiling_support register generated code with GDB / perf; both are developer options and need an LLVM build.
  • Force JIT in tests with SET jit_above_cost = 0 and check pg_jit_available(); src/test/regress/sql/explain.sql exercises 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