Skip to content

perf(r2rml): scan-side top-k for ORDER BY DESC LIMIT (PR-5, cold 37s→3.1s) - #1495

Closed
aaj3f wants to merge 6 commits into
perf/r2rml-pr7-numeric-statsfrom
perf/r2rml-pr5-scan-topk
Closed

perf(r2rml): scan-side top-k for ORDER BY DESC LIMIT (PR-5, cold 37s→3.1s)#1495
aaj3f wants to merge 6 commits into
perf/r2rml-pr7-numeric-statsfrom
perf/r2rml-pr5-scan-topk

Conversation

@aaj3f

@aaj3f aaj3f commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

What

PR-5 (the final roadmap item): scan-side top-k for ORDER BY DESC(<scan col>) LIMIT k over a single-table R2RML/Iceberg scan. The scan reads files in upper_bound(sort_col)-DESC order, keeps a running k-th bound, and stops once no unread file can beat it — reading ~k files instead of the whole table.

Closing gate (DW_SF01, 7670 files per fact table)

q046 wall files_pruned / selected hash
ON (hot) 136ms 7660 / 10 == oracle
OFF (hot) 406ms 0 / 7670 == oracle (byte-identical revert)
COLD 3129ms 7660 == oracle

Cold ~37s → 3.1s (~12×), files_pruned 7660/7670 = 99.87% — the headline. ON is faster than OFF even hot. ORDER-BY-class parity sweep (q005/q012 declined via fallback, q046 identical), native 54/54, and the W3C ORDER BY/LIMIT suite: 0 hash mismatch, 0 perf violations.

Honest scope: q046 is the only corpus query this helps (raw-column top-k over a fact table), and the win is cold (warm q046 was already ~400ms). PR-7's numeric stats paid the hard cost; this is the harvest on the canonical "top N by measure" shape.

How

