@@ -19,6 +19,7 @@ use databend_common_exception::Result;
1919use crate :: optimizer:: ir:: Matcher ;
2020use crate :: optimizer:: ir:: RelExpr ;
2121use crate :: optimizer:: ir:: SExpr ;
22+ use crate :: optimizer:: ir:: StatInfo ;
2223use crate :: optimizer:: optimizers:: rule:: Rule ;
2324use crate :: optimizer:: optimizers:: rule:: RuleID ;
2425use crate :: optimizer:: optimizers:: rule:: TransformResult ;
@@ -32,6 +33,45 @@ fn contains_recursive_cte(expr: &SExpr) -> bool {
3233 || expr. children ( ) . any ( contains_recursive_cte)
3334}
3435
36+ fn should_commute ( join_type : JoinType , left : & StatInfo , right : & StatInfo ) -> bool {
37+ if left. cardinality < right. cardinality {
38+ return matches ! (
39+ join_type,
40+ JoinType :: Inner
41+ | JoinType :: Cross
42+ | JoinType :: Left
43+ | JoinType :: Right
44+ | JoinType :: LeftSingle
45+ | JoinType :: RightSingle
46+ | JoinType :: LeftSemi
47+ | JoinType :: RightSemi
48+ | JoinType :: LeftAnti
49+ | JoinType :: RightAnti
50+ | JoinType :: LeftMark
51+ | JoinType :: RightMark
52+ ) ;
53+ }
54+
55+ if left. cardinality != right. cardinality {
56+ return false ;
57+ }
58+
59+ if left. cardinality == 0.0 && matches ! ( join_type, JoinType :: Left | JoinType :: Right ) {
60+ let left_proven_empty = left. statistics . precise_cardinality == Some ( 0 ) ;
61+ let right_proven_empty = right. statistics . precise_cardinality == Some ( 0 ) ;
62+ if left_proven_empty != right_proven_empty {
63+ // The right child is the hash-build side. Prefer the input that is
64+ // known to be empty over one whose zero cardinality is only estimated.
65+ return left_proven_empty;
66+ }
67+ }
68+
69+ matches ! (
70+ join_type,
71+ JoinType :: Right | JoinType :: RightSingle | JoinType :: RightSemi | JoinType :: RightAnti
72+ )
73+ }
74+
3575/// Rule to apply commutativity of join operator.
3676/// Since we will always use the right child as build side, this
3777/// rule will help us measure which child is the better one.
@@ -79,33 +119,9 @@ impl Rule for RuleCommuteJoin {
79119
80120 let left_rel_expr = RelExpr :: with_s_expr ( left_child) ;
81121 let right_rel_expr = RelExpr :: with_s_expr ( right_child) ;
82- let left_card = left_rel_expr. derive_cardinality ( ) ?. cardinality ;
83- let right_card = right_rel_expr. derive_cardinality ( ) ?. cardinality ;
84-
85- let need_commute = if left_card < right_card {
86- matches ! (
87- join. join_type,
88- JoinType :: Inner
89- | JoinType :: Cross
90- | JoinType :: Left
91- | JoinType :: Right
92- | JoinType :: LeftSingle
93- | JoinType :: RightSingle
94- | JoinType :: LeftSemi
95- | JoinType :: RightSemi
96- | JoinType :: LeftAnti
97- | JoinType :: RightAnti
98- | JoinType :: LeftMark
99- | JoinType :: RightMark
100- )
101- } else if left_card == right_card {
102- matches ! (
103- join. join_type,
104- JoinType :: Right | JoinType :: RightSingle | JoinType :: RightSemi | JoinType :: RightAnti
105- )
106- } else {
107- false
108- } ;
122+ let left_stat = left_rel_expr. derive_cardinality ( ) ?;
123+ let right_stat = right_rel_expr. derive_cardinality ( ) ?;
124+ let need_commute = should_commute ( join. join_type , & left_stat, & right_stat) ;
109125 if need_commute {
110126 // Swap the join conditions side
111127 for condition in join. equi_conditions . iter_mut ( ) {
@@ -135,3 +151,123 @@ impl Default for RuleCommuteJoin {
135151 Self :: new ( )
136152 }
137153}
154+
155+ #[ cfg( test) ]
156+ mod tests {
157+ use databend_common_expression:: Scalar ;
158+
159+ use super :: * ;
160+ use crate :: optimizer:: ir:: Statistics ;
161+ use crate :: plans:: ConstantExpr ;
162+ use crate :: plans:: DummyTableScan ;
163+ use crate :: plans:: Filter ;
164+ use crate :: plans:: MutationSource ;
165+ use crate :: plans:: ScalarExpr ;
166+
167+ fn empty_stat ( precise : bool ) -> StatInfo {
168+ StatInfo {
169+ cardinality : 0.0 ,
170+ statistics : Statistics {
171+ precise_cardinality : precise. then_some ( 0 ) ,
172+ column_stats : Default :: default ( ) ,
173+ top_n : Default :: default ( ) ,
174+ count_min_sketch : Default :: default ( ) ,
175+ } ,
176+ }
177+ }
178+
179+ fn proven_empty_expr ( ) -> SExpr {
180+ SExpr :: create_unary (
181+ Filter {
182+ predicates : vec ! [ ScalarExpr :: ConstantExpr ( ConstantExpr {
183+ span: None ,
184+ value: Scalar :: Boolean ( false ) ,
185+ } ) ] ,
186+ } ,
187+ SExpr :: create_leaf ( DummyTableScan :: default ( ) ) ,
188+ )
189+ }
190+
191+ #[ test]
192+ fn test_outer_join_zero_tie_prefers_proven_empty_build_side ( ) {
193+ let proven_empty = empty_stat ( true ) ;
194+ let estimated_empty = empty_stat ( false ) ;
195+
196+ assert ! ( should_commute(
197+ JoinType :: Left ,
198+ & proven_empty,
199+ & estimated_empty
200+ ) ) ;
201+ assert ! ( !should_commute(
202+ JoinType :: Left ,
203+ & estimated_empty,
204+ & proven_empty
205+ ) ) ;
206+ assert ! ( should_commute(
207+ JoinType :: Right ,
208+ & proven_empty,
209+ & estimated_empty
210+ ) ) ;
211+ assert ! ( !should_commute(
212+ JoinType :: Right ,
213+ & estimated_empty,
214+ & proven_empty
215+ ) ) ;
216+ }
217+
218+ #[ test]
219+ fn test_outer_join_zero_tie_preserves_existing_canonicalization ( ) {
220+ let left = empty_stat ( false ) ;
221+ let right = empty_stat ( false ) ;
222+
223+ assert ! ( !should_commute( JoinType :: Left , & left, & right) ) ;
224+ assert ! ( should_commute( JoinType :: Right , & left, & right) ) ;
225+ }
226+
227+ #[ test]
228+ fn test_commute_join_builds_proven_empty_input ( ) -> Result < ( ) > {
229+ let join = Join {
230+ join_type : JoinType :: Left ,
231+ ..Default :: default ( )
232+ } ;
233+ let expr = SExpr :: create_binary (
234+ join,
235+ proven_empty_expr ( ) ,
236+ SExpr :: create_leaf ( MutationSource :: default ( ) ) ,
237+ ) ;
238+ let mut state = TransformResult :: new ( ) ;
239+
240+ RuleCommuteJoin :: new ( ) . apply ( & expr, & mut state) ?;
241+
242+ assert_eq ! ( state. results( ) . len( ) , 1 ) ;
243+ let result_join: Join = state. results ( ) [ 0 ] . plan ( ) . clone ( ) . try_into ( ) ?;
244+ assert_eq ! ( result_join. join_type, JoinType :: Right ) ;
245+ assert_eq ! (
246+ RelExpr :: with_s_expr( state. results( ) [ 0 ] . child( 1 ) ?)
247+ . derive_cardinality( ) ?
248+ . statistics
249+ . precise_cardinality,
250+ Some ( 0 )
251+ ) ;
252+ Ok ( ( ) )
253+ }
254+
255+ #[ test]
256+ fn test_commute_join_keeps_proven_empty_build_input ( ) -> Result < ( ) > {
257+ let join = Join {
258+ join_type : JoinType :: Left ,
259+ ..Default :: default ( )
260+ } ;
261+ let expr = SExpr :: create_binary (
262+ join,
263+ SExpr :: create_leaf ( MutationSource :: default ( ) ) ,
264+ proven_empty_expr ( ) ,
265+ ) ;
266+ let mut state = TransformResult :: new ( ) ;
267+
268+ RuleCommuteJoin :: new ( ) . apply ( & expr, & mut state) ?;
269+
270+ assert ! ( state. results( ) . is_empty( ) ) ;
271+ Ok ( ( ) )
272+ }
273+ }
0 commit comments