fix: charge a capped per-row term on clustered index fetches (#1039, rebased + ledger) - #1095
Conversation
…prompt#913) The group-decode count is flat through the first stripe, so a 50,000-row range stayed on a fetching index. Charge reconstruction per row, capped at half a group so a clustered ORDER BY stays on the index (commandprompt#355). Co-authored-by: Cursor <cursoragent@cursor.com>
…dger rows The matrix refuses an unsorted SUITES array, and a suite with no ledger rows raises the uncovered-suite count. Insert crossover before width and record its six checks so the ceiling stays put. Co-authored-by: Cursor <cursoragent@cursor.com>
… majors CI suites (PG 17) refused these checks because a PG18-only seed left majors=18. The suite was run on PGDG 15.19, 16.15, 17.11 and Ubuntu 18.6 and those logs were merged. PG19 is not installed here. Co-authored-by: Cursor <cursoragent@cursor.com>
jdatcmd
left a comment
There was a problem hiding this comment.
Approving.
The cost model is sound and the test bounds it in all three directions, which is what I most wanted to see, because a penalty term that is too aggressive breaks #355 silently:
a selective point lookup still uses the index (small: unchanged)
a 50000-row correlated range uses the custom scan (large: moves off)
the fetch penalty leaves a clustered ORDER BY on its index (#355: stays on)
both paths return the same aggregate at 50000 (equivalence)
The cap at half a group is what separates the second from the third, and both are pinned, so the cap cannot quietly become wrong in either direction.
The unit choice is right: cpu_tuple_cost is what core already charges per heap tuple, and the comment correctly warns against scaling it from heap instructions-per-cost, citing #766.
The ledger correction is load-bearing, proved the same way as on #1092: the four-major rows are refused by the gate on PG19 and the five-major rows pass, with the observation log held constant.
Commit preservation checked: every commit from #1039 is an ancestor of this branch.
Withdrawing my own open item: the cap IS exercised, and I was wrong to call it untestedIn the PR body I wrote that the cap-saturation case is untested — that the arms exercise the cap being respected but nothing drives the workload until the cap is the binding constraint. That is false, and I should have tested it before writing it. The clustered - if (per_row > half_group)
- per_row = half_group;
+ (void) half_group;gives: So the cap binds in exactly the case it was written for (#355), and an arm reddens when it stops binding. What I would still change, and it is a comment rather than an armThe arm's name says what it protects ( I will add a line to the suite header when I rebase this for section 47, pointing at the relationship: that the ORDER BY arm is what reddens if the cap is removed, with the measured result above. No new check — the coverage already exists, only the signpost is missing. Apologies for the noise on the original note. The lesson is the obvious one and it is the same one that has bitten me repeatedly today: I described a gap from reading the tests rather than from mutating the code, and a mutation answered it in one run. |
… edit The rows read 15;16;17;18 while every other row in the ledger reads 15;16;17;18;19. covered_majors is the union over all rows, so it includes 19, and pgc_ledger.py refuses a known check seen on a major its own row does not name. ci.yml:503: the per-PR gate runs 17+18, nightly runs 15-18, and the local five-major matrix adding PG19 remains the release gate. So CI was green and the release gate would have refused this suite's checks. The rows are re-derived by running the suite on all five majors and merging those logs, not by editing field 4. A row is a claim about where a check was observed, and widening it by hand makes that claim without the observation. check_ledger_budget.txt needed no change to suites_not_covered; the census is re-derived from the merged ledger. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs
…mes-pg19 # Conflicts: # CHANGELOG.md # test/check_ledger.tsv # test/check_ledger_budget.txt # test/pytest/TESTS.md # test/pytest/expected_tests.txt # test/pytest/test_compare_to_bash.py
c4e2d70 to
ce090c6
Compare
This is @linuxhikerpm's #1039, rebased onto
cfdb393with its ledger rows re-derived. Their commits are preserved; mine is the last one. Opened from my fork because I cannot push to theirs.What I reviewed and found legitimate
The C is sound and well-documented. The per-row term is in
cpu_tuple_costunits, which is the same unit core already uses for a heap tuple, and the comment explicitly warns against scaling it from heap instructions-per-cost (citing #766, where that conversion predicted the wrong winner). The cap at half a group is reasoned rather than tuned: uncapped it grows with the whole table and costs a clusteredORDER BYoff its index, which #355 must not do.I diffed the inputs against the merge base: nothing this depends on changed underneath it.
What I changed
Only the ledger. Their rows read
15;16;17;18while every other row reads15;16;17;18;19, socovered_majorsincludes 19 and the release gate refuses this suite's six checks — while CI stays green, becauseci.yml:503runs 17+18 per PR and only the local five-major matrix adds 19.Re-derived from runs, not edited. A row is a claim about where a check was observed.
Open, not blocking
The cap-saturation case is still untested. The arms exercise the cap being respected; I could not find one that drives the workload until the cap is the binding constraint and shows the behaviour is still correct there. That is the interesting edge and where a cap usually goes wrong.
If saturation is unreachable in practice, a sentence in the suite header saying so is worth more than an arm — a stated reason beats an untested assertion. If it is reachable, one arm at the boundary closes it.
Merge order
Takes TESTS.md section 44, correct while main is at 43. All five take 44; the numbering is gated, so the second to land renumbers and re-derives its census — the conflict offers two numbers and neither is right (#996).
Suggested order: #1092 first, then this, then #1095, #1094, #1093.
Original: #1039. Author: @linuxhikerpm.