Skip to content

db,collections: columnar two-pass top-K (ORDER BY <numeric> LIMIT k at MAX speed) - #223

Merged
bbockelm merged 2 commits into
mainfrom
columnar-topk
Aug 23, 2026
Merged

db,collections: columnar two-pass top-K (ORDER BY <numeric> LIMIT k at MAX speed)#223
bbockelm merged 2 commits into
mainfrom
columnar-topk

Conversation

@bbockelm

Copy link
Copy Markdown
Collaborator

Makes ORDER BY <numeric attr> {DESC|ASC} LIMIT k as fast as the equivalent MAX/MIN. Based on main.

Problem

db.TopK fed QueryProject — it built a classad.Value for every matching row (e.g. ~1M for ProcId < 5), heaped them, and kept k. On a production archive ORDER BY ClusterId DESC LIMIT 1 took 9.1s while the logically identical MAX(ClusterId) WHERE ProcId < 5 took 2.5s, because MAX reduces raw column values with no per-row allocation and TopK allocated a million Values to discard.

Fix: two-pass columnar top-K

  1. Cutoff (columnar). Collection.TopKOrderThreshold scans the order column the same way the numeric aggregate does — block-zone pruning, MVCC visibility, cold-tail escape handling, active-segment row fallback — but keeps the best k values and returns the k-th (the cutoff). It reads only floats; it never materializes a record. Same availability gate as NumStatsQuery (accelerator on, numeric schema field, conjunction of scalar numeric comparisons).
  2. Fetch (narrowed). db.TopK re-runs the projection with orderAttr >= cutoff (desc) / <= cutoff (asc) appended. The columns and zone maps narrow that to ~k rows (plus ties), so only those are projected and heaped.

Falls back to the original full-scan path unchanged when the cutoff is unavailable (accelerator off, non-numeric order key, non-columnar constraint) or when there are ≤k matching rows, so behavior is never worse than before. Capped at k ≤ 65536 (beyond that "top-k" ≈ full sort). Both the archive and mutable-table paths use it.

Expected: ORDER BY ClusterId DESC LIMIT 1 WHERE ProcId < 5 drops from ~9s to ~MAX speed.

Correctness

db.TestArchiveTopKColumnarMatchesBrute compares the two-pass to an independent brute-force top-K across desc/asc, k ∈ {1, 3, 50, >matches}, four filters (including one that excludes the row holding the global max, forcing the cutoff to reflect the filtered max), on both a columnarized archive and a non-columnarized one — they must agree with each other and with brute. It also checks argmax reassembly (selecting a non-order column with LIMIT 1 returns that column from the actual winning row). Mutation-checked: perturbing the cutoff makes the columnar case fail, proving the fast path is exercised, not silently bypassed. collections.TestTopKOrderThreshold pins the cutoff value + match count directly. Full db suite and the columnar collections suite pass.

🤖 Generated with Claude Code

…t MAX speed)

ORDER BY ClusterId DESC LIMIT 1 WHERE ProcId < 5 took 9.1s while MAX(ClusterId)
WHERE ProcId < 5 took 2.5s -- the same answer. TopK fed QueryProject, building a
classad.Value for every matching row (~1M) and heaping them to keep k; MAX reduces
raw column values with no per-row allocation.

Two-pass:
- collections.TopKOrderThreshold scans the order column like the numeric aggregate
  (block-zone pruning, MVCC visibility, cold-tail escape, active-segment row
  fallback) but keeps the best k VALUES and returns the k-th (the cutoff) -- raw
  floats, never materializing a record. Same availability gate as NumStatsQuery.
- db.TopK re-runs the projection with  appended, which
  columns and zone maps narrow to ~k rows (plus ties); only those are projected.

Falls back to the original full scan when the cutoff is unavailable or there are
<=k matches, so it is never worse; capped at k<=65536. Archive and mutable paths.

TestArchiveTopKColumnarMatchesBrute: two-pass == brute-force top-K across desc/asc,
k in {1,3,50,>matches}, four filters (incl. one excluding the global-max row), on
columnarized AND fallback archives; argmax reassembly of a non-order column;
mutation-checked. TestTopKOrderThreshold pins the cutoff + match count. db suite +
columnar collections suite green.

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

codecov Bot commented Aug 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…sibility)

A top-K query was invisible to EXPLAIN ANALYZE: the opTopK op returned rows with no
scan-stats trailer, so the diagnostic showed only fetch/rows and no scan breakdown.

The cutoff scan (TopKOrderThreshold) now fills a ScanStats -- records visited,
column-decided (columnar sealed segments) vs reassembled (active-segment fallback),
and matched -- reusing the same ScanStats the projected scan reports. db.TopKStats
returns it; a new opTopKStats op streams the k rows plus a stStreamStats trailer
(reusing putScanStats/readScanStats); Client.TopKStats reads it. Separate opcode so
an older server rejects it and the client falls back to opTopK (rows, no breakdown).

TestTopKStatsOverRPC asserts the trailer round-trips (visited>0, matched == the
filtered set) over a columnarized archive; db TopKStats assertion added. Full db and
dbrpc suites green.

Follow-up (htcondordb): wire EXPLAIN ANALYZE of a top-K-routed query to Client.TopKStats
and render via the existing scanStatsLines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bbockelm
bbockelm merged commit 5335d8d into main Aug 23, 2026
11 checks passed
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 participant