fix(mem_wal): push LIMIT/OFFSET down to fresh-tier scan sources#7256
Conversation
The LSM scan planner applied a query's LIMIT/OFFSET above the per-source union, so every flushed generation and the base table were fully scanned before the limit was imposed at the top. For a bounded read over a many- generation fresh tier this materialized the entire tier per query. Thread an `overfetch_factor` through `LsmScanner`/`LsmScanPlanner` and push a per-source fetch of `(offset + limit) * overfetch` into each Base/Flushed source scan (the active memtable is exempt). Block-listed sources over-fetch so cross-generation PK dedup still leaves ~limit live rows; a `LocalLimit` re-imposes the exact cap above the merge. Unbounded reads are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fccd679 to
1211c17
Compare
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
| flushed_cache: Option<Arc<FlushedMemTableCache>>, | ||
| /// Over-fetch multiple for block-listed sources in search plans | ||
| /// (see [`super::LsmFtsSearchPlanner::with_overfetch_factor`]). | ||
| overfetch_factor: Option<f64>, |
There was a problem hiding this comment.
oh better to understand whether it's similar to refine factors in vector / fts
not saying we should use refine factor
There was a problem hiding this comment.
Great point. I'm trying to keep these separate because they are logically different things. IIUC the refine_factor is used because IVF distances may not be exact, so we over retrieve so that we can become more accurate. The proposed overfetch_factor is strictly for probabilistically ensuring we have K values after we remove duplicates. This is a simple solution for the problem, but ultimately we will probably want to fix it in another way. So by separating them, we can remove the overfetch_factor more easily.
Summary
The LSM scan planner (
LsmScanPlanner) applied a query'sLIMIT/OFFSETabove the per-source union, so every flushed generation and the base table were fully scanned before the limit was imposed at the top. For a bounded read over a fresh tier with many flushed generations this materialized the entire tier on every query.This threads an
overfetch_factorthroughLsmScanner→LsmScanPlannerand pushes a per-source fetch of(offset + limit) * overfetchinto eachBase/Flushedsource scan. The active memtable is exempt (its within-source dedup needs the full match set). Block-listed sources over-fetch byoverfetch_factorso cross-generation PK dedup still leaves ~limitlive rows; aLocalLimitre-imposes the exactskip/fetchcap above the merge. Unbounded reads (no limit) are unchanged.Changes
scanner/planner.rs:overfetch_factorfield +with_overfetch_factoronLsmScanPlanner; per-source limit pushdown inplan_scan;fetchparameter onbuild_source_scan(pushed into Base/Flushed scans, ActiveMemTable exempt).scanner/builder.rs:overfetch_factoronLsmScanner+with_overfetch_factor, threaded into both the scan and FTS planners.Validation
Validated end-to-end against a WAL benchmark on minikube with object storage behind a latency proxy: a bounded
offset+limitread over an 18-generation fresh tier dropped from a full-tier scan (~360 generation scans) to a bounded per-source read.🤖 Generated with Claude Code