feat: skip hash shuffle for date_bin/date_trunc on Range([timestamp]) - #24501
feat: skip hash shuffle for date_bin/date_trunc on Range([timestamp])#24501NGA-TRAN wants to merge 9 commits into
Conversation
Pin today's Partial + hash RepartitionExec + Final plan for GROUP BY key, date_bin(timestamp) on a table that is already Range([timestamp]) and sorted on (key, timestamp), so a follow-up can remove the shuffle. Co-authored-by: Cursor <cursoragent@cursor.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #24501 +/- ##
==========================================
+ Coverage 81.32% 81.33% +0.01%
==========================================
Files 1117 1117
Lines 396269 396682 +413
Branches 396269 396682 +413
==========================================
+ Hits 322260 322642 +382
- Misses 55186 55206 +20
- Partials 18823 18834 +11 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Use generic column and table names so the coverage does not expose a metrics-specific schema. Co-authored-by: Cursor <cursoragent@cursor.com>
Accept RecordBatches and optional file sort order so the time-bin table reuses the same registration path. Co-authored-by: Cursor <cursoragent@cursor.com>
Treat Range([x]) as a subset of KeyPartitioned([..., f(x), ...]) when f is monotonic and bins do not straddle split points, so GROUP BY key, date_bin/date_trunc can stream without a hash RepartitionExec. Co-authored-by: Cursor <cursoragent@cursor.com>
05705f6 to
b26846d
Compare
Drop the local order-preserving walk and scalar predecessor so date_bin/date_trunc range projection uses EquivalenceProperties and interval_arithmetic instead of duplicating them. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…che#24500 Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Tag @gene-bordegaray and @jayshrivastava for review |
jayshrivastava
left a comment
There was a problem hiding this comment.
Looks good. Far better than my approach in #23536.
I left some comments re. completeness.
| # -> RepartitionExec Hash([key, date_bin(...)]) | ||
| # -> FinalPartitioned AggregateExec (ordering_mode=Sorted) | ||
| # date_trunc('day') bins straddle the hour split, so that query still | ||
| # hash-repartitions. |
There was a problem hiding this comment.
nit: can you use actual numbers as an example to show a date_bin/date_trunc which preserves partitioning and one that doesn't?
| ORDER BY key, time_bin; | ||
| ---- | ||
| k1 2024-01-01T00:00:00 33 | ||
| k2 2024-01-01T00:00:00 42 |
There was a problem hiding this comment.
These tests look good 👍🏽
| matches!( | ||
| get_expr_properties(expr, &dependencies, &self.schema) | ||
| .map(|properties| properties.sort_properties), | ||
| Ok(SortProperties::Ordered(_)) |
There was a problem hiding this comment.
This doesn't capture the ordering, so a non monotonic function like f(x) = -x will return true here even though it flips the ordering from ASC to DESC. Even if -x works for range partitioning, this function may be used for some different use case, in which it will incorrectly return true.
Let's just assert that the options in SortProperties::Ordered(options) are the same as sort_expr.options and add a test for -x.
| let mut sort_exprs = Vec::with_capacity(self.ordering.len()); | ||
| for (key_idx, sort_expr) in self.ordering.iter().enumerate() { | ||
| if let Some(projected) = | ||
| input_eq_properties.project_expr(&sort_expr.expr, mapping) |
There was a problem hiding this comment.
Can you add a test where we're range partitioned on a and project select a, date_bin(a) from foo?
I think this case will preserve range partitioning on a. This behavior is a bit subtle. Your comment is good though "If a projection drops a range key but keeps a monotonic function of it".
Testing select a as b, date_bin(a) as bucket from foo would be good too. This should preserve the same range partitioning.
| /// | ||
| /// The identity `expr == range_key` returns false so callers can treat "emit | ||
| /// the key as-is" separately from "emit a function of the key". | ||
| pub(crate) fn is_monotonic_function_of( |
There was a problem hiding this comment.
I'd rename this.
- Doing
equivalence_properties.is_monotonic_function_ofsounds like the properies are a monotonic function. - The two expressions aren't necessarily either functions. They are related via the
Dependenciesrelationship.
Maybe rename to check_monotonic_dependency or check_monotonic_transform?
| pub(crate) fn is_monotonic_function_of( | ||
| &self, | ||
| expr: &Arc<dyn PhysicalExpr>, | ||
| range_key: &Arc<dyn PhysicalExpr>, |
There was a problem hiding this comment.
Rename these to source_expr and transformed_expr? Definately would not call this range_key because this function has nothing to do with range partitioning.
| mapping.iter().find_map(|(source, targets)| { | ||
| eq_properties | ||
| .is_monotonic_function_of(source, &sort_expr.expr) | ||
| .then(|| (Arc::clone(&targets.first().0), Arc::clone(source))) |
There was a problem hiding this comment.
I think it's okay to only look at the first target. Target just contains the alias IIUC. Ex.
select non_monotonic_function(a), date_bin(a) from foo has 2 sources (which contain the projection) with 1 target each, so this function works and should find the monotonic date_bin function. Can you add a test for this query which calls this function and ensure that it correctly finds the first monotonic function?
select non_monotonic_function(a), date_bin(a) as bin1, date_bin(a) as bin2 from foo would also be interesting.
Also note that the way the code is written, select date_bin(a, 45s) as bin1, date_bin(a, 60s) as bin2 from foo will only check the first date_bin.
What if the 45s straddles the partitions but 60s doesn't? Then the range partitioning will fall back to Unknown right? Maybe we should search for ALL the monotonic functions here and then in RangePartitioning::project(...), we check each one to see monotonic_fn_keeps_partitions_disjoint and use the first one that meets both requirements.
| ) | ||
| ); | ||
| if satisfaction == PartitioningSatisfaction::NotSatisfied | ||
| && allow_subset |
There was a problem hiding this comment.
If partitioned on just timestamp and you do select date_bin(timestamp) from foo, then we should check range_monotonic_fn_satisfies_keys even if allow_subset is false.
| required_exprs: &[Arc<dyn PhysicalExpr>], | ||
| eq_properties: &EquivalenceProperties, | ||
| ) -> bool { | ||
| if range.ordering().len() != 1 { |
There was a problem hiding this comment.
Do you plan to expand this in a follow up?
| split_points: &[SplitPoint], | ||
| key_idx: usize, | ||
| ) -> bool { | ||
| split_points.iter().all(|split_point| { |
There was a problem hiding this comment.
This is pretty clean. My approach would have been to store the previous mapped split point in a variable and check monotonicity that way
gene-bordegaray
left a comment
There was a problem hiding this comment.
all my comments largely ditto @jayshrivastava I can take a review when these are addressed 👍
Which issue does this PR close?
Rationale for this change
A table that is range-partitioned on
timestamp(for example by hour) and grouped bydate_bin/date_truncof that timestamp is already partition-disjoint when bins do not straddle split points. DataFusion still planned Partial → hashRepartitionExec→ Final.After this change,
Range([timestamp])subset-satisfiesKeyPartitioned([key, f(timestamp)])whenfis monotonic (date_bin,date_trunc) and evaluatingfat each split vs. its predecessor shows the bins are disjoint. Aggregation then runs as one streamingSinglePartitionedstep withordering_mode=Sorted.Bins that do straddle the split (for example
date_trunc('day')on hour-partitioned data) still hash-repartition.#24500's expected plan changes from:
to:
What changes are included in this PR?
Rangethrough monotonic grouping expressions when bins stay disjoint.SanityCheckPlanacceptsSinglePartitionedoverRange([timestamp]).range_sorted_time_bin_agg.sltfordate_bin(60s)anddate_trunc('hour')(no shuffle) anddate_trunc('day')(shuffle remains).Are these changes tested?
partitioning.rsfor aligneddate_bin/date_trunc('hour')vs unaligned split anddate_trunc('day').cargo test --test sqllogictests -- range_sorted_time_bin_agg.sltAre there any user-facing changes?
Queries that group by
date_bin/date_truncof a range-partitioned timestamp may skip a hash shuffle when bins do not cross file-group boundaries. Results are unchanged.Test plan
cargo test -p datafusion-physical-expr --lib -- partitioningcargo test --test sqllogictests -- range_sorted_time_bin_agg.sltCo-author with Cursor