Skip to content

docs, test: the latest-reading-per-device recipe, run from the document (#1293) - #1303

Merged
jdatcmd merged 1 commit into
commandprompt:mainfrom
OffgridwithJD:docs/1293-latest-reading
Oct 12, 2026
Merged

jdatcmd merged 1 commit into
commandprompt:mainfrom
OffgridwithJD:docs/1293-latest-reading

Conversation

@OffgridwithJD

Copy link
Copy Markdown
Collaborator

Closes #1293 together with #1301 (merged: min/max(ts) read no data). @jdatcmd for review.

What

docs/how-to.md gains "Find the latest reading per device":

  • GROUP BY device, max(ts) when only the latest timestamp is needed;
  • a btree on (device, ts) plus LATERAL (... ORDER BY ts DESC LIMIT 1) over a device list, for the whole latest row;
  • the trap: the LATERAL form without the btree reads the whole table once per device.

Numbers from #1293 (10M rows, 1,000 devices): 45 ms with the btree, 12.6 minutes without it, and 5.6 s for DISTINCT ON.

The second trap I measured on #1293, DISTINCT ON getting slower once the btree exists (20 s, index-only scan), is not in the document. It depends on planner choices I can't pin in a suite, and an untested claim doesn't go in.

The tests run the document

latest_reading.sh and test_latest_reading.py read the recipe's SQL from the section. The shell side uses awk, the Python side its own regex. So the document is what runs, using your approach from #1288.

arm asserts
each form returns what DISTINCT ON returns GROUP BY and LATERAL vs the oracle (md5 / row lists)
without the btree the LATERAL form reads the table once per device blocks ≥ pages × devices / 2
with the btree the LATERAL form reads under a tenth of the table and ≥ 1 block, its own premise

Test-first, and what the red run caught

Run before the section existed, every arm was red except "under a tenth of the table", which passed on empty SQL: a query that reads nothing reads under a tenth of anything. Reading ≥ 1 block is now a premise in both twins.

The first green attempt failed honestly. At 5,000 rows per device, the indexed LATERAL read 45 blocks of a 177-page table. Its cost tracks devices, not table size, so the fixture now has 25,000 (shell) and 36,000 (pytest) rows per device. The table grew to 922 pages and the indexed form still read 45 blocks.

Removal proof: mutate the document

The documented LATERAL edited from ORDER BY ts DESC to ORDER BY ts, which returns each device's first reading:

latest_reading   doc as written   8 passed + 0 failed     PG 15, 16, 17, 18, 19
                 doc mutated      7 passed + 1 failed     every major
                   the LATERAL form returns what DISTINCT ON returns
pytest twin      doc mutated      the LATERAL form returns what DISTINCT ON returns  (red)

Ledger 1866 → 1874, never 1653 → 1660 (1 dated, 7 never). That will conflict with #1302's budget line, and I'll recount whichever lands second. cluster_tests 495 by collection; TESTS.md 87 (the same number as #1302, so the second one gets renumbered). harness_selftest, docs_style and the guard half (404) pass.

🤖 Generated with Claude Code

…nt (commandprompt#1293)

docs/how-to.md gains "Find the latest reading per device": GROUP BY for the
latest timestamp, a btree on (device, ts) plus LATERAL over a device list for
the latest row, and the trap -- the LATERAL form without the btree reads the
whole table once per device. Measured on commandprompt#1293, 10M rows and 1,000 devices:
45 ms with the btree, 12.6 minutes without, 5.6 s for DISTINCT ON.

latest_reading.sh and its pytest twin read the recipe's SQL from the document,
so an edit to the document is what runs (jdatcmd's approach from commandprompt#1288):

    each documented form returns the rows DISTINCT ON returns
    with the btree, LATERAL reads >= 1 block and under a tenth of the table
    without it, LATERAL reads >= table pages x devices / 2

Red first, before the section existed: every arm red -- except "under a tenth
of the table", which passed on empty SQL. A zero now is not a measurement.
Removal proof: the document's LATERAL edited to order ascending reddens the
parity arm alone, on PG 15..19, and in the pytest twin.

    latest_reading  8 passed, PG 15..19
    ledger 1866 -> 1874, never 1653 -> 1660
    harness_selftest, docs_style, guard half 404: pass

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@jdatcmd jdatcmd 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.

Approving 39c53ff. I'll merge when CI is green on this head, after #1288 (the registry order is #1288, then this and #1302, then #1286; whichever lands later renumbers TESTS.md and re-collects).

Reproduced on PG17, my worktrees from this head:

doc as written                     latest_reading  8 passed + 0 failed
doc's ORDER BY ts DESC -> ts        latest_reading  7 passed + 1 failed
  the LATERAL form returns what DISTINCT ON returns   got [ec72aab7d334] want [ba9c88f42158]

The red run catching "under a tenth of the table" passing on empty SQL is exactly why the ">= 1 block" premise belongs there. And leaving out the DISTINCT ON-with-btree trap because you could not pin it is the right call.

Follow-up, not blocking: 7 of the 8 arms are never beside a dated sibling, so this part joins #1236's reading list. Unlike #1302's pg_partman arms, these go through pgcolumnar's own scan, aggregate and fetch paths, so mutations exist. Two examples: the GROUP BY equality arm would redden under a fold that drops a group, and "with the btree ... under a tenth" under a fetch that decodes the whole row group's columns. That makes them unattacked rather than misnamed, which is #1236 follow-up work, not a blocker here.

🤖 Generated with Claude Code

@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

CI 15/15 at 39c53ff. From the pytest-cluster job logs, not the tick: PG 17 and PG 18 each report expecting 495 and 495 passed, with 0 unrunnable lines. So test_latest_reading ran on both and read the recipe from the document.

@jdatcmd

jdatcmd commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator

Merging now: approved on 39c53ff, 15/15 green on that head, same check set as #1301. I'm taking it ahead of #1288 because it is ready and #1288 still needs your approval; I'll rebase #1288 (TESTS.md 89/90, counts by collection) and #1286 after it.

@jdatcmd
jdatcmd merged commit 038600e into commandprompt:main Oct 12, 2026
15 checks passed
jdatcmd added a commit that referenced this pull request Oct 12, 2026
guard_tests 405 and cluster_tests 496, by collection on 038600e.
TESTS.md sections renumbered 88 and 89 after #1303's 87; ledger rows
replayed from the saved logs onto #1303's ledger (census 1660 -> 1664).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1.0: answer max(ts) and latest-per-device queries from zone maps

2 participants