datafusion_physical_optimizer/
topk_repartition.rs1use crate::PhysicalOptimizerRule;
48use datafusion_common::Result;
49use datafusion_common::config::ConfigOptions;
50use datafusion_common::tree_node::{Transformed, TransformedResult, TreeNode};
51use datafusion_physical_plan::execution_plan::replace_children_if_necessary;
52use std::sync::Arc;
53#[expect(deprecated)]
56use datafusion_physical_plan::coalesce_batches::CoalesceBatchesExec;
57use datafusion_physical_plan::repartition::RepartitionExec;
58use datafusion_physical_plan::sorts::sort::SortExec;
59use datafusion_physical_plan::{ExecutionPlan, Partitioning};
60
61#[derive(Debug, Clone, Default)]
66pub struct TopKRepartition;
67
68impl TopKRepartition {
69 pub fn new() -> Self {
70 Self {}
71 }
72}
73
74impl PhysicalOptimizerRule for TopKRepartition {
75 #[expect(deprecated)] fn optimize(
77 &self,
78 plan: Arc<dyn ExecutionPlan>,
79 config: &ConfigOptions,
80 ) -> Result<Arc<dyn ExecutionPlan>> {
81 if !config.optimizer.enable_topk_repartition {
82 return Ok(plan);
83 }
84 plan.transform_down(|node| {
85 let Some(sort_exec) = node.downcast_ref::<SortExec>() else {
87 return Ok(Transformed::no(node));
88 };
89 let Some(fetch) = sort_exec.fetch() else {
90 return Ok(Transformed::no(node));
91 };
92
93 let sort_input = sort_exec.input();
95 let (repart_parent, repart_exec) = if let Some(rp) =
96 sort_input.downcast_ref::<RepartitionExec>()
97 {
98 (None, rp)
100 } else if let Some(cb_exec) = sort_input.downcast_ref::<CoalesceBatchesExec>()
101 {
102 let cb_input = cb_exec.input();
105 let Some(rp) = cb_input.downcast_ref::<RepartitionExec>() else {
106 return Ok(Transformed::no(node));
107 };
108 (Some(Arc::clone(sort_input)), rp)
109 } else {
110 return Ok(Transformed::no(node));
111 };
112
113 let Partitioning::Hash(hash_exprs, num_partitions) =
115 repart_exec.partitioning()
116 else {
117 return Ok(Transformed::no(node));
118 };
119
120 let sort_exprs = sort_exec.expr();
121
122 if hash_exprs.len() > sort_exprs.len() {
126 return Ok(Transformed::no(node));
127 }
128 for (hash_expr, sort_expr) in hash_exprs.iter().zip(sort_exprs.iter()) {
129 if !hash_expr.eq(&sort_expr.expr) {
130 return Ok(Transformed::no(node));
131 }
132 }
133
134 let repart_input = repart_exec.input();
137 if repart_input.is::<SortExec>() {
138 return Ok(Transformed::no(node));
139 }
140
141 let new_sort: Arc<dyn ExecutionPlan> = Arc::new(
143 SortExec::new(sort_exprs.clone(), Arc::clone(repart_input))
144 .with_fetch(Some(fetch))
145 .with_preserve_partitioning(sort_exec.preserve_partitioning()),
146 );
147
148 let new_partitioning =
149 Partitioning::Hash(hash_exprs.clone(), *num_partitions);
150 let new_repartition: Arc<dyn ExecutionPlan> =
151 Arc::new(RepartitionExec::try_new(new_sort, new_partitioning)?);
152
153 let new_sort_input = if let Some(parent) = repart_parent {
155 replace_children_if_necessary(parent, vec![new_repartition])?
156 } else {
157 new_repartition
158 };
159
160 let new_top_sort: Arc<dyn ExecutionPlan> = Arc::new(
161 SortExec::new(sort_exprs.clone(), new_sort_input)
162 .with_fetch(Some(fetch))
163 .with_preserve_partitioning(sort_exec.preserve_partitioning()),
164 );
165
166 Ok(Transformed::yes(new_top_sort))
167 })
168 .data()
169 }
170
171 fn name(&self) -> &str {
172 "TopKRepartition"
173 }
174
175 fn schema_check(&self) -> bool {
176 true
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use arrow::datatypes::{DataType, Field, Schema};
184 use datafusion_physical_expr::expressions::col;
185 use datafusion_physical_expr_common::sort_expr::{LexOrdering, PhysicalSortExpr};
186 use datafusion_physical_plan::displayable;
187 use datafusion_physical_plan::test::scan_partitioned;
188 use insta::assert_snapshot;
189
190 fn schema() -> Arc<Schema> {
191 Arc::new(Schema::new(vec![
192 Field::new("a", DataType::Utf8, false),
193 Field::new("b", DataType::Int64, false),
194 ]))
195 }
196
197 fn sort_exprs(schema: &Schema) -> LexOrdering {
198 LexOrdering::new(vec![
199 PhysicalSortExpr::new_default(col("a", schema).unwrap()).asc(),
200 PhysicalSortExpr::new_default(col("b", schema).unwrap()).asc(),
201 ])
202 .unwrap()
203 }
204
205 #[test]
208 fn topk_pushed_below_hash_repartition() {
209 let s = schema();
210 let input = scan_partitioned(1);
211 let ordering = sort_exprs(&s);
212
213 let repartition = Arc::new(
214 RepartitionExec::try_new(
215 input,
216 Partitioning::Hash(vec![col("a", &s).unwrap()], 4),
217 )
218 .unwrap(),
219 );
220
221 let sort = Arc::new(
222 SortExec::new(ordering, repartition)
223 .with_fetch(Some(3))
224 .with_preserve_partitioning(true),
225 );
226
227 let config = ConfigOptions::new();
228 let optimized = TopKRepartition::new().optimize(sort, &config).unwrap();
229
230 let display = displayable(optimized.as_ref()).indent(true).to_string();
231 assert_snapshot!(display, @r"
232 SortExec: TopK(fetch=3), expr=[a@0 ASC, b@1 ASC], preserve_partitioning=[true], sort_prefix=[a@0 ASC]
233 RepartitionExec: partitioning=Hash([a@0], 4), input_partitions=1, maintains_sort_order=true
234 SortExec: TopK(fetch=3), expr=[a@0 ASC, b@1 ASC], preserve_partitioning=[true]
235 DataSourceExec: partitions=1, partition_sizes=[1]
236 ");
237 }
238
239 #[test]
241 fn unbounded_sort_not_pushed() {
242 let s = schema();
243 let input = scan_partitioned(1);
244 let ordering = sort_exprs(&s);
245
246 let repartition = Arc::new(
247 RepartitionExec::try_new(
248 input,
249 Partitioning::Hash(vec![col("a", &s).unwrap()], 4),
250 )
251 .unwrap(),
252 );
253
254 let sort: Arc<dyn ExecutionPlan> = Arc::new(
255 SortExec::new(ordering, repartition).with_preserve_partitioning(true),
256 );
257
258 let config = ConfigOptions::new();
259 let optimized = TopKRepartition::new().optimize(sort, &config).unwrap();
260
261 let display = displayable(optimized.as_ref()).indent(true).to_string();
262 assert_snapshot!(display, @r"
263 SortExec: expr=[a@0 ASC, b@1 ASC], preserve_partitioning=[true]
264 RepartitionExec: partitioning=Hash([a@0], 4), input_partitions=1
265 DataSourceExec: partitions=1, partition_sizes=[1]
266 ");
267 }
268
269 #[test]
271 fn non_prefix_hash_key_not_pushed() {
272 let s = schema();
273 let input = scan_partitioned(1);
274 let ordering = sort_exprs(&s);
275
276 let repartition = Arc::new(
278 RepartitionExec::try_new(
279 input,
280 Partitioning::Hash(vec![col("b", &s).unwrap()], 4),
281 )
282 .unwrap(),
283 );
284
285 let sort: Arc<dyn ExecutionPlan> = Arc::new(
286 SortExec::new(ordering, repartition)
287 .with_fetch(Some(3))
288 .with_preserve_partitioning(true),
289 );
290
291 let config = ConfigOptions::new();
292 let optimized = TopKRepartition::new().optimize(sort, &config).unwrap();
293
294 let display = displayable(optimized.as_ref()).indent(true).to_string();
295 assert_snapshot!(display, @r"
296 SortExec: TopK(fetch=3), expr=[a@0 ASC, b@1 ASC], preserve_partitioning=[true]
297 RepartitionExec: partitioning=Hash([b@1], 4), input_partitions=1
298 DataSourceExec: partitions=1, partition_sizes=[1]
299 ");
300 }
301
302 #[expect(deprecated)]
305 #[test]
306 fn topk_pushed_through_coalesce_batches() {
307 let s = schema();
308 let input = scan_partitioned(1);
309 let ordering = sort_exprs(&s);
310
311 let repartition = Arc::new(
312 RepartitionExec::try_new(
313 input,
314 Partitioning::Hash(vec![col("a", &s).unwrap()], 4),
315 )
316 .unwrap(),
317 );
318
319 let coalesce: Arc<dyn ExecutionPlan> =
320 Arc::new(CoalesceBatchesExec::new(repartition, 8192));
321
322 let sort = Arc::new(
323 SortExec::new(ordering, coalesce)
324 .with_fetch(Some(3))
325 .with_preserve_partitioning(true),
326 );
327
328 let config = ConfigOptions::new();
329 let optimized = TopKRepartition::new().optimize(sort, &config).unwrap();
330
331 let display = displayable(optimized.as_ref()).indent(true).to_string();
332 assert_snapshot!(display, @r"
333 SortExec: TopK(fetch=3), expr=[a@0 ASC, b@1 ASC], preserve_partitioning=[true], sort_prefix=[a@0 ASC]
334 CoalesceBatchesExec: target_batch_size=8192
335 RepartitionExec: partitioning=Hash([a@0], 4), input_partitions=1, maintains_sort_order=true
336 SortExec: TopK(fetch=3), expr=[a@0 ASC, b@1 ASC], preserve_partitioning=[true]
337 DataSourceExec: partitions=1, partition_sizes=[1]
338 ");
339 }
340
341 #[test]
343 fn round_robin_not_pushed() {
344 let s = schema();
345 let input = scan_partitioned(1);
346 let ordering = sort_exprs(&s);
347
348 let repartition = Arc::new(
349 RepartitionExec::try_new(input, Partitioning::RoundRobinBatch(4)).unwrap(),
350 );
351
352 let sort: Arc<dyn ExecutionPlan> = Arc::new(
353 SortExec::new(ordering, repartition)
354 .with_fetch(Some(3))
355 .with_preserve_partitioning(true),
356 );
357
358 let config = ConfigOptions::new();
359 let optimized = TopKRepartition::new().optimize(sort, &config).unwrap();
360
361 let display = displayable(optimized.as_ref()).indent(true).to_string();
362 assert_snapshot!(display, @r"
363 SortExec: TopK(fetch=3), expr=[a@0 ASC, b@1 ASC], preserve_partitioning=[true]
364 RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1
365 DataSourceExec: partitions=1, partition_sizes=[1]
366 ");
367 }
368}