Repository navigation
DuckDB V2 scan: cut chunks on file boundaries, default it for TPC-DS and ClickBench - #10833
joseph-isaacs wants to merge 27 commits into
Conversation
A planner says what it needs, compute or reads. Where either runs is an engine's decision, so this crate stops driving: `ScanBuilder::prepare` lowers and plans a scan without IO into a `PreparedScan`, which exposes its rows, selection, filter plans and IO service, and `PrunePlanner` and `ChunkPlanner` are public for an engine to compose, the prune planner continuing into whatever it is built with and passing `None` when no proof ran. The builder's ordering, concurrency, mapping and streams go with the driver, and the layout's tests drive their scans on a run of their own. Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The opener composes a scan's planners, a prune planner into a chunk planner when the scan has a filter and the chunk planner alone when it does not, and a driver of this crate runs them: the root run stepped on `spawn_cpu` tasks as DataFusion polls the stream, each spawned chunk on a task of its own up to the concurrency, or on the root run when the engine's partitions already fill the cores, with the concurrency derived from cores per partition unless configured. Batches are mapped to Arrow on the task that produced them. Every spawning decision of a V2 scan under DataFusion is in this crate. Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`vortex_file::v2::ScanDriver` composes a scan's planners and runs them on the session's runtime, the way an engine's driver does, for the file API's callers: the compress and random-access benchmarks move to it. Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The opener prepares the scan where compute runs, composes its planners itself, a prune planner continuing into a chunk planner when there is a filter and the chunk planner alone when there is not, and hands the driver the first planner. The driver only drives. Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The prune planner skipped prefetching the zone maps of proofs an earlier scan of the file had proved. The segment list now comes from compiling the proof, and a proved proof compiles to its kept result without a segment scan, so the check repeated what compile already knew. Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The scan code named the filter's zone-map plans proofs. They are the filter's pruning expressions evaluated over the file's zone map, so the layout, file and DataFusion code now says pruning, matching prune_zones, PrunedZones and the pruning field they already used. Behaviour is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>
DuckDB scans can now run on the V2 scan executor, like DataFusion's scan_v2 option. Each file is cut into one block-aligned row range per core, and every range is a V2 scan stream that shares the file's ScanCache. The extension enables it with VORTEX_SCAN_V2=1 or vortex_duckdb::set_scan_v2. duckdb-bench gains --scan-v2, which defaults to true for TPC-H, matching datafusion-bench. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8NsCJMx4C45Wf2Df24qzZ
Cutting a file into one equal row range per core put split ends inside the layout's 65,536-row chunks. On many-core machines a split covered only part of a chunk, so every chunk was read and decoded by up to eight splits; CI's TPC-H SF1 DuckDB run regressed by 15%. Splits now end on the natural split boundaries the V1 scan uses, spread evenly over the cores, so each chunk belongs to one split. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8NsCJMx4C45Wf2Df24qzZ
…kDB V2 branch The DuckDB driver composes and drives the V2 planners the same way. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8NsCJMx4C45Wf2Df24qzZ
Like the DataFusion opener, the DuckDB extension now prepares a V2 scan and composes its planners itself, and drives them with its own driver. Each file is prepared once; the root planner's run is shared by every thread scanning the file. A thread takes the next part the root spawns and runs its chunks inline on a run of its own, waiting only for its reads, so there are no per-split scans, task hops or channels. The root cuts the rows into a few parts per thread on whole 65,536-row blocks, so filtered scans, whose chunks are up to 1,048,576 rows, still spread over every thread and no chunk spans two parts. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8NsCJMx4C45Wf2Df24qzZ
Brings in the Cargo.lock update and zstd advisory ignores (#10810). Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8NsCJMx4C45Wf2Df24qzZ
Brings in the cc 1.8.0 pin that fixes the Windows CI build (#10815). Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8NsCJMx4C45Wf2Df24qzZ
With #10708 on develop, this branch's diff is only the DuckDB driver. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8NsCJMx4C45Wf2Df24qzZ
The CI SQL benchmarks then compare DuckDB's V2 scan with the V1 baseline on those suites too, as for TPC-H. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8NsCJMx4C45Wf2Df24qzZ
Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8NsCJMx4C45Wf2Df24qzZ
Polar Signals Profiling ResultsLatest Run
Previous Runs (57)
Powered by Polar Signals Cloud |
Merging this PR will degrade performance by 10.63%3 warnings, 1 note
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing Footnotes
|
Benchmarks: PolarSignals Profiling 📖Commits: PR datafusion / vortex-file-compressed / ns (0.991x ➖, 0↑ 0↓)
No file size changes detected. |
Benchmarks: TPC-H SF=1 on NVME 📖Commits: PR How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (0.959x ➖, 0↑ 0↓)
datafusion / parquet / ns (0.998x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed / ns (0.977x ➖, 3↑ 0↓)
duckdb / parquet / ns (0.999x ➖, 0↑ 0↓)
No file size changes detected. |
Benchmarks: TPC-H SF=10 on NVME 📖Commits: PR How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (0.980x ➖, 0↑ 0↓)
datafusion / parquet / ns (0.998x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed / ns (0.992x ➖, 1↑ 0↓)
duckdb / parquet / ns (0.997x ➖, 0↑ 0↓)
No file size changes detected. |
Benchmarks: TPC-DS SF=1 on NVME 📖Commits: PR How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (0.995x ➖, 0↑ 1↓)
datafusion / parquet / ns (0.996x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed / ns (1.003x ➖, 1↑ 1↓)
duckdb / parquet / ns (0.998x ➖, 0↑ 0↓)
No file size changes detected. |
Benchmarks: FineWeb NVMe 📖Commits: PR How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (1.003x ➖, 0↑ 0↓)
datafusion / parquet / ns (1.006x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed / ns (0.994x ➖, 0↑ 0↓)
duckdb / parquet / ns (1.008x ➖, 0↑ 0↓)
No file size changes detected. |
Benchmarks: Clickbench on NVME 📖Commits: PR How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (0.998x ➖, 0↑ 0↓)
datafusion / parquet / ns (0.993x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed / ns (1.006x ➖, 2↑ 0↓)
duckdb / parquet / ns (1.008x ➖, 0↑ 0↓)
No file size changes detected. |
Benchmarks: TPC-H SF=1 on S3 📖Commits: PR How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (0.881x ➖, 2↑ 0↓)
datafusion / parquet / ns (0.936x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed / ns (0.979x ➖, 0↑ 1↓)
duckdb / parquet / ns (1.026x ➖, 0↑ 2↓)
|
The DuckDB V2 driver cut parts on 64K-row blocks and the chunk planner cut chunks on a fixed grid. Files whose chunks are not 64K rows, such as ClickBench's 87K and 95K-row chunks, had nearly every chunk span two scan chunks, so it was decoded twice. Take the boundaries V1 splits on (the referenced columns' chunk boundaries, with wide spans subdivided) and cut chunks only there, merging spans up to the scan's chunk size. Parts group whole chunks. ChunkPlanner gains a with_chunks constructor for drivers that cut the rows themselves. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8NsCJMx4C45Wf2Df24qzZ
|
The Generated by Claude Code |
Pass --scan-v2 true from the benchmark orchestrator for DuckDB's TPC-DS and ClickBench runs instead of changing duckdb-bench's default, which goes back to V2 for TPC-H only. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8NsCJMx4C45Wf2Df24qzZ
Each chunk a part spawned was admitted as its own root with a fresh IO session, and a session caches only its own reads. For a wide projection the chunks are cut on every column's boundaries, so a column chunk spanning several scan chunks was read once per scan chunk. DuckDB's late-materialization fetch of ClickBench q23 (SELECT * ... ORDER BY ... LIMIT 10) read many segments two or three times. ChunkPlanner::inline emits the chunks as children of the part's root, on its run and session, instead of spawning them. A thread runs one part at a time, so this gives up no parallelism. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8NsCJMx4C45Wf2Df24qzZ
datafusion-bench and duckdb-bench no longer pick the V2 Vortex scan by benchmark or read VORTEX_SCAN_V2: --scan-v2 defaults to false. The benchmark orchestrator passes --scan-v2 true for DataFusion TPC-H and for DuckDB TPC-H, TPC-DS and ClickBench. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8NsCJMx4C45Wf2Df24qzZ
A file is planned, pruning included, when DuckDB initializes it. With 100 files and 96 threads every file is planned before DuckDB's top-N bound has tightened, so a dynamic comparison prunes nothing and every chunk is read. V1 prunes each split when it runs and skips the splits the tightened bound excludes: on ClickBench q24 V2 read 44 segments (8.5 MB) V1 never touched. When the filter has a dynamic comparison, run a part behind its own pruning when a thread takes it, intersected with the file's static pruning. The zone maps were read when the file was planned, so this reads nothing new. Adds top-N cases to the V2-matches-V1 test. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8NsCJMx4C45Wf2Df24qzZ
Planning a file runs its pruning plans before DuckDB's top-N bound exists, so under a dynamic filter they prove little and only delay the file's first part behind the zone map reads, which for a long string column such as ClickBench's URL are large. Each part is pruned when a thread takes it anyway, so skip the file-level pruning when the filter is dynamic and let the parts prune, in parallel, against the latest bound. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8NsCJMx4C45Wf2Df24qzZ
Every part of a V2 scan reads through its own IO session, so parts whose chunks cut into the same column segment each read it. ClickBench q23's row-id fetch, which reads all 105 columns for a few rows, re-requested 430 segments (46 MB) that V1's layout readers read once. Keep a scan-wide map of each segment's read, held weakly, so a session shares a read another session still holds. A read lives no longer than before: the map only keeps it while some session does. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8NsCJMx4C45Wf2Df24qzZ
This reverts e4eb6f2. CI measured ClickBench q24 at +7.6% against V1 without the file-level pruning, where it was -8.8% with it, and no query gained, so prune the file at initialize and each part again when a thread takes it. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8NsCJMx4C45Wf2Df24qzZ
A projection that packs fields by path, such as DuckDB's row-id fetch of every column beside its row ids, kept an Eval of the whole pack above a Pack plan. Each chunk then built the struct, renamed its fields and packed it again, comparing the 105-field ClickBench dtypes deeply at each step, and rebuilt a row index for every row of the chunk before taking the few selected ones. EvalPackRule now lifts a pack whose fields are all field paths into a non-nullable Pack child into one PackPlan, stepping each path straight into its column's plan, and ExpressionPackRule steps a field path into its field without annotating every field. RowIdx compiles a lazy sequence, as the V1 row index reader does, so a sparse mask produces only its rows. On ClickBench q23, single threaded under callgrind, the V2 scan now runs 3.21G instructions, against 3.56G before and 3.32G for V1, and plans each file in a third of the time. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8NsCJMx4C45Wf2Df24qzZ
A row-id fetch, such as DuckDB's late materialization in ClickBench q23, selects a few rows of each file, but each wide column segment it touches is needed by every chunk that selects a row of it. The chunks run one after another, so sharing a read only while another session holds it read those segments again: 3905 requests for 2312 segments, 849 MB read for 321 MB of segments. A scan whose selection keeps at most one row in 1024 now retains its reads for its lifetime, as V1's layout readers do. The DuckDB driver also planned every chunk of such a file as a part, most of them empty, and offered DuckDB a thread for each. It now keeps only the chunks with a selected row, each its own part. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8NsCJMx4C45Wf2Df24qzZ
|
The failing CodSpeed Performance Analysis check on 0ad75b1 flags one benchmark: This isn't from this PR's changes: the PR changes only Generated by Claude Code |
Summary
Follow-up to #10809, which added the DuckDB V2 scan driver and turned it on for TPC-H only. This PR also runs TPC-DS and ClickBench on V2, and fixes the driver problems those suites exposed. The choice of which benchmarks run V2 now lives in
vx-bench, not in the Rust benchmark runners.Changes
vx-bench(bench-orchestrator/bench_orchestrator/runner/executor.py).SCAN_V2_BENCHMARKSpasses--scan-v2 truefor DuckDB TPC-H, TPC-DS and ClickBench, and DataFusion TPC-H.duckdb-benchanddatafusion-benchhave no per-benchmark special cases:--scan-v2is a plain flag that defaults tofalse.vortex-duckdb/src/scan_v2.rs,file_reader.rs).SplitBy::default()over the referenced columns).parallelism × 4chunks per file.reader_initializeon the thread DuckDB uses to initialize the file, as V1 builds its splits there. Its parts are queued.ChunkPlanner::inlineinvortex-layout/src/scan/execution/chunk.rs). A part's chunks run as children of the part, so they share one IO session.ORDER BY … LIMITfilter, file-level pruning runs before any bound exists.PrunePlannerwhen a thread takes it, against the latest bound.vortex-layout/src/scan/v2/io.rs).SegmentScanIokeeps a map of segment reads, so a session reuses a read another session still holds. The map holds reads weakly, so no segment is kept alive longer than before.vortex-layout/src/plan/physical/plans/eval.rs,pack.rs).EvalPackRulelifts a pack whose fields are all field paths into aPackchild into onePackPlan, stepping each path into its column's plan.ExpressionPackRulesteps a field path into its field without annotating every field.plans/row_idx.rs), as the V1 row-index reader does, so a sparse mask produces only its rows.vortex-duckdb/src/scan_v2.rs). Each is its own part, so DuckDB is offered a thread per chunk that has work.Benchmarks (DuckDB V2 vs base)
Each run's base is the
developcommit it merged with: TPC-H's base already runs V2, while TPC-DS and ClickBench compare against V1. All suites read "No clear signal". DataFusion TPC-H (V2 on both sides) is −3.9% at SF1 and −1.8% at SF10 on 0ad75b1, so the plan-rule change slowed nothing.The three ClickBench
ORDER BY … LIMIT 10queries (hot):e4eb6f2 dropped the file-level pruning under dynamic filters and kept only the per-part pruning. q24 went back to +7.6%, so 3b2d95d reverts it.
Checks
cargo clippy -p vortex-layout -p vortex-duckdb --all-targets --all-features -- -D warningsand nightlyfmt: clean.cargo test -p vortex-layout --lib: 668 pass, includingsessions_share_a_held_read,retained_reads_outlive_their_sessionsandfield_projection_with_row_idx_packs_column_plans.cargo test -p vortex-duckdb --lib: 275 pass, including thetop_n,filtered_top_nandlate_materialized_top_ncases oftest_vortex_scan_v2_matches_v1.pytest bench-orchestrator/tests/test_executor.py: 13 pass.Generated by Claude Code