fix: probe options and projection catalogs through their primary keys - #1198
Conversation
jdatcmd
left a comment
There was a problem hiding this comment.
Reviewed e8285ba, built and measured in pgcolumnar-dev on PG 18.4. The change is right, the causation proof is the right instrument, and the index choices check out against the install script. Three things: one is red in CI, one is a site the change's own argument covers and skipped, and one is a number I measured that belongs in the record.
CI is red, and the cause is precise
Three pytest legs fail on the same thing:
AssertionError: every pair in the tree is declared, so none is silently ungraded:
got 'catalog_plan_index' want 'none'
AssertionError: premise: every pair on disk is declared somewhere, so this loop
and the INCOMPLETE list account for all of them: got 37 want 38
test/catalog_plan_index.sh and test/pytest/test_catalog_plan_index.py are a new PAIR, and compare_to_bash.py declares neither. It needs to go in the COMPLETE list if the two grade one for one, or the INCOMPLETE list with the reason if they do not. Nothing else is failing.
Worth noticing what that guard is: it is the one that refuses a silently ungraded pair, and it fired the moment a pair appeared. It is silent when a suite ships with NO twin at all, because then there is no pair to grade.
One site the change's own argument covers
options_pkey is (regclass), the same column this key names is true of six call sites, and five of them moved:
| line | function | index |
|---|---|---|
| 3086 | PgColumnarReadOptions |
options_pkey |
| 3308 | PgColumnarReadTtl |
options_pkey |
| 3355 | PgColumnarReadSortBy |
options_pkey |
| 3409 | PgColumnarDeleteOptions |
options_pkey |
| 1988 | PgColumnarRenameDeclaredSortByColumn |
InvalidOid |
It is the same shape as PgColumnarReadSortBy line for line: same Anum_options_regclass key, same BTEqualStrategyNumber, same single-row if (HeapTupleIsValid(...)) read, and like PgColumnarDeleteOptions it takes a RowExclusiveLock with a NULL snapshot after a CommandCounterIncrement().
The cost is small and that is not the reason to fix it. It runs on ALTER TABLE ... RENAME COLUMN, not on a plan, so no measurement will show it. The reason is that the population here was defined by a list of six function names rather than by what the code does, and a list is what silently regrows. Defining it as "every scan whose key column is the index's column" finds this one and finds the next one.
Four more of the same shape, measured, and NOT on the plan path
projection_declaration_pkey is (rel, name), and four scans key on Anum_projection_declaration_rel, which is that leading column: PgColumnarListProjectionDeclarations, PgColumnarRenameProjectionDeclarationColumn, PgColumnarDeleteProjectionDeclaration, PgColumnarDeleteProjectionDeclarationsForRel.
I measured before saying it mattered, and it does not, on this path:
reset; a backend that plans and runs one columnar query; read pg_stat_all_tables
relname | idx_scan | seq_scan
------------------------+----------+----------
options | 1 | 0 <- your fix
projection | 1 | 0 <- your fix
projection_declaration | 0 | 0 <- never reached
storage | 0 | 2
A first pass of mine read projection_declaration seq_scan=1 and I nearly reported it as a live defect; resetting immediately before the measured backend instead of before the DDL showed the scan belonged to CREATE TABLE, not to planning. So these four are in kind with the one above and cost DDL time only. Your call whether they belong in this change.
The measured remainder, which is not yours to fix here
pgcolumnar.storage is sequentially scanned twice per planned query and never through an index, in the control above, on your build. Your note is correct about why: storage_pkey is (storage_id) and the lookup is by relation_oid, and I confirmed the install script gives pgcolumnar.storage that one index and no other.
So the headline defect is two thirds fixed, and the remaining third is the one that scans twice. Closing it needs a new index on relation_oid, which is a catalog change in pgcolumnar--1.0-alpha4--1.0-alpha5.sql and in the base script, plus the convergence check. That is a different change and I am not asking for it here. I am asking that the number appear somewhere, because "stays sequential" reads like a small residue and it is the largest one left.
The control that makes those numbers mean something
The zero is real. A backend that runs SELECT 1 after the same reset reports 0 | 0 on all four catalogs, so the counts above are what one planned columnar query costs and not what a connection costs.
What I verified rather than took on trust
| claim | how |
|---|---|
options_pkey is on (regclass) |
read from the install script, not from the PR body |
projection_pkey is (storage_id, projection_id) |
same, and storage_id leads, so the one-key ListProjections scan uses it |
pgcolumnar_index_oid is cheap enough for the plan path |
it is get_relname_relid, a syscache lookup, and the pattern predates this change |
the OidIsValid fallback matches the house pattern |
the other catalogs already do exactly this |
| deleting while scanning the index is safe | PgColumnarDeleteOptions and PgColumnarDeleteProjectionRow do what core does in RemoveAttributeById |
| the pytest twin's stats are not read before they are flushed | pg_stat_force_next_flush() on the reader, in autocommit, before the reader closes |
Census, at merge time
checks_never_observed_red 1455 and cluster_tests 464 are right for this tree. There are four other open PRs moving the same two files, and #1196 composes to 1476 against main's 1453. Whichever of these merges second has to RE-DERIVE both rather than keep either side, and be aware that check_ledger.tsv auto-merges silently while check_ledger_budget.txt conflicts loudly. The loud one is the safe half.
Requesting changes for the CI red and the line 1988 site. The rest is for you to weigh.
|
Follow-up on the census paragraph in my review: it is no longer a hazard, it is live, and I proved it on a real compose rather than predicting it. #1180 merged and took Composed And the conflict that does happen points the wrong way.
So when you rebase for the Worth noting which PR is safe here: #1196 says 465, a DIFFERENT value, so it conflicts loudly. The ones carrying the same number as main are the dangerous ones. |
…ee (commandprompt#1198) TWO THINGS THIS BRANCH WAS RED OR SHORT ON. CI was failing on three pytest legs for one reason: catalog_plan_index.sh and test_catalog_plan_index.py are a new PAIR and compare_to_bash.py declared it in neither list. every pair in the tree is declared, so none is silently ungraded: got 'catalog_plan_index' want 'none' premise: every pair on disk is declared somewhere: got 37 want 38 Declared in COMPLETE, which is where it belongs: the grader now reads 6 literal matches, 0 template, missing 0, and test_compare_to_bash.py is 40 passed / 196 checks. AND THE SIXTH CALL SITE. `options_pkey is (regclass), the same column this key names` is true of six scans and five of them moved. The sixth, PgColumnarRenameDeclaredSortByColumn, is the same shape line for line as PgColumnarReadSortBy which did move: same key, same strategy, same single-row read, same RowExclusiveLock and NULL snapshot after a CommandCounterIncrement. NO MEASUREMENT WILL SHOW IT. It runs on ALTER TABLE ... RENAME COLUMN, not on a plan, so the planner-path probe that found the other five cannot reach it. The reason to fix it is that the population was a list of six function names rather than the property "the key column IS the index's column", and a list is what silently regrows. All five Anum_options_regclass scans now pass options_pkey with the same OidIsValid fallback. Rebased onto e2638b7 with every tracked number re-derived on the composed tree rather than carried across the rebase: cluster_tests 464 -> 468 collection, 48 cluster files guard_tests 398 -> 403 main's, untouched by this branch checks_never_observed_red 1455 -> 1494 awk over the ledger suites_not_covered 249 unchanged check_ledger.tsv conflicted and was resolved ADDITIVELY, then checked rather than trusted: 1523 rows, zero duplicate (suite, part, check) keys. TESTS.md rebuilt from main with this branch's section re-applied once and its number DERIVED as max + 1: main's highest is 76, so test_catalog_plan_index.py is 77 and not the 74 it was written as. 77 sections, 77 TOC entries, 77 of 77 pairing on number AND title, contiguous, zero bad anchors. catalog_plan_index.sh 6 checks, PASSED test_catalog_plan_index.py 6 checks, 1 passed the corpus guards 104 checks, 40 passed Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XiFn3HteTXnGdRiA2xDP2n
e8285ba to
1abe12e
Compare
|
Pushed both fixes and a rebase to this branch, at the owner's request. This clears my The CI red
Declared in The sixth call site
All five No measurement will show this one -- it runs on Bookkeeping, re-derived on the composed tree
What I did not do, from my review and still open: @OffgridwithJD: I wrote these fixes and I am also clearing the block, so a second read of the sixth site would be welcome -- in particular whether |
OffgridwithJD
left a comment
There was a problem hiding this comment.
Independent read on 1abe12e, since @jdatcmd wrote the sixth call site and is also clearing the block. It is not an argument from similarity — the two call sites are identical in every parameter that governs the scan.
You asked whether PgColumnarDeleteOptions is really equivalent as a precedent. Read side by side:
/* PgColumnarDeleteOptions */
rel = open_columnar_table("options", RowExclusiveLock);
Oid optIdx = pgcolumnar_index_oid("options_pkey");
scan = systable_beginscan(rel, optIdx, OidIsValid(optIdx), NULL, 1, key);
/* PgColumnarRenameDeclaredSortByColumn, as changed */
rel = open_columnar_table("options", RowExclusiveLock);
Oid optIdx = pgcolumnar_index_oid("options_pkey");
scan = systable_beginscan(rel, optIdx, OidIsValid(optIdx), NULL, 1, key);Same relation, same lock mode, same index, same NULL snapshot, same single scan key. The only divergence is what each does with the tuple it finds — CatalogTupleDelete against a tuple update — and that is downstream of everything the scan decides. So the change rests on the four parameters being the same rather than on the two functions looking alike.
And it is exercised, which I checked rather than assumed
The path is DDL-time, so no planner measurement reaches it — but test/sorted_mark_rename.sh renames a declared sort-by column directly. On your branch:
sorted_mark_rename 46 passed + 0 failed
catalog_natts 16 passed + 0 failed
projection_rename_restore 8 passed + 0 failed
46 checks over the rename path, green. That is the behavioural answer your "argument from similarity" worry needed, and it is stronger than the code comparison because it does not depend on my reading being right.
Census
Derived with two instruments on an independently cloned tree, since you flagged using one command for all three:
file says 468/403 pytest's own total 468/403 --pgc-expect-tests accepted
ledger never 1494 = declared 1494 duplicate (suite, part, check) keys: 0
never differing from #1127 and #1155's 1499 is what shows these are three separate derivations rather than one carried across.
Nothing else from me on this one.
…commandprompt#1198) Original work by @linuxhikerpm; rebuilt on ee52910 by @jdatcmd after five PRs landed under it, with the two review fixes and the census re-derived. THE CHANGE. Planning one columnar table sequentially scanned pgcolumnar.options and pgcolumnar.projection. options_pkey is (regclass) and projection_pkey leads with storage_id, which is what those lookups ask for, and both scans passed InvalidOid. They now pass the index with the same OidIsValid fallback the other catalogs use. FIRST REVIEW FIX: THE PAIR THE GRADER COULD NOT SEE. catalog_plan_index.sh and test_catalog_plan_index.py are a new PAIR and compare_to_bash.py declared it in neither list, which failed three pytest legs: every pair in the tree is declared, so none is silently ungraded: got 'catalog_plan_index' want 'none' Declared in COMPLETE. The grader now reads 6 literal, 0 template, missing 0. SECOND REVIEW FIX: THE SIXTH CALL SITE. `options_pkey is (regclass), the same column this key names` is true of six scans and five of them moved. PgColumnarRenameDeclaredSortByColumn is the same shape line for line as PgColumnarReadSortBy, which did: same key, same strategy, same single-row read, same RowExclusiveLock and NULL snapshot after a CommandCounterIncrement. NO MEASUREMENT WILL SHOW IT. It runs on ALTER TABLE ... RENAME COLUMN, not on a plan, so the planner-path probe that found the other five cannot reach it. The reason to fix it is that the population was a list of six function names rather than the property "the key column IS the index's column", and a list is what silently regrows. @OffgridwithJD confirmed it is exercised rather than argued: test/sorted_mark_rename.sh renames a declared sort-by column, 46 passed. REBUILT RATHER THAN REPLAYED, for the reason commandprompt#1155 records: a replay conflicts on TESTS.md at every commit and one attempt committed four conflict markers before it was caught. src/columnar_metadata.c applied cleanly. catalog_plan_index.sh 6/0 test_catalog_plan_index.py 6/0 the parity and corpus guards 302 checks, 80 passed CENSUS RE-DERIVED TWICE, once per rebase: cluster_tests 468 -> 469 collection, 49 cluster files guard_tests 403 main's, untouched checks_never_observed_red 1499 -> 1501 awk over the ledger; it read 1494 against the previous main, and neither number is the other plus a delta suites_not_covered 249 unchanged check_ledger.tsv checked rather than trusted: zero duplicate (suite, part, check) keys. TESTS.md rebuilt from main with the section applied once and its number derived as max + 1 -- 78, having been 74 and then 77 as main moved twice under it. 78 sections, 78 TOC entries, 78 of 78 pairing on number AND title, contiguous, zero bad anchors. STILL OPEN, from the review and not fixed here: pgcolumnar.storage is scanned sequentially twice per planned query, because storage_pkey is on storage_id and the lookup is by relation_oid. Closing it needs a new index and a catalog migration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XiFn3HteTXnGdRiA2xDP2n
1abe12e to
eb2f060
Compare
OffgridwithJD
left a comment
There was a problem hiding this comment.
Approving eb2f060. Verified on a build of this branch, with the gate green
(15/15) on the same head.
The suite is sensitive, proven by removal
catalog_plan_index.sh as submitted, against PG 18:
-- options idx_scan=2 seq_scan=0
-- projection idx_scan=4 seq_scan=0
accounting: 6 passed + 0 failed + 0 unrunnable + 0 skipped = 6
Then I reverted the seven converted call sites back to InvalidOid by pattern,
rebuilt, and re-ran. The mutation reported the sites it changed, and the tree
restored to 0 modified files afterwards:
reverted sites: 7
-- options idx_scan=0 seq_scan=2
-- projection idx_scan=0 seq_scan=4
FAIL planning probed pgcolumnar.options through options_pkey: got [0] want [1]
FAIL planning did not sequentially scan pgcolumnar.options: got [2] want [0]
FAIL planning probed pgcolumnar.projection through projection_pkey: got [0] want [1]
FAIL planning did not sequentially scan pgcolumnar.projection: got [4] want [0]
accounting: 2 passed + 4 failed
So the arms move with the fix rather than passing either way, and the noise
tables make a sequential scan visible in the counter rather than inferred.
The counts
main 31 systable_beginscan sites with InvalidOid, 0 options/projection resolutions
eb2f060 24 sites with InvalidOid, 5 options_pkey + 2 projection_pkey
One follow-up, not a blocker
The comment here states the right frame — the population is the property "the
key column IS the index's column", not a list of function names. I applied that
property to the whole file, and it still has instances. Of the 24 remaining
InvalidOid sites, 21 have a scan key that is a prefix of an existing index:
row_group.storage_id x5 prefix of (storage_id, group_number)
free_space.storage_id x5 prefix of (storage_id, file_offset)
native_storage.storage_id x6 exactly storage_pkey
projection_declaration.rel x4 prefix of (rel, name)
The other 3 are one key that is not a prefix (native_storage.relation_oid) and
two sites where I could not find the ScanKeyInit within 40 lines.
I hand-checked two of these against the source rather than trusting the sweep,
because my first pass printed "0 of 24" — it split Anum_row_group_storage_id
at the wrong underscore, so I rebuilt it from the #define block itself.
I am not calling these defects; none is measured. I am saying the property this
PR names has 21 more instances, so it wants a follow-up issue rather than a
wider diff here.
…andprompt#1198) The comment above the sixth call site argues that the population should be the property "the key column IS the index's column" rather than a list of function names. @OffgridwithJD applied that property to the whole file and found the comment claims more than the change delivers: of 46 systable_beginscan sites, 24 still pass InvalidOid after these six, and 21 of those 24 have a scan key that is a prefix of an existing index. Recorded in the comment rather than fixed here, and explicitly NOT called a defect, because none of the 21 has been measured. A comment that states a rule while leaving 21 sites outside it is the same list wearing a better sentence. catalog_plan_index.sh 6/0 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XiFn3HteTXnGdRiA2xDP2n
OffgridwithJD
left a comment
There was a problem hiding this comment.
Re-approving on 9243f28, 15/15 green. My earlier APPROVED is on eb2f060 and
this repo carries reviews forward, so it needed replacing rather than trusting.
What changed since that head
src/columnar_metadata.c | 15 +++++++++++++++
Comment-only, recording the 21 remaining sites. The executable content is
unchanged from eb2f060, which I proved sensitive by removal: 6/0 as submitted,
and reverting the converted sites gives options idx_scan 2->0, seq_scan 0->2
and projection 4->0, 0->4, four reds, with the tree restored to 0 modified
files afterwards.
One count in the new comment is one short
of 46
systable_beginscansites, 24 still pass InvalidOid after these six
It is seven, not six. Measured two ways that agree:
main 31 sites pass InvalidOid
9243f28 24 sites pass InvalidOid -> 7 converted
9243f28 5 x pgcolumnar_index_oid("options_pkey")
2 x pgcolumnar_index_oid("projection_pkey") -> 7 resolutions
The five options sites are PgColumnarRenameDeclaredSortByColumn,
PgColumnarReadOptions, PgColumnarReadTtl, PgColumnarReadSortBy and
PgColumnarDeleteOptions; the two projection sites are
PgColumnarListProjections and the scan at line 4019.
Worth one character precisely because this comment's own subject is that counting
a population by name goes stale silently. "46" is right, and I nearly reported it
as 47 — my grep -c systable_beginscan counted the new comment's own sentence
about the 46 sites. Same trap, one line apart.
Not blocking; fix it if you push again for another reason.
The rest
The 21 remaining prefix-indexable sites are now #1207, which states that none of
them is measured and so none is called a defect. The breakdown in the comment
matches what I filed.
…commandprompt#1198) Original work by @linuxhikerpm; rebuilt on the current main by @jdatcmd with both review fixes and the census re-derived. Third rebuild. THE CHANGE. Planning one columnar table sequentially scanned pgcolumnar.options and pgcolumnar.projection. options_pkey is (regclass) and projection_pkey leads with storage_id, which is what those lookups ask for, and both scans passed InvalidOid. They now pass the index with the same OidIsValid fallback the other catalogs use. FIRST REVIEW FIX: catalog_plan_index.sh and test_catalog_plan_index.py are a new PAIR and compare_to_bash.py declared it in neither list, which failed three pytest legs. Declared in COMPLETE; the grader reads missing 0. SECOND REVIEW FIX: the sixth options_pkey call site, PgColumnarRenameDeclaredSortByColumn, which is the same shape line for line as PgColumnarReadSortBy. No measurement can show it -- it runs on ALTER TABLE ... RENAME COLUMN, not on a plan -- and @OffgridwithJD confirmed it is exercised rather than argued: test/sorted_mark_rename.sh, 46 passed. AND THE COUNTS IN THAT COMMENT ARE CORRECTED, having been wrong twice in the way the comment is about. It said "46 sites, 24 still InvalidOid after these six". Counted on both trees: main 44 systable_beginscan calls, 31 InvalidOid, 13 indexed this branch 44 , 24 , 20 So 44, not 46, and SEVEN resolved, not six. The 46 came from a `grep -c systable_beginscan` that counted the paragraph's own sentence ABOUT systable_beginscan -- a comment describing a sweep is input to that sweep. @OffgridwithJD caught the six and nearly shipped 47 from the same cause. cluster_tests re-derived on the composed tree: 476, having read 464, 468 and 469 as main moved under this branch four times. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XiFn3HteTXnGdRiA2xDP2n
9243f28 to
aa9b88a
Compare
OffgridwithJD
left a comment
There was a problem hiding this comment.
Not approving aa9b88a yet, for one thing that is my fault and is worth the
round trip.
The headline numbers you added are right. I re-derived them on this head,
counting calls rather than string matches:
CALLS: 44 InvalidOid: 24 indexed: 20
So "44 calls, 31 InvalidOid on main, 24 here, seven resolved" all check out, and
the paragraph recording that the first draft's 46 came from a grep counting its
own sentence is exactly right.
But the breakdown underneath it is the table I got wrong, and you inherited
it. It reads:
row_group.storage_id x5
free_space.storage_id x5
native_storage.storage_id x6
projection_declaration.rel x4
That sums to 20, directly above a sentence saying 21. Re-derived on
aa9b88a:
row_group 6 lines 488, 567, 671, 1020, 1041, 1596
storage 6 lines 1896, 1934, 1966, 2230, 2302, 2373
free_space 5 lines 747, 903, 1102, 1147, 1315
projection_declaration 4 lines 3673, 3814, 3889, 3929
21
row_group is six. The total was right and one row was wrong.
And native_storage is not a table. pgcolumnar--1.0-alpha5.sql declares no
such relation; the Anum_native_storage_* constants address pgcolumnar.storage,
as this file's own delete_rows_by_storage_id("storage", Anum_native_storage_storage_id, ...) shows. The index is right —
storage_pkey (storage_id) — but the row should name the catalog, not the C
constant prefix.
I have corrected both in #1207 and added the line numbers so the counts can be
checked instead of believed.
A table that does not sum to its own stated total, sitting inside the comment
whose subject is that counting a population by name goes stale, is the joke
writing itself. It came from me and I am sorry it reached your tree.
Push the two edits and I will approve immediately — everything else here I have
already verified, including the removal proof (6/0 as submitted, four reds with
the conversions reverted, tree restored clean).
…commandprompt#1198) Original work by @linuxhikerpm; rebuilt on the current main by @jdatcmd with both review fixes and the census re-derived. Third rebuild. THE CHANGE. Planning one columnar table sequentially scanned pgcolumnar.options and pgcolumnar.projection. options_pkey is (regclass) and projection_pkey leads with storage_id, which is what those lookups ask for, and both scans passed InvalidOid. They now pass the index with the same OidIsValid fallback the other catalogs use. FIRST REVIEW FIX: catalog_plan_index.sh and test_catalog_plan_index.py are a new PAIR and compare_to_bash.py declared it in neither list, which failed three pytest legs. Declared in COMPLETE; the grader reads missing 0. SECOND REVIEW FIX: the sixth options_pkey call site, PgColumnarRenameDeclaredSortByColumn, which is the same shape line for line as PgColumnarReadSortBy. No measurement can show it -- it runs on ALTER TABLE ... RENAME COLUMN, not on a plan -- and @OffgridwithJD confirmed it is exercised rather than argued: test/sorted_mark_rename.sh, 46 passed. AND THE COUNTS IN THAT COMMENT ARE CORRECTED, having been wrong twice in the way the comment is about. It said "46 sites, 24 still InvalidOid after these six". Counted on both trees: main 44 systable_beginscan calls, 31 InvalidOid, 13 indexed this branch 44 , 24 , 20 So 44, not 46, and SEVEN resolved, not six. The 46 came from a `grep -c systable_beginscan` that counted the paragraph's own sentence ABOUT systable_beginscan -- a comment describing a sweep is input to that sweep. @OffgridwithJD caught the six and nearly shipped 47 from the same cause. cluster_tests re-derived on the composed tree: 476, having read 464, 468 and 469 as main moved under this branch four times. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XiFn3HteTXnGdRiA2xDP2n
aa9b88a to
6cb9f9a
Compare
OffgridwithJD
left a comment
There was a problem hiding this comment.
Approving 6cb9f9a.
You were right and my correction was wrong in the other direction. I rebuilt
the sweep on the relation handle and it now agrees with yours exactly, site for
site:
free_space storage_id x6 747, 903, 1041, 1102, 1147, 1315
storage storage_id x6 1896, 1934, 1966, 2256, 2328, 2399
projection_declaration rel x4 3699, 3840, 3915, 3955
row_group storage_id x3 488, 567, 1020
row_group storage_id,group_number x2 671, 1596
21
unclassified: 645 and 1766 (handle unresolved), 3389 (storage.relation_oid, no index)
My bug was not quite the one you diagnosed, which makes the point better rather
than worse: my sweep did not search backwards for the nearest ScanKeyInit, it
collected every one in the preceding 40 lines and took the earliest. At 1041
that is row_group at 1018 instead of free_space at 1038. Two different wrong
rules, same wrong answer, and the handle settles both.
Removing the table rather than correcting it a third time is the right call.
A per-catalog table in that comment is the thing the comment argues against, and
three sweeps agreeing on 21 while disagreeing on its composition is exactly the
evidence for not putting one there. What is left — 44 calls, 31 InvalidOid on
main, 24 here, seven resolved, the method, and a pointer to #1207 — is the part
that is stable under re-derivation.
I re-derived those on this head before approving:
CALLS: 44 InvalidOid: 24 indexed: 20
One thing I have added that none of the three sweeps had. Every
systable_beginscan states its own nkeys. Comparing the keys collected against
that argument is a free self-check on the exact class of bug all three of us hit,
and it reports 0 mismatches across all 24 sites. It is in #1207 with the method.
The rest of the PR I verified earlier and it is unchanged: 6/0 as submitted, four
reds with the conversions reverted (options seq_scan 0→2, projection 0→4),
tree restored to 0 modified files.
#1207 now carries the corrected table with line numbers, and credits both
catches.
…commandprompt#1198) Rebuilt on main carrying commandprompt#1155 rather than rebased, because every census file this branch touches is a measurement of the tree and commandprompt#1155 moved all of them. Re-derived on the composed tree, never by arithmetic across the merge: cluster_tests 476 -> 477, by collection. Both sides carried 476 and the merge was silent: main alone collects 476 (50 cluster files), this tree collects 477 (51). checks_never_red 1506 -> 1508, by counting field five. This branch had derived 1501 against the previous main; neither number is the other plus a delta. suites_not_covered unchanged at 249, measured on both trees: a new registered suite arriving with ledger rows raises registered and covered by one each. TESTS.md takes section 80, not 79; commandprompt#1155 took 79. Checked by pairing TOC entries to headings in both directions, with all four mutations proved red (broken anchor, TOC entry deleted, duplicate number, heading deleted). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XiFn3HteTXnGdRiA2xDP2n
6cb9f9a to
651acf2
Compare
Rebuilt on main carrying #1155 (
|
OffgridwithJD
left a comment
There was a problem hiding this comment.
Approving 651acf2, replacing my approval of 6cb9f9a.
The census, re-collected on both halves rather than read
You asked me to check this hardest because it was the silent one. Confirmed, with
pytest's own total rather than a grep — a ::test_ count is inflated by the
vacuity diagnostic that quotes test ids, and this corpus uses parametrize
widely enough that a static def test_ count would be wrong too:
main 5f8753d 50 cluster files declared 476 collected 476
651acf2 51 cluster files declared 477 collected 477
So both halves carrying 476 was exactly the accidental agreement you describe:
the branch measured it against the previous main, main measured it without the
branch, and neither is wrong about the tree it was taken on. 477 is right for the
composed tree.
expected_tests.txt conflicting in the comment block one line above the value is
the nastiest detail here — resolving that conflict puts the wrong number directly
under the cursor and nothing downstream complains.
What changed since the head I approved
The PR's own contribution is unchanged apart from CHANGELOG wording, the ledger
budget comment and a TESTS.md entry. I compared each head against its own merge
base rather than diffing the two shas, because a plain git diff 6cb9f9a 651acf2 -- src/ reports columnar_customscan.c +117 and columnar_tableam.c +17 — that
is #1155 arriving in main underneath this branch, not this branch's work. Its
own three-dot file list touches customscan zero times.
No conflict markers in any changed file.
Unchanged and already verified
The removal proof from eb2f060 still stands: 6/0 as submitted, four reds with
the seven conversions reverted (options seq_scan 0→2, projection 0→4), tree
restored to 0 modified files. And the counts in the comment re-derived on this
head: 44 calls, 24 InvalidOid, 20 indexed, seven resolved.
On not approving it yourself
Agreed, and for the reason you give. The gate asks who OPENED the PR, so your
rebuild of a @linuxhikerpm branch clears an author-exclusion check while being
your own content. No field fixes that; the reviewer has to be someone else.
Dismissing my own CHANGES_REQUESTED from e8285ba. Every itemized ask in it was addressed on the branch, and the head has since moved twice (to 6cb9f9a, which @OffgridwithJD approved, and to 651acf2, the rebuild onto main carrying #1155, which @OffgridwithJD has now approved as well). Leaving it standing would block on a request that no longer describes the tree.
|
The red on Three reasons, the first of which needs no history: 1. The sign is physically impossible for a real effect. 2. The same content passed twice on earlier heads.
3. The floor is where main sits. I measured this arm on main at This is the sixth occurrence in #1203 and the second time it has landed on a PR |
delete_group_rows() opens its catalog from a `const char *tableName`
PARAMETER, and PgColumnarDeleteGroupMetadata calls it five times. One
systable_beginscan in the source was five sequential scans per retired group
at run time, each walking every other columnar table's rows.
Two audits of these scans missed it, including my own. Both attributed a scan
to a catalog by reading the open_columnar_table("<name>") that produced its
relation handle; a relation that arrives as an argument has no name at the
call site. What found it was probing all 44 systable_beginscan sites and
requiring sum(probes that ran with InvalidOid) == sum(seq_scan over every
pgcolumnar catalog). The six-site enumeration failed that at 41 counted
against 22 probed.
Nine sites now pass an index oid. Every key was already a prefix of an index
that exists, so no catalog migration.
Measured, 40 groups with 20 retired, counters reset immediately before
compact(). seq_scan before -> after: bloom 20->0, column_chunk 20->0,
delete_vector 20->0, zone_map 20->0, free_space 22->0, row_group 43->0. The
path cost 7 x (retired groups) + 3 and now costs none of them; the
reconciliation is exact at 5, 10, 20 and 40 groups.
Two of the nine are in PgColumnarCheckFreeSpaceNoOverlap, which is
assert-only, so a measurement on a release build reports zero there while
every assert-enabled CI leg pays two per maintenance operation.
Both harnesses, independent: catalog_delete_index.sh (21 checks) and
test_catalog_delete_index.py (22). Restoring InvalidOid on all nine reddens 13
of the shell suite's 21; the 8 survivors are the 7 premises and
delete_vector's index arm, which passes beforehand. Ledger verdicts are
transcribed from that run. Suite measured green on PG 15, 16, 17, 18 (assert
and non-assert) and 19.
REBUILT ON MAIN CARRYING #1198, which also edits columnar_metadata.c. The two
sets of sites are disjoint and git merged that file silently, so it was
checked by counting on the composed tree -- 9 converted sites from this change
and 7 from #1198 -- and by running #1198's own suite here, 6/6.
Census re-derived on this tree, never by arithmetic: cluster_tests 477 -> 479
by collection, checks_never_observed_red 1508 -> 1516 by counting,
suites_not_covered unchanged. TESTS.md takes 81; #1198 took 80.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XiFn3HteTXnGdRiA2xDP2n
The crossover, measured — and a case where there is no crossover at all@jdatcmd found that this change costs planning buffers at small catalog sizes and
The crossover is between 1000 and 2000 tables, near 1200 by interpolation. The shape is the interesting part. That is a real optimisation with a real price, not a mistake — above ~1200 tables But there is a configuration where it never wins
Measured deliberately, 1000 columnar tables, no Both catalogs are empty, so the two sequential scans this change removes cost On the two sets of numbersMine are +5 at 10 tables; @jdatcmd measured +16, and +37 for this change on main. What I got wrong in my own reviewI approved this on a removal proof: the arms move with the change, A removal proof establishes that the test can detect the change. It says What this does not decideWhether to revert, hoist the lookups, or document this as a large-installation |
|
Filed the cause as #1216, with both measurement tables and the empty-catalog case. Short version for anyone finding this later: this change is a real optimisation above roughly 1200 columnar tables and should not be reverted — it converts a linear planning cost into a constant one. But it costs about 5 buffers per plan below that, and for an installation that never calls The cause is I merged this. I reported |
delete_group_rows() opens its catalog from a `const char *tableName`
PARAMETER, and PgColumnarDeleteGroupMetadata calls it five times. One
systable_beginscan in the source was five sequential scans per retired group
at run time, each walking every other columnar table's rows.
Two audits of these scans missed it, including my own. Both attributed a scan
to a catalog by reading the open_columnar_table("<name>") that produced its
relation handle; a relation that arrives as an argument has no name at the
call site. What found it was probing all 44 systable_beginscan sites and
requiring sum(probes that ran with InvalidOid) == sum(seq_scan over every
pgcolumnar catalog). The six-site enumeration failed that at 41 counted
against 22 probed.
Nine sites now pass an index oid. Every key was already a prefix of an index
that exists, so no catalog migration.
Measured, 40 groups with 20 retired, counters reset immediately before
compact(). seq_scan before -> after: bloom 20->0, column_chunk 20->0,
delete_vector 20->0, zone_map 20->0, free_space 22->0, row_group 43->0. The
path cost 7 x (retired groups) + 3 and now costs none of them; the
reconciliation is exact at 5, 10, 20 and 40 groups.
Two of the nine are in PgColumnarCheckFreeSpaceNoOverlap, which is
assert-only, so a measurement on a release build reports zero there while
every assert-enabled CI leg pays two per maintenance operation.
Both harnesses, independent: catalog_delete_index.sh (21 checks) and
test_catalog_delete_index.py (22). Restoring InvalidOid on all nine reddens 13
of the shell suite's 21; the 8 survivors are the 7 premises and
delete_vector's index arm, which passes beforehand. Ledger verdicts are
transcribed from that run. Suite measured green on PG 15, 16, 17, 18 (assert
and non-assert) and 19.
REBUILT ON MAIN CARRYING #1198, which also edits columnar_metadata.c. The two
sets of sites are disjoint and git merged that file silently, so it was
checked by counting on the composed tree -- 9 converted sites from this change
and 7 from #1198 -- and by running #1198's own suite here, 6/6.
Census re-derived on this tree, never by arithmetic: cluster_tests 477 -> 479
by collection, checks_never_observed_red 1508 -> 1516 by counting,
suites_not_covered unchanged. TESTS.md takes 81; #1198 took 80.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XiFn3HteTXnGdRiA2xDP2n
delete_group_rows() opens its catalog from a `const char *tableName`
PARAMETER, and PgColumnarDeleteGroupMetadata calls it five times. One
systable_beginscan in the source was five sequential scans per retired group
at run time, each walking every other columnar table's rows.
Two audits of these scans missed it, including my own. Both attributed a scan
to a catalog by reading the open_columnar_table("<name>") that produced its
relation handle; a relation that arrives as an argument has no name at the
call site. What found it was probing all 44 systable_beginscan sites and
requiring sum(probes that ran with InvalidOid) == sum(seq_scan over every
pgcolumnar catalog). The six-site enumeration failed that at 41 counted
against 22 probed.
Nine sites now pass an index oid. Every key was already a prefix of an index
that exists, so no catalog migration.
Measured, 40 groups with 20 retired, counters reset immediately before
compact(). seq_scan before -> after: bloom 20->0, column_chunk 20->0,
delete_vector 20->0, zone_map 20->0, free_space 22->0, row_group 43->0. The
path cost 7 x (retired groups) + 3 and now costs none of them; the
reconciliation is exact at 5, 10, 20 and 40 groups.
Two of the nine are in PgColumnarCheckFreeSpaceNoOverlap, which is
assert-only, so a measurement on a release build reports zero there while
every assert-enabled CI leg pays two per maintenance operation.
Both harnesses, independent: catalog_delete_index.sh (21 checks) and
test_catalog_delete_index.py (22). Restoring InvalidOid on all nine reddens 13
of the shell suite's 21; the 8 survivors are the 7 premises and
delete_vector's index arm, which passes beforehand. Ledger verdicts are
transcribed from that run. Suite measured green on PG 15, 16, 17, 18 (assert
and non-assert) and 19.
REBUILT ON MAIN CARRYING #1198, which also edits columnar_metadata.c. The two
sets of sites are disjoint and git merged that file silently, so it was
checked by counting on the composed tree -- 9 converted sites from this change
and 7 from #1198 -- and by running #1198's own suite here, 6/6.
Census re-derived on this tree, never by arithmetic: cluster_tests 477 -> 479
by collection, checks_never_observed_red 1508 -> 1516 by counting,
suites_not_covered unchanged. TESTS.md takes 81; #1198 took 80.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XiFn3HteTXnGdRiA2xDP2n
configuration that is a loss at every database size, with no crossover to be above: a row reaches pgcolumnar.options only when set_options is called and pgcolumnar.projection only when a projection is added, so an installation doing neither has both empty. At zero rows the heap the probe replaces is zero pages and the scan is literally free. Measured on 6c3a951, 50 plans, at 10, 200 and 1000 columnar tables alike: options heap=0 index=100, projection heap=0 index=200. Six index buffers per plan, flat, because there is nothing to scan more of. shipped. The cost returns to nothing -- 300 buffers over 50 plans down to 0, the pre-#1198 number exactly -- and the probe is still taken where it pays: with projection at seven pages of other tables' rows, planning reads 451 against 1050 for reading it whole. THE THRESHOLD NEEDED NO SECOND VALUE, AND THAT WAS MEASURED RATHER THAN ASSUMED. 3 was derived entirely on the compaction path and #1217's data put the planner crossover near 1200 tables, so a separate constant looked likely. Swept over five database sizes on the planner path, 3 is optimal or tied at every one. The first sweep said otherwise and its FIXTURE was wrong: it planned the first-created table, whose row sits at the head of the heap, so the scan it replaces stops almost immediately. That made a higher threshold look 51 buffers better at 1200 tables; on the last-created table it is 199 worse. FOUR ARMS WERE REMOVED RATHER THAN REPAIRED, and their ledger history goes with them. They asserted idx_scan >= 1 and seq_scan == 0 per catalog -- which path was taken, not how much work was done -- and they fail against a build that made planning strictly cheaper. orphan-scan refused to prune rows carrying history and told me to say why the history may go: they are not renames. Each replacement asserts a different property, and moving a path claim's history onto a work claim's row would record arms as observed red under mutations they never ran. All of it was predicted before a production line moved: the prediction named those four arms, the direction each would move, and the storage arms as the ones that must not move. All three held. A second fixture defect, caught the same way: putting the projections on the MEASURED table made every row match the key, and the probe's cost scales with matching rows rather than catalog size. It read the probe losing at seven pages. The bulk now goes on a noise table and a premise asserts the measured table owns a small share. 14 shell checks and 15 in the port, green on PG 15 through 19 with identical readings; harness_selftest 1175/1175; the full 482-test cluster corpus. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XiFn3HteTXnGdRiA2xDP2n
Summary
pgcolumnar.optionsandpgcolumnar.projection.options_pkeyis(regclass)andprojection_pkeyleads withstorage_id, which is what those lookups ask for, and both scans passedInvalidOid.PgColumnarReadOptions,PgColumnarReadTtl,PgColumnarReadSortBy,PgColumnarDeleteOptions,PgColumnarListProjections, andPgColumnarDeleteProjectionRownow pass those indexes, with the sameOidIsValidfallback the other catalogs use. Thepgcolumnar.storagelookup byrelation_oidstays sequential:storage_pkeyis onstorage_id.Test plan
Independent twins
test/catalog_plan_index.shandtest/pytest/test_catalog_plan_index.py. Same public seam (pg_stat_all_tablesafter one filtered scan). Different tables, row counts, and aggregates. The pytest scan runs on a second connection.Unfixed
.so472bd6ec8301, source8dbf8d2fd6fe, PG18:Shell:
Pytest:
Fixed
.soa88744fa6d65, source966700aa9e49:Pytest on that same
.so:idx_scan=1 seq_scan=0on both catalogs, 6 pass.Causation,
InvalidOidput back on the two planner scans,.so215ab2396eaa:Pytest on that
.so:options idx_scan=0 seq_scan=1 projection idx_scan=0 seq_scan=1.Shell green on PG 15, 16, 17, 18, and 19, source
966700aa9e49, 6 passed on each. Ledger rows carry15;16;17;18;19.checks_never_observed_redre-counted at 1455.suites_not_coveredunchanged.cluster_testsre-derived by collection at 464.Made with Cursor