Skip to content

Commit 38ef8a8

Browse files
committed
fix(query): prefer proven-empty outer join build side
1 parent 91809b4 commit 38ef8a8

9 files changed

Lines changed: 593 additions & 35 deletions

File tree

src/query/sql/src/planner/optimizer/ir/stats/selectivity.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ pub struct SelectivityEstimator {
6565
top_n: TopNSet,
6666
count_min_sketch: CountMinSketchSet,
6767
overrides: ColumnStatSet,
68+
proven_empty: bool,
6869
}
6970

7071
impl SelectivityEstimator {
@@ -75,6 +76,7 @@ impl SelectivityEstimator {
7576
top_n: TopNSet::new(),
7677
count_min_sketch: CountMinSketchSet::new(),
7778
overrides: ColumnStatSet::new(),
79+
proven_empty: cardinality == StatCardinality::Exact(0),
7880
}
7981
}
8082

@@ -88,6 +90,13 @@ impl SelectivityEstimator {
8890
self
8991
}
9092

93+
/// Returns true when the predicates deterministically produce no rows.
94+
///
95+
/// This is deliberately stronger than an estimated cardinality of zero.
96+
pub fn is_proven_empty(&self) -> bool {
97+
self.proven_empty
98+
}
99+
91100
fn merged_column_stats(&self) -> ColumnStatSet {
92101
let mut merged = self.column_stats.clone();
93102
merged.extend(self.overrides.clone());
@@ -139,6 +148,7 @@ impl SelectivityEstimator {
139148
return match constant_filter_truthiness(&constant.scalar) {
140149
Some(true) => Ok(self.cardinality.value()),
141150
Some(false) => {
151+
self.proven_empty = true;
142152
self.clear_column_stats_for_empty_result();
143153
Ok(0.0)
144154
}
@@ -159,6 +169,7 @@ impl SelectivityEstimator {
159169
}) => !domain.has_true,
160170
_ => false,
161171
}) {
172+
self.proven_empty = true;
162173
self.clear_column_stats_for_empty_result();
163174
return Ok(0.0);
164175
}
@@ -263,6 +274,7 @@ impl SelectivityEstimator {
263274
Selectivity::Unknown => DEFAULT_SELECTIVITY,
264275
Selectivity::LowerBound => UNKNOWN_COL_STATS_FILTER_SEL_LOWER_BOUND,
265276
Selectivity::Zero => {
277+
self.proven_empty = true;
266278
self.clear_column_stats_for_empty_result();
267279
return 0.0;
268280
}

src/query/sql/src/planner/optimizer/optimizers/rule/join_rules/rule_commute_join.rs

Lines changed: 163 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use databend_common_exception::Result;
1919
use crate::optimizer::ir::Matcher;
2020
use crate::optimizer::ir::RelExpr;
2121
use crate::optimizer::ir::SExpr;
22+
use crate::optimizer::ir::StatInfo;
2223
use crate::optimizer::optimizers::rule::Rule;
2324
use crate::optimizer::optimizers::rule::RuleID;
2425
use 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+
}

src/query/sql/src/planner/plans/filter.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ impl Operator for Filter {
9898
.with_top_n(stat_info.statistics.top_n.clone())
9999
.with_count_min_sketch(stat_info.statistics.count_min_sketch.clone());
100100
let cardinality = sb.apply(&self.predicates)?;
101+
let precise_cardinality = sb.is_proven_empty().then_some(0);
101102
// Derive column statistics
102103
let column_stats = if cardinality == 0.0 {
103104
HashMap::new()
@@ -107,11 +108,54 @@ impl Operator for Filter {
107108
Ok(Arc::new(StatInfo {
108109
cardinality,
109110
statistics: Statistics {
110-
precise_cardinality: None,
111+
precise_cardinality,
111112
column_stats,
112113
top_n: Default::default(),
113114
count_min_sketch: Default::default(),
114115
},
115116
}))
116117
}
117118
}
119+
120+
#[cfg(test)]
121+
mod tests {
122+
use databend_common_expression::Scalar;
123+
124+
use super::*;
125+
use crate::optimizer::ir::SExpr;
126+
use crate::plans::ConstantExpr;
127+
use crate::plans::DummyTableScan;
128+
use crate::plans::MutationSource;
129+
130+
fn constant_filter(value: bool, input: SExpr) -> SExpr {
131+
SExpr::create_unary(
132+
Filter {
133+
predicates: vec![ScalarExpr::ConstantExpr(ConstantExpr {
134+
span: None,
135+
value: Scalar::Boolean(value),
136+
})],
137+
},
138+
input,
139+
)
140+
}
141+
142+
#[test]
143+
fn test_filter_preserves_proven_empty_cardinality() -> Result<()> {
144+
let expr = constant_filter(false, SExpr::create_leaf(DummyTableScan::default()));
145+
let stat = RelExpr::with_s_expr(&expr).derive_cardinality()?;
146+
147+
assert_eq!(stat.cardinality, 0.0);
148+
assert_eq!(stat.statistics.precise_cardinality, Some(0));
149+
Ok(())
150+
}
151+
152+
#[test]
153+
fn test_filter_does_not_promote_estimated_zero_to_precise() -> Result<()> {
154+
let expr = constant_filter(true, SExpr::create_leaf(MutationSource::default()));
155+
let stat = RelExpr::with_s_expr(&expr).derive_cardinality()?;
156+
157+
assert_eq!(stat.cardinality, 0.0);
158+
assert_eq!(stat.statistics.precise_cardinality, None);
159+
Ok(())
160+
}
161+
}

0 commit comments

Comments
 (0)