patternsqlMajor
Postgres re-evaluates an inlined SQL function at every reference: read it once per row with cross join lateral (... offset 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.