perf(query): F17 — UNION/BIND row-budget forwarding + budget-met branch skip (q029 125s→2.6s)#1507
Open
aaj3f wants to merge 6 commits into
Open
perf(query): F17 — UNION/BIND row-budget forwarding + budget-met branch skip (q029 125s→2.6s)#1507aaj3f wants to merge 6 commits into
aaj3f wants to merge 6 commits into
Conversation
aaj3f
force-pushed
the
test/exploration-corpus-family
branch
from
July 15, 2026 17:42
2ae2f99 to
fd06533
Compare
Design for q029 (UNION under LIMIT re-scans both FACT_WEB_EVENT branches fully because the LIMIT budget dies at the UnionOperator AND BindOperator, both of which inherit the no-op set_row_budget). Fix = forward the budget through both (UNION to each branch, full k; BIND to its child), switch-gated FLUREE_R2RML_UNION_BUDGET. Every decline case (ORDER BY/DISTINCT/in-branch filter) is handled by the existing absorb boundary. Lead-approved.
q029 (`{WebEvent eventType=purchase} UNION {...add_to_cart} LIMIT 100`)
re-scanned both FACT_WEB_EVENT branches in full (~1.94M file reads for a
100-row answer) because the LIMIT budget died at TWO absorb points: the
UnionOperator and the BindOperator (q029's branch root is BIND(const AS
?et)), both inheriting the no-op set_row_budget.
Forward the budget through both, gated by FLUREE_R2RML_UNION_BUDGET
(default on):
- UnionOperator stores the budget and re-applies the FULL k to each branch
operator tree as it is built (a single branch may supply all k rows, so
it is not split across branches).
- BindOperator forwards to its child — reclassified from absorb to forward,
categorically sound because BIND is 1:1 and order-preserving.
The R2RML scan then caps its materialize window at k and stops after k
matching rows (the eventType equality is a fused scan constraint, so the
scan's `emitted` counts matches). Exact-LIMIT-preserving: the budget only
removes rows the LIMIT already discards, so switch ON == OFF.
Every decline case is handled by the EXISTING absorb boundary, no new
logic: ORDER BY (Sort absorbs), DISTINCT (Distinct absorbs), an in-branch
FILTER/reorder (absorb). Hermetic tests cover Bind-forwards, Sort/Distinct
absorb, the union store, and Limit->Union correctness; the per-branch scan
early-stop + q029 ON==OFF are the live gate.
Corpus blast radius = q029 only (q042 is UNION without LIMIT; q029 is the
sole BIND under LIMIT).
… outcome Live gate refuted two sketch predictions: (1) ON!=OFF hash — eventType is a post-decode residual + geometric window growth, so the budget picks a different valid first-100 (rows_only, not byte-identical) and each branch still sweeps the full table; the win is re-drive elimination (166.7->45.9s, decodes 1.94M->15.4K/126x), residual = PR-2a per-file floor. (2) the secondary lever DOES help q029 (branch-2 skip, ~46->~23s) — the earlier 'no corpus query needs it' was wrong. Dated corrections in sketch + register.
The union runs every branch for an input row before checking the budget, so once branch-1 fills the LIMIT budget, branch-2 still scans fully — for q029 that is a whole extra FACT_WEB_EVENT sweep (the 2nd of the two sweeps in the measured 45.9s). Break the union's branch/input loop once the buffered output meets the row budget (new `budget_met` flag, same FLUREE_R2RML_UNION_BUDGET switch). Sound on two counts: a budget reaches the union only when every operator up to the LIMIT is row/order-preserving (Sort/Distinct absorb), so the consumer wants an arbitrary k-subset (rows_only); AND the check fires only when the budget is actually MET, so an under-filled branch-1 still runs branch-2 and contributes its rows. Build-count hermetic tests: branch-1 fills k -> branch-2 not built; branch-1 under-fills -> branch-2 built; no budget -> both run. Reverses the earlier in-sketch deferral — its "no corpus query needs it" premise was refuted by the ON trace (q029's branch-2 sweeps fully after branch-1 supplies 100). Expected q029 -> ~1 sweep (~23s-class); wall re-gate pending the limited-network window.
…nter scope) q029 closes its burndown row at 2.61s in-protocol (F17 + cache stack); cold isolated first-ask ~57s-class (S3-fetch floor). The F17 3-rep baseline's 5 perf violations (q002/q004/q024/q030/q043) are attributed and are NOT F17: all five are UNION/BIND-free so F17's switch-gated code is inert, and it is NOT cache eviction (fetch_bytes≈0; artifact cache 90MB, 5.7x under the 512MiB cap). The wall is the loadTable catalog GET (~1.5s vs ~0.4s at bless — network-path). Filed as F21: the #1503 loadTable-pointer cache is TTL-window-bounded (300s + prune-on-read), so it spares the GET only within ~5min of a table's last touch (only 1 .pointer.json survived on disk). Mechanism question — TTL-window vs per-table/keying gap — deferred to a design sketch. q043's 6-GET edw:name fan-out is noted as an F20 widening candidate (class-constrained lang-tag star). doc-21 gets the dated TTL scope correction to its "warm first-ask = zero GET" claim.
…g note, SWITCHES inventory, fmt - union.rs: add the coupling cross-reference the review asked for at the budget-met lever — count (1)'s soundness rests entirely on the forward/absorb classification in Operator::set_row_budget's doc, so an author who forwards a budget through an order-SENSITIVE operator breaks this lever, not the forwarding site. Documented at the dangerous end. - SWITCHES.md: the standing switch-inventory ask — one page, one row per virtual perf lever (default / mechanism / PR / OFF-restores), the falsy convention, the loadTable TTL governing-bound note (#1503) + F21 pointer, and the corpus-side timeout semantics (timeout_s is CI headroom, not the bar). - cargo fmt (union.rs own-delta hunks). The FLUREE_R2RML_UNION_BUDGET naming nit is addressed by the SWITCHES.md scope row (it gates BIND forwarding too) rather than a rename. The scan early-stop test-seam suggestion is declined on binary-purity grounds (see review response).
aaj3f
force-pushed
the
perf/r2rml-f17-union-budget
branch
from
July 15, 2026 17:49
b096a37 to
c948f0a
Compare
This was referenced Jul 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
q029 (
{ WebEvent, eventType="purchase" } UNION { WebEvent, eventType="add_to_cart" } LIMIT 100) re-scanned bothFACT_WEB_EVENTbranches ~253× — 1.94 M file decodes for a 100-row answer — because theLIMIT 100budget never reached the branch scans. It died at two absorb points:UnionOperatorandBindOperator(q029's branch root isBIND(const AS ?et)), both inheriting the trait's no-opset_row_budget; and once branch-1 filled the budget the union still drained branch-2.Two changes, both gated by
FLUREE_R2RML_UNION_BUDGET(default on):UnionOperator(the fullkto each branch — a single branch may supply allk) andBindOperator(to its child; BIND is categorically 1:1/order-preserving, reclassified from absorb to forward). The R2RML scan then caps its materialize window atk.pending_output_rows >= row_budget. Sound on two counts — a budget reaches the union only under row/order-preserving operators up to the LIMIT (Sort/Distinct absorb ⇒ arbitrary-k-subset /rows_only), and the check fires only when the budget is actually MET (an under-filled branch-1 still runs branch-2).Result
q029: 125 s (pre-slate) → 2.61 s in-protocol (3-rep per-head baseline, F17 + the #1503/artifact cache stack) — under the ≤3 s bar; burndown row closes. Deterministic, network-invariant evidence (live gate):
scan_table253 → 2, parquet decodes 1,940,510 → 7,703 (2 sweeps → 1), OFF byte-preserved, oracle-parity 0-mismatch. Full-corpus baseline: 59 records, 0 hash mismatches.Cold isolated first-ask is ~57 s-class and is reported, not gated: the residual is one full
FACT_WEB_EVENTsweep, S3-fetch-bound (decode trivial). Wall model:~7 s fixed (loadTable+oauth+scan_plan) + N_sweeps × one-sweep-fetch; the union is sequential, so the lever removes one whole sweep. This is the shared PR-2a floor (q016/q031/q056) — fetch-rate-sensitive, not decode.Honest notes
eventTypeis a post-decode residual and the scan window is geometric-growth, so the budget materializes a different valid first-100 →rows_only(same class as q055/q057). Oracle parity holds.Perf violations at this head — documented, not blessed away
The 3-rep baseline flagged 5 pre-existing perf violations (q002/q004/q024/q030/q043, 2.5–8× over blessed). Attributed and NOT caused by F17:
fetch_bytes≈ 0 for every one (files disk-served; artifact cache 90 MB, 5.7× under the 512 MiB cap) — disproves any cache-eviction / regression story.edw:namename-dim fan-out (F20-family, pre-existing).Tests
fluree-db-query, 13 union tests): BIND forwards; Sort/Distinct absorb (decline proofs); union stores; Limit→Union correctness; 3 build-count lever tests (branch-1 fills k ⇒ branch-2 not built; under-fills ⇒ built; no budget ⇒ both run).-p fluree-db-querysuite: 1256 passed, 0 failed.row_budgetand union branches are pattern-built, so there is no hermetic mock-scan seam (a#[cfg(test)]injection hook was considered and declined to keep the binary scaffolding-free).Harness
The live-gate script's OFF arm ceiling was raised 180 s → 300 s (its ~167 s baseline is fragile at 180 — same headroom ratio as q056 168/180). Manifest
timeout_sstays 180 (shipped F17-ON q029 is ~2.6 s in-protocol, far under it).Stacked on #1506 (
test/exploration-corpus-family). Commits: sketch → forwarding → doc-correction → lever → baseline-verdict/F21-docs.