Repository navigation
stream ROW_NUMBER output by input batch - #7461
nat-openai wants to merge 9 commits into
Conversation
Generate ROW_NUMBER results per input batch, carrying the final partition key and counter across batches. This avoids per-group slicing and window state allocations, reducing overhead for many small partitions. Preserve existing plan contracts and result casts while accounting for retained key buffers. Add regression tests for boundaries and cleanup, plus benchmarks comparing the streaming and generic executors. Benchmark timing: | Rows per partition | Generic | Streaming | Speedup | |---:|---:|---:|---:| | 1 | 243.08 ms | 3.38 ms | 71.8× | | 4 | 66.53 ms | 2.75 ms | 24.2× | | 64 | 9.82 ms | 2.34 ms | 4.2× | | 16,384 | 6.39 ms | 2.32 ms | 2.8× | | 262,144 | 7.22 ms | 2.36 ms | 3.1× |
Reject Linear and PartiallySorted windows because a partition can recur after another key, which the streaming evaluator cannot track. Cover both modes within and across batches and verify that the generic executor still produces the expected row numbers.
There was a problem hiding this comment.
Thanks for this. A counter plus boundary detection is all ROW_NUMBER needs, and skipping the per-partition state in BoundedWindowAggExec pays off. I ran the branch (2ab0d3e) in a Linux arm64 container on the default profile:
- The 13
row_numbertests indatafusion-comet-operatorsand the planner testwindow_cast_projection_reports_window_timepass. - The
row_number_executorbench reproduces your numbers, and the gap is a bit wider here: 229 ms vs 1.97 ms at one row per partition, 65.6 ms vs 1.86 ms at 4, 10.9 ms vs 1.50 ms at 64, and 5.3 to 5.5 ms vs 1.5 ms at 16,384 and 262,144.
A few things:
The new Spark test fails as written. window: streaming ROW_NUMBER across input batches stops with PATH_ALREADY_EXISTS before any query runs. withTempDir has already created dir, so the write needs .mode("overwrite"), as the neighbouring tests use. With that one line, the whole of CometWindowExecSuite passes (92 tests).
The Rust tests don't pin the cross-batch key comparison. I changed let start = if range.start == 0 && continues { to if range.start == 0 { in row_number.rs, so every batch continues the previous partition whatever its key. All 13 tests still pass. In every multi-batch input in tests.rs, the first row of each batch belongs to the partition the previous batch ended on. Could you add a case where a partition ends exactly at a batch boundary, such as [1, 1] | [2, 2], with a dictionary variant too? Relatedly, dropping || ranges.len() != 1 from the key replacement condition is caught by only the struct half of nested_keys_and_nullable_children_match_generic_windows. A direct check that the stored key is the last row after a batch like [1, 2, 2, 3] would make that guard harder to break by accident.
Floating-point partition keys. The existing window: floating partition keys loop in CometWindowExecSuite covers RANK, PERCENT_RANK and NTILE but not ROW_NUMBER, which now has its own boundary logic. Adding "ROW_NUMBER()" -> false and "ROW_NUMBER()" -> true to that list covers -0.0 next to 0.0, NaN payloads and null on the new path. I tried it locally and all four cases match Spark, so it's a cheap guard against losing the key normalization later.
Which issue this belongs to. #7092 is about BoundedWindowAggExec not reserving the rows its frames keep, and dhruv-dagar has taken it. This PR sends ROW_NUMBER-only windows to a different executor rather than adding that reservation, so I don't think it addresses #7092, even partially. The cost it removes is the one in apache/datafusion#26541, assigned to comphead. Could you open a Comet issue for the performance change and link that one instead? It would also help to note in RowNumberExec's doc comment that the operator works around apache/datafusion#26541, so it's clear when it can be removed.
comphead
left a comment
There was a problem hiding this comment.
Thanks for working on this, and for the benchmark that checks every row number before timing. I left out the points from the earlier review (the PATH_ALREADY_EXISTS fix, the batch boundary tests, float keys and the issue link) and have a few others below.
docs/source/contributor-guide/debugging.md conflicts with #7256, which edits the same BoundedWindowAggExec example. The Rust in window.rs merges cleanly, so whichever lands second only needs to update that doc block.
|
Thanks for the comments @dwsmith1983 and @comphead. I'm going to spend some moments internalising them and will loop back to address them properly. I'll also open a new issue that this resolves specifically rather than linking the other issue this sort of jumps over. |
Validate the child's declared partition ordering before opening its stream. Cover exact batch-boundary resets and final-key updates for primitive and dictionary keys, and allow the Spark fixture to overwrite its temporary path.
Move batch-boundary checks into a SQL fixture covering seven nullable partition-key types and three batch sizes. Exercise floating keys with ROW_NUMBER and retain global, repeated-result and mixed-frame controls.
Move the partition expressions through the unfolding stream state so every batch reuses the same vector. Avoid allocating a replacement vector and cloning its expression references on each iteration, including completion.
Removed unnecesary free/try_resize churn. I've stuck to try_resize for now while I think about it more.
|
@dwsmith1983/@comphead I believe I've now addressed all the of previous comments. |
dwsmith1983
left a comment
There was a problem hiding this comment.
Thanks, this covers everything. I ran 91e3357 in a Linux container on the default profile, and the new boundary tests and ROW_NUMBER float-key cases catch the continues change that slipped through before.
The SQL file fails on its last query. The suite turns off constant folding, so 1 PRECEDING never becomes a literal and Spark's window throws Unhandled bound in windows expressions: -1. Switching to ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING fixes it.
Which issue does this PR close?
Closes #7466.
Rationale for this change
ROW_NUMBERcurrently uses the generic bounded window executor, which performs per-partition state management and batch slicing even though this function only needs a counter and partition boundary detection. That overhead is particularly expensive for many small window partitions.What changes are included in this PR?
RowNumberExecto generate row numbers directly within each input batch, carrying the final partition key and counter across batches.ROW_NUMBERwith matching partition and ordering keys.ROW_NUMBERexpressions.How are these changes tested?
The 12 operator tests pass in an isolated harness. The Spark test additions compile but have not been executed locally.
Benchmark results
Median runtime for 262,144 rows in 32 batches of 8,192 rows, using three string partition keys. Measurements pool two runs of 10 samples each on an Apple M3 Max. The repeat measures streaming first.
The unchanged benchmark source ran in an isolated harness using Comet’s release settings, DataFusion 55.2.0, and Arrow 59.3.2. These measurements cover window execution over already-sorted input.