Two facts that are harmless today and become a silent wrong answer together, the
moment anyone makes decode honour the skip vector — which is the obvious next
optimisation and is Phase 1b of #452.
Reported by jdatcmd from reading; verified here before filing.
1. The batch fold fetches the skip vector and never reads it
columnar_vector.c:3161 declares const bool *skipVec, :3167 fills it from
PgColumnarReadFoldGroupInfo, and skipVec[ appears nowhere in the file.
It is correct today for a reason that has nothing to do with the skip vector: the
fold evaluates the scan keys itself, per value, at :3234
(pgcolumnar_batch_key_pass over every key for every row). So it reads vectors
the zone maps already ruled out and re-checks each value by hand. Wasteful, and
right.
2. Decode never skips anything, and "Vectors Skipped" does not mean it does
pgcolumnar_native_decode_chunk (columnar_reader.c:768) takes no skip mask:
static char *
pgcolumnar_native_decode_chunk(MemoryContext cx, Form_pg_attribute att,
char *values, uint32 valuesLen,
const char *desc, uint32 descLen, int blockCodec,
uint32 **outVecRawLen, int *outVecCount)
A chunk is decoded whole at group load. vectorsSkipped — the counter behind
EXPLAIN's Columnar Vectors Skipped — is incremented at columnar_reader.c:1691,
in the branch that advances the value cursors past a vector. So the line means
"not turned into Datums", never "not decoded". #452's text says only that
decompression is not skipped, which understates it: nothing about decode is
skipped, and the EXPLAIN line invites the opposite reading.
Why they are one issue
Skipping a vector's decode leaves a hole in the raw buffer. The row producer
survives that — it steps its cursors past skipped vectors and never dereferences
the gap. The fold does not: it walks every value in the group and re-checks it
against the scan keys, so it would read uninitialised memory and answer from it.
An aggregate returning a wrong number, with no error and no crash.
And it is the common case, not an edge. 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.
What I would do
Not "make the fold read skipVec" as a follow-up to 1b. The ordering is the point:
the fold must honour the skip vector before decode is allowed to skip
anything, and whoever writes the decode change is the person least likely to
look in columnar_vector.c.
Cheapest safe interim, if 1b is not imminent: have the fold assert that no vector
in the group is skipped, so the day someone changes decode they get an assertion
rather than an arithmetic error. That is a few lines and it converts a silent
wrong answer into a loud one.
Separately, Columnar Vectors Skipped should either be renamed to say what it
counts, or gain a sibling once decode can skip, so the two are not conflated in a
plan someone is using to decide whether decode is the bottleneck.
Not measured
This is a reading of the code and a hazard analysis, not a reproduction — the
decode change does not exist, so there is nothing to reproduce yet. Filing it now
rather than after, because the trigger is a change someone is about to make and
the failure it produces is silent.
Refs #452
Two facts that are harmless today and become a silent wrong answer together, the
moment anyone makes decode honour the skip vector — which is the obvious next
optimisation and is Phase 1b of #452.
Reported by jdatcmd from reading; verified here before filing.
1. The batch fold fetches the skip vector and never reads it
columnar_vector.c:3161declaresconst bool *skipVec,:3167fills it fromPgColumnarReadFoldGroupInfo, andskipVec[appears nowhere in the file.It is correct today for a reason that has nothing to do with the skip vector: the
fold evaluates the scan keys itself, per value, at
:3234(
pgcolumnar_batch_key_passover every key for every row). So it reads vectorsthe zone maps already ruled out and re-checks each value by hand. Wasteful, and
right.
2. Decode never skips anything, and "Vectors Skipped" does not mean it does
pgcolumnar_native_decode_chunk(columnar_reader.c:768) takes no skip mask:A chunk is decoded whole at group load.
vectorsSkipped— the counter behindEXPLAIN's
Columnar Vectors Skipped— is incremented atcolumnar_reader.c:1691,in the branch that advances the value cursors past a vector. So the line means
"not turned into Datums", never "not decoded". #452's text says only that
decompression is not skipped, which understates it: nothing about decode is
skipped, and the EXPLAIN line invites the opposite reading.
Why they are one issue
Skipping a vector's decode leaves a hole in the raw buffer. The row producer
survives that — it steps its cursors past skipped vectors and never dereferences
the gap. The fold does not: it walks every value in the group and re-checks it
against the scan keys, so it would read uninitialised memory and answer from it.
An aggregate returning a wrong number, with no error and no crash.
And it is the common case, not an edge.
pgcolumnar_batch_shape_eligiblerequiresevery qual to be convertible to a scan key, so the fold runs precisely when
predicates exist — which is exactly when vectors get skipped.
What I would do
Not "make the fold read skipVec" as a follow-up to 1b. The ordering is the point:
the fold must honour the skip vector before decode is allowed to skip
anything, and whoever writes the decode change is the person least likely to
look in
columnar_vector.c.Cheapest safe interim, if 1b is not imminent: have the fold assert that no vector
in the group is skipped, so the day someone changes decode they get an assertion
rather than an arithmetic error. That is a few lines and it converts a silent
wrong answer into a loud one.
Separately,
Columnar Vectors Skippedshould either be renamed to say what itcounts, or gain a sibling once decode can skip, so the two are not conflated in a
plan someone is using to decide whether decode is the bottleneck.
Not measured
This is a reading of the code and a hazard analysis, not a reproduction — the
decode change does not exist, so there is nothing to reproduce yet. Filing it now
rather than after, because the trigger is a change someone is about to make and
the failure it produces is silent.
Refs #452