The SortOperator offers a TopK(primary DESC var, k = LIMIT + OFFSET) directive to the scan below (mirroring row_budget); GraphOperator threads it into its per-parent inner subplan (where a virtual query's scan lives), and ProjectOperator forwards it. The api scan reads files in bound-DESC order with a size-(k+offset) heap and early-stops; a TOPK_SEQUENTIAL_CAP fallback to the parallel reader means an ineffective prune is never slower than the full scan. sort.rs::new_topk above stays the sole authority for the exact (compound) order + LIMIT — the scan only skips files it proves cannot contribute, so results are byte-identical to full sort.

Invariants

  • Strict superset: the scan streams every row of every non-pruned file; the authoritative sort above re-selects the exact top-k → results identical to full sort.
  • Heap fed post-filter qualifying rows only: resolve_topk_directive DECLINES the pushdown when the scan carries a RESIDUAL filter (consumed_filter / object_constant / subject_constant / star_constraints) the operator enforces after emit — feeding a superset would ride the k-th bound too high and silently drop qualifying rows. Pushed scan filters are applied by the reader (the emitted batch is already post-filter), so they compose fine.
  • No pruned result is ever cached: a top-k scan bypasses the per-operator scan cache (cacheable && self.topk.is_none()), keyed on the STORED directive — so a declined-topk scan runs FULL and merely skips the cache, never caching a subset that a later full scan of the same (table, projection) could replay.
  • k = LIMIT + OFFSET (single-owner arithmetic) so the scan retains the rows the sort's OFFSET then skips.
  • DESC only: ASC declines to fallback — SPARQL orders unbound values first in ASC, so a null-bearing file could never be pruned.

Kill switch

FLUREE_R2RML_TOPK_PUSHDOWN (default on); off ⇒ no directive ⇒ today's full-materialize top-k.

Tests

iceberg core (10): TopKBound bound/tie/NaN, plan_topk_read / batch_sort_values, read-loop simulations (prune + k-exceeds-non-null + all-null-file). query: topk_declines_on_residual_filter (all four residual shapes + pure-scan positive), topk_scan_bypasses_scan_cache (cache-poison — a topk scan re-reads each batch vs a cached full scan), topk_directive_carries_limit_plus_offset (k = LIMIT+OFFSET). All green; clippy/fmt clean; the full -p fluree-db-api --features iceberg suite is green.

Note on observability: the topk branch emits an iceberg.scan_plan span for the harness counters; on the cold miss arm the planner also emits one (the pre-topk plan), so files_selected double-counts there while files_pruned stays authoritative — the gate ratio is files_pruned / total_files.

Stacked on perf/r2rml-pr7-numeric-stats. The F13 native-perf re-bless rides on this branch. Closes the virtual-dataset perf roadmap.

aaj3f added 5 commits July 15, 2026 13:04
…(F13)

The native micro-query perf baselines had drifted from the current machine
state, firing recurring false SLOW alarms on every gate (q034 blessed 176ms vs
~320ms observed, q050 96ms vs ~300ms observed). A base A/B on c4a9b79 already
showed the ratio pre-dates the code under test (04-findings-register F13), so
this is machine drift, not a regression. Re-blessed from a fresh quiet-machine
native-sf01 5-rep run: q034 176->322, q050 96->283; others stable (q046 202->193,
q005 3->3, q012 1031->1047). Kills the false SLOWs before PR-5's gates.
…ers (PR-5 core)

The correctness core for the PR-5 scan-side ORDER BY DESC LIMIT pushdown, ahead of
the directive plumbing + async read branch + ORDER BY detection:

- TopKBound: the running k-th bound over the sort column's non-null values, with
  strict-`<` over-keep at the boundary, a heap-FULL precondition for any prune
  (so k-exceeds-non-null reads everything), and NaN-safe compares (a NaN key only
  weakens pruning, never over-prunes).
- plan_topk_read: the DESC read order by decoded upper_bound, with no-bound files
  (all-NULL column / missing stats) LAST so they are always read.
- batch_sort_values: null-skipping extraction of the sort column as TypedValues.

Pure + unit-tested (10 tests: tie boundary, evict-k-th, k-exceeds-non-null
reads-all, all-null-file-last, null-skipping, int/string/double keys). Also adds
the approved design doc (15-pr5-scan-topk.md) with the four riders + DESC-only.
Plumbs the DESC ORDER BY LIMIT top-k directive from the SortOperator down to the
Iceberg scan, on top of the PR-5 core engine (d94d6e4):

- provider: ScanTopK{sort_column,k} + a topk param on the scan_table trait.
- query operator: a topk (sort_var,k) field + set_topk (mirroring set_row_budget,
  gated by FLUREE_R2RML_TOPK_PUSHDOWN) + resolve_topk_directive (var -> single
  scalar scan column, the build_scan_filters soundness gate). The main-table scan
  carries the resolved directive; parent/dimension and aggregate scans never do. A
  topk scan bypasses the scan cache (cacheable gains && topk.is_none()) -- a pruned
  subset under (table, projection) would silently drop rows for a later full scan.
- detection: apply_solution_modifiers offers the primary DESC sort var + (limit +
  offset) to the scan below for a non-DISTINCT ORDER BY DESC LIMIT; ASC and the
  pre-sort-DISTINCT path decline.
- api scan: when the directive resolves, read files in upper_bound-DESC order with
  the running k-th bound and stop early, streaming a strict superset; the sort
  above stays authoritative. Bounded by TOPK_SEQUENTIAL_CAP (128) -- an ineffective
  prune finishes on the normal parallel reader, so topk is never slower than the
  full scan. The disk artifact cache (path+size keyed, whole-file) can't be
  poisoned by a subset.

All 1232 query + 182 iceberg unit tests pass; clippy/fmt clean.
…s guard (PR-5)

Completes the PR-5 scan-side top-k pushdown: the directive now reaches the R2RML
scan through the GraphOperator wrapper, with the residual-filter soundness guard,
observability, and the cache-poison / OFFSET locks.

- Forwarding: a virtual query's scan lives in a GraphOperator's per-parent inner
  subplan, so set_topk offered to the wrapper hit its default no-op. GraphOperator
  now threads the directive into both inner-subplan build sites (exactly as it does
  row_budget); ProjectOperator forwards it; Offset (above the sort, k already owns
  +offset) and Join decline.
- Soundness guard: resolve_topk_directive DECLINES the pushdown when the scan
  carries a RESIDUAL filter (consumed_filter / object_constant / subject_constant /
  star_constraints) the operator enforces after emit -- otherwise the heap would be
  fed a superset and the k-th bound could ride too high and drop qualifying rows.
  Feed-site invariant comment; the cache bypass keys on the STORED directive so a
  declined-topk scan runs FULL and merely skips the cache (never caches a subset).
- Observability: the topk branch emits an iceberg.scan_plan span
  (files_selected=reads, files_pruned=total-reads) so the harness counters capture
  the prune. On the cold miss arm the planner also emits a scan_plan span (the
  pre-topk plan), so files_selected double-counts there while files_pruned stays
  authoritative -- the gate ratio is files_pruned/total_files.
- Tests: topk_declines_on_residual_filter (4 residual shapes + pure positive),
  topk_scan_bypasses_scan_cache (a topk scan re-reads each batch vs a cached full
  scan), topk_directive_carries_limit_plus_offset (k = LIMIT+OFFSET). The ScanTopK
  trait param threaded into the remaining mock impls.

Closing gate (DW_SF01, 7670 files): q046 cold ~37s -> 3.1s (~12x), files_pruned
7660/7670 = 99.87%; hot ON 136ms vs OFF 406ms with byte-identical hashes (the
revert differential); ORDER-BY sweep + native 54/54 + W3C green, 0 perf violations.
A data-identical virtual-sf01 clone used by the stacked-rebaseline protocol
(docs/audit/2026-07-virtual-dataset-perf/09-stacked-rebaseline.md) to bless a
post-PR-stack perf reference without clobbering the pre-PR baseline. It was left
untracked in the worktree; tracking it so the protocol target is reproducible.
@aaj3f
aaj3f force-pushed the perf/r2rml-pr7-numeric-stats branch from 2a07bbb to b0bba4f Compare July 15, 2026 17:04
…op-k

resolve_topk_directive silently declined the scan-side top-k when the sort
predicate mapped to more than one POM (a duplicate-predicate mapping), with
nothing in the logs. Add a debug! breadcrumb matching PR-7's decline-observably
convention (#1495 review). No behavior change; the other #1495 doc-comment
optionals (can_topk !distinct note, topk.rs stats line) are noted in the response.
@aaj3f

aaj3f commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Consolidating at maintainer request. #1495 (PR-5 — scan-side top-k for ORDER BY DESC + LIMIT) is carried forward in #1507 (the R2RML/Iceberg virtual-dataset performance program), which brings the #1475#1507 lineage forward as a single reviewable unit. Its verification of record is the program's corpus benchmark, published as C2-bench-wave1.md (native == virtual, 0 hash mismatches). The pre-consolidation branch tip is preserved at tag archive/pre-refactor-2026-07-21/perf_r2rml-pr5-scan-topk — no history was rewritten. Closing in favor of #1507; discussion continues there.

@aaj3f aaj3f closed this Jul 21, 2026
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