From 4f604da079e60c7af7389f525a2e1e8b932202ba Mon Sep 17 00:00:00 2001 From: leiysky Date: Wed, 11 Jan 2023 23:29:53 +0800 Subject: [PATCH] refine join reordering --- .../optimizer/cascades/explore_rules.rs | 7 + .../planner/optimizer/cascades/scheduler.rs | 13 + .../planner/optimizer/heuristic/heuristic.rs | 2 +- src/query/sql/src/planner/optimizer/m_expr.rs | 10 + src/query/sql/src/planner/optimizer/memo.rs | 1 + .../sql/src/planner/optimizer/rule/factory.rs | 6 + .../rewrite/rule_push_down_filter_join.rs | 2 +- .../rewrite/rule_push_down_limit_union.rs | 2 +- .../sql/src/planner/optimizer/rule/rule.rs | 6 + .../planner/optimizer/rule/transform/mod.rs | 6 + .../rule/transform/rule_commute_join.rs | 11 +- .../rule/transform/rule_exchange_join.rs | 260 +++++++++++ .../transform/rule_left_associate_join.rs | 15 +- .../rule/transform/rule_left_exchange_join.rs | 213 +++++++++ .../transform/rule_right_associate_join.rs | 15 +- .../transform/rule_right_exchange_join.rs | 211 +++++++++ src/query/sql/src/planner/optimizer/s_expr.rs | 2 +- .../mode/standalone/explain/explain.test | 420 +++++++++--------- .../suites/mode/standalone/explain/join.test | 145 +++--- .../explain/join_reorder/chain.test | 128 +++--- .../join_reorder/star_multiple_tables.test | 128 ++++++ 21 files changed, 1259 insertions(+), 344 deletions(-) create mode 100644 src/query/sql/src/planner/optimizer/rule/transform/rule_exchange_join.rs create mode 100644 src/query/sql/src/planner/optimizer/rule/transform/rule_left_exchange_join.rs create mode 100644 src/query/sql/src/planner/optimizer/rule/transform/rule_right_exchange_join.rs create mode 100644 tests/sqllogictests/suites/mode/standalone/explain/join_reorder/star_multiple_tables.test diff --git a/src/query/sql/src/planner/optimizer/cascades/explore_rules.rs b/src/query/sql/src/planner/optimizer/cascades/explore_rules.rs index 2cb45cf031b64..bcf63eeb07bdb 100644 --- a/src/query/sql/src/planner/optimizer/cascades/explore_rules.rs +++ b/src/query/sql/src/planner/optimizer/cascades/explore_rules.rs @@ -16,10 +16,17 @@ use crate::optimizer::RuleID; use crate::optimizer::RuleSet; pub fn get_explore_rule_set() -> RuleSet { + join_rule_set_rs_b2() +} + +/// Get rule set of join order RS-B2. +/// Read paper "The Complexity of Transformation-Based Join Enumeration" for more details. +fn join_rule_set_rs_b2() -> RuleSet { RuleSet::create_with_ids(vec![ RuleID::CommuteJoin, RuleID::LeftAssociateJoin, RuleID::RightAssociateJoin, + RuleID::ExchangeJoin, ]) .unwrap() } diff --git a/src/query/sql/src/planner/optimizer/cascades/scheduler.rs b/src/query/sql/src/planner/optimizer/cascades/scheduler.rs index 9c5254bd7b9e6..6709b91671d2c 100644 --- a/src/query/sql/src/planner/optimizer/cascades/scheduler.rs +++ b/src/query/sql/src/planner/optimizer/cascades/scheduler.rs @@ -21,12 +21,17 @@ use super::CascadesOptimizer; pub struct Scheduler { task_queue: VecDeque, + + /// A counter to track the number of tasks + /// that have been scheduled. + scheduled_task_count: u64, } impl Scheduler { pub fn new() -> Self { Self { task_queue: Default::default(), + scheduled_task_count: 0, } } @@ -38,8 +43,16 @@ impl Scheduler { continue; } task.execute(optimizer, self)?; + + // Update the counter + self.scheduled_task_count += 1; } + tracing::debug!( + "CascadesOptimizer: scheduled {} tasks", + self.scheduled_task_count + ); + Ok(()) } diff --git a/src/query/sql/src/planner/optimizer/heuristic/heuristic.rs b/src/query/sql/src/planner/optimizer/heuristic/heuristic.rs index 71d63559849d4..a344e9eb19a5c 100644 --- a/src/query/sql/src/planner/optimizer/heuristic/heuristic.rs +++ b/src/query/sql/src/planner/optimizer/heuristic/heuristic.rs @@ -117,7 +117,7 @@ impl HeuristicOptimizer { for rule in rule_list.iter() { let mut state = TransformResult::new(); if s_expr.match_pattern(rule.pattern()) && !s_expr.applied_rule(&rule.id()) { - s_expr.apply_rule(&rule.id()); + s_expr.set_applied_rule(&rule.id()); rule.apply(&s_expr, &mut state)?; if !state.results().is_empty() { // Recursive optimize the result diff --git a/src/query/sql/src/planner/optimizer/m_expr.rs b/src/query/sql/src/planner/optimizer/m_expr.rs index bd7310d8ba217..ddf704415f778 100644 --- a/src/query/sql/src/planner/optimizer/m_expr.rs +++ b/src/query/sql/src/planner/optimizer/m_expr.rs @@ -18,6 +18,7 @@ use common_exception::Result; use super::group::Group; use crate::optimizer::memo::Memo; use crate::optimizer::pattern_extractor::PatternExtractor; +use crate::optimizer::rule::AppliedRules; use crate::optimizer::rule::RulePtr; use crate::optimizer::rule::TransformResult; use crate::optimizer::SExpr; @@ -36,6 +37,9 @@ pub struct MExpr { pub plan: RelOperator, pub children: Vec, + + // Disable rules for current `MExpr` + pub applied_rules: AppliedRules, } impl MExpr { @@ -44,12 +48,14 @@ impl MExpr { index: IndexType, plan: RelOperator, children: Vec, + applied_rules: AppliedRules, ) -> Self { MExpr { group_index, plan, children, index, + applied_rules, } } @@ -87,6 +93,10 @@ impl MExpr { rule: &RulePtr, transform_state: &mut TransformResult, ) -> Result<()> { + if self.applied_rules.get(&rule.id()) { + return Ok(()); + } + let mut extractor = PatternExtractor::create(); let exprs = extractor.extract(memo, self, rule.pattern())?; diff --git a/src/query/sql/src/planner/optimizer/memo.rs b/src/query/sql/src/planner/optimizer/memo.rs index 4ebca00631b2a..0462b0de79ed5 100644 --- a/src/query/sql/src/planner/optimizer/memo.rs +++ b/src/query/sql/src/planner/optimizer/memo.rs @@ -109,6 +109,7 @@ impl Memo { self.group(group_index)?.num_exprs(), s_expr.plan, children_group, + s_expr.applied_rules, ); self.insert_m_expr(group_index, m_expr)?; diff --git a/src/query/sql/src/planner/optimizer/rule/factory.rs b/src/query/sql/src/planner/optimizer/rule/factory.rs index 7dbc21084eb75..34427c587fb3a 100644 --- a/src/query/sql/src/planner/optimizer/rule/factory.rs +++ b/src/query/sql/src/planner/optimizer/rule/factory.rs @@ -35,6 +35,9 @@ use crate::optimizer::rule::rewrite::RulePushDownLimitSort; use crate::optimizer::rule::rewrite::RulePushDownLimitUnion; use crate::optimizer::rule::rewrite::RulePushDownSortScan; use crate::optimizer::rule::rewrite::RuleSplitAggregate; +use crate::optimizer::rule::transform::RuleExchangeJoin; +use crate::optimizer::rule::transform::RuleLeftExchangeJoin; +use crate::optimizer::rule::transform::RuleRightExchangeJoin; use crate::optimizer::rule::RuleID; use crate::optimizer::rule::RulePtr; @@ -70,6 +73,9 @@ impl RuleFactory { RuleID::CommuteJoin => Ok(Box::new(RuleCommuteJoin::new())), RuleID::LeftAssociateJoin => Ok(Box::new(RuleLeftAssociateJoin::new())), RuleID::RightAssociateJoin => Ok(Box::new(RuleRightAssociateJoin::new())), + RuleID::LeftExchangeJoin => Ok(Box::new(RuleLeftExchangeJoin::new())), + RuleID::RightExchangeJoin => Ok(Box::new(RuleRightExchangeJoin::new())), + RuleID::ExchangeJoin => Ok(Box::new(RuleExchangeJoin::new())), } } } diff --git a/src/query/sql/src/planner/optimizer/rule/rewrite/rule_push_down_filter_join.rs b/src/query/sql/src/planner/optimizer/rule/rewrite/rule_push_down_filter_join.rs index 088ce690ac717..861d814e3d4c1 100644 --- a/src/query/sql/src/planner/optimizer/rule/rewrite/rule_push_down_filter_join.rs +++ b/src/query/sql/src/planner/optimizer/rule/rewrite/rule_push_down_filter_join.rs @@ -308,7 +308,7 @@ impl Rule for RulePushDownFilterJoin { if !need_push { return Ok(()); } - result.apply_rule(&self.id); + result.set_applied_rule(&self.id); state.add_result(result); Ok(()) diff --git a/src/query/sql/src/planner/optimizer/rule/rewrite/rule_push_down_limit_union.rs b/src/query/sql/src/planner/optimizer/rule/rewrite/rule_push_down_limit_union.rs index ddd44dc709291..093c9f7708ca8 100644 --- a/src/query/sql/src/planner/optimizer/rule/rewrite/rule_push_down_limit_union.rs +++ b/src/query/sql/src/planner/optimizer/rule/rewrite/rule_push_down_limit_union.rs @@ -100,7 +100,7 @@ impl Rule for RulePushDownLimitUnion { let mut result = SExpr::create_binary(union.into(), union_left_child, union_right_child); // Add original limit to top result = SExpr::create_unary(limit.into(), result); - result.apply_rule(&self.id); + result.set_applied_rule(&self.id); state.add_result(result); Ok(()) diff --git a/src/query/sql/src/planner/optimizer/rule/rule.rs b/src/query/sql/src/planner/optimizer/rule/rule.rs index 6467b12af61b7..00aa7d0bab837 100644 --- a/src/query/sql/src/planner/optimizer/rule/rule.rs +++ b/src/query/sql/src/planner/optimizer/rule/rule.rs @@ -56,6 +56,9 @@ pub enum RuleID { CommuteJoin, LeftAssociateJoin, RightAssociateJoin, + LeftExchangeJoin, + RightExchangeJoin, + ExchangeJoin, } impl Display for RuleID { @@ -83,6 +86,9 @@ impl Display for RuleID { RuleID::CommuteJoin => write!(f, "CommuteJoin"), RuleID::LeftAssociateJoin => write!(f, "LeftAssociateJoin"), RuleID::RightAssociateJoin => write!(f, "RightAssociateJoin"), + RuleID::LeftExchangeJoin => write!(f, "LeftExchangeJoin"), + RuleID::RightExchangeJoin => write!(f, "RightExchangeJoin"), + RuleID::ExchangeJoin => write!(f, "ExchangeJoin"), } } } diff --git a/src/query/sql/src/planner/optimizer/rule/transform/mod.rs b/src/query/sql/src/planner/optimizer/rule/transform/mod.rs index a7316e76c671a..6833d214e5310 100644 --- a/src/query/sql/src/planner/optimizer/rule/transform/mod.rs +++ b/src/query/sql/src/planner/optimizer/rule/transform/mod.rs @@ -13,10 +13,16 @@ // limitations under the License. mod rule_commute_join; +mod rule_exchange_join; mod rule_left_associate_join; +mod rule_left_exchange_join; mod rule_right_associate_join; +mod rule_right_exchange_join; mod util; pub use rule_commute_join::RuleCommuteJoin; +pub use rule_exchange_join::RuleExchangeJoin; pub use rule_left_associate_join::RuleLeftAssociateJoin; +pub use rule_left_exchange_join::RuleLeftExchangeJoin; pub use rule_right_associate_join::RuleRightAssociateJoin; +pub use rule_right_exchange_join::RuleRightExchangeJoin; diff --git a/src/query/sql/src/planner/optimizer/rule/transform/rule_commute_join.rs b/src/query/sql/src/planner/optimizer/rule/transform/rule_commute_join.rs index e30fe8d10af4f..5c599df5058e8 100644 --- a/src/query/sql/src/planner/optimizer/rule/transform/rule_commute_join.rs +++ b/src/query/sql/src/planner/optimizer/rule/transform/rule_commute_join.rs @@ -78,8 +78,17 @@ impl Rule for RuleCommuteJoin { (join.left_conditions, join.right_conditions) = (join.right_conditions, join.left_conditions); join.join_type = join.join_type.opposite(); - let result = + let mut result = SExpr::create_binary(join.into(), right_child.clone(), left_child.clone()); + + // Disable the following rules for the generated expression + result.set_applied_rule(&RuleID::CommuteJoin); + result.set_applied_rule(&RuleID::LeftAssociateJoin); + result.set_applied_rule(&RuleID::LeftExchangeJoin); + result.set_applied_rule(&RuleID::RightAssociateJoin); + result.set_applied_rule(&RuleID::RightExchangeJoin); + result.set_applied_rule(&RuleID::ExchangeJoin); + state.add_result(result); } _ => {} diff --git a/src/query/sql/src/planner/optimizer/rule/transform/rule_exchange_join.rs b/src/query/sql/src/planner/optimizer/rule/transform/rule_exchange_join.rs new file mode 100644 index 0000000000000..4b959f9755129 --- /dev/null +++ b/src/query/sql/src/planner/optimizer/rule/transform/rule_exchange_join.rs @@ -0,0 +1,260 @@ +// Copyright 2023 Datafuse Labs. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::vec; + +use common_exception::Result; + +use super::util::get_join_predicates; +use crate::binder::JoinPredicate; +use crate::optimizer::rule::Rule; +use crate::optimizer::rule::TransformResult; +use crate::optimizer::RelExpr; +use crate::optimizer::RuleID; +use crate::optimizer::SExpr; +use crate::plans::Join; +use crate::plans::JoinType; +use crate::plans::PatternPlan; +use crate::plans::RelOp; + +/// Rule to apply exchange on a bushy join tree. +/// If we have a join tree like: +/// join +/// / \ +/// join join +/// / \ / \ +/// t1 t2 t3 t4 +/// +/// We can represent it as `(t1 ⋈ t2) ⋈ (t3 ⋈ t4)`. +/// With this rule, we can transform it to +/// `(t1 ⋈ t3) ⋈ (t2 ⋈ t4)`, which looks like: +/// join +/// / \ +/// join join +/// / \ / \ +/// t1 t3 t2 t4 +pub struct RuleExchangeJoin { + id: RuleID, + pattern: SExpr, +} + +impl RuleExchangeJoin { + pub fn new() -> Self { + Self { + id: RuleID::ExchangeJoin, + + // LogicalJoin + // | \ + // | LogicalJoin + // | | \ + // | * * + // LogicalJoin + // | \ + // * * + pattern: SExpr::create_binary( + PatternPlan { + plan_type: RelOp::Join, + } + .into(), + SExpr::create_binary( + PatternPlan { + plan_type: RelOp::Join, + } + .into(), + SExpr::create_pattern_leaf(), + SExpr::create_pattern_leaf(), + ), + SExpr::create_binary( + PatternPlan { + plan_type: RelOp::Join, + } + .into(), + SExpr::create_pattern_leaf(), + SExpr::create_pattern_leaf(), + ), + ), + } + } +} + +impl Rule for RuleExchangeJoin { + fn id(&self) -> RuleID { + self.id + } + + fn apply(&self, s_expr: &SExpr, state: &mut TransformResult) -> Result<()> { + // We denote the join tree with: + // join1 + // / \ + // join2 join3 + // / \ / \ + // t1 t2 t3 t4 + // + // After applying the transform, we will get: + // join4 + // / \ + // join5 join6 + // / \ / \ + // t1 t3 t2 t4 + let join1: Join = s_expr.plan.clone().try_into()?; + let join2: Join = s_expr.child(0)?.plan.clone().try_into()?; + let join3: Join = s_expr.child(1)?.plan.clone().try_into()?; + let t1 = s_expr.child(0)?.child(0)?; + let t2 = s_expr.child(0)?.child(1)?; + let t3 = s_expr.child(1)?.child(0)?; + let t4 = s_expr.child(1)?.child(1)?; + + // Ensure inner joins or cross joins. + if !matches!(join1.join_type, JoinType::Inner | JoinType::Cross) + || !matches!(join2.join_type, JoinType::Inner | JoinType::Cross) + || !matches!(join3.join_type, JoinType::Inner | JoinType::Cross) + { + return Ok(()); + } + + // Check if original sexpr contains cross join. + // We will reject the results contain cross join if there is no cross join in original sexpr. + let contains_cross_join = join1.join_type == JoinType::Cross + || join2.join_type == JoinType::Cross + || join3.join_type == JoinType::Cross; + + let predicates = vec![ + get_join_predicates(&join1)?, + get_join_predicates(&join2)?, + get_join_predicates(&join3)?, + ] + .concat(); + + let mut join_4 = Join::default(); + let mut join_5 = Join::default(); + let mut join_6 = Join::default(); + + let t1_prop = RelExpr::with_s_expr(t1).derive_relational_prop()?; + let t2_prop = RelExpr::with_s_expr(t2).derive_relational_prop()?; + let t3_prop = RelExpr::with_s_expr(t3).derive_relational_prop()?; + let t4_prop = RelExpr::with_s_expr(t4).derive_relational_prop()?; + let join5_prop = RelExpr::with_s_expr(&SExpr::create_binary( + join_5.clone().into(), + t1.clone(), + t3.clone(), + )) + .derive_relational_prop()?; + let join6_prop = RelExpr::with_s_expr(&SExpr::create_binary( + join_6.clone().into(), + t2.clone(), + t4.clone(), + )) + .derive_relational_prop()?; + + let mut join_5_preds = vec![]; + let mut join_6_preds = vec![]; + + // Resolve predicates for join3 + for predicate in predicates.iter() { + let join_pred = JoinPredicate::new(predicate, &join5_prop, &join6_prop); + match join_pred { + JoinPredicate::Left(pred) => { + join_5_preds.push(pred.clone()); + } + JoinPredicate::Right(pred) => { + join_6_preds.push(pred.clone()); + } + JoinPredicate::Both { left, right } => { + join_4.left_conditions.push(left.clone()); + join_4.right_conditions.push(right.clone()); + } + JoinPredicate::Other(pred) => { + join_4.non_equi_conditions.push(pred.clone()); + } + } + } + + if !join_4.left_conditions.is_empty() && !join_4.right_conditions.is_empty() { + join_4.join_type = JoinType::Inner; + } + + // Resolve predicates for join5 + for predicate in join_5_preds.iter() { + let join_pred = JoinPredicate::new(predicate, &t1_prop, &t3_prop); + match join_pred { + JoinPredicate::Left(_) | JoinPredicate::Right(_) | JoinPredicate::Other(_) => { + // TODO(leiysky): push down the predicate + join_5.non_equi_conditions.push(predicate.clone()); + } + JoinPredicate::Both { left, right } => { + join_5.left_conditions.push(left.clone()); + join_5.right_conditions.push(right.clone()); + } + } + } + + if !join_5.left_conditions.is_empty() && !join_5.right_conditions.is_empty() { + join_5.join_type = JoinType::Inner; + } + + // Resolve predicates for join6 + for predicate in join_6_preds.iter() { + let join_pred = JoinPredicate::new(predicate, &t2_prop, &t4_prop); + match join_pred { + JoinPredicate::Left(_) | JoinPredicate::Right(_) | JoinPredicate::Other(_) => { + // TODO(leiysky): push down the predicate + join_6.non_equi_conditions.push(predicate.clone()); + } + JoinPredicate::Both { left, right } => { + join_6.left_conditions.push(left.clone()); + join_6.right_conditions.push(right.clone()); + } + } + } + + if !join_6.left_conditions.is_empty() && !join_6.right_conditions.is_empty() { + join_6.join_type = JoinType::Inner; + } + + // Reject inefficient cross join + if !contains_cross_join + && (join_4.join_type == JoinType::Cross + || join_5.join_type == JoinType::Cross + || join_6.join_type == JoinType::Cross) + { + return Ok(()); + } + + let mut result = SExpr::create( + join_4.into(), + vec![ + SExpr::create_binary(join_5.into(), t1.clone(), t3.clone()), + SExpr::create_binary(join_6.into(), t2.clone(), t4.clone()), + ], + None, + None, + ); + + // Disable the following rules for join 4 + result.set_applied_rule(&RuleID::CommuteJoin); + result.set_applied_rule(&RuleID::LeftAssociateJoin); + result.set_applied_rule(&RuleID::LeftExchangeJoin); + result.set_applied_rule(&RuleID::RightAssociateJoin); + result.set_applied_rule(&RuleID::RightExchangeJoin); + result.set_applied_rule(&RuleID::ExchangeJoin); + + state.add_result(result); + + Ok(()) + } + + fn pattern(&self) -> &SExpr { + &self.pattern + } +} diff --git a/src/query/sql/src/planner/optimizer/rule/transform/rule_left_associate_join.rs b/src/query/sql/src/planner/optimizer/rule/transform/rule_left_associate_join.rs index c20e8884e49ee..c37ccb20a248d 100644 --- a/src/query/sql/src/planner/optimizer/rule/transform/rule_left_associate_join.rs +++ b/src/query/sql/src/planner/optimizer/rule/transform/rule_left_associate_join.rs @@ -106,8 +106,10 @@ impl Rule for RuleLeftAssociateJoin { let t2 = s_expr.child(0)?.child(1)?; let t3 = s_expr.child(1)?; - // Ensure inner joins - if join1.join_type != JoinType::Inner || join2.join_type != JoinType::Inner { + // Ensure inner joins or cross joins. + if !matches!(join1.join_type, JoinType::Inner | JoinType::Cross) + || !matches!(join2.join_type, JoinType::Inner | JoinType::Cross) + { return Ok(()); } @@ -184,7 +186,7 @@ impl Rule for RuleLeftAssociateJoin { return Ok(()); } - let result = SExpr::create( + let mut result = SExpr::create( join_3.into(), vec![ t1.clone(), @@ -194,6 +196,13 @@ impl Rule for RuleLeftAssociateJoin { None, ); + // Disable the following rules for join 3 + result.set_applied_rule(&RuleID::LeftAssociateJoin); + result.set_applied_rule(&RuleID::LeftExchangeJoin); + result.set_applied_rule(&RuleID::RightAssociateJoin); + result.set_applied_rule(&RuleID::RightExchangeJoin); + result.set_applied_rule(&RuleID::ExchangeJoin); + state.add_result(result); Ok(()) diff --git a/src/query/sql/src/planner/optimizer/rule/transform/rule_left_exchange_join.rs b/src/query/sql/src/planner/optimizer/rule/transform/rule_left_exchange_join.rs new file mode 100644 index 0000000000000..f087cbac0ba19 --- /dev/null +++ b/src/query/sql/src/planner/optimizer/rule/transform/rule_left_exchange_join.rs @@ -0,0 +1,213 @@ +// Copyright 2023 Datafuse Labs. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::vec; + +use common_exception::Result; + +use super::util::get_join_predicates; +use crate::binder::JoinPredicate; +use crate::optimizer::rule::Rule; +use crate::optimizer::rule::TransformResult; +use crate::optimizer::RelExpr; +use crate::optimizer::RuleID; +use crate::optimizer::SExpr; +use crate::plans::Join; +use crate::plans::JoinType; +use crate::plans::PatternPlan; +use crate::plans::RelOp; + +/// Rule to apply swap on a left-deep join. +/// If we have a join tree like: +/// join +/// / \ +/// join t3 +/// / \ +/// t1 t2 +/// +/// We can represent it as `(t1 ⋈ t2) ⋈ t3`. With this rule, we can transform +/// it to `(t1 ⋈ t3) ⋈ t2`, which looks like: +/// join +/// / \ +/// join t2 +/// / \ +/// t1 t3 +pub struct RuleLeftExchangeJoin { + id: RuleID, + pattern: SExpr, +} + +impl RuleLeftExchangeJoin { + pub fn new() -> Self { + Self { + id: RuleID::LeftExchangeJoin, + + // LogicalJoin + // | \ + // | * + // LogicalJoin + // | \ + // | * + // * + pattern: SExpr::create_binary( + PatternPlan { + plan_type: RelOp::Join, + } + .into(), + SExpr::create_binary( + PatternPlan { + plan_type: RelOp::Join, + } + .into(), + SExpr::create_pattern_leaf(), + SExpr::create_pattern_leaf(), + ), + SExpr::create_pattern_leaf(), + ), + } + } +} + +impl Rule for RuleLeftExchangeJoin { + fn id(&self) -> RuleID { + self.id + } + + fn apply(&self, s_expr: &SExpr, state: &mut TransformResult) -> Result<()> { + // We denote the join tree with: + // join1 + // / \ + // join2 t3 + // / \ + // t1 t2 + // + // After applying the transform, we will get: + // join3 + // / \ + // join4 t2 + // / \ + // t1 t3 + let join1: Join = s_expr.plan.clone().try_into()?; + let join2: Join = s_expr.child(0)?.plan.clone().try_into()?; + let t1 = s_expr.child(0)?.child(0)?; + let t2 = s_expr.child(0)?.child(1)?; + let t3 = s_expr.child(1)?; + + // Ensure inner joins or cross joins. + if !matches!(join1.join_type, JoinType::Inner | JoinType::Cross) + || !matches!(join2.join_type, JoinType::Inner | JoinType::Cross) + { + return Ok(()); + } + + // Check if original sexpr contains cross join. + // We will reject the results contain cross join if there is no cross join in original sexpr. + let contains_cross_join = + join1.join_type == JoinType::Cross || join2.join_type == JoinType::Cross; + + let predicates = vec![get_join_predicates(&join1)?, get_join_predicates(&join2)?].concat(); + + let mut join_3 = Join::default(); + let mut join_4 = Join::default(); + + let t1_prop = RelExpr::with_s_expr(t1).derive_relational_prop()?; + let t2_prop = RelExpr::with_s_expr(t2).derive_relational_prop()?; + let t3_prop = RelExpr::with_s_expr(t3).derive_relational_prop()?; + let join4_prop = RelExpr::with_s_expr(&SExpr::create_binary( + join_4.clone().into(), + t1.clone(), + t3.clone(), + )) + .derive_relational_prop()?; + + let mut join_4_preds = vec![]; + + // Resolve predicates for join3 + for predicate in predicates.iter() { + let join_pred = JoinPredicate::new(predicate, &join4_prop, &t2_prop); + match join_pred { + JoinPredicate::Left(pred) => { + join_4_preds.push(pred.clone()); + } + JoinPredicate::Right(pred) => { + // TODO(leiysky): push down the predicate + join_3.non_equi_conditions.push(pred.clone()); + } + JoinPredicate::Both { left, right } => { + join_3.left_conditions.push(left.clone()); + join_3.right_conditions.push(right.clone()); + } + JoinPredicate::Other(pred) => { + join_3.non_equi_conditions.push(pred.clone()); + } + } + } + + if !join_3.left_conditions.is_empty() && !join_3.right_conditions.is_empty() { + join_3.join_type = JoinType::Inner; + } + + // Resolve predicates for join4 + for predicate in join_4_preds.iter() { + let join_pred = JoinPredicate::new(predicate, &t1_prop, &t3_prop); + match join_pred { + JoinPredicate::Left(_) | JoinPredicate::Right(_) | JoinPredicate::Other(_) => { + // TODO(leiysky): push down the predicate + join_4.non_equi_conditions.push(predicate.clone()); + } + JoinPredicate::Both { left, right } => { + join_4.left_conditions.push(left.clone()); + join_4.right_conditions.push(right.clone()); + } + } + } + + if !join_4.left_conditions.is_empty() && !join_4.right_conditions.is_empty() { + join_4.join_type = JoinType::Inner; + } + + // Reject inefficient cross join + if !contains_cross_join + && (join_3.join_type == JoinType::Cross || join_4.join_type == JoinType::Cross) + { + return Ok(()); + } + + let mut result = SExpr::create( + join_3.into(), + vec![ + SExpr::create_binary(join_4.into(), t1.clone(), t3.clone()), + t2.clone(), + ], + None, + None, + ); + + // Disable the following rules for join 3 + result.set_applied_rule(&RuleID::CommuteJoin); + result.set_applied_rule(&RuleID::LeftAssociateJoin); + result.set_applied_rule(&RuleID::LeftExchangeJoin); + result.set_applied_rule(&RuleID::RightAssociateJoin); + result.set_applied_rule(&RuleID::RightExchangeJoin); + result.set_applied_rule(&RuleID::ExchangeJoin); + + state.add_result(result); + + Ok(()) + } + + fn pattern(&self) -> &SExpr { + &self.pattern + } +} diff --git a/src/query/sql/src/planner/optimizer/rule/transform/rule_right_associate_join.rs b/src/query/sql/src/planner/optimizer/rule/transform/rule_right_associate_join.rs index a117da5874c27..ad7ae9e23747e 100644 --- a/src/query/sql/src/planner/optimizer/rule/transform/rule_right_associate_join.rs +++ b/src/query/sql/src/planner/optimizer/rule/transform/rule_right_associate_join.rs @@ -102,8 +102,10 @@ impl Rule for RuleRightAssociateJoin { let t2 = s_expr.child(1)?.child(0)?; let t3 = s_expr.child(1)?.child(1)?; - // Ensure inner joins - if join1.join_type != JoinType::Inner || join2.join_type != JoinType::Inner { + // Ensure inner joins or cross joins. + if !matches!(join1.join_type, JoinType::Inner | JoinType::Cross) + || !matches!(join2.join_type, JoinType::Inner | JoinType::Cross) + { return Ok(()); } @@ -180,7 +182,7 @@ impl Rule for RuleRightAssociateJoin { return Ok(()); } - let result = SExpr::create( + let mut result = SExpr::create( join_3.into(), vec![ SExpr::create_binary(join_4.into(), t1.clone(), t2.clone()), @@ -190,6 +192,13 @@ impl Rule for RuleRightAssociateJoin { None, ); + // Disable the following rules for join 3 + result.set_applied_rule(&RuleID::LeftAssociateJoin); + result.set_applied_rule(&RuleID::LeftExchangeJoin); + result.set_applied_rule(&RuleID::RightAssociateJoin); + result.set_applied_rule(&RuleID::RightExchangeJoin); + result.set_applied_rule(&RuleID::ExchangeJoin); + state.add_result(result); Ok(()) diff --git a/src/query/sql/src/planner/optimizer/rule/transform/rule_right_exchange_join.rs b/src/query/sql/src/planner/optimizer/rule/transform/rule_right_exchange_join.rs new file mode 100644 index 0000000000000..6547e625cba9b --- /dev/null +++ b/src/query/sql/src/planner/optimizer/rule/transform/rule_right_exchange_join.rs @@ -0,0 +1,211 @@ +// Copyright 2023 Datafuse Labs. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::vec; + +use common_exception::Result; + +use super::util::get_join_predicates; +use crate::binder::JoinPredicate; +use crate::optimizer::rule::Rule; +use crate::optimizer::rule::TransformResult; +use crate::optimizer::RelExpr; +use crate::optimizer::RuleID; +use crate::optimizer::SExpr; +use crate::plans::Join; +use crate::plans::JoinType; +use crate::plans::PatternPlan; +use crate::plans::RelOp; + +/// Rule to apply swap on a right-deep join. +/// If we have a join tree like: +/// join +/// / \ +/// t1 join +/// / \ +/// t2 t3 +/// +/// We can represent it as `t1 ⋈ (t2 ⋈ t3)`. With this rule, we can transform +/// it to `t2 ⋈ (t2 ⋈ t3)`, which looks like: +/// join +/// / \ +/// t2 join +/// / \ +/// t1 t3 +pub struct RuleRightExchangeJoin { + id: RuleID, + pattern: SExpr, +} + +impl RuleRightExchangeJoin { + pub fn new() -> Self { + Self { + id: RuleID::RightExchangeJoin, + + // LogicalJoin + // | \ + // * LogicalJoin + // | \ + // * * + pattern: SExpr::create_binary( + PatternPlan { + plan_type: RelOp::Join, + } + .into(), + SExpr::create_pattern_leaf(), + SExpr::create_binary( + PatternPlan { + plan_type: RelOp::Join, + } + .into(), + SExpr::create_pattern_leaf(), + SExpr::create_pattern_leaf(), + ), + ), + } + } +} + +impl Rule for RuleRightExchangeJoin { + fn id(&self) -> RuleID { + self.id + } + + fn apply(&self, s_expr: &SExpr, state: &mut TransformResult) -> Result<()> { + // We denote the join tree with: + // join1 + // / \ + // t1 join2 + // / \ + // t2 t3 + // + // After applying the transform, we will get: + // join3 + // / \ + // t2 join4 + // / \ + // t1 t3 + let join1: Join = s_expr.plan.clone().try_into()?; + let join2: Join = s_expr.child(1)?.plan.clone().try_into()?; + let t1 = s_expr.child(0)?; + let t2 = s_expr.child(1)?.child(0)?; + let t3 = s_expr.child(1)?.child(1)?; + + // Ensure inner joins or cross joins. + if !matches!(join1.join_type, JoinType::Inner | JoinType::Cross) + || !matches!(join2.join_type, JoinType::Inner | JoinType::Cross) + { + return Ok(()); + } + + // Check if original sexpr contains cross join. + // We will reject the results contain cross join if there is no cross join in original sexpr. + let contains_cross_join = + join1.join_type == JoinType::Cross || join2.join_type == JoinType::Cross; + + let predicates = vec![get_join_predicates(&join1)?, get_join_predicates(&join2)?].concat(); + + let mut join_3 = Join::default(); + let mut join_4 = Join::default(); + + let t1_prop = RelExpr::with_s_expr(t1).derive_relational_prop()?; + let t2_prop = RelExpr::with_s_expr(t2).derive_relational_prop()?; + let t3_prop = RelExpr::with_s_expr(t3).derive_relational_prop()?; + let join4_prop = RelExpr::with_s_expr(&SExpr::create_binary( + join_4.clone().into(), + t1.clone(), + t3.clone(), + )) + .derive_relational_prop()?; + + let mut join_4_preds = vec![]; + + // Resolve predicates for join3 + for predicate in predicates.iter() { + let join_pred = JoinPredicate::new(predicate, &join4_prop, &t2_prop); + match join_pred { + JoinPredicate::Left(pred) => { + // TODO(leiysky): push down the predicate + join_3.non_equi_conditions.push(pred.clone()); + } + JoinPredicate::Right(pred) => { + join_4_preds.push(pred.clone()); + } + JoinPredicate::Both { left, right } => { + join_3.left_conditions.push(left.clone()); + join_3.right_conditions.push(right.clone()); + } + JoinPredicate::Other(pred) => { + join_3.non_equi_conditions.push(pred.clone()); + } + } + } + + if !join_3.left_conditions.is_empty() && !join_3.right_conditions.is_empty() { + join_3.join_type = JoinType::Inner; + } + + // Resolve predicates for join4 + for predicate in join_4_preds.iter() { + let join_pred = JoinPredicate::new(predicate, &t1_prop, &t3_prop); + match join_pred { + JoinPredicate::Left(_) | JoinPredicate::Right(_) | JoinPredicate::Other(_) => { + // TODO(leiysky): push down the predicate + join_4.non_equi_conditions.push(predicate.clone()); + } + JoinPredicate::Both { left, right } => { + join_4.left_conditions.push(left.clone()); + join_4.right_conditions.push(right.clone()); + } + } + } + + if !join_4.left_conditions.is_empty() && !join_4.right_conditions.is_empty() { + join_4.join_type = JoinType::Inner; + } + + // Reject inefficient cross join + if !contains_cross_join + && (join_3.join_type == JoinType::Cross || join_4.join_type == JoinType::Cross) + { + return Ok(()); + } + + let mut result = SExpr::create( + join_3.into(), + vec![ + t2.clone(), + SExpr::create_binary(join_4.into(), t1.clone(), t3.clone()), + ], + None, + None, + ); + + // Disable the following rules for join 3 + result.set_applied_rule(&RuleID::CommuteJoin); + result.set_applied_rule(&RuleID::LeftAssociateJoin); + result.set_applied_rule(&RuleID::LeftExchangeJoin); + result.set_applied_rule(&RuleID::RightAssociateJoin); + result.set_applied_rule(&RuleID::RightExchangeJoin); + result.set_applied_rule(&RuleID::ExchangeJoin); + + state.add_result(result); + + Ok(()) + } + + fn pattern(&self) -> &SExpr { + &self.pattern + } +} diff --git a/src/query/sql/src/planner/optimizer/s_expr.rs b/src/query/sql/src/planner/optimizer/s_expr.rs index cf3f23b643f34..ca0ceb20156df 100644 --- a/src/query/sql/src/planner/optimizer/s_expr.rs +++ b/src/query/sql/src/planner/optimizer/s_expr.rs @@ -139,7 +139,7 @@ impl SExpr { } /// Record the applied rule id in current SExpr - pub(crate) fn apply_rule(&mut self, rule_id: &RuleID) { + pub(crate) fn set_applied_rule(&mut self, rule_id: &RuleID) { self.applied_rules.set(rule_id, true); } diff --git a/tests/sqllogictests/suites/mode/standalone/explain/explain.test b/tests/sqllogictests/suites/mode/standalone/explain/explain.test index 613290fb871a9..e4a1c9517551f 100644 --- a/tests/sqllogictests/suites/mode/standalone/explain/explain.test +++ b/tests/sqllogictests/suites/mode/standalone/explain/explain.test @@ -5,84 +5,84 @@ statement ok drop table if exists t2 all statement ok -create table t1(a int, b int) +create table t1 as select number as a, number as b from numbers(1) statement ok -create table t2(a int, b int) +create table t2 as select number as a, number as b from numbers(5) query T explain select t1.a from t1 where a > 0 ---- Filter ├── filters: [gt(t1.a (#0), 0_u8)] -├── estimated rows: 0.00 +├── estimated rows: 0.33 └── TableScan ├── table: default.default.t1 ├── read rows: 0 ├── read bytes: 0 - ├── partitions total: 0 + ├── partitions total: 1 ├── partitions scanned: 0 - ├── push downs: [filters: [gt(a, 0_i32)], limit: NONE] + ├── push downs: [filters: [gt(a, 0_u64)], limit: NONE] ├── output columns: [0] - └── estimated rows: 0.00 + └── estimated rows: 1.00 query T explain select * from t1, t2 where (t1.a = t2.a and t1.a > 3) or (t1.a = t2.a and t2.a > 5 and t1.a > 1) ---- Filter ├── filters: [or(gt(t1.a (#0), 3_u8), and(gt(t2.a (#2), 5_u8), gt(t1.a (#0), 1_u8)))] -├── estimated rows: 0.00 +├── estimated rows: 0.56 └── HashJoin ├── join type: INNER - ├── build keys: [t2.a (#2)] - ├── probe keys: [t1.a (#0)] + ├── build keys: [t1.a (#0)] + ├── probe keys: [t2.a (#2)] ├── filters: [] - ├── estimated rows: 0.00 - ├── TableScan(Build) - │ ├── table: default.default.t2 - │ ├── read rows: 0 - │ ├── read bytes: 0 - │ ├── partitions total: 0 - │ ├── partitions scanned: 0 - │ ├── push downs: [filters: [], limit: NONE] - │ └── estimated rows: 0.00 - └── Filter(Probe) - ├── filters: [or(gt(t1.a (#0), 3_u8), gt(t1.a (#0), 1_u8))] - ├── estimated rows: 0.00 - └── TableScan - ├── table: default.default.t1 - ├── read rows: 0 - ├── read bytes: 0 - ├── partitions total: 0 - ├── partitions scanned: 0 - ├── push downs: [filters: [or(gt(a, 3_i32), gt(a, 1_i32))], limit: NONE] - └── estimated rows: 0.00 + ├── estimated rows: 5.00 + ├── Filter(Build) + │ ├── filters: [or(gt(t1.a (#0), 3_u8), gt(t1.a (#0), 1_u8))] + │ ├── estimated rows: 0.33 + │ └── TableScan + │ ├── table: default.default.t1 + │ ├── read rows: 0 + │ ├── read bytes: 0 + │ ├── partitions total: 1 + │ ├── partitions scanned: 0 + │ ├── push downs: [filters: [or(gt(a, 3_u64), gt(a, 1_u64))], limit: NONE] + │ └── estimated rows: 1.00 + └── TableScan(Probe) + ├── table: default.default.t2 + ├── read rows: 5 + ├── read bytes: 94 + ├── partitions total: 1 + ├── partitions scanned: 1 + ├── push downs: [filters: [], limit: NONE] + └── estimated rows: 5.00 query T explain select * from t1, t2 where (t1.a = t2.a and t1.a > 3) or (t1.a = t2.a) ---- HashJoin ├── join type: INNER -├── build keys: [t2.a (#2)] -├── probe keys: [t1.a (#0)] +├── build keys: [t1.a (#0)] +├── probe keys: [t2.a (#2)] ├── filters: [] -├── estimated rows: 0.00 +├── estimated rows: 5.00 ├── TableScan(Build) -│ ├── table: default.default.t2 -│ ├── read rows: 0 -│ ├── read bytes: 0 -│ ├── partitions total: 0 -│ ├── partitions scanned: 0 +│ ├── table: default.default.t1 +│ ├── read rows: 1 +│ ├── read bytes: 62 +│ ├── partitions total: 1 +│ ├── partitions scanned: 1 │ ├── push downs: [filters: [], limit: NONE] -│ └── estimated rows: 0.00 +│ └── estimated rows: 1.00 └── TableScan(Probe) - ├── table: default.default.t1 - ├── read rows: 0 - ├── read bytes: 0 - ├── partitions total: 0 - ├── partitions scanned: 0 + ├── table: default.default.t2 + ├── read rows: 5 + ├── read bytes: 94 + ├── partitions total: 1 + ├── partitions scanned: 1 ├── push downs: [filters: [], limit: NONE] - └── estimated rows: 0.00 + └── estimated rows: 5.00 query T explain raw select * from t1, t2 where (t1.a = t2.a and t1.a > 3) or (t1.a = t2.a) @@ -516,141 +516,141 @@ query T explain select a from t1 UNION ALL select a from t2 ---- UnionAll -├── estimated rows: 0.00 +├── estimated rows: 6.00 ├── TableScan │ ├── table: default.default.t1 -│ ├── read rows: 0 -│ ├── read bytes: 0 -│ ├── partitions total: 0 -│ ├── partitions scanned: 0 +│ ├── read rows: 1 +│ ├── read bytes: 31 +│ ├── partitions total: 1 +│ ├── partitions scanned: 1 │ ├── push downs: [filters: [], limit: NONE] │ ├── output columns: [0] -│ └── estimated rows: 0.00 +│ └── estimated rows: 1.00 └── TableScan ├── table: default.default.t2 - ├── read rows: 0 - ├── read bytes: 0 - ├── partitions total: 0 - ├── partitions scanned: 0 + ├── read rows: 5 + ├── read bytes: 47 + ├── partitions total: 1 + ├── partitions scanned: 1 ├── push downs: [filters: [], limit: NONE] ├── output columns: [0] - └── estimated rows: 0.00 + └── estimated rows: 5.00 query T explain select * from t1,t2 where (t1.a > 1 and t2.a > 2) or (t1.b < 3 and t2.b < 4) ---- Filter ├── filters: [or(and(gt(t1.a (#0), 1_u8), gt(t2.a (#2), 2_u8)), and(lt(t1.b (#1), 3_u8), lt(t2.b (#3), 4_u8)))] -├── estimated rows: 0.00 +├── estimated rows: 0.09 └── HashJoin ├── join type: CROSS ├── build keys: [] ├── probe keys: [] ├── filters: [] - ├── estimated rows: 0.00 + ├── estimated rows: 0.83 ├── Filter(Build) - │ ├── filters: [or(gt(t2.a (#2), 2_u8), lt(t2.b (#3), 4_u8))] - │ ├── estimated rows: 0.00 + │ ├── filters: [or(gt(t1.a (#0), 1_u8), lt(t1.b (#1), 3_u8))] + │ ├── estimated rows: 0.33 │ └── TableScan - │ ├── table: default.default.t2 - │ ├── read rows: 0 - │ ├── read bytes: 0 - │ ├── partitions total: 0 - │ ├── partitions scanned: 0 - │ ├── push downs: [filters: [or(gt(a, 2_i32), lt(b, 4_i32))], limit: NONE] - │ └── estimated rows: 0.00 + │ ├── table: default.default.t1 + │ ├── read rows: 1 + │ ├── read bytes: 62 + │ ├── partitions total: 1 + │ ├── partitions scanned: 1 + │ ├── push downs: [filters: [or(gt(a, 1_u64), lt(b, 3_u64))], limit: NONE] + │ └── estimated rows: 1.00 └── Filter(Probe) - ├── filters: [or(gt(t1.a (#0), 1_u8), lt(t1.b (#1), 3_u8))] - ├── estimated rows: 0.00 + ├── filters: [or(gt(t2.a (#2), 2_u8), lt(t2.b (#3), 4_u8))] + ├── estimated rows: 2.50 └── TableScan - ├── table: default.default.t1 - ├── read rows: 0 - ├── read bytes: 0 - ├── partitions total: 0 - ├── partitions scanned: 0 - ├── push downs: [filters: [or(gt(a, 1_i32), lt(b, 3_i32))], limit: NONE] - └── estimated rows: 0.00 + ├── table: default.default.t2 + ├── read rows: 5 + ├── read bytes: 94 + ├── partitions total: 1 + ├── partitions scanned: 1 + ├── push downs: [filters: [or(gt(a, 2_u64), lt(b, 4_u64))], limit: NONE] + └── estimated rows: 5.00 query T explain select * from t1,t2 where (t1.a > 1 and t2.a > 2) or (t1.b < 3 and t2.b < 4) or t1.a = 2 ---- Filter ├── filters: [or(or(and(gt(t1.a (#0), 1_u8), gt(t2.a (#2), 2_u8)), and(lt(t1.b (#1), 3_u8), lt(t2.b (#3), 4_u8))), eq(t1.a (#0), 2_u8))] -├── estimated rows: 0.00 +├── estimated rows: 0.19 └── HashJoin ├── join type: CROSS ├── build keys: [] ├── probe keys: [] ├── filters: [] - ├── estimated rows: 0.00 - ├── TableScan(Build) - │ ├── table: default.default.t2 - │ ├── read rows: 0 - │ ├── read bytes: 0 - │ ├── partitions total: 0 - │ ├── partitions scanned: 0 - │ ├── push downs: [filters: [], limit: NONE] - │ └── estimated rows: 0.00 - └── Filter(Probe) - ├── filters: [or(gt(t1.a (#0), 1_u8), or(lt(t1.b (#1), 3_u8), eq(t1.a (#0), 2_u8)))] - ├── estimated rows: 0.00 - └── TableScan - ├── table: default.default.t1 - ├── read rows: 0 - ├── read bytes: 0 - ├── partitions total: 0 - ├── partitions scanned: 0 - ├── push downs: [filters: [or(gt(a, 1_i32), or(lt(b, 3_i32), eq(a, 2_i32)))], limit: NONE] - └── estimated rows: 0.00 + ├── estimated rows: 1.67 + ├── Filter(Build) + │ ├── filters: [or(gt(t1.a (#0), 1_u8), or(lt(t1.b (#1), 3_u8), eq(t1.a (#0), 2_u8)))] + │ ├── estimated rows: 0.33 + │ └── TableScan + │ ├── table: default.default.t1 + │ ├── read rows: 1 + │ ├── read bytes: 62 + │ ├── partitions total: 1 + │ ├── partitions scanned: 1 + │ ├── push downs: [filters: [or(gt(a, 1_u64), or(lt(b, 3_u64), eq(a, 2_u64)))], limit: NONE] + │ └── estimated rows: 1.00 + └── TableScan(Probe) + ├── table: default.default.t2 + ├── read rows: 5 + ├── read bytes: 94 + ├── partitions total: 1 + ├── partitions scanned: 1 + ├── push downs: [filters: [], limit: NONE] + └── estimated rows: 5.00 statement ok drop table if exists t3 statement ok -create table t3(a int, b int) +create table t3 as select number as a, number as b from numbers(10) query T explain select * from t1,t2, t3 where (t1.a > 1 and t2.a > 2) or (t1.b < 3 and t2.b < 4) or t3.a = 2 ---- Filter ├── filters: [or(or(and(gt(t1.a (#0), 1_u8), gt(t2.a (#2), 2_u8)), and(lt(t1.b (#1), 3_u8), lt(t2.b (#3), 4_u8))), eq(t3.a (#4), 2_u8))] -├── estimated rows: 0.00 +├── estimated rows: 5.56 └── HashJoin ├── join type: CROSS ├── build keys: [] ├── probe keys: [] ├── filters: [] - ├── estimated rows: 0.00 - ├── TableScan(Build) - │ ├── table: default.default.t3 - │ ├── read rows: 0 - │ ├── read bytes: 0 - │ ├── partitions total: 0 - │ ├── partitions scanned: 0 - │ ├── push downs: [filters: [], limit: NONE] - │ └── estimated rows: 0.00 - └── HashJoin(Probe) - ├── join type: CROSS - ├── build keys: [] - ├── probe keys: [] - ├── filters: [] - ├── estimated rows: 0.00 - ├── TableScan(Build) - │ ├── table: default.default.t2 - │ ├── read rows: 0 - │ ├── read bytes: 0 - │ ├── partitions total: 0 - │ ├── partitions scanned: 0 - │ ├── push downs: [filters: [], limit: NONE] - │ └── estimated rows: 0.00 - └── TableScan(Probe) - ├── table: default.default.t1 - ├── read rows: 0 - ├── read bytes: 0 - ├── partitions total: 0 - ├── partitions scanned: 0 - ├── push downs: [filters: [], limit: NONE] - └── estimated rows: 0.00 + ├── estimated rows: 50.00 + ├── HashJoin(Build) + │ ├── join type: CROSS + │ ├── build keys: [] + │ ├── probe keys: [] + │ ├── filters: [] + │ ├── estimated rows: 5.00 + │ ├── TableScan(Build) + │ │ ├── table: default.default.t1 + │ │ ├── read rows: 1 + │ │ ├── read bytes: 62 + │ │ ├── partitions total: 1 + │ │ ├── partitions scanned: 1 + │ │ ├── push downs: [filters: [], limit: NONE] + │ │ └── estimated rows: 1.00 + │ └── TableScan(Probe) + │ ├── table: default.default.t2 + │ ├── read rows: 5 + │ ├── read bytes: 94 + │ ├── partitions total: 1 + │ ├── partitions scanned: 1 + │ ├── push downs: [filters: [], limit: NONE] + │ └── estimated rows: 5.00 + └── TableScan(Probe) + ├── table: default.default.t3 + ├── read rows: 10 + ├── read bytes: 136 + ├── partitions total: 1 + ├── partitions scanned: 1 + ├── push downs: [filters: [], limit: NONE] + └── estimated rows: 10.00 query T explain select * from t1,t2, t3 where ((t1.a > 1 and t2.a > 2) or (t1.b < 3 and t2.b < 4)) and t3.a > 1 @@ -660,84 +660,84 @@ HashJoin ├── build keys: [] ├── probe keys: [] ├── filters: [] -├── estimated rows: 0.00 +├── estimated rows: 0.82 ├── Filter(Build) -│ ├── filters: [gt(t3.a (#4), 1_u8)] -│ ├── estimated rows: 0.00 -│ └── TableScan -│ ├── table: default.default.t3 -│ ├── read rows: 0 -│ ├── read bytes: 0 -│ ├── partitions total: 0 -│ ├── partitions scanned: 0 -│ ├── push downs: [filters: [gt(a, 1_i32)], limit: NONE] -│ └── estimated rows: 0.00 +│ ├── filters: [or(and(gt(t1.a (#0), 1_u8), gt(t2.a (#2), 2_u8)), and(lt(t1.b (#1), 3_u8), lt(t2.b (#3), 4_u8)))] +│ ├── estimated rows: 0.09 +│ └── HashJoin +│ ├── join type: CROSS +│ ├── build keys: [] +│ ├── probe keys: [] +│ ├── filters: [] +│ ├── estimated rows: 0.83 +│ ├── Filter(Build) +│ │ ├── filters: [or(gt(t1.a (#0), 1_u8), lt(t1.b (#1), 3_u8))] +│ │ ├── estimated rows: 0.33 +│ │ └── TableScan +│ │ ├── table: default.default.t1 +│ │ ├── read rows: 1 +│ │ ├── read bytes: 62 +│ │ ├── partitions total: 1 +│ │ ├── partitions scanned: 1 +│ │ ├── push downs: [filters: [or(gt(a, 1_u64), lt(b, 3_u64))], limit: NONE] +│ │ └── estimated rows: 1.00 +│ └── Filter(Probe) +│ ├── filters: [or(gt(t2.a (#2), 2_u8), lt(t2.b (#3), 4_u8))] +│ ├── estimated rows: 2.50 +│ └── TableScan +│ ├── table: default.default.t2 +│ ├── read rows: 5 +│ ├── read bytes: 94 +│ ├── partitions total: 1 +│ ├── partitions scanned: 1 +│ ├── push downs: [filters: [or(gt(a, 2_u64), lt(b, 4_u64))], limit: NONE] +│ └── estimated rows: 5.00 └── Filter(Probe) - ├── filters: [or(and(gt(t1.a (#0), 1_u8), gt(t2.a (#2), 2_u8)), and(lt(t1.b (#1), 3_u8), lt(t2.b (#3), 4_u8)))] - ├── estimated rows: 0.00 - └── HashJoin - ├── join type: CROSS - ├── build keys: [] - ├── probe keys: [] - ├── filters: [] - ├── estimated rows: 0.00 - ├── Filter(Build) - │ ├── filters: [or(gt(t2.a (#2), 2_u8), lt(t2.b (#3), 4_u8))] - │ ├── estimated rows: 0.00 - │ └── TableScan - │ ├── table: default.default.t2 - │ ├── read rows: 0 - │ ├── read bytes: 0 - │ ├── partitions total: 0 - │ ├── partitions scanned: 0 - │ ├── push downs: [filters: [or(gt(a, 2_i32), lt(b, 4_i32))], limit: NONE] - │ └── estimated rows: 0.00 - └── Filter(Probe) - ├── filters: [or(gt(t1.a (#0), 1_u8), lt(t1.b (#1), 3_u8))] - ├── estimated rows: 0.00 - └── TableScan - ├── table: default.default.t1 - ├── read rows: 0 - ├── read bytes: 0 - ├── partitions total: 0 - ├── partitions scanned: 0 - ├── push downs: [filters: [or(gt(a, 1_i32), lt(b, 3_i32))], limit: NONE] - └── estimated rows: 0.00 + ├── filters: [gt(t3.a (#4), 1_u8)] + ├── estimated rows: 8.89 + └── TableScan + ├── table: default.default.t3 + ├── read rows: 10 + ├── read bytes: 136 + ├── partitions total: 1 + ├── partitions scanned: 1 + ├── push downs: [filters: [gt(a, 1_u64)], limit: NONE] + └── estimated rows: 10.00 query T explain select * from t1,t2 where ((t1.a > 1 or t1.b < 2) and t2.a > 2) or (t1.b < 3 and t2.b < 4) ---- Filter ├── filters: [or(and(or(gt(t1.a (#0), 1_u8), lt(t1.b (#1), 2_u8)), gt(t2.a (#2), 2_u8)), and(lt(t1.b (#1), 3_u8), lt(t2.b (#3), 4_u8)))] -├── estimated rows: 0.00 +├── estimated rows: 0.09 └── HashJoin ├── join type: CROSS ├── build keys: [] ├── probe keys: [] ├── filters: [] - ├── estimated rows: 0.00 + ├── estimated rows: 0.83 ├── Filter(Build) - │ ├── filters: [or(gt(t2.a (#2), 2_u8), lt(t2.b (#3), 4_u8))] - │ ├── estimated rows: 0.00 + │ ├── filters: [or(or(gt(t1.a (#0), 1_u8), lt(t1.b (#1), 2_u8)), lt(t1.b (#1), 3_u8))] + │ ├── estimated rows: 0.33 │ └── TableScan - │ ├── table: default.default.t2 - │ ├── read rows: 0 - │ ├── read bytes: 0 - │ ├── partitions total: 0 - │ ├── partitions scanned: 0 - │ ├── push downs: [filters: [or(gt(a, 2_i32), lt(b, 4_i32))], limit: NONE] - │ └── estimated rows: 0.00 + │ ├── table: default.default.t1 + │ ├── read rows: 1 + │ ├── read bytes: 62 + │ ├── partitions total: 1 + │ ├── partitions scanned: 1 + │ ├── push downs: [filters: [or(or(gt(a, 1_u64), lt(b, 2_u64)), lt(b, 3_u64))], limit: NONE] + │ └── estimated rows: 1.00 └── Filter(Probe) - ├── filters: [or(or(gt(t1.a (#0), 1_u8), lt(t1.b (#1), 2_u8)), lt(t1.b (#1), 3_u8))] - ├── estimated rows: 0.00 + ├── filters: [or(gt(t2.a (#2), 2_u8), lt(t2.b (#3), 4_u8))] + ├── estimated rows: 2.50 └── TableScan - ├── table: default.default.t1 - ├── read rows: 0 - ├── read bytes: 0 - ├── partitions total: 0 - ├── partitions scanned: 0 - ├── push downs: [filters: [or(or(gt(a, 1_i32), lt(b, 2_i32)), lt(b, 3_i32))], limit: NONE] - └── estimated rows: 0.00 + ├── table: default.default.t2 + ├── read rows: 5 + ├── read bytes: 94 + ├── partitions total: 1 + ├── partitions scanned: 1 + ├── push downs: [filters: [or(gt(a, 2_u64), lt(b, 4_u64))], limit: NONE] + └── estimated rows: 5.00 query T explain select * from t1,t2 where (t1.a > 1 or t1.b < 2) and (t1.a > 1 or t1.b < 2) @@ -747,26 +747,26 @@ HashJoin ├── build keys: [] ├── probe keys: [] ├── filters: [] -├── estimated rows: 0.00 -├── TableScan(Build) -│ ├── table: default.default.t2 -│ ├── read rows: 0 -│ ├── read bytes: 0 -│ ├── partitions total: 0 -│ ├── partitions scanned: 0 -│ ├── push downs: [filters: [], limit: NONE] -│ └── estimated rows: 0.00 -└── Filter(Probe) - ├── filters: [or(gt(t1.a (#0), 1_u8), lt(t1.b (#1), 2_u8))] - ├── estimated rows: 0.00 - └── TableScan - ├── table: default.default.t1 - ├── read rows: 0 - ├── read bytes: 0 - ├── partitions total: 0 - ├── partitions scanned: 0 - ├── push downs: [filters: [or(gt(a, 1_i32), lt(b, 2_i32))], limit: NONE] - └── estimated rows: 0.00 +├── estimated rows: 1.67 +├── Filter(Build) +│ ├── filters: [or(gt(t1.a (#0), 1_u8), lt(t1.b (#1), 2_u8))] +│ ├── estimated rows: 0.33 +│ └── TableScan +│ ├── table: default.default.t1 +│ ├── read rows: 1 +│ ├── read bytes: 62 +│ ├── partitions total: 1 +│ ├── partitions scanned: 1 +│ ├── push downs: [filters: [or(gt(a, 1_u64), lt(b, 2_u64))], limit: NONE] +│ └── estimated rows: 1.00 +└── TableScan(Probe) + ├── table: default.default.t2 + ├── read rows: 5 + ├── read bytes: 94 + ├── partitions total: 1 + ├── partitions scanned: 1 + ├── push downs: [filters: [], limit: NONE] + └── estimated rows: 5.00 statement ok drop table t1 diff --git a/tests/sqllogictests/suites/mode/standalone/explain/join.test b/tests/sqllogictests/suites/mode/standalone/explain/join.test index 23f58e4ac04d3..ce8f30536a8b9 100644 --- a/tests/sqllogictests/suites/mode/standalone/explain/join.test +++ b/tests/sqllogictests/suites/mode/standalone/explain/join.test @@ -1,159 +1,177 @@ +statement ok +drop table if exists t + +statement ok +drop table if exists t1 + +statement ok +drop table if exists t2 + +statement ok +create table t as select number from numbers(1) + +statement ok +create table t1 as select number from numbers(10) + +statement ok +create table t2 as select number from numbers(100) + query T -explain select t.number from numbers(1) as t, numbers(1) as t1 where t.number = t1.number +explain select t.number from t, t1 where t.number = t1.number ---- HashJoin ├── join type: INNER -├── build keys: [t1.number (#1)] -├── probe keys: [t.number (#0)] +├── build keys: [t.number (#0)] +├── probe keys: [t1.number (#1)] ├── filters: [] -├── estimated rows: 1.00 +├── estimated rows: 10.00 ├── TableScan(Build) -│ ├── table: default.system.numbers +│ ├── table: default.default.t │ ├── read rows: 1 -│ ├── read bytes: 8 +│ ├── read bytes: 31 │ ├── partitions total: 1 │ ├── partitions scanned: 1 │ ├── push downs: [filters: [], limit: NONE] │ └── estimated rows: 1.00 └── TableScan(Probe) - ├── table: default.system.numbers - ├── read rows: 1 - ├── read bytes: 8 + ├── table: default.default.t1 + ├── read rows: 10 + ├── read bytes: 68 ├── partitions total: 1 ├── partitions scanned: 1 ├── push downs: [filters: [], limit: NONE] - └── estimated rows: 1.00 + └── estimated rows: 10.00 query T -explain select t.number from numbers(1) as t, numbers(1) as t1 where t.number = t1.number and t.number = t1.number + 1 +explain select t.number from t, t1 where t.number = t1.number and t.number = t1.number + 1 ---- HashJoin ├── join type: INNER -├── build keys: [t1.number (#1), plus(t1.number (#1), 1_u8)] -├── probe keys: [t.number (#0), t.number (#0)] +├── build keys: [t.number (#0), t.number (#0)] +├── probe keys: [t1.number (#1), plus(t1.number (#1), 1_u8)] ├── filters: [] -├── estimated rows: 1.00 +├── estimated rows: 10.00 ├── TableScan(Build) -│ ├── table: default.system.numbers +│ ├── table: default.default.t │ ├── read rows: 1 -│ ├── read bytes: 8 +│ ├── read bytes: 31 │ ├── partitions total: 1 │ ├── partitions scanned: 1 │ ├── push downs: [filters: [], limit: NONE] │ └── estimated rows: 1.00 └── TableScan(Probe) - ├── table: default.system.numbers - ├── read rows: 1 - ├── read bytes: 8 + ├── table: default.default.t1 + ├── read rows: 10 + ├── read bytes: 68 ├── partitions total: 1 ├── partitions scanned: 1 ├── push downs: [filters: [], limit: NONE] - └── estimated rows: 1.00 + └── estimated rows: 10.00 query T -explain select t.number from numbers(1) as t, numbers(1) as t1 where t.number > 1 and 1 < t1.number +explain select t.number from t, t1 where t.number > 1 and 1 < t1.number ---- HashJoin ├── join type: CROSS ├── build keys: [] ├── probe keys: [] ├── filters: [] -├── estimated rows: 0.11 +├── estimated rows: 1.11 ├── Filter(Build) -│ ├── filters: [lt(1_u8, t1.number (#1))] +│ ├── filters: [gt(t.number (#0), 1_u8)] │ ├── estimated rows: 0.33 │ └── TableScan -│ ├── table: default.system.numbers -│ ├── read rows: 1 -│ ├── read bytes: 8 +│ ├── table: default.default.t +│ ├── read rows: 0 +│ ├── read bytes: 0 │ ├── partitions total: 1 -│ ├── partitions scanned: 1 -│ ├── push downs: [filters: [lt(1_u64, number)], limit: NONE] +│ ├── partitions scanned: 0 +│ ├── push downs: [filters: [gt(number, 1_u64)], limit: NONE] │ └── estimated rows: 1.00 └── Filter(Probe) - ├── filters: [gt(t.number (#0), 1_u8)] - ├── estimated rows: 0.33 + ├── filters: [lt(1_u8, t1.number (#1))] + ├── estimated rows: 3.33 └── TableScan - ├── table: default.system.numbers - ├── read rows: 1 - ├── read bytes: 8 + ├── table: default.default.t1 + ├── read rows: 10 + ├── read bytes: 68 ├── partitions total: 1 ├── partitions scanned: 1 - ├── push downs: [filters: [gt(number, 1_u64)], limit: NONE] - └── estimated rows: 1.00 + ├── push downs: [filters: [lt(1_u64, number)], limit: NONE] + └── estimated rows: 10.00 query T -explain select t.number from numbers(1) as t, numbers(1) as t1 where t.number + t1.number = 1 +explain select t.number from t, t1 where t.number + t1.number = 1 ---- Filter ├── filters: [eq(plus(t.number (#0), t1.number (#1)), 1_u8)] -├── estimated rows: 0.33 +├── estimated rows: 3.33 └── HashJoin ├── join type: CROSS ├── build keys: [] ├── probe keys: [] ├── filters: [] - ├── estimated rows: 1.00 + ├── estimated rows: 10.00 ├── TableScan(Build) - │ ├── table: default.system.numbers + │ ├── table: default.default.t │ ├── read rows: 1 - │ ├── read bytes: 8 + │ ├── read bytes: 31 │ ├── partitions total: 1 │ ├── partitions scanned: 1 │ ├── push downs: [filters: [], limit: NONE] │ └── estimated rows: 1.00 └── TableScan(Probe) - ├── table: default.system.numbers - ├── read rows: 1 - ├── read bytes: 8 + ├── table: default.default.t1 + ├── read rows: 10 + ├── read bytes: 68 ├── partitions total: 1 ├── partitions scanned: 1 ├── push downs: [filters: [], limit: NONE] - └── estimated rows: 1.00 + └── estimated rows: 10.00 query T -explain select t.number from numbers(1) as t, numbers(1) as t1, numbers(1) as t2 where t1.number = t2.number and t.number = 1 +explain select t.number from t, t1, t2 where t1.number = t2.number and t.number = 1 ---- HashJoin ├── join type: INNER ├── build keys: [t1.number (#1)] ├── probe keys: [t2.number (#2)] ├── filters: [] -├── estimated rows: 1.00 +├── estimated rows: 100.00 ├── HashJoin(Build) │ ├── join type: CROSS │ ├── build keys: [] │ ├── probe keys: [] │ ├── filters: [] -│ ├── estimated rows: 0.33 +│ ├── estimated rows: 3.33 │ ├── Filter(Build) │ │ ├── filters: [eq(t.number (#0), 1_u8)] │ │ ├── estimated rows: 0.33 │ │ └── TableScan -│ │ ├── table: default.system.numbers -│ │ ├── read rows: 1 -│ │ ├── read bytes: 8 +│ │ ├── table: default.default.t +│ │ ├── read rows: 0 +│ │ ├── read bytes: 0 │ │ ├── partitions total: 1 -│ │ ├── partitions scanned: 1 +│ │ ├── partitions scanned: 0 │ │ ├── push downs: [filters: [eq(number, 1_u64)], limit: NONE] │ │ └── estimated rows: 1.00 │ └── TableScan(Probe) -│ ├── table: default.system.numbers -│ ├── read rows: 1 -│ ├── read bytes: 8 +│ ├── table: default.default.t1 +│ ├── read rows: 10 +│ ├── read bytes: 68 │ ├── partitions total: 1 │ ├── partitions scanned: 1 │ ├── push downs: [filters: [], limit: NONE] -│ └── estimated rows: 1.00 +│ └── estimated rows: 10.00 └── TableScan(Probe) - ├── table: default.system.numbers - ├── read rows: 1 - ├── read bytes: 8 + ├── table: default.default.t2 + ├── read rows: 100 + ├── read bytes: 431 ├── partitions total: 1 ├── partitions scanned: 1 ├── push downs: [filters: [], limit: NONE] - └── estimated rows: 1.00 + └── estimated rows: 100.00 ## check outer join is converted to inner join @@ -321,3 +339,12 @@ HashJoin ├── partitions scanned: 1 ├── push downs: [filters: [], limit: NONE] └── estimated rows: 4.00 + +statement ok +drop table t + +statement ok +drop table t1 + +statement ok +drop table t2 diff --git a/tests/sqllogictests/suites/mode/standalone/explain/join_reorder/chain.test b/tests/sqllogictests/suites/mode/standalone/explain/join_reorder/chain.test index e1d80ed5defaf..2782a50e206ca 100644 --- a/tests/sqllogictests/suites/mode/standalone/explain/join_reorder/chain.test +++ b/tests/sqllogictests/suites/mode/standalone/explain/join_reorder/chain.test @@ -70,40 +70,40 @@ explain select * from t, t2, t1 where t.a = t1.a and t1.a = t2.a ---- HashJoin ├── join type: INNER -├── build keys: [t1.a (#2), t1.a (#2)] -├── probe keys: [t.a (#0), t2.a (#1)] +├── build keys: [t1.a (#2)] +├── probe keys: [t2.a (#1)] ├── filters: [] ├── estimated rows: 100.00 -├── TableScan(Build) -│ ├── table: default.join_reorder.t1 -│ ├── read rows: 10 -│ ├── read bytes: 68 -│ ├── partitions total: 1 -│ ├── partitions scanned: 1 -│ ├── push downs: [filters: [], limit: NONE] -│ └── estimated rows: 10.00 -└── HashJoin(Probe) - ├── join type: CROSS - ├── build keys: [] - ├── probe keys: [] - ├── filters: [] - ├── estimated rows: 100.00 - ├── TableScan(Build) - │ ├── table: default.join_reorder.t - │ ├── read rows: 1 - │ ├── read bytes: 31 - │ ├── partitions total: 1 - │ ├── partitions scanned: 1 - │ ├── push downs: [filters: [], limit: NONE] - │ └── estimated rows: 1.00 - └── TableScan(Probe) - ├── table: default.join_reorder.t2 - ├── read rows: 100 - ├── read bytes: 431 - ├── partitions total: 1 - ├── partitions scanned: 1 - ├── push downs: [filters: [], limit: NONE] - └── estimated rows: 100.00 +├── HashJoin(Build) +│ ├── join type: INNER +│ ├── build keys: [t.a (#0)] +│ ├── probe keys: [t1.a (#2)] +│ ├── filters: [] +│ ├── estimated rows: 10.00 +│ ├── TableScan(Build) +│ │ ├── table: default.join_reorder.t +│ │ ├── read rows: 1 +│ │ ├── read bytes: 31 +│ │ ├── partitions total: 1 +│ │ ├── partitions scanned: 1 +│ │ ├── push downs: [filters: [], limit: NONE] +│ │ └── estimated rows: 1.00 +│ └── TableScan(Probe) +│ ├── table: default.join_reorder.t1 +│ ├── read rows: 10 +│ ├── read bytes: 68 +│ ├── partitions total: 1 +│ ├── partitions scanned: 1 +│ ├── push downs: [filters: [], limit: NONE] +│ └── estimated rows: 10.00 +└── TableScan(Probe) + ├── table: default.join_reorder.t2 + ├── read rows: 100 + ├── read bytes: 431 + ├── partitions total: 1 + ├── partitions scanned: 1 + ├── push downs: [filters: [], limit: NONE] + └── estimated rows: 100.00 query T explain select * from t1, t, t2 where t.a = t1.a and t1.a = t2.a @@ -230,40 +230,40 @@ explain select * from t2, t, t1 where t.a = t1.a and t1.a = t2.a ---- HashJoin ├── join type: INNER -├── build keys: [t1.a (#2), t1.a (#2)] -├── probe keys: [t.a (#1), t2.a (#0)] +├── build keys: [t1.a (#2)] +├── probe keys: [t2.a (#0)] ├── filters: [] ├── estimated rows: 100.00 -├── TableScan(Build) -│ ├── table: default.join_reorder.t1 -│ ├── read rows: 10 -│ ├── read bytes: 68 -│ ├── partitions total: 1 -│ ├── partitions scanned: 1 -│ ├── push downs: [filters: [], limit: NONE] -│ └── estimated rows: 10.00 -└── HashJoin(Probe) - ├── join type: CROSS - ├── build keys: [] - ├── probe keys: [] - ├── filters: [] - ├── estimated rows: 100.00 - ├── TableScan(Build) - │ ├── table: default.join_reorder.t - │ ├── read rows: 1 - │ ├── read bytes: 31 - │ ├── partitions total: 1 - │ ├── partitions scanned: 1 - │ ├── push downs: [filters: [], limit: NONE] - │ └── estimated rows: 1.00 - └── TableScan(Probe) - ├── table: default.join_reorder.t2 - ├── read rows: 100 - ├── read bytes: 431 - ├── partitions total: 1 - ├── partitions scanned: 1 - ├── push downs: [filters: [], limit: NONE] - └── estimated rows: 100.00 +├── HashJoin(Build) +│ ├── join type: INNER +│ ├── build keys: [t.a (#1)] +│ ├── probe keys: [t1.a (#2)] +│ ├── filters: [] +│ ├── estimated rows: 10.00 +│ ├── TableScan(Build) +│ │ ├── table: default.join_reorder.t +│ │ ├── read rows: 1 +│ │ ├── read bytes: 31 +│ │ ├── partitions total: 1 +│ │ ├── partitions scanned: 1 +│ │ ├── push downs: [filters: [], limit: NONE] +│ │ └── estimated rows: 1.00 +│ └── TableScan(Probe) +│ ├── table: default.join_reorder.t1 +│ ├── read rows: 10 +│ ├── read bytes: 68 +│ ├── partitions total: 1 +│ ├── partitions scanned: 1 +│ ├── push downs: [filters: [], limit: NONE] +│ └── estimated rows: 10.00 +└── TableScan(Probe) + ├── table: default.join_reorder.t2 + ├── read rows: 100 + ├── read bytes: 431 + ├── partitions total: 1 + ├── partitions scanned: 1 + ├── push downs: [filters: [], limit: NONE] + └── estimated rows: 100.00 query T explain select * from t left join t1 on t1.a = t.a diff --git a/tests/sqllogictests/suites/mode/standalone/explain/join_reorder/star_multiple_tables.test b/tests/sqllogictests/suites/mode/standalone/explain/join_reorder/star_multiple_tables.test new file mode 100644 index 0000000000000..6016db8a0fc3d --- /dev/null +++ b/tests/sqllogictests/suites/mode/standalone/explain/join_reorder/star_multiple_tables.test @@ -0,0 +1,128 @@ +statement ok +create database join_reorder + +statement ok +use join_reorder + +statement ok +create table t as select number as a from numbers(1) + +statement ok +create table t1 as select number as a from numbers(10) + +statement ok +create table t2 as select number as a from numbers(100) + +statement ok +create table t3 as select number as a from numbers(500) + +statement ok +create table t4 as select number as a from numbers(2500) + +statement ok +create table t5 as select number as a from numbers(100000) + +statement ok +analyze table t + +statement ok +analyze table t1 + +statement ok +analyze table t2 + +statement ok +analyze table t3 + +statement ok +analyze table t4 + +statement ok +analyze table t5 + +query T +explain select * from t, t3, t2, t5, t4, t1 +where t5.a = t1.a and t5.a = t2.a and t5.a = t3.a and t5.a = t4.a and t5.a = t.a +---- +HashJoin +├── join type: INNER +├── build keys: [t1.a (#5)] +├── probe keys: [t5.a (#3)] +├── filters: [] +├── estimated rows: 100000.00 +├── TableScan(Build) +│ ├── table: default.join_reorder.t1 +│ ├── read rows: 10 +│ ├── read bytes: 68 +│ ├── partitions total: 1 +│ ├── partitions scanned: 1 +│ ├── push downs: [filters: [], limit: NONE] +│ └── estimated rows: 10.00 +└── HashJoin(Probe) + ├── join type: INNER + ├── build keys: [t4.a (#4)] + ├── probe keys: [t5.a (#3)] + ├── filters: [] + ├── estimated rows: 100000.00 + ├── TableScan(Build) + │ ├── table: default.join_reorder.t4 + │ ├── read rows: 2500 + │ ├── read bytes: 10029 + │ ├── partitions total: 1 + │ ├── partitions scanned: 1 + │ ├── push downs: [filters: [], limit: NONE] + │ └── estimated rows: 2500.00 + └── HashJoin(Probe) + ├── join type: INNER + ├── build keys: [t2.a (#2), t3.a (#1), t.a (#0)] + ├── probe keys: [t5.a (#3), t5.a (#3), t5.a (#3)] + ├── filters: [] + ├── estimated rows: 100000.00 + ├── HashJoin(Build) + │ ├── join type: CROSS + │ ├── build keys: [] + │ ├── probe keys: [] + │ ├── filters: [] + │ ├── estimated rows: 50000.00 + │ ├── HashJoin(Build) + │ │ ├── join type: CROSS + │ │ ├── build keys: [] + │ │ ├── probe keys: [] + │ │ ├── filters: [] + │ │ ├── estimated rows: 100.00 + │ │ ├── TableScan(Build) + │ │ │ ├── table: default.join_reorder.t + │ │ │ ├── read rows: 1 + │ │ │ ├── read bytes: 31 + │ │ │ ├── partitions total: 1 + │ │ │ ├── partitions scanned: 1 + │ │ │ ├── push downs: [filters: [], limit: NONE] + │ │ │ └── estimated rows: 1.00 + │ │ └── TableScan(Probe) + │ │ ├── table: default.join_reorder.t2 + │ │ ├── read rows: 100 + │ │ ├── read bytes: 431 + │ │ ├── partitions total: 1 + │ │ ├── partitions scanned: 1 + │ │ ├── push downs: [filters: [], limit: NONE] + │ │ └── estimated rows: 100.00 + │ └── TableScan(Probe) + │ ├── table: default.join_reorder.t3 + │ ├── read rows: 500 + │ ├── read bytes: 2030 + │ ├── partitions total: 1 + │ ├── partitions scanned: 1 + │ ├── push downs: [filters: [], limit: NONE] + │ └── estimated rows: 500.00 + └── TableScan(Probe) + ├── table: default.join_reorder.t5 + ├── read rows: 100000 + ├── read bytes: 400274 + ├── partitions total: 2 + ├── partitions scanned: 2 + ├── push downs: [filters: [], limit: NONE] + └── estimated rows: 100000.00 + + +statement ok +drop database join_reorder