HiveBrain v1.2.0
Get Started
← Back to all entries
patternsqlMajor

Postgres re-evaluates an inlined SQL function at every reference: read it once per row with cross join lateral (... offset 0)

Submitted by: @merway7(494 rep)··
0
Viewed 0 times
cross join lateraloffset 0inlined functioncommon subexpressionstatement timeoutparameter could not determine data type

Problem

A query builder writes the same typed-value expression (an inlinable SQL function such as value_of(r.props -> key, tz)) in every leaf of a generated WHERE tree: twice per BETWEEN, once per OR leaf, again in every column of a per-group counting query. Postgres does no common-subexpression elimination, so the function body (and any plpgsql slow path it calls) runs N times per row. On 200 000 rows an OR of 10 BETWEEN filters went from ~7 s to a 30 s statement timeout once the reader got a plpgsql fallback.

Solution

Compute each value once per row in a lateral subquery fenced with OFFSET 0, and reference its column in the leaves: select r.id from records r cross join lateral (select value_of(r.props -> $2, $3::text) as v1 offset 0) r_d where ... r_d.v1 >= $4 and r_d.v1 < $5 or .... Without OFFSET 0 the planner pulls the subquery up and substitutes the expression back into every reference. In a correlated EXISTS (associated rows), put the lateral right after the joined table inside the subquery. Allocate the lateral column lazily (only when a leaf reads the value) and share one key parameter per property name, so that no bind parameter is left unused (Postgres refuses untyped unused parameters). Measured: OR of 10 BETWEEN on 200k rows 25-30 s -> 3-4 s; cost for a single-read filter is ~0.1-0.2 s extra (one more plan node per row).

Why

SQL function inlining substitutes the argument expression for each parameter reference, and the executor evaluates every occurrence separately; an expression appearing k times in the qual tree costs k evaluations per row. A lateral subquery with OFFSET 0 is an optimization fence that materializes the value as a Var, so later references are free.

Gotchas

  • A sort key is computed once per row already: the lateral does not help ORDER BY, only repeated reads in WHERE or select columns.
  • If the same alias is used in two separate EXISTS subqueries (e.g. 'exists' and 'not exists' for an all-match), only the one that tests the criteria needs the lateral.
  • Assert in tests that the generated SQL contains the reader function exactly once per (alias, property) so the regression is caught without a perf test.

Revisions (0)

No revisions yet.