Skip to main content

query/optimizer/
pass_distribution.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::sync::Arc;
16
17use datafusion::config::ConfigOptions;
18use datafusion::physical_optimizer::PhysicalOptimizerRule;
19use datafusion::physical_plan::projection::ProjectionExec;
20use datafusion::physical_plan::repartition::RepartitionExec;
21use datafusion::physical_plan::{
22    ChildrenPropertiesMode, ExecutionPlan, Partitioning, ReplaceChildrenOptions,
23};
24use datafusion_common::Result as DfResult;
25use datafusion_physical_expr::Distribution;
26use datafusion_physical_expr::utils::map_columns_before_projection;
27
28use crate::dist_plan::MergeScanExec;
29
30/// This is a [`PhysicalOptimizerRule`] to pass distribution requirement to
31/// [`MergeScanExec`] to avoid unnecessary shuffling.
32///
33/// This rule is expected to be run before [`EnforceDistribution`].
34///
35/// [`EnforceDistribution`]: datafusion::physical_optimizer::enforce_distribution::EnforceDistribution
36/// [`MergeScanExec`]: crate::dist_plan::MergeScanExec
37#[derive(Debug)]
38pub struct PassDistribution;
39
40impl PhysicalOptimizerRule for PassDistribution {
41    fn optimize(
42        &self,
43        plan: Arc<dyn ExecutionPlan>,
44        config: &ConfigOptions,
45    ) -> DfResult<Arc<dyn ExecutionPlan>> {
46        Self::do_optimize(plan, config)
47    }
48
49    fn name(&self) -> &str {
50        "PassDistributionRule"
51    }
52
53    fn schema_check(&self) -> bool {
54        false
55    }
56}
57
58impl PassDistribution {
59    fn do_optimize(
60        plan: Arc<dyn ExecutionPlan>,
61        _config: &ConfigOptions,
62    ) -> DfResult<Arc<dyn ExecutionPlan>> {
63        // Start from root with no requirement
64        Self::rewrite_with_distribution(plan, None)
65    }
66
67    /// Top-down rewrite that propagates distribution requirements to children.
68    fn rewrite_with_distribution(
69        plan: Arc<dyn ExecutionPlan>,
70        current_req: Option<Distribution>,
71    ) -> DfResult<Arc<dyn ExecutionPlan>> {
72        // If this is a MergeScanExec, try to apply the current requirement.
73        if let Some(merge_scan) = plan.downcast_ref::<MergeScanExec>()
74            && let Some(Distribution::KeyPartitioned(hash_exprs)) = current_req.as_ref()
75        {
76            if let Partitioning::Hash(current_hash_exprs, _) = &merge_scan.properties().partitioning
77                && *current_hash_exprs == *hash_exprs
78            {
79                return Ok(plan);
80            }
81
82            if let Some(new_plan) = merge_scan
83                .try_with_new_distribution(Distribution::KeyPartitioned(hash_exprs.clone()))
84            {
85                // Leaf node; no children to process
86                return Ok(Arc::new(new_plan) as _);
87            }
88
89            let partitioning = Partitioning::Hash(
90                hash_exprs.clone(),
91                merge_scan.properties().partitioning.partition_count(),
92            );
93            return Ok(Arc::new(RepartitionExec::try_new(plan, partitioning)?) as _);
94        }
95
96        // Compute per-child requirements from the current node.
97        let children = plan.children();
98        if children.is_empty() {
99            return Ok(plan);
100        }
101
102        let required = plan.input_distribution_requirements();
103        let mut new_children = Vec::with_capacity(children.len());
104        for (idx, child) in children.into_iter().enumerate() {
105            let child_req = match required.child_distribution(idx) {
106                Some(Distribution::UnspecifiedDistribution) if idx == 0 => {
107                    Self::map_hash_requirement_through_projection(plan.as_ref(), &current_req)
108                }
109                Some(Distribution::UnspecifiedDistribution) => None,
110                None => current_req.clone(),
111                Some(req) => Some(req.clone()),
112            };
113            let new_child = Self::rewrite_with_distribution(child.clone(), child_req)?;
114            new_children.push(new_child);
115        }
116
117        // Rebuild the node only if any child changed (pointer inequality)
118        let unchanged = plan
119            .children()
120            .into_iter()
121            .zip(new_children.iter())
122            .all(|(old, new)| Arc::ptr_eq(old, new));
123        if unchanged {
124            Ok(plan)
125        } else {
126            plan.replace_children(
127                new_children,
128                ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
129            )
130        }
131    }
132
133    fn map_hash_requirement_through_projection(
134        plan: &dyn ExecutionPlan,
135        current_req: &Option<Distribution>,
136    ) -> Option<Distribution> {
137        let Some(Distribution::KeyPartitioned(required_exprs)) = current_req else {
138            return None;
139        };
140
141        let projection = plan.downcast_ref::<ProjectionExec>()?;
142        let proj_exprs = projection
143            .expr()
144            .iter()
145            .map(|expr| (Arc::clone(&expr.expr), expr.alias.clone()))
146            .collect::<Vec<_>>();
147        let mapped = map_columns_before_projection(required_exprs, &proj_exprs);
148
149        (mapped.len() == required_exprs.len()).then_some(Distribution::KeyPartitioned(mapped))
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use std::collections::{BTreeMap, BTreeSet};
156
157    use api::v1::region::{RemoteDynFilterUnregister, RemoteDynFilterUpdate};
158    use arrow_schema::{DataType, Field, Schema, SchemaRef, TimeUnit};
159    use async_trait::async_trait;
160    use common_query::request::QueryRequest;
161    use datafusion::common::NullEquality;
162    use datafusion::execution::SessionStateBuilder;
163    use datafusion::physical_optimizer::PhysicalOptimizerRule;
164    use datafusion::physical_plan::joins::{HashJoinExec, PartitionMode};
165    use datafusion::physical_plan::projection::{ProjectionExec, ProjectionExpr};
166    use datafusion::physical_plan::{ExecutionPlanProperties, Partitioning};
167    use datafusion_expr::{JoinType, LogicalPlanBuilder};
168    use datafusion_physical_expr::PhysicalExpr;
169    use datafusion_physical_expr::expressions::Column as PhysicalColumn;
170    use session::ReadPreference;
171    use session::context::QueryContext;
172    use store_api::metric_engine_consts::DATA_SCHEMA_TSID_COLUMN_NAME;
173    use store_api::storage::RegionId;
174    use table::table_name::TableName;
175
176    use super::*;
177    use crate::dist_plan::RemoteDynFilterProducerId;
178    use crate::error::Result as QueryResult;
179    use crate::region_query::RegionQueryHandler;
180
181    struct NoopRegionQueryHandler;
182
183    #[async_trait]
184    impl RegionQueryHandler for NoopRegionQueryHandler {
185        async fn select_target(
186            &self,
187            _read_preference: ReadPreference,
188            _region_id: RegionId,
189        ) -> QueryResult<crate::region_query::RegionQueryTarget> {
190            unreachable!("pass distribution tests should not execute remote queries")
191        }
192
193        async fn do_get(
194            &self,
195            _target: &crate::region_query::RegionQueryTarget,
196            _request: QueryRequest,
197        ) -> QueryResult<common_recordbatch::SendableRecordBatchStream> {
198            unreachable!("pass distribution tests should not execute remote queries")
199        }
200
201        async fn handle_remote_dyn_filter_update(
202            &self,
203            _target: &crate::region_query::RegionQueryTarget,
204            _query_id: String,
205            _update: RemoteDynFilterUpdate,
206        ) -> QueryResult<()> {
207            unreachable!("pass distribution tests should not send remote dyn filter updates")
208        }
209
210        async fn handle_remote_dyn_filter_unregister(
211            &self,
212            _target: &crate::region_query::RegionQueryTarget,
213            _query_id: String,
214            _unregister: RemoteDynFilterUnregister,
215        ) -> QueryResult<()> {
216            unreachable!("pass distribution tests should not send remote dyn filter unregisters")
217        }
218    }
219
220    #[test]
221    fn passes_hash_requirement_through_projection_to_merge_scan() {
222        let schema = test_schema();
223        let left_merge_scan = Arc::new(test_merge_scan_exec(schema.clone()));
224        let right_merge_scan = Arc::new(test_merge_scan_exec(schema.clone()));
225        let left_projection = Arc::new(
226            ProjectionExec::try_new(
227                vec![
228                    ProjectionExpr::new(partition_column("greptime_value", 3), "greptime_value"),
229                    ProjectionExpr::new(
230                        partition_column(DATA_SCHEMA_TSID_COLUMN_NAME, 1),
231                        DATA_SCHEMA_TSID_COLUMN_NAME,
232                    ),
233                    ProjectionExpr::new(
234                        partition_column("greptime_timestamp", 2),
235                        "greptime_timestamp",
236                    ),
237                ],
238                left_merge_scan,
239            )
240            .unwrap(),
241        ) as Arc<dyn datafusion::physical_plan::ExecutionPlan>;
242        let join = Arc::new(
243            HashJoinExec::try_new(
244                left_projection,
245                right_merge_scan,
246                vec![
247                    (
248                        partition_column(DATA_SCHEMA_TSID_COLUMN_NAME, 1),
249                        partition_column(DATA_SCHEMA_TSID_COLUMN_NAME, 1),
250                    ),
251                    (
252                        partition_column("greptime_timestamp", 2),
253                        partition_column("greptime_timestamp", 2),
254                    ),
255                ],
256                None,
257                &JoinType::Inner,
258                None,
259                PartitionMode::Partitioned,
260                NullEquality::NullEqualsNull,
261                false,
262            )
263            .unwrap(),
264        ) as Arc<dyn datafusion::physical_plan::ExecutionPlan>;
265
266        let optimized = PassDistribution
267            .optimize(join, &ConfigOptions::default())
268            .unwrap();
269        let hash_join = optimized.downcast_ref::<HashJoinExec>().unwrap();
270        let left_projection = hash_join.left().downcast_ref::<ProjectionExec>().unwrap();
271        let left_partitioning = left_projection.input().output_partitioning();
272        let right_partitioning = hash_join.right().output_partitioning();
273
274        let Partitioning::Hash(left_exprs, left_count) = left_partitioning else {
275            panic!("expected left merge scan hash partitioning");
276        };
277        let Partitioning::Hash(right_exprs, right_count) = right_partitioning else {
278            panic!("expected right merge scan hash partitioning");
279        };
280
281        assert_eq!(*left_count, 2);
282        assert_eq!(*right_count, 2);
283        assert_eq!(
284            column_names(left_exprs),
285            vec![DATA_SCHEMA_TSID_COLUMN_NAME, "greptime_timestamp"]
286        );
287        assert_eq!(
288            column_names(right_exprs),
289            vec![DATA_SCHEMA_TSID_COLUMN_NAME, "greptime_timestamp"]
290        );
291    }
292
293    #[test]
294    fn merge_scan_rejects_hash_requirement_on_partition_key_subset() {
295        let merge_scan = test_merge_scan_exec(test_schema());
296
297        let new_plan = merge_scan.try_with_new_distribution(Distribution::KeyPartitioned(vec![
298            partition_column(DATA_SCHEMA_TSID_COLUMN_NAME, 1),
299        ]));
300
301        assert!(
302            new_plan.is_none(),
303            "partitioning by a subset of multi-column partition keys is not sufficient"
304        );
305    }
306
307    fn test_merge_scan_exec(schema: SchemaRef) -> MergeScanExec {
308        let session_state = SessionStateBuilder::new().with_default_features().build();
309        let partition_cols = BTreeMap::from([
310            (
311                DATA_SCHEMA_TSID_COLUMN_NAME.to_string(),
312                BTreeSet::from([datafusion_common::Column::from_name(
313                    DATA_SCHEMA_TSID_COLUMN_NAME,
314                )]),
315            ),
316            (
317                "greptime_timestamp".to_string(),
318                BTreeSet::from([datafusion_common::Column::from_name("greptime_timestamp")]),
319            ),
320        ]);
321        let plan = LogicalPlanBuilder::empty(false).build().unwrap();
322
323        MergeScanExec::new(
324            &session_state,
325            TableName::new("greptime", "public", "test"),
326            vec![RegionId::new(1, 0), RegionId::new(1, 1)],
327            plan,
328            schema.as_ref(),
329            Arc::new(NoopRegionQueryHandler),
330            QueryContext::arc(),
331            32,
332            partition_cols,
333            Some(RemoteDynFilterProducerId::new(1)),
334            false,
335        )
336        .unwrap()
337    }
338
339    fn test_schema() -> SchemaRef {
340        Arc::new(Schema::new(vec![
341            Field::new("host", DataType::Utf8, true),
342            Field::new(DATA_SCHEMA_TSID_COLUMN_NAME, DataType::UInt64, false),
343            Field::new(
344                "greptime_timestamp",
345                DataType::Timestamp(TimeUnit::Millisecond, None),
346                false,
347            ),
348            Field::new("greptime_value", DataType::Float64, true),
349        ]))
350    }
351
352    fn partition_column(name: &str, index: usize) -> Arc<dyn PhysicalExpr> {
353        Arc::new(PhysicalColumn::new(name, index))
354    }
355
356    fn column_names(exprs: &[Arc<dyn PhysicalExpr>]) -> Vec<&str> {
357        exprs
358            .iter()
359            .map(|expr| expr.downcast_ref::<PhysicalColumn>().unwrap().name())
360            .collect()
361    }
362}