Article

Isolating PostgreSQL JIT Failures with Two GUCs

A PostgreSQL diagnostic technique uses jit_expressions and jit_tuple_deforming to narrow suspected JIT crashes or wrong results to expression compilation or tuple deforming. This article explains their asymmetric relationship and a controlled reproduction procedure.

Share

Koharu's reading tip

Go one step beyond disabling JIT entirely: use the two switches to narrow the failing path, while keeping diagnostic controls separate from performance tuning.

Koharu's reading tip

When a PostgreSQL query crashes under particular conditions or returns an unexpected result, having the symptom disappear with jit = off implicates JIT. The next question is how much further the failing path can be narrowed.

A technical article published on August 15, 2026 presents a sequence using jit_expressions and jit_tuple_deforming to separate two internal JIT paths. Both settings are Boolean, but they are not independent, symmetric switches.

Understanding that asymmetry turns “the problem goes away when JIT is disabled” into a more useful reproduction and bug report. The path from the two mechanisms to a three-stage test also reveals important cautions around prepared plans and production systems.

PostgreSQL JIT accelerates expression evaluation and tuple deforming

PostgreSQL JIT converts general execution machinery into native code at runtime. The PostgreSQL 18 documentation identifies two accelerated operations: expression evaluation and tuple deforming.

Expression evaluation covers work such as WHERE clauses, target lists, aggregates, and projections. Generating code for the expressions in a particular query reduces the overhead of general-purpose evaluation.

Tuple deforming converts an on-disk row into the in-memory values used by the executor. A generated function can specialize this work for a table layout and the number of columns being extracted instead of repeatedly handling null bitmaps, variable-width values, and alignment through a generic path.

JIT was introduced in PostgreSQL 11 on October 18, 2018, when it remained disabled by default even in supported builds. PostgreSQL 12 enabled it by default on October 3, 2019 for servers built with JIT support. In the current PostgreSQL 18 documentation, both jit_expressions and jit_tuple_deforming default to on.

Disabling jit_expressions also prevents tuple deforming JIT

The setting names suggest that expression evaluation and tuple deforming can be enabled independently. In practice, generated tuple-deforming code belongs to the process of compiling the expressions that consume those tuples.

Setting jit_tuple_deforming = off therefore removes deforming while leaving expression compilation available. Setting jit_expressions = off, however, prevents the expression compilation request from reaching the JIT provider, so tuple deforming cannot run on its own even when its setting remains on.

The current PostgreSQL jit_compile_expr() implementation returns before calling the provider when PGJIT_EXPR is absent. The planner builds the expression and deform flags from the two settings, so Expressions false, Deforming true can exist as configuration values without producing a deform-only JIT execution state.

That dependency determines the test order. Do not begin by disabling jit_expressions; first remove JIT as a whole, then keep expression compilation while removing tuple deforming alone.

A three-stage reproduction narrows the failing JIT path

Start by recording the state of the target session. Keeping the SHOW output with the PostgreSQL version, query, parameters, and exact error or wrong result makes the conditions reconstructible.

SQL
SHOW server_version;
SHOW jit;
SHOW jit_expressions;
SHOW jit_tuple_deforming;

Next, compare three states with the same query and data. Session-level SET commands are sufficient, so no server restart is required.

SQL
-- 1. Baseline: enable JIT and both paths
SET jit = on;
SET jit_expressions = on;
SET jit_tuple_deforming = on;
-- Run the reproducing query

-- 2. Disable JIT as a whole
SET jit = off;
-- Run the same query

-- 3. Keep expression compilation but disable deforming
SET jit = on;
SET jit_expressions = on;
SET jit_tuple_deforming = off;
-- Run the same query

-- Restore the session settings after the investigation
RESET jit;
RESET jit_expressions;
RESET jit_tuple_deforming;

Interpret the outcomes as follows.

  • The symptom persists with jit = off: JIT alone does not explain the reproduction, so other execution paths remain in scope.
  • The symptom disappears with both jit = off and jit_tuple_deforming = off: the scope can be narrowed to the JIT tuple-deforming path.
  • The symptom disappears with jit = off but persists with jit_tuple_deforming = off: focus on the remaining JIT path, principally expression compilation rather than deforming.

The JIT block in EXPLAIN shows whether JIT ran. The official example includes Expressions and Deforming on its Options line and code-generation time on its Timing line. Function counts and timings depend on the query, data, version, and environment, so fixed numbers are not a portable correctness test.

EXPLAIN ANALYZE and production systems require controlled side effects

The two settings live under “Developer Options,” and that documentation says the parameters in the section are intended for developer testing rather than production databases. Reproduce first in a test environment, a read-only replica, or another session where the impact can be contained.

EXPLAIN (ANALYZE, SETTINGS) can preserve execution statistics together with non-default planning settings. However, the ANALYZE option actually executes the statement, so it is not a safe way to casually rerun a crashing query or a write statement in production. Plain EXPLAIN avoids execution but does not provide the runtime JIT block, which is the tradeoff.

For wrong-result investigations, keep more than the JIT settings fixed. A different data snapshot, session configuration, SQL parameter set, extension set, PostgreSQL version, or LLVM version can turn a visually similar query into a different comparison.

Prepared statements need a new plan after each setting change

PostgreSQL decides whether to use JIT and how much optimization to apply when planning. The JIT decision documentation notes that when a prepared statement uses a generic plan, the JIT-related values in effect when the plan is prepared control the decision rather than values changed only at execution time.

In an application using connection pools or server-side PREPARE, changing SET values and reusing an existing plan may therefore fail to produce the intended comparison. Use a new connection after each setting change, or DEALLOCATE and prepare the target statement again.

SQL
DEALLOCATE reproduce_query;

SET jit = on;
SET jit_expressions = on;
SET jit_tuple_deforming = off;

PREPARE reproduce_query AS
SELECT /* minimal reproducer */ 1;

If a client library prepares statements automatically, include its preparation threshold and plan-cache behavior in the investigation record. The unit of comparison is not only when a setting changed, but also which connection created which plan.

Performance tuning belongs at jit_above_cost or the JIT boundary

These two GUCs are diagnostic switches for locating a failing JIT path. Leaving expression compilation or tuple deforming partially disabled as a routine way to trim compilation time produces a system that permanently omits part of the intended JIT behavior.

For a performance problem, first ask why the query became eligible for JIT. PostgreSQL compares estimated total cost with jit_above_cost, then uses jit_inline_above_cost and jit_optimize_above_cost for inlining and expensive optimization. The current configuration documentation lists defaults of 100000, 500000, and 500000, respectively.

Code-generation overhead can outweigh savings for short queries, while long-running CPU-bound analytical queries are the main intended beneficiaries. Performance work should therefore measure the real workload and evaluate the cost thresholds or jit = off when appropriate. That decision is separate from the two switches used for fault isolation.

The opening question now has a concrete answer: use jit = off to test JIT as a whole, then use jit_tuple_deforming = off to remove only deforming. Following the asymmetric dependency moves the diagnosis from “JIT or not” to a specific internal path and produces a materially better reproducer.

Source

  • Title: Christophe Pettus: All Your GUCs in a Row: jit_expressions and jit_tuple_deforming
  • URL: https://postgr.es/p/9sn

Share

Related Articles

These articles share nearby categories or tags, so you can keep reading along the same thread.