Encoding: measure what selection costs, and let it decide - #123
Conversation
Step 1 of design/CASCADE_ENCODING_PLAN.md, which said not to build a sampling selector before knowing what selection costs. Measured three builds over a 6,000,000-row load on PostgreSQL 18.4, non-assert, the second and third patched in a throwaway copy rather than instrumenting the shipped code: try every candidate and apply the winner (20.8 s, 8.6 MB), try every candidate then discard it and store raw (21.2 s, 29.2 MB), and skip selection entirely (5.2 s, 29.2 MB). The split is the useful part. Trying candidates is about 16 s, roughly 77% of load time. Applying the winner is about zero: baseline minus discard is -0.4 s, within noise and slightly negative because the winning encoding is 3.4x smaller and there is less to write. So the cost is almost entirely in trials that are thrown away, which is exactly what a sampling selector removes. Decision recorded: build the sampling selector. Correctness is not at stake either way, since the chosen encoding is recorded per chunk and a worse choice costs size, not correctness. Caveats recorded with the numbers: one run per build, so the 2% baseline-discard gap is noise rather than a measurement of apply cost, while the 4x that drives the decision is far outside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ChronicallyJD
left a comment
There was a problem hiding this comment.
Docs-only, and the right way to reach this decision: the plan said not to build a
sampling selector before knowing what selection costs, and this measures it
before building anything. Approving.
The arithmetic checks out and one number is load-bearing in a way the text
overstates slightly. That does not change the decision, but it will change what
success looks like when step 1 lands, so it is worth fixing while the record is
being written.
Checked
- 21.2 - 5.2 = 16.0 s of candidate trials; 16.0 / 20.8 = 77%; 20.8 / 5.2 = 4.0x;
29.2 / 8.6 = 3.4x. All four match the text. - The -0.4 s is quantitatively consistent with the explanation given, which is
worth more than calling it noise. The baseline writes 8.6 MB where the discard
build writes 29.2 MB, so it moves about 20.6 MB less through the write path and
WAL. At any plausible throughput that is a few tenths of a second, which is the
size and sign of what was observed. The explanation is not just available, it is
the right magnitude. - Builds 2 and 3 storing byte-identical 29.2 MB is a real control, not a
coincidence worth passing over: it confirms the discard patch changed only
whether the result was kept, not what was stored. Without that agreement the
16.0 s subtraction would not mean much. - The caveats are the ones I would have asked for: single run, data-shape
dependence, and the honest note that the 2% gap is not a measurement of apply
cost.
The one number to qualify: 16.0 s is not what sampling recovers
"Applying the winner: about zero" is true, and the reason it is true is that the
winner's encoded output is a by-product of its own trial. The selector encodes
with each candidate and keeps the best buffer, so "apply" is a pointer
assignment. That makes the 16.0 s inclusive of the winner's full-data encode.
A sampling selector does not remove that part. It removes the losing candidates'
full-data work, and still has to encode all rows with whichever candidate it
picks -- which is exactly what the design sentence in this same section already
says: "encodes only the one or two most promising candidates in full". So the
recoverable ceiling is 16.0 s minus one full encode (or two, if two finalists are
encoded), not 16.0 s.
The sentence that overstates it is "essentially all of the encoding cost is
candidate trials rather than the encoding that is kept". The encoding that is
kept is one of those trials; it just does not cost anything extra on top of
them. Suggest something like "essentially all of the encoding cost is in running
candidates over the full data, and all but the winner's is discarded" -- same
conclusion, and it stays consistent with the design sentence two lines later.
Measuring it is one more throwaway build: pin the known winner per column and
encode only that, then subtract the 5.2 s no-selection baseline. That gives the
winner's full encode directly, so the record can state the ceiling rather than
implying 4x. My guess is it lands around 16.0/k for k candidates tried per
column, which would put the realistic speedup meaningfully below 4x but still
far outside anything that would change the decision.
Failing that, recording k -- how many candidates each column type actually
tries -- costs nothing now and makes the ceiling estimable later. It is also
the number someone re-running this in six months will need and will not be able
to recover from the table.
Minor
"A worse choice costs size, never correctness" is right about correctness, and
worth one qualifier: size is also scan cost, since a larger chunk is more bytes
to read and decompress on every query that touches it. Since step 2 is justified
on size, a selector that trades a few percent of ratio for load speed should be
judged against that, not treated as free. Nothing in the decision changes;
the sentence just reads as though the only thing at stake is bytes on disk.
Verdict
Approving. The method is sound, the control between builds 2 and 3 makes the
subtraction trustworthy, and the decision follows from the number regardless of
the qualification above. Worth taking the wording fix and, if the throwaway
tree still exists, the fourth build -- a decision record is most useful when the
number in it is the one the next person will measure against.
The review of #123 was right that "applying the winner is about zero" was misleading. The winner's encode IS one of the trials; it costs nothing extra on top of them, which is not the same thing, and stating it that way implied a sampling selector could recover the whole 4x gap against the no-selection build. Added the fourth build the review asked for: pin the winner each column actually gets and encode only that. Winners came from pgcolumnar.column_chunk after a baseline load (bigint delta-of-delta, both ints frame-of-reference, float dictionary, text FSST), and the pinned build produces a byte-identical 8,585,216, which is what confirms the pins are the real winners rather than a guess. That splits the cost properly: winner's own encode 6.6 s (pinned minus no-selection) trials thrown away 9.0 s (baseline minus pinned) So the ceiling for sampling is 9.0 s of a 20.8 s load, about 43%, a 1.76x speedup, not 4x. The decision does not change, but the number someone measures against later does. Also records k, the candidate count per column type, which is what sets that ceiling: 5 for packable ints, 4 for floats, 2 for other fixed-width, 2 for varlena. Takes the minor too: size is scan cost as well as disk, so a selector that trades ratio for load speed is judged on both. While reading the catalog for the winners, one thing worth recording for step 2: the encoding descriptor is already versioned and the reader rejects an unrecognized version cleanly, so a version 3 entry carrying a chain can coexist with version 2 and an older build meets a clear error rather than a wrong value. That makes the format change smaller than the plan assumed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
You were right, and the correction matters more than the wording: stating it as "applying the winner is about zero" implied a sampling selector could recover the whole 4x, which it cannot. Ran the fourth build you suggested. Winners came from
So the winner's own encode is 6.6 s and the discarded trials are 9.0 s. The ceiling for sampling is 43% of load time, a 1.76x speedup, not 4x. Your estimate of 16.0/k was the right shape: with k of 5 for the packable ints and 4 for the float, the winner's share landing at about 40% of the trial cost is what that predicts. Also recorded k per column type, since as you say it is not recoverable from the table later: 5 for packable ints (RLE, FOR, delta, delta-of-delta, dictionary), 4 for floats (RLE, Gorilla, ALP, dictionary), 2 for other fixed-width, 2 for varlena. Minor taken: size is scan cost too, not just disk, so a selector that trades ratio for load speed gets judged on both. The sentence no longer reads as though only bytes on disk are at stake. One thing that fell out of reading the catalog for the winners, relevant to step 2: the encoding descriptor is already versioned ( |
Step 1 of
design/CASCADE_ENCODING_PLAN.md(#119), which said not to build a sampling selector before knowing what selection costs. Docs only: the deliverable is the number and the decision it forces.Method
6,000,000 rows, PostgreSQL 18.4 non-assert, five columns chosen to exercise different encoders (sequential bigint for delta, low-cardinality int for RLE and dictionary, scattered int where nothing helps, float for Gorilla and ALP, repetitive text for FSST). Three builds, the second and third patched in a throwaway copy rather than instrumenting the shipped code.
Result
So essentially all of the encoding cost is trials that get thrown away, which is exactly the part a sampling selector removes. Decision: build it. Correctness is not at stake either way, since the chosen encoding is recorded per chunk and a worse choice costs size, not correctness.
Caveats kept with the numbers
One run per build, so the 2% baseline-versus-discard gap is noise rather than a measurement of apply cost; the 4x that drives the decision is far outside any plausible noise. Data shape drives this heavily, and a table of incompressible columns would spend less time in candidates that bail early.
Cascading (step 2) is unaffected: it remains the size lever, and it remains a format change needing the version decision recorded in the plan.
🤖 Generated with Claude Code