T07 complete SQL aggregation over native batches

Historical ticket-stage record. Source revisions, measurements and limitations below retain their original context. See the PR 2 verification record for the current isolated aggregation branch and refreshed measurements.

Implementation base: e193815a (T05 plus measure QL documentation), on PR #1412. Verification uses Go 1.26.9. No protobuf, frame format, native operator, or stored format changes. Final production source and generated-contract content hashes are recorded in .omx/t07-source-manifest.sha256.

Execution contract

Extended measure queries lower to one complete native raw scan, optional exact time residual, local HashAggregate, final projection and pagination, then the existing response serializer. There is one logical scan with one storage leaf per data node. Native version/replica deduplication precedes local aggregation. The native request carries no aggregation/grouping, zero offset and an unbounded limit. The native source closes before synthesized groups are serialized.

Source shorthand, aliases, WHERE time, or multiple aggregate calls select this execution path. Existing single-aggregate native syntax remains unchanged, including native MEAN/AVG and COUNT(DISTINCT) routing/stage restrictions. Extended COUNT(DISTINCT), TOP, TIME_BUCKET and ORDER BY are rejected explicitly. Scalar tag/field grouping is supported, including hidden keys; expressions, array grouping and group-only queries remain later tickets.

COUNT returns exact int64 and ignores nulls. Numeric SUM/MIN/MAX preserve int64 or float64; integer SUM overflow fails. AVG/MEAN use a float64 sum and exact non-null count. Matching calls share lanes. Integer AVG keeps a floating sum separate from an explicitly selected exact integer SUM, so AVG alone does not inherit int64-sum overflow. Float SUM/COUNT/AVG together use the same two lanes as AVG alone. Scalar empty input returns COUNT zero and other calls null; grouped empty input returns no groups. Pagination applies to finalized groups. LIMIT 0 skips storage; contradictory time bounds still synthesize the scalar empty-input row when pagination allows it.

Grouping encodes type, explicit nullness, and variable-width lengths. Null, empty string and zero differ; signed zeros share a key, and all NaNs share one canonical key. Open-addressed uint32 group-ID slots retain full encoded keys to resolve collisions. Dense state arrays scale with actual groups and geometric capacity rather than input rows. The index and key headers reserve 24 bytes per capacity slot; key payload reservations conservatively cover allocation classes. Key bytes/capacity and used/allocated state bytes are reported separately.

Every growing array reserves the shared query allowance before allocation, including overlap during growth. Native cells are borrowed only while consuming a batch; retained keys own their bytes. Cancellation and construction, source, growth, output or encoding errors close owners and return live reservations. The implementation adds no new memory pool.

Correctness evidence

The initial planner RED is .omx/t07-planner-red.log: an aliased multi-aggregate query was rejected by typed raw execution. Operator tests independently check fixture F, native passthrough and constant columns, null keys, signed zero/NaN, selection, poisoned owners, exact COUNT above 2^53, checked sums, cancellation, allocation failures and batches crossing 65,536 rows.

The generator preserves all 293 existing cases and 18 T05 reuse scenarios. T07 adds 41 aggregate scenarios with three changed bindings each, executed both cached and bypassed, plus seven invalid shapes. Expected values come from an independent scalar fixture model. No native Query RPC supplies an oracle. Readiness verifies every final version through bounded raw QL windows. The 513-group fixture has 1,026 logical rows, two entity hosts, distinct non-entity group keys and stale/final writes; this isolates SQL groups from per-series native allocation costs. Its unordered output is validated with unique keys and sorted on both test sides before the existing typed/null/tolerance comparator. This comparison does not assert an ORDER BY contract.

Focused benchmark protocol

P(T07) has four distributed RPC variants only: scalar SUM, four aggregates over 1,024 nullable groups, one-group AVG, and SUM/COUNT/AVG over N integer groups. Final LIMIT 1 bounds N-group egress; instrumentation must still prove N input rows and N groups. Independent scalar checks validate values and nulls before timing and on retained samples. Setup and visibility are outside timing.

The original successful-RPC protocol targeted 1,024, 100,000 and 1,000,000 input rows at 100/5, 20/3 and 10/2 timed/warmup calls. The first run passed 1,024 rows, then failed before local aggregation at 100,000 rows. The native limit and the revised focused measurement protocol are recorded below. This ticket-specific schedule replaces the shared old 1,000-call boilerplate. Ten samples cannot establish a stable tail distribution; all samples and profiles are retained. There is no old-version latency baseline for previously unsupported SQL, and old ticket matrices are not repeated.

K(T07) compares the complete dense operator with an independent scalar map reference over 65,537 rows, 1/1,024/N groups, nullable input and four aggregate shapes, including AVG versus shared SUM/COUNT/AVG. It validates results before timing. Resource probes use 16/256-MiB tracker settings over 100,000 and 1,000,000 groups and permit only complete correct results or ResourceExhausted, followed by clean recovery and no owner/reservation leaks. Protector admission remains active and can be tighter than the configured vectorized tracker.

Final test runs

Both final resource-limited topology runs passed all 215 selected specs, including all 48 T07 specs and the 167 earlier ticket specs. Reports: .omx/t07-standalone-docker.json and .omx/t07-distributed-docker.json. The old 293 generated cases, 18 reuse scenarios, reuse readiness and pair report also compare unchanged to the implementation base as decoded JSON values.

Host standalone verification passed too. Host distributed verification passed 214/215, with only the complete 513-group response rejected by admission. This host reports an invalid/unbounded cgroup limit, so its fallback query allowance is only 8 MiB (64-MiB pool divided by eight). The same full test succeeds in the 8-GiB container with normal cgroup-based admission. No production allowance was changed, and the required input/group cardinalities were retained. Earlier readiness failures led to bounded raw visibility windows; a slow quadratic unordered protobuf comparison led to unique-key sorting in the test adapter.

Focused packages, generator, querybench and the full gRPC suite passed race checks with querymetrics. make lint passed after fixing new lint findings. The final readiness-helper functional run passed in 6.018 seconds, its focused querybench race run passed in 146.562 seconds, and tagged lint reported zero issues. Relevant logs are .omx/t07-aggregate-race.log, .omx/t07-planner-race.log, .omx/t07-grpc-race.log, .omx/t07-generator-race.log and .omx/t07-make-lint-complete.log. Deterministic generator checks and whitespace checks passed. This ticket does not alter the nine known pre-existing generated failures.

A final topology command, run in the Go benchmark image with the workspace and module/build caches mounted, is:

go test -tags=querymetrics ./test/integration/distributed/query \
  -count=1 -timeout=30m -run '^TestPropertyQuery$' \
  -ginkgo.label-filter='measure-ql && (QLB-T01 || QLB-T02 || QLB-T03 || QLB-T05 || QLB-T07)' \
  -ginkgo.fail-on-empty

Use ./test/integration/standalone/query and ^TestPropertyIntegrationQuery$ for the other topology. Containers use --cpus 4 --memory 8g --memory-swap 8g --pids-limit 4096.

Kernel measurements

The limited-container kernel run uses the command below. All 28 variants have five nonempty samples and pass independent value/null checks. The table reports medians over five samples; the first 12 shapes use 65,537 rows and the last two use 1,000,000 rows.

go test -tags=querymetrics ./pkg/query/ql -run '^$' \
  -bench '^BenchmarkAggregate(Kernel|MillionGroups)$' \
  -benchmem -benchtime=1s -count=5 -timeout=20m
Groups Shape Dense ms Scalar ms Dense allocated B/op Scalar allocated B/op
1 sum 6.807 1.082 2,312 352
1 multiple 6.889 1.098 3,248 400
1 avg 6.868 1.082 2,312 352
1 sum-count-avg 6.825 1.085 2,872 384
1,024 sum 7.731 2.086 121,368 289,016
1,024 multiple 8.037 2.204 179,592 338,168
1,024 avg 7.764 2.093 121,368 289,016
1,024 sum-count-avg 7.855 2.105 138,296 321,784
65,537 sum 27.658 27.258 11,557,683 18,883,872
65,537 multiple 38.652 27.753 15,777,938 22,029,649
65,537 avg 28.773 28.256 11,557,675 18,883,887
65,537 sum-count-avg 32.831 28.015 11,574,840 20,981,065
1,000,000 avg 538.491 582.169 99,909,512 301,065,352
1,000,000 sum-count-avg 583.593 636.658 99,926,680 333,065,352

The scalar reference is a specialized Go map over int keys. The dense path runs the public operator lifecycle, general typed key codec, memory admission and columnar output. It is slower at low group counts; these results do not claim a CPU speedup. At high cardinality its allocation volume is lower. AVG and shared SUM/COUNT/AVG both use 16 state bytes per group; four aggregates use 32 bytes. At 65,537 groups, geometric capacity is 131,072, so two-lane allocated state is 2,097,152 bytes versus 1,048,592 used bytes. Key capacity is separately 4,194,384 bytes. This distinguishes retained state from total allocated bytes. The final harness collects validation-only garbage before starting the dense timer, excluding reclamation of independent oracle maps. The initial 24-variant run is retained as .omx/t07-kernel-docker.log; final artifacts are .omx/t07-kernel-final.log and .omx/t07-kernel-summary-final.json.

At one million groups both AVG shapes use 16,000,000 state bytes and 16,777,216 capacity bytes, with 41,165,888 bytes of key capacity. Their local operator-owned state fits a 128-MiB tracker, excluding caller-owned fixture inputs and the untimed scalar oracle. All million expected keys, values and nulls are checked exactly once before timing; post-Close reservations are zero.

Successful distributed RPC measurements

All five repetitions passed their eight variants in fresh 4-CPU, 8-GiB containers. Each cardinality starts a fresh cluster process. The 1,024-row variants use five warmups and 100 timed requests; the 32,768-row variants use three warmups and 20 timed requests. There is one RPC worker. Warmups and full scalar comparisons are untimed. CPU profiling is active during measured requests; the heap profile and its collection run afterward.

The table reports the median of each repetition’s metric. MB uses decimal bytes. Allocation and CPU measurements cover the whole benchmark process, including RPC, native execution and background work; they are not local aggregate-only costs. These small samples describe the observed distributions and do not establish a stable tail-latency guarantee.

Input rows Shape p50 ms p95 ms Allocated MB/query Process CPU ms/query
1,024 AVG, one group 3.816 5.359 1.430 9.7
1,024 COUNT/SUM/MIN/MAX, 1,024 groups 10.761 16.976 4.769 21.5
1,024 SUM/COUNT/AVG, N groups 4.135 6.034 1.595 9.8
1,024 Scalar SUM 3.498 5.274 1.325 8.6
32,768 AVG, one group 46.993 52.975 43.551 92.5
32,768 COUNT/SUM/MIN/MAX, 1,024 groups 58.940 69.388 54.913 122.0
32,768 SUM/COUNT/AVG, N groups 52.572 59.309 48.699 101.5
32,768 Scalar SUM 40.620 44.101 38.014 81.5

Every timed interval records exact post-deduplication input/group counts, one local aggregate execution per request, two native leaf calls per request (one per data node), no intermediate DataPoints, and zero post-Close owner, pool, result or reservation deltas. SUM/COUNT/AVG retains the same two state lanes as AVG. Plan compilation is zero during timing and every request hits the compiled cache. Full samples, CPU/heap profiles and metrics are in .omx/bench-reports/t07-bounded-r1 through t07-bounded-r5; the validated cross-run summary is .omx/t07-rpc-summary.json.

After these measurements, only untimed readiness/recovery helpers changed to prune hosts and project one field explicitly. Production execution, measured queries, fixtures and timing protocol stayed the same; the P5 binaries and profiles are retained. The resource probes below use the rebuilt helper.

The report explicitly marks physical storage rows, native retained frame bytes and total parent-lease charges unavailable. Its process-wide peak reservation must not be interpreted as native residency alone. Prior-ticket latency matrices were not rerun; compatibility evidence here is the earlier 167 E2E specs.

Inherited native execution bound

The original large-RPC performance gate is not met. The native execution boundary eagerly retains replica frames and reserves a conservative estimate before constructing the QL operator. In an 8-GiB container, the memory protector allows 75% (6 GiB), divides its query pool by four and its per-query allowance by eight, leaving 192 MiB for a query. This is independent of a larger vectorized tracker flag.

A reproducible one-query diagnostic first verified all 100,000 final fixture rows through bounded raw QL windows, then executed the exact four-aggregate RPC. It failed in approximately 100 ms with zero local aggregate executions, input rows or state reservations. The first native frame requested 157,505,864 bytes while the lease had already charged 51,609,596 of its 201,326,592-byte allowance. The second replica frame would require another similar reservation. The failure is at reserveNativeFrames in pkg/query/vectorized/measure/plan/distributed.go; its estimate charges 128 bytes per column cell plus 128 bytes per row, 13 additional encoded-payload copies and shared workspace. Live reservations return to zero after the rejection. The first original run is retained at .omx/bench-reports/t07-r1 and .omx/t07-rpc-r1.log; exact diagnostics are .omx/t07-charge-diag.log and .omx/t07-charge-diag-2.log.

Changing the QL aggregate cannot remove this pre-execution reservation. Native operators, frame accounting and protector policy are outside this QL-only ticket. The successful RPC measurements therefore use 1,024 and 32,768 input rows with the same four shapes and five fresh 4-CPU/8-GiB repetitions. Larger real RPCs at 100,000 and 1,000,000 rows remain explicit resource outcomes, not successful SQL throughput results. A separate local million-group kernel run checks actual large aggregate execution without replacing the missing successful native RPC experiment. The original large-RPC requirement remains visible rather than being marked passed by smaller queries or a relaxed memory policy.

Resource outcomes and remaining gates

Both 256-MiB distributed resource probes verified their complete fixtures before issuing five N-group SUM/COUNT/AVG requests. Every request returned ResourceExhausted before local aggregation, without a partial response. All live owner, pool, result and reservation checks returned to baseline, and a bounded raw QL recovery query passed afterward. These are successful rejection/recovery checks, not successful large-query performance measurements.

Input rows/groups Tracker setting Aggregate outcomes Process peak reservations Recovery
100,000 256 MiB 5 ResourceExhausted; 0 aggregate executions 558,215,922–577,992,738 B Passed
1,000,000 256 MiB 5 ResourceExhausted; 0 aggregate executions 523,869,908–591,298,391 B Passed

The process peak sums three independent node trackers and stays below their combined 768-MiB limit. At 100,000 rows, the native data-node raw-frame emitter rejects 258,911,011 used bytes plus 26,359,256 requested bytes against a 268,435,456-byte tracker. At one million rows, native pipeline advancement rejects 264,746,587 plus 21,880,784 bytes against the same tracker. These data-node failures differ from the earlier liaison protector failure with a 1-GiB tracker setting; neither reaches local aggregate state allocation.

Artifacts are .omx/bench-reports/t07-res-final-100000-256 and .omx/bench-reports/t07-res-final-1000000-256, with logs .omx/t07-res-final-100000-256.log and .omx/t07-res-final-1000000-256.log. The validated summary is .omx/t07-resource-summary.json.

The 16-MiB real-RPC resource gate remains blocked during native readiness. The final setup prunes one of sixteen hosts, explicitly projects RID plus one INT field, and checks 1,024 rows per time window against an independent oracle. Even its first window needs 13,829,731 used bytes plus 3,299,800 requested bytes against a 16,777,216-byte native tracker. It fails before any owning aggregate probe or recovery query, so there is no passing resource verdict or result artifact for that configuration. Evidence is .omx/t07-res-fixed2-100000-16.log; prior readiness attempts are retained too. The million-row/16-MiB configuration was not repeated after this setup blocker. Functional tests verify host/window coverage, request binding and fail-fast handling independently; they do not replace the blocked real-RPC check.

The local operator’s separate 262,145-group tests exercise 16-MiB rejection, 256-MiB success and complete release. The million-group kernel succeeds within 128 MiB of operator-owned live reservations. Those tests establish local allocation behavior without implying that inherited native execution fits the same allowance. Thus correctness is delivered, while the original successful large-RPC gate and the 16-MiB real-RPC gate remain explicitly outstanding.

The subsequent scope decision defers disk spilling to a later ticket, including its generated E2E tests and focused benchmarks. T07 retains existing memory protection and ResourceExhausted behavior. The measurements and limitations above remain unchanged; no spilling implementation is pending within T07.