Repository navigation
Conversation
Draft RFC: the v2 planner prices request waves on an alternative's critical path, charged by a storage profile recorded in the plan; the local profile charges nothing, so local and DST plans are unchanged. First decisions: expand mode and destination access path.
Review: request-aware plan cost RFCRecommendation: accept after the four optional items below are addressed or explicitly deferred. Why this RFCThe v2 planner picks operators by row count alone. On object storage, the plan with fewer rows can be 3x slower because its operators wait in series on dependent lookups. The RFC adds one term -- serial request-wave depth times a storage-profile latency -- to the existing row-cost formula, then sets the local-storage profile to zero so every local and DST plan is unchanged. The problem is real, the evidence is checked in, and the proposal is the smallest change that closes the gap. What it does
How it aligns
Alignment is clean. No invariant is under strain. What I verified
FindingsAll optional. None block the draft or its progression to accepted.
Tradeoffs and liability
SummaryThis is a well-motivated, narrowly scoped draft RFC with concrete measurements, clean invariant alignment, and a reversible design. The local-profile-is-zero property is the strongest design choice: it eliminates regression risk for every existing test and user. The four optional items above are clarifications, not design objections. No blocking issue. |
Price bytes per operator, make the AntiJoin csr_cached correction a co-requisite of step 2, clarify the indexed hop's per-batch lookup, require a reproducible calibration record, and record the average-degree limitation.
|
I addressed the review in 99ba93a, after merging
I also added a "Known limitation: average-degree estimates" section, following the hub-root note in #973's review. It now has Unresolved question 5: a declared run-time switch from
|
Review: request-aware plan cost RFC, head
|
| Warm step | Plan asserted (--- expect plan) |
Requests | Sim ms | RFC |
|---|---|---|---|---|
$c last |
expand $p $c: mode csr |
104 | 52.9 | 104, 53 ms |
$c first |
expand $c $p: mode indexed_scan, scan Person as $p: access id_lookup |
314 | 178.9 | 313, 161 ms |
The extra ~17 ms on the $c-first step is one more id_lookup slice: ⌈1,095 / 256⌉ = 5 slices, against 4 for 1,005 ids. The case was not added to the tree.
Code inspection at this head confirms: the cost formulas and constants; the 256-id serial id_lookup slices; the hash join's probe-first ordering; the indexed hop's per-batch, per-orientation scans; the serial CSR build per edge type; storage_kind_for_uri (with shared-memory:// as Local under dst); the Assumptions fields; and that issue_659_typed_edge_alternation.gqt is the only case that declares both environments and asserts a mode. The S3 traversal test (s3_storage.rs) pins Traversal::Csr, so the profile does not affect it.
Lance 11.0.0 source: filtered_read.rs (fragment_readahead defaults to io_parallelism × 2), lance-io object_store.rs (64 for cloud, 8 for local, LANCE_IO_THREADS), and dataset/statistics.rs. Upstream pages read in full: object stores, performance, observability, read/write, DataFusion, and BTREE.
Limits. No implementation exists to test, so no before/after regression proof applies: this PR fixes no bug. I did not measure the per-row CPU cost. B1 rests on the RFC's formula and constants, not on a timing. The performance claims stay unverified outside DST's storage-only model.
What & why
This draft RFC proposes that the v2 planner price storage round trips as well as rows. On object storage, the rows-only cost model picks plans built from dependent lookups (indexed hops,
id_lookupslices). Each of those waits on the one before. The plans it passes over read a table in parallel, and on object storage they win.The RFC adds one term to today's row costs: the request waves on an alternative's critical path, priced by a storage profile chosen from the snapshot's storage kind and recorded in the plan. A wave is a set of requests issued together, costing about one round trip. The local profile charges nothing for a wave, so local and DST plans are exactly today's plans, and the GQT corpus with them. The object-store profile charges about 20 ms per wave, which matches requests timed from inside a hosted server's region. The first two decisions to use the model are expand mode (indexed or CSR, at plan time and at run time) and destination access path (
id_lookuporhash_join). Results never change.The motivating measurement uses GQT
--measure --model s3-likeon DST, with 200,000 Person nodes in 50 fragments and a selectiveCompany { slug }binding, warm:hash_joinid_lookupOn a local filesystem the order reverses: 0.03 s against 0.16 s. So no rows-only constant serves both stores. Cardinality-driven anchor selection, Kuzu-style, would slow hosted reads until the cost model can see waves.
Backing issue / RFC
draft)Checklist
Local verification
python3 scripts/check-docs.py: Documentation OK.typoson the changed files: ok.Notes for reviewers
@azimafroozeh, this builds directly on the index-facts RFC and the way
gatherandAssumptionsrecord planner inputs, so your read would help most on:Assumptionsfield, because the storage kind cannot be derived from the root-relative dataset pins. Its constants ride onExpandCostInputs, so the run-time re-decisions price with the same model.CSR_BUILD_FACTOR,HASH_JOIN_RATIO) as per-row time ratios, and treats an indexed probe row and a scanned row as one unit. The local profile then reproduces today's decisions exactly.The open correlated-block strategy PR #973 is related. Its S3 request evidence and summed-rows pricing would read the same profile in a later change. Anchor selection, cache-aware CSR pricing, and correcting
AntiJoin's unconditionalcsr_cachedare explicitly out of scope.