fix: the batch fold must refuse a group whose decode skipped vectors (#512) - #523
Conversation
The batch fold does not honour the skip vector. It reads every vector and reaches the right answer by re-checking every value against the scan keys, which is correct only while decode produces every vector -- and today it does, because pgcolumnar_native_decode_chunk takes no skip mask at all. `skipVec` is fetched into a local that is never indexed. The day decode is taught to skip ruled-out vectors (#452 phase 1b, the obvious next optimisation) the decoded buffer gains holes, and that loop would re-check UNINITIALISED memory: a wrong aggregate, silently, and only on data whose zone maps rule something out. The row producer is safe there because it steps its cursors past skipped vectors; the fold is not. And pgcolumnar_batch_shape_eligible requires every qual to be convertible to a scan key, so the fold runs precisely when predicates exist -- which is exactly when vectors get skipped. Common case, not an edge. So the ordering constraint is enforced rather than written in a comment. The loader reports whether it skipped any vector's decode (false always, today) and the fold refuses a group it cannot read safely. Whoever teaches decode to skip gets an error instead of arithmetic, and they are the person least likely to look in columnar_vector.c. Not the "assert no vector is skipped" form first proposed: the fold routinely sees skip vectors with skips set, so that assertion would trip on ordinary aggregate queries. This asserts the condition that makes the hazard REAL, not the one that makes it possible. Measured, so the ordering is a finding and not a preference: teaching the fold to honour skipVec costs about 2% (76.8 ms -> 78.4 ms, 1952 vectors skipped, one group read, answers identical) and saves nothing until decode changes, because the gather must still run to keep each column's present-index aligned. So it belongs IN that change, not before it. test/native_fold_skipguard.sh proves the guard is REACHABLE, which is the part that is easy to get wrong. Two premises: the reader must skip vectors for the predicate, and the query must reach the fold. Either alone would let the suite pass forever while guarding nothing. An earlier attempt at this guard could not be proven and was correctly not shipped; the cause was a wrong GUC name (enable_vectorization rather than enable_ungrouped_vector_agg), so the fixture ran the scalar path and the guard was unreachable. Proven by simulating the hazard exactly as a decode-skipping change would -- the loader reporting skipped wherever a skip vector exists -- with the .so fingerprinted across both arms: ERROR: pgcolumnar: the vectorized aggregate cannot fold a row group whose decode skipped vectors (#512) Refs #512, #452 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FeNm2Gw6h16Z123We3F1vJ
|
Reviewed. The analysis, the scoping and the placement argument are all right, and I could not find a hole in any of them. One measured gap in the test, non-blocking, and it is about what the suite protects rather than what the guard does. What I verified rather than took
The gap: the suite passes with the guard disabledThe suite never makes
So What I am not asking forI am not asking you to add a test seam to production code on my say-so. I declined exactly that on Options as I see them, and this is your call:
I lean 1 or 3 — the cost of 2 is a permanent knob for a transient gap — but the header should say which, because "this suite proves the guard is REACHABLE" reads as a stronger claim than the suite makes. One interlockThe premise reads |
|
Re-checked against current main, since this branched six commits back (#521, #522, #524 landed underneath it) and my own #527 is open against
Nothing here changes my earlier read: the analysis and scoping hold, and the only open point is that the suite still passes with the guard neutered ( |
The batch fold does not honour the skip vector. It reads every vector and reaches the right answer by re-checking every value against the scan keys — correct only while decode produces every vector, which today it does, because
pgcolumnar_native_decode_chunktakes no skip mask at all.skipVecis fetched into a local that is never indexed.The hazard, now stated as a number rather than a code reading
@ChronicallyJD measured it on
native_vecskip's fixture — 8192 rows, one row group, eight 1024-row vectors, monotonic id so each zone map is tight. Same table, same predicate, only the node differing:Columnar Vectors SkippedBatch Fold: yes)The scalar node steps past seven of eight vectors. The fold traverses all eight. That is why holes in the decoded buffer would be read: the fold genuinely walks vectors the zone maps ruled out.
The day decode is taught to skip them (#452 phase 1b, the obvious next optimisation), that loop would re-check uninitialised memory — a wrong aggregate, silently, and only on data whose zone maps rule something out. The row producer is safe there because it steps its cursors past skipped vectors. And
pgcolumnar_batch_shape_eligiblerequires every qual to be convertible to a scan key, so the fold runs precisely when predicates exist, which is exactly when vectors get skipped. Common case, not an edge.What that 7-against-0 does not say. It is a hazard argument, not a cost one. The work the fold does on those rows is a scan-key re-check, not materialisation. Measured on a 2M-row single-group fixture with 1,952 vectors skipped, teaching the fold to honour
skipVecruns 76.8 ms → 78.4 ms — slower — with identical answers, because the gather must still run to keep each column's present-index aligned. So the saving is only the key test and the aggregate apply, which is less than the per-row vector tracking costs. Both numbers belong here, or someone reads 7-against-0 as a performance defect and optimises a 2% regression into place.That measurement is also the argument for where the fix belongs: honouring
skipVeccosts today and pays only once decode skips, so it goes in that change, not before it.The guard
The loader reports whether it skipped any vector's decode —
falsealways, today — and the fold refuses a group it cannot read safely:Not the form first proposed. "Assert no vector is skipped" would trip on ordinary aggregate queries, since the fold runs precisely when predicates exist and predicates are what cause skipping. This asserts the condition that makes the hazard real, not the one that makes it possible.
Proof
test/native_fold_skipguard.shproves the guard is reachable, which is the part that is easy to get wrong, and carries the two premises that make it worth anything:Either alone lets the suite pass forever while guarding nothing.
Fired by simulating the hazard exactly as a decode-skipping change would — the loader reporting skipped wherever a skip vector exists — with the
.sofingerprinted across both arms. Guard present and hazard simulated: the error above. Guard present, no simulation: 4/4 green.An earlier attempt at this guard could not be proven and was correctly not shipped. The cause was a wrong GUC name —
enable_vectorizationrather thanenable_ungrouped_vector_agg— so the fixture ran the scalar path and the guard was unreachable. The guard was fine; the probe was not.Gate
native_fold_skipguard=PASSon all five and absent from every skip list.Refs #512, #452