Skip to content

docs: record #890's measurement in the roadmap, not the claim - #1080

Merged
jdatcmd merged 1 commit into
mainfrom
docs/890-record-the-measurement-in-the-roadmap
Sep 16, 2026
Merged

jdatcmd merged 1 commit into
mainfrom
docs/890-record-the-measurement-in-the-roadmap

Conversation

@jdatcmd

@jdatcmd jdatcmd commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

design/ROADMAP.md:192 read as an outstanding task sourced to Zeng et al., VLDB
2024. It was measured on this engine (#890) and did not reproduce. Left as
written it would have regenerated the same issue against a different corpus in a
year
, which is the whole reason to amend rather than just close.

jd ruled on this directly: change nothing about the default, amend the roadmap.

What both files now carry

shape            S = 1 - zstd/none     C       verdict under the rule
repetitive             0.750         0.92      NET WIN
text_heavy             0.958          --       NET WIN
realistic              0.127         0.92      UNDECIDED
random_int             0.000         1.00      UNDECIDED
incompressible        -0.015         1.01      UNDECIDED

No shape fires NET COST. And there is no storage tier to key a default off: a
native table's blocks always live in the data directory; object storage is an
import/export surface.

The narrow reading, not the headline

The shape the pre-registered rule names as the decider is realistic, and it
comes out UNDECIDED, not NET WIN. The outcome is identical because an
undecided measurement does not move a default — but "the premise is refuted" is
true only of the two most compressible shapes. Someone reopening this needs to
know which shapes carried the conclusion. @OffgridwithJD's point, and it is the
part I would most want preserved.

Two things that would have been lost with the entry

Dictionary encoding on float columns was a separate clause of the same bullet.
Nothing in #890 measures it. It is called out as still open rather than struck
through with the rest.

The alpha4 item cost nothing in written bytes, so it need not have been alpha
work at all.
It was scheduled as alpha because the plan assumed it would change
the writer, and that assumption was the part that needed checking first. Worth
recording because it is a scheduling lesson, not a compression one.

Also recorded

On incompressible, zstd is measurably cheaper to write than none
(W = 0.92) — the FSST keep test only runs when a codec is configured, and dropping
FSST saves more than zstd costs. A property of our cascade, not of zstd, and the
opposite of the naive expectation.

Scope

Docs only. design/ is outside docs_style.sh's scope; the one rule that does
police the CHANGELOG is the em/en dash ban, and this diff adds none (verified: 0).
No test reads either design file.

Three defects found by the investigation are tracked separately and do not
close with #890: #1074, #1075, #1076 — all three are next.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK

@OffgridwithJD

Copy link
Copy Markdown
Collaborator

The conclusion is right and the narrow reading is exactly what I would want preserved. But the table has a provenance problem, and for a document whose whole purpose is to be the durable record that is the thing to fix before it lands.

Not approving yet on that basis alone. Everything else here I would take as written.

The C column is missing a value that exists, and that is the tell

shape            S = 1 - zstd/none     C       verdict
repetitive             0.750         0.92      NET WIN
text_heavy             0.958          --       NET WIN      <- why is this blank?
realistic              0.127         0.92      UNDECIDED
random_int             0.000         1.00      UNDECIDED
incompressible        -0.015         1.01      UNDECIDED

A single five-shape run would have produced a C for all five shapes. The -- says the two columns did not come from the same measurement.

Reconciling against #890's own comments, the S column is the five-shape 200,000-row table throughout — every value matches. The C column is not:

shape           S matches      C in this table    C in the five-shape run
repetitive      5-shape ✓          0.92                1.001
text_heavy      5-shape ✓          --                  0.911
realistic       5-shape ✓          0.92                1.001
random_int      5-shape ✓          1.00                1.000   ✓
incompressible  5-shape ✓          1.01                1.010   ✓

Two of the five C values match the five-shape run. Two match the earlier three-shape 400,000-row run (rep 0.92, mix 0.92). One exists and is omitted.

And text_heavy's real C is 0.911 — the most favourable read cost in the whole set, the one where zstd is cheapest. Leaving it blank understates the case the table is making.

I may be misreading which comment is which run, and I would rather be corrected than have you take my reconstruction on trust. But the internal tell does not depend on that: a blank in a column that is otherwise complete means two sources.

The entry records no conditions

No row count, no build type, no box. #890 learned this the hard way — a table was published from an assert build without naming it, and a conditions header was added afterwards precisely so the numbers could be read correctly.

The ROADMAP inherits the numbers without the header. A reader in a year gets five ratios and no way to know what they were taken on, which is the state that makes someone re-run it and get different numbers with no way to tell why.

Three lines would carry it:

Measured 2026-09-16 on PG17, non-assert, 200,000 rows per shape, backend
instructions retired, page-cache warm. The CONCLUSION travels; the ratios are
this hardware and this corpus. Instructions cannot measure an I/O saving at any
working-set size, so `C` bounds the CPU trade and not the I/O benefit.

That last sentence matters more here than in the issue, because the ROADMAP is where someone will look when they want to reopen this.

What I would not change

"Do not re-open this without reading that issue first" in bold at the top is the right instruction, and putting it before the claim rather than after is what makes it work.

Keeping the float-dictionary clause explicitly open is the save. It was a separate clause of the same bullet, nothing measured it, and striking the bullet would have taken it silently — which is the failure mode the amendment exists to prevent.

Recording that the alpha4 item cost nothing in written bytes and need not have been alpha work is the most useful sentence in the PR. It says the scheduling assumption was the thing that needed checking first, which is a lesson about the plan and not about compression.

And the narrow reading is load-bearing rather than a footnote: realistic is UNDECIDED, so "the premise is refuted" is true of two shapes and not of the decider. Anyone reopening this needs exactly that.

On the closing-window point

Agreed, and it is the reason to land this one first. A closed #890 pointing at an unamended ROADMAP is the state the amendment exists to prevent — so the window is real, but it is a short one and worth spending on getting the table's provenance right rather than merging it fast.

`design/ROADMAP.md:192` read as an outstanding task sourced to Zeng et al., VLDB
2024: block compression costs more CPU than it saves in I/O on fast local NVMe,
so make it opt-in per storage tier. It was measured on this engine (#890) and did
not reproduce. Left as written it would have regenerated the same issue against a
different corpus in a year, which is the failure this amendment exists to
prevent. jd ruled it: change nothing about the default, amend the roadmap.

    shape            S = 1 - zstd/none      C        W       verdict
    repetitive             0.750          1.001    0.998     NET WIN
    text_heavy             0.958          0.911    0.920     NET WIN
    realistic              0.127          1.001    0.996     UNDECIDED
    random_int             0.000          1.000    1.012     UNDECIDED
    incompressible        -0.015          1.010    0.913     UNDECIDED

No shape fires NET COST, and there is no storage tier to key a default off: a
native table's blocks always live in the data directory, and object storage is an
import/export surface rather than a location for native table storage.

THE FIRST VERSION OF THIS ENTRY MIXED TWO RUNS IN ONE COLUMN. Found in review by
@OffgridwithJD. The summary table it was transcribed from labelled two of its
read costs `0.92 (rep)` and `0.92 (mix)` -- shape names from an EARLIER
three-shape run, with a different row count and wall-clock rather than
instruction counts. Transcribing it dropped the parentheses, which were the only
marker that those two values came from somewhere else. Reconciled row by row
against the five-shape run, three of five read costs were wrong:

    shape            C as written     C in the five-shape run
    repetitive           0.92                 1.001
    text_heavy            --                  0.911
    realistic            0.92                 1.001
    random_int           1.00                 1.000     matched
    incompressible       1.01                 1.010     matched

Every `S` was correct and NO VERDICT CHANGES: the two NET WINs are decided on `S`
alone, and the three UNDECIDED rows stay UNDECIDED because their `C` was already
under the 1.10 threshold. The conclusion never depended on the wrong numbers,
which is precisely why nothing would have caught them.

`text_heavy`'s blank was the costly one. Its real `C` of 0.911 is the most
favourable read cost in the set, so omitting it UNDERSTATED the case.

THE CONDITIONS NOW TRAVEL WITH THE NUMBERS: rows, build, counter, and that the
codec was asserted from `column_chunk.block_codec` rather than assumed from the
GUC. Plus the limit that an instruction count cannot see an I/O saving at ANY
working-set size, so `C` near 1.0 means "costs nothing to read" and never "saves
nothing". #890 learned the header the hard way when a table went out from an
assert build without naming it; the roadmap is where someone goes to reopen this,
so it belongs there more than on the issue.

THE NARROW READING IS RECORDED RATHER THAN THE HEADLINE. The shape the rule names
as the DECIDER is `realistic`, and it comes out UNDECIDED, not NET WIN. The
outcome is identical because an undecided measurement does not move a default,
but "the premise is refuted" is true only of the two most compressible shapes.

Two things that would otherwise have been lost with the entry. Dictionary
encoding on float columns was a SEPARATE clause of the same bullet and nothing
here measures it, so it is called out as still open. And the alpha4 item is
marked as having cost nothing in written bytes, so it need not have been alpha
work at all -- it was scheduled as alpha because the plan assumed it would change
the writer, and that assumption was the part that needed checking first.

Three defects found by the investigation are tracked separately and do NOT close
with this: #1074, #1075, #1076.

Docs only. `design/` is outside `docs_style.sh`'s scope; the CHANGELOG entry
carries no em or en dash, which is the one rule that does police it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK
@jdatcmd
jdatcmd force-pushed the docs/890-record-the-measurement-in-the-roadmap branch from e5963d9 to 73c9d65 Compare September 16, 2026 18:05
@jdatcmd

jdatcmd commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator Author

You were right, and it was worse than the reconstruction you could see.
Pushed the fix at 73c9d65.

I verified it against
the phase 1 table
rather than taking your reconstruction, as you asked. Three of five C values
were wrong, and your assignment of which came from which run is exactly right:

shape            C as written     C in the five-shape run
repetitive           0.92                 1.001     WRONG
text_heavy            --                  0.911     MISSING
realistic            0.92                 1.001     WRONG
random_int           1.00                 1.000     matched
incompressible       1.01                 1.010     matched

The mechanism, which is the part worth keeping

The summary table I transcribed labelled those two 0.92 (rep) and 0.92 (mix).
rep and mix are shape names from the earlier three-shape run — different
row count, wall clock rather than instruction counts. The parentheses were the
provenance marker, and I dropped them in transcription. They were the only thing
saying those two numbers came from somewhere else.

So the defect is not "I copied a wrong number", it is "I deleted the label that
made a mixed column visible". That generalises past this table.

No verdict changes, which is why nothing caught it

repetitive and text_heavy are NET WIN on S alone, so C never entered their
verdicts, and the three UNDECIDED rows stay UNDECIDED because their real C is
under the 1.10 threshold. The conclusion never depended on the wrong numbers.
That is the whole reason a re-read would not have found them and a reconciliation
did.

And your point about the blank stands: text_heavy's real C of 0.911 is the
most favourable read cost in the set and the strongest single number against the
premise. Omitting it understated the case.

Your second point, taken in full

The entry now carries rows, build, counter, and that the codec was asserted from
column_chunk.block_codec rather than assumed from the GUC — plus the limit you
established on #890, that an instruction count cannot see an I/O saving at any
working-set size, so C near 1.0 means "costs nothing to read" and never "saves
nothing". You are right that it belongs in the ROADMAP more than on the issue:
the ROADMAP is where someone goes to reopen this.

I also added the W column, since the table had the space and incompressible's
W = 0.913 is the row that explains why none is not the cheap end.

Same correction posted on #890

The closing comment there carried the identical table, and a closed issue is where
a wrong table does the most damage. Corrected in place rather than left standing.
#1082's docs do not carry this table at all, checked rather than assumed.

@OffgridwithJD OffgridwithJD left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE at 73c9d65. 14/14 green. I re-derived the corrected table rather than taking the fix on trust.

The five C values now match the five-shape run

shape            C as written    five-shape run
repetitive           1.001            1.001   ✓
text_heavy           0.911            0.911   ✓   (was blank)
realistic            1.001            1.001   ✓
random_int           1.000            1.000   ✓
incompressible       1.010            1.010   ✓

And "all five values from the SAME run" is now stated in the text, which is what stops the next transcription doing this again.

The narrative reconciles against its own columns

Checked rather than read past, because a table and its prose drifting apart is the same class of defect one level up:

  • "text_heavy is the only shape where compression measurably pays on the READ side" — C column is 1.001, 0.911, 1.001, 1.000, 1.010. 0.911 is the only value below 1. Holds.
  • "on incompressible, zstd is measurably cheaper to write (W = 0.913)" — matches the W column, and the claim is not one of uniqueness, so text_heavy's 0.920 does not contradict it.
  • "no shape fires NET COST" — the rule needs W >= 1.25 or C >= 1.10 with S < 0.10. The largest W is 1.012 and the largest C is 1.010. Holds with a wide margin.

The mechanism you found is the part worth keeping

The parentheses were the provenance marker and I deleted them in transcription.

That is a better account than "a wrong number was copied", and it generalises: 0.92 (rep) and 0.92 (mix) were a table telling its reader that column was mixed, and flattening it removed the only signal. A transcription that drops an annotation drops the thing that made the annotation necessary.

And no verdict moved, which is why nothing caught it. The two NET WINs are decided on S alone and the three UNDECIDEDs stay undecided at the real C. A wrong value that leaves the conclusion intact is invisible to anyone reading for the argument — which is everyone, including me on the first pass. I only found it because the blank in an otherwise complete column is a structural tell rather than a numerical one.

Your point back at me is the one I would not have reached: omitting a value that helps you is a failure direction nobody looks for. text_heavy's 0.911 was the strongest number against the premise and it was the one left out.

The conditions block

Everything I asked for and one thing I did not: the codec asserted from column_chunk.block_codec rather than the GUC, and PgColumnarScan asserted on every arm. Those are the two ways this measurement could have been of something else entirely.

The instruction-count limit landed in the right form — "C near 1.0 means costs nothing to read, never saves nothing" — and it belongs here rather than in the issue, because the ROADMAP is where someone goes to reopen this.

Correcting #890's closing comment too

Right call, and the reason is the sharper half: a closed issue is where a wrong table does the most damage, because nobody re-reads it and it is cited as settled. Checking that #1082's docs do not carry the same table, rather than assuming, is the check I would have asked for.

Land this one first. A closed #890 pointing at an unamended roadmap is the state the amendment exists to prevent.

@jdatcmd
jdatcmd merged commit ecf28b1 into main Sep 16, 2026
14 checks passed
@jdatcmd
jdatcmd deleted the docs/890-record-the-measurement-in-the-roadmap branch September 16, 2026 18:19
jdatcmd added a commit that referenced this pull request Sep 16, 2026
…ntees

TWO FIXES, one of them a hole in this PR that review did not look for.

THE CHANGELOG ENTRY WAS NEVER COMMITTED. `f115d0b` contains only
`test/native_fetch_projection.sh`. The entry was written, sat unstaged, and was
lost: `git reset --soft` leaves the index alone, `git commit` commits the index,
and the working-tree edit never entered it.

I checked with `git diff --stat origin/main` AFTER committing, which compares the
WORKING TREE to main rather than the commit to main, so it showed both files and
read as confirmation. That check cannot see this class of mistake at all. The one
that can is `git show <sha> --name-only`, which reads the commit.

Checked the sibling branches by the same means: #1082 and #1083 both carry their
CHANGELOG entries. Only this one was affected.

AND THE THIRD PREMISE'S COMMENT OVERSTATED ITS JOB. It read "the whole arm rests
on it". It does not. A broken prefix relationship cannot pass silently, because
the other two arms already contradict each other under it: if `Cols(` matched the
wide pattern then every narrow call would be counted twice, so `full >= cols`, and
`cols >= 1` with `full == 0` is a contradiction.

    cols=1, prefix intact   -> full=0   arm PASS
    cols=1, prefix broken   -> full=1   arm RED
    cols=3, prefix broken   -> full=3   arm RED

The premise documents the assumption and names what a future rename would break.
It is not the guarantee. Kept, with the comment now saying which it is. Correction
from @OffgridwithJD's review.

NO CHECK NAME MOVES, so no ledger key moves: the 17 names are identical to
`f115d0b` by sorted diff. Suite re-run on PG17 non-assert after the change,
17 passed + 0 failed + 0 unrunnable + 0 skipped = 17.

Merged `origin/main` to pick up #1080, and verified the roadmap amendment survives
in the merged tree rather than assuming it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK
jdatcmd added a commit that referenced this pull request Sep 16, 2026
The only conflict is CHANGELOG.md, and both sides add entries to `### Fixed`, so
both are kept. #1080, #1081 and #1083 landed while this was open.

Verified by count rather than by reading the diff:

    markers left                           0
    entries present exactly once           #1074/#1076, #1075, #1077, #1080, #1081
    bodiless headings in [Unreleased]      0

Nothing else moved. Per-file patch md5 of my seven files, merged result against
the pre-merge branch, added and removed lines only:

    docs/administration.md                       6a456b0edcac  same
    docs/best-practices.md                       0c84c03e77bd  same
    docs/configuration.md                        68b2705ebe8a  same
    test/fsst_margin.sh                          978e4e448429  same
    test/pytest/TESTS.md                         787346d68405  same
    test/pytest/expected_tests.txt               d1807c9dfd54  same
    test/pytest/test_compression_reaches_the_cascade.py  025da1ba7426  same

main moved none of `expected_tests.txt`, `check_ledger.tsv` or
`check_ledger_budget.txt` -- checked by md5 against 8e88f42 rather than assumed
from the fact that the merges were docs, shell suites and one `src/` file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK
jdatcmd added a commit that referenced this pull request Sep 16, 2026
CHANGELOG.md only. #1081 and #1083 landed while this was open and both add to
`### Fixed`, as this does, so all three entries are kept.

Verified by count rather than by reading the diff:

    conflict markers left                0
    each entry present exactly once      #1077 sweep, #1075, #1080, #1081,
                                         and #1078's, which was already there
    bodiless headings in [Unreleased]    0

The suite file is untouched by the merge: its patch md5 against main is unchanged
from before it, and the 17 check names are identical by sorted diff, so no ledger
key moves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants