047. Average eligible relation projections directly

  • Status: Implemented
  • Date: 2026-10-14

Problem and mechanism

PostgreSQL previously materialized a relation and its projected values as JSON before evaluating avg(). When the relation and callback result have a known numeric SQL representation, the builder can instead compute a correlated AVG(numeric) directly from the related rows.

Int contributions enter the aggregate as exact numeric. Float contributions are converted through PostgreSQL's round-trip-safe Float text before entering numeric, and the final result is checked when converted back to Float. This requires PostgreSQL 16 or later and caller-configured extra_float_digits > 0; HFF does not set or inspect the session setting. Null and guarded no-value contributions are excluded from the aggregate. The configured relation presence and null state still determines whether the aggregate is observable.

The direct path supports an eligible bound relation whose projection has one statically known numeric family, including a supported callback over a bound field, a captured outer value, or a numeric expression. A no-argument items:avg also qualifies when the relation item is statically Int or Float. A dynamic mixed or Any projection, a guarded/computed relation receiver, or an ineligible binding retains the general relation materialization path. avg() does not observe relation order, although a nested expression can have its own order requirements.

Applicability

Relation and projection Status Reason
Eligible bound relation with Int, Float, or null-only projection Applied Correlated AVG(numeric) preserves the numeric carrier and skips null contributions.
Statically numeric callback over a relation field or supported expression Applied The callback result is evaluated in the correlated row scope before aggregation.
Callback capturing a numeric outer column Applied The capture remains correlated to the current outer row.
Callback result with dynamic mixed or Any numeric family Not applied The generic relation materialization path retains runtime subtype recovery.
Guarded or computed relation receiver Not applied The generic path retains receiver presence and no-value semantics.
Nested expression with an order-sensitive operation Partial The average itself needs no relation order; nested operations retain their own ordering requirements.

Reproduction

The baseline is commit af373be. The candidate is the implementation accompanying this record. The focused shape fixture was added with the candidate. Preserve a copy outside the checkout before switching revisions, then place the same source at hack/observe-relation-average/main.go and run it on both revisions. The candidate fixture is available at hack/observe-relation-average/main.go. SQL-shape output was generated with Go 1.27.1 on Darwin arm64. The schema has an items list whose struct item has a nullable Int price and nullable Float reading, plus a required Int threshold; the relation correlates order_items.order_id to orders.id and binds the two item fields to price and reading. The fixture builds these filters:

items:avg(x=>x.price)>10.0
items:avg(x=>x.reading)>10.0
items:avg(x=>x.price+threshold)>10.0
items:avg(x=>x.price!)>10.0
items:avg(x=>threshold)>10.0

Run the builder-shape fixture and focused tests with Go 1.27.1:

go run ./hack/observe-relation-average
go test ./backends/sql \
  -run '^Test(RelationAverageAvoidsJSONMaterialization|NumericRelationItemAverageUsesNativeColumn)$' \
  -count=1
HFF_POSTGRES_CONTAINER=<container> go test -tags=integration ./backends/sql \
  -run 'RelationAverage|RelationParity|RelationStateParity|UnorderedTransformParity' \
  -count=1

The paired SQL-shape logs are kept outside the repository at /private/tmp/hff-relation-avg-baseline.jsonl and /private/tmp/hff-relation-avg-candidate.jsonl. The empty-table plan fixture uses temporary orders(id bigint, threshold bigint) and order_items(order_id bigint, price bigint, reading double precision) tables. It runs plain EXPLAIN (FORMAT JSON, COSTS OFF) for the representative items:avg(x=>x.price)>10.0 query without ANALYZE, on PostgreSQL 18.6 running on Debian aarch64 with extra_float_digits=1.

Results

Filter Baseline WHERE bytes Direct WHERE bytes Args
items:avg(x=>x.price)>10.0 2,710 487 4 → 1
items:avg(x=>x.reading)>10.0 2,900 495 4 → 1
items:avg(x=>x.price+threshold)>10.0 3,027 865 4 → 1
items:avg(x=>x.price!)>10.0 2,832 695 4 → 1
items:avg(x=>threshold)>10.0 2,259 568 3 → 1

For the first filter, the empty-table plan contained 23 nodes in the baseline and 7 in the candidate. Both plans retained two sequential scans; the candidate removed a row-expansion function scan and reduced nested loop and result nodes. No timing, allocation, retained-memory, EXPLAIN ANALYZE, or benchmark result was collected.

Correctness and tradeoffs

PostgreSQL/local parity covers Int and Float relation items, empty and all-null relations, SQL-null and missing relation state, guarded no-value projections, null-only projections, division-by-zero failure, outer captures, and nested relation averages. TestPostgreSQLRelationAverageKeepsAncestorState also verifies flattened parent presence and null propagation. The direct path keeps the configured parent and relation presence/null state and preserves callback correlation. Ineligible receivers and dynamic numeric families retain the general lowering.

For Float values, PostgreSQL converts round-trip-safe Float text to numeric before aggregation. AVG(numeric) can differ from sequential Float addition in cancellation, intermediate range, and rounding; the checked final Float conversion returns null when it cannot produce a valid result. This follows the backend-native Float Profile rather than promising bitwise agreement with Local Float aggregation.

Decision and limits

Use the correlated aggregate for eligible statically numeric projections. Keep the generic relation-to-JSON path for dynamic or otherwise unrecoverable projections and receiver states. These SQL-size and plan-shape observations describe the tested profile only; they do not establish lower execution time or a particular plan for populated data.

Explore HFF
Search documentation

Search the language specification and guides.