Repository navigation
fix: twelve metadata lookups read their whole catalog (#1207) - #1285
Conversation
…1207) Twelve systable_beginscan calls in columnar_metadata.c passed InvalidOid on a key an existing index starts with. The pgcolumnar catalogs are shared by every columnar table in the database, so each paid for every other table's rows: free_space compaction reuse, both end-truncation reads row_group row_group_exists, reached by EVERY DELETE storage three existence checks on every write, and the sorted-extent update projection_declaration list, rename a column, drop one, drop all Each now goes through pgcolumnar_scan_index_oid(), so it probes at or above pgcolumnar.index_min_blocks pages and reads the heap below it, as commandprompt#1213 and commandprompt#1217 do. PG 17, 400 other projected tables, catalog buffers, before/after: one-row DELETE row_group 45 -> 19 INSERT storage 19 -> 13 vacuum_sorted storage 128 -> 93 truncate() free_space 26 -> 17 test/catalog_scan_index.sh and its pytest twin drive eleven operations that reach all twelve sites. Every arm asserts no sequential scan of a catalog a premise has shown to be above the threshold, where declining the probe cannot be correct. Two cost-based designs were tried first and a revert passed both, which the suite header records. fix, PG 15..19 49 passed + 0 failed, each major revert, PG 15..19 38 passed + 11 failed, each major one site reverted each of the 12 reddens the arm that reaches it (PG 17) Regression: 34 suites that drive these paths pass on PG 17 at the default threshold, and 32 of 34 with index_min_blocks forced to 0. The two that fail under forcing are catalog_delete_index and catalog_plan_index, whose failing arms assert the default declines on small catalogs, which forcing contradicts by construction. Ledger 1810 -> 1859 rows, never 1633 -> 1671, dated 177 -> 188. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jdatcmd
left a comment
There was a problem hiding this comment.
Approving 8734f1e. I will merge once the full CI matrix is green on this head.
What I checked, and how
The twelve conversions are correct. Each converted scan keys on the leading column or columns of the index it names, which is what systable_beginscan needs when it maps keys onto index columns:
| index | columns | scan keys |
|---|---|---|
free_space_pkey |
(storage_id, file_offset) |
storage_id |
row_group_pkey |
(storage_id, group_number) |
both |
storage_pkey |
(storage_id) |
storage_id |
projection_declaration_pkey |
(rel, name) |
rel |
Your fix-against-revert result reproduces here, PG17, in my own worktrees from this head. I reversed only the src/columnar_metadata.c hunk:
fix catalog_scan_index 49 passed + 0 failed
revert catalog_scan_index 38 passed + 11 failed
These are the same 11 path arms your table names. The values range from got [1] to got [11] want [0], so none is a borderline count.
The ledger arithmetic reproduces: 1859 rows, 1671 never-red, budget 1671. All 49 new rows carry 15;16;17;18;19, 11 dated and 38 never.
The pytest twin is independent. It does not import or call the shell suite.
On asserting the path
I accept the argument. The objection in catalog_delete_index.sh is about the region where declining a probe is correct. Your premise pins every arm above the threshold against the live setting, so it does not apply there. The two failed cost designs you describe are the better evidence: each was measured and rejected, which is more than the rule's own text did.
Two things for later, neither blocking
- #1284 conflicts with this PR in
check_ledger.tsvand the budget's census line. This one blocks alpha5 and mine does not, so this merges first and I rebase #1284 and recount. catalog_scan_indexjoins the #1236 reading list. It has 11 dated arms beside 38 never-red checks. That is the issue's inference working as designed, not a defect in this PR. I will record it there.
🤖 Generated with Claude Code
) Rebuilt onto c1e253f after #1285 landed. Same content as eec3199. The ledger rows come from the same mutation logs, merged again by a script; that script run on be31874 reproduces eec3199's ledger byte for byte. Ten parts no longer hold a never-red check: harness_selftest 570, 190, 290, 560 and 580, rewrite_storage_oid, catalog_delete_index, catalog_plan_index, projection_scan_cost and projection_parallel. Twenty checks are dated from mutation runs on all five majors, one worktree per mutation per major, merged one log at a time with --mutation and --target. Three checks that only describe their own fixture are renamed premise:, the owner's decision of 2026-10-12. The never-red census moves 1671 -> 1651. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closes #1207. @jdatcmd for review. jd's rule from today applies: this lands only on your approval.
What
Twelve
systable_beginscancalls incolumnar_metadata.cpassedInvalidOidon a key an existing index starts with. Each now goes throughpgcolumnar_scan_index_oid(), the size-aware resolver from #1213/#1217. Belowindex_min_blocksthe behaviour is unchanged.free_spaceAllocateFreeSpace,TrailingFreeSpaceSafe,DeleteFreeSpaceAtOrAbovecompact_rewrite,truncate()row_grouprow_group_existsstorageInsertNativeStorageRow,SetSortedExtentparallel_copy,vacuum_sortedprojection_declarationTRUNCATE/rewrite,RENAME COLUMN,drop_projection,DROP TABLEpg_constraintatcolumnar_tableam.c:2582(the 13th) is a core catalog and out of scope.My #1207 comment from this morning called
free_space"reachable, low frequency". Therow_group_existssite is the important one: it runs on everyDELETE.Evidence
A/B on populated catalogs, PG 17, catalog buffers per operation (from the suite's own output, fix against revert):
The new suite,
catalog_scan_index.sh, plus a pytest twin. It covers eleven operations that reach all twelve sites:Single-site sweep, PG 17. Each site was reverted alone, and each one reddens the arm for the operation that reaches it:
Regression, PG 17. These are the 34 suites that drive these paths: all 34 pass at the default threshold. With
PGOPTIONS='-c pgcolumnar.index_min_blocks=0'forcing every converted site onto the index, 32 of 34 pass. The two that fail arecatalog_delete_indexandcatalog_plan_index. Their failing arms assert that the default declines probes on small catalogs, which forcing contradicts by construction.The full pytest guard half passes (404), and so do
harness_selftest.sh(1213 checks) anddocs_style.sh. Parity withcompare_to_bash.pyis clean, with 0 missing.The design choice I most want attacked: it asserts the path
catalog_delete_index.shsays not to assertseq_scan. I do here, and the argument is in the suite header. That objection holds where declining a probe can be correct, which is below the threshold. Every arm runs after 400 other projected tables have pushed all four catalogs above it, and a premise checks that against the live setting.Two cost designs failed first, both measured:
index_min_blocksforced high: a revert passed 4 arms of 9. The setting also forces the earlier conversions (fix: choose the catalog access path by size, and assert the work (#1207) #1213, planner: pgcolumnar_sorted_pathkeys sequentially scans pgcolumnar.storage on storage_id, which storage_pkey already indexes #1237) to read whole, so the forced reading came out worse than main's.vacuum_sortedmakes 28 storage scans.Fixture traps the premises caught (both silent, both
rc=0)compact()in the samepsql -cas its DELETE retires nothing (one implicit transaction). Everyfree_spacearm was measuring an empty free list.pgcolumnar.truncate()refuses a transaction block; a multi-statement-cis one. It raised,q()swallowed the error, and the arm read zero.truncate()has no tail to reclaim, because the projection's storage sits behind the base's last group. Measured: 0 blocks with the projection, 3 without. The truncation arm uses a projection-free target.Side effects, neither relied on
AllocateFreeSpacebest-fit: a tie now goes to the lowest offset rather than heap order.server_dirfixture moved fromtest_sorted_pathkeys.pytoconftest.py, because a second file needs it. That follows thepgc_own_dbprecedent.Ledger
1810 → 1859 rows; never 1633 → 1671; dated 177 → 188. The 11 path arms are dated by the revert, merged one major at a time from a five-major run. The budget comment shows the arithmetic.
🤖 Generated with Claude Code