Skip to content

Commit 7edd8a9

Browse files
authored
fix(query): canonicalize keys before deriving join stats (#20393)
1 parent f3d33f1 commit 7edd8a9

2 files changed

Lines changed: 139 additions & 96 deletions

File tree

src/query/service/src/physical_plans/physical_hash_join.rs

Lines changed: 17 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ use databend_common_expression::DataSchema;
2727
use databend_common_expression::DataSchemaRef;
2828
use databend_common_expression::DataSchemaRefExt;
2929
use databend_common_expression::RemoteExpr;
30-
use databend_common_expression::conversion::classify_conversion;
3130
use databend_common_expression::type_check::check_cast;
3231
use databend_common_expression::type_check::common_super_type;
3332
use databend_common_expression::types::DataType;
@@ -101,69 +100,6 @@ type MergedFieldsResult = (
101100
Vec<(usize, (bool, bool))>,
102101
);
103102

104-
/// Remove an integer-to-string round trip from an equality key when the other key is an integer.
105-
///
106-
/// Mixed string/integer equality normally uses `Decimal(38, 5)` as the hash key. That coercion is
107-
/// needed for arbitrary strings such as `"1.2"`, but it is unnecessary when the string is produced
108-
/// directly from another integer. Keep the rewrite deliberately narrow: both integers must fit
109-
/// losslessly in their normal common numeric type. In that case formatting and parsing the value
110-
/// cannot change equality.
111-
fn unwrap_integer_to_string_cast<'a>(
112-
integer_expr: &ScalarExpr,
113-
string_expr: &'a ScalarExpr,
114-
) -> Result<Option<&'a ScalarExpr>> {
115-
let ScalarExpr::CastExpr(cast) = string_expr else {
116-
return Ok(None);
117-
};
118-
if cast.is_try || !matches!(cast.target_type.remove_nullable(), DataType::String) {
119-
return Ok(None);
120-
}
121-
122-
let integer_type = integer_expr.data_type();
123-
let DataType::Number(integer_type) = integer_type.remove_nullable() else {
124-
return Ok(None);
125-
};
126-
if !integer_type.is_integer() {
127-
return Ok(None);
128-
}
129-
130-
let source_type = cast.argument.data_type();
131-
let DataType::Number(source_type) = source_type.remove_nullable() else {
132-
return Ok(None);
133-
};
134-
if !source_type.is_integer() {
135-
return Ok(None);
136-
}
137-
138-
let integer_type = DataType::Number(integer_type);
139-
let source_type = DataType::Number(source_type);
140-
let common_type = common_super_type(
141-
integer_type.clone(),
142-
source_type.clone(),
143-
&BUILTIN_FUNCTIONS.default_cast_rules,
144-
);
145-
let Some(common_type @ DataType::Number(_)) = common_type else {
146-
return Ok(None);
147-
};
148-
let preserves_equality = classify_conversion(&integer_type, &common_type)
149-
.is_safe_for_equality_inference()
150-
&& classify_conversion(&source_type, &common_type).is_safe_for_equality_inference();
151-
Ok(preserves_equality.then_some(cast.argument.as_ref()))
152-
}
153-
154-
fn simplify_integer_string_join_keys<'a>(
155-
left: &'a ScalarExpr,
156-
right: &'a ScalarExpr,
157-
) -> Result<(&'a ScalarExpr, &'a ScalarExpr)> {
158-
if let Some(right) = unwrap_integer_to_string_cast(left, right)? {
159-
return Ok((left, right));
160-
}
161-
if let Some(left) = unwrap_integer_to_string_cast(right, left)? {
162-
return Ok((left, right));
163-
}
164-
Ok((left, right))
165-
}
166-
167103
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
168104
pub struct NestedLoopFilterInfo {
169105
pub predicates: Vec<RemoteExpr>,
@@ -875,10 +811,7 @@ impl PhysicalPlanBuilder {
875811
for condition in join.equi_conditions.iter() {
876812
let original_left_condition = &condition.left;
877813
let original_right_condition = &condition.right;
878-
let (left_condition, right_condition) = simplify_integer_string_join_keys(
879-
original_left_condition,
880-
original_right_condition,
881-
)?;
814+
let (left_condition, right_condition) = condition.canonical_keys();
882815

883816
// Type check expressions
884817
let right_expr = right_condition
@@ -1569,7 +1502,8 @@ mod tests {
15691502
let string_source = typed_column(1, DataType::Number(NumberDataType::Int32));
15701503
let string = cast_to_string(string_source.clone());
15711504

1572-
let (left, right) = simplify_integer_string_join_keys(&integer, &string)?;
1505+
let condition = JoinEquiCondition::new(integer.clone(), string, false);
1506+
let (left, right) = condition.canonical_keys();
15731507

15741508
assert_eq!(left, &integer);
15751509
assert_eq!(right, &string_source);
@@ -1582,7 +1516,8 @@ mod tests {
15821516
let string = cast_to_string(string_source.clone());
15831517
let integer = typed_column(1, DataType::Number(NumberDataType::Int64));
15841518

1585-
let (left, right) = simplify_integer_string_join_keys(&string, &integer)?;
1519+
let condition = JoinEquiCondition::new(string, integer.clone(), false);
1520+
let (left, right) = condition.canonical_keys();
15861521

15871522
assert_eq!(left, &string_source);
15881523
assert_eq!(right, &integer);
@@ -1601,7 +1536,8 @@ mod tests {
16011536
false,
16021537
);
16031538

1604-
let (left, right) = simplify_integer_string_join_keys(&integer, &string)?;
1539+
let condition = JoinEquiCondition::new(integer.clone(), string, false);
1540+
let (left, right) = condition.canonical_keys();
16051541

16061542
assert_eq!(left, &integer);
16071543
assert_eq!(right, &string_source);
@@ -1614,12 +1550,12 @@ mod tests {
16141550
let source = typed_column(1, DataType::Number(NumberDataType::Int32));
16151551
let try_cast = cast(source, DataType::String, true);
16161552

1617-
let simplified = simplify_integer_string_join_keys(&integer, &try_cast)?;
1618-
assert_eq!(simplified, (&integer, &try_cast));
1553+
let condition = JoinEquiCondition::new(integer.clone(), try_cast.clone(), false);
1554+
assert_eq!(condition.canonical_keys(), (&integer, &try_cast));
16191555

16201556
let string_column = typed_column(1, DataType::String);
1621-
let simplified = simplify_integer_string_join_keys(&integer, &string_column)?;
1622-
assert_eq!(simplified, (&integer, &string_column));
1557+
let condition = JoinEquiCondition::new(integer.clone(), string_column.clone(), false);
1558+
assert_eq!(condition.canonical_keys(), (&integer, &string_column));
16231559
Ok(())
16241560
}
16251561

@@ -1635,14 +1571,14 @@ mod tests {
16351571

16361572
for (index, source_type) in source_types.into_iter().enumerate() {
16371573
let string = cast_to_string(typed_column(index + 1, source_type));
1638-
let simplified = simplify_integer_string_join_keys(&integer, &string)?;
1639-
assert_eq!(simplified, (&integer, &string));
1574+
let condition = JoinEquiCondition::new(integer.clone(), string.clone(), false);
1575+
assert_eq!(condition.canonical_keys(), (&integer, &string));
16401576
}
16411577

16421578
let float = typed_column(1, DataType::Number(NumberDataType::Float64));
16431579
let string = cast_to_string(typed_column(2, DataType::Number(NumberDataType::Int32)));
1644-
let simplified = simplify_integer_string_join_keys(&float, &string)?;
1645-
assert_eq!(simplified, (&float, &string));
1580+
let condition = JoinEquiCondition::new(float.clone(), string.clone(), false);
1581+
assert_eq!(condition.canonical_keys(), (&float, &string));
16461582
Ok(())
16471583
}
16481584

@@ -1651,9 +1587,8 @@ mod tests {
16511587
let integer = typed_column(0, DataType::Number(NumberDataType::Int64));
16521588
let string = cast_to_string(typed_column(1, DataType::Number(NumberDataType::UInt64)));
16531589

1654-
let simplified = simplify_integer_string_join_keys(&integer, &string)?;
1655-
1656-
assert_eq!(simplified, (&integer, &string));
1590+
let condition = JoinEquiCondition::new(integer.clone(), string.clone(), false);
1591+
assert_eq!(condition.canonical_keys(), (&integer, &string));
16571592
Ok(())
16581593
}
16591594
}

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

Lines changed: 122 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ use databend_common_exception::Result;
2424
use databend_common_expression::conversion::classify_conversion;
2525
use databend_common_expression::stat_distribution::StatCardinality;
2626
use databend_common_expression::stat_distribution::StatCount;
27+
use databend_common_expression::type_check::common_super_type;
28+
use databend_common_expression::types::DataType;
29+
use databend_common_functions::BUILTIN_FUNCTIONS;
2730

2831
use crate::ColumnSet;
2932
use crate::Symbol;
@@ -271,6 +274,66 @@ impl JoinEquiCondition {
271274
})
272275
.collect()
273276
}
277+
278+
/// Return the equality-preserving key expressions used by both statistics and execution.
279+
pub fn canonical_keys(&self) -> (&ScalarExpr, &ScalarExpr) {
280+
if let Some(right) = unwrap_integer_to_string_cast(&self.left, &self.right) {
281+
return (&self.left, right);
282+
}
283+
if let Some(left) = unwrap_integer_to_string_cast(&self.right, &self.left) {
284+
return (left, &self.right);
285+
}
286+
(&self.left, &self.right)
287+
}
288+
}
289+
290+
/// Remove an integer-to-string round trip when the other equality key is an integer.
291+
///
292+
/// Mixed string/integer equality normally uses `Decimal(38, 5)` as the hash key. That coercion is
293+
/// needed for arbitrary strings such as `"1.2"`, but it is unnecessary when the string is produced
294+
/// directly from another integer. Both integers must fit losslessly in their normal common numeric
295+
/// type, so formatting and parsing the value cannot change equality.
296+
fn unwrap_integer_to_string_cast<'a>(
297+
integer_expr: &ScalarExpr,
298+
string_expr: &'a ScalarExpr,
299+
) -> Option<&'a ScalarExpr> {
300+
let ScalarExpr::CastExpr(cast) = string_expr else {
301+
return None;
302+
};
303+
if cast.is_try || !matches!(cast.target_type.remove_nullable(), DataType::String) {
304+
return None;
305+
}
306+
307+
let integer_type = integer_expr.data_type();
308+
let DataType::Number(integer_type) = integer_type.remove_nullable() else {
309+
return None;
310+
};
311+
if !integer_type.is_integer() {
312+
return None;
313+
}
314+
315+
let source_type = cast.argument.data_type();
316+
let DataType::Number(source_type) = source_type.remove_nullable() else {
317+
return None;
318+
};
319+
if !source_type.is_integer() {
320+
return None;
321+
}
322+
323+
let integer_type = DataType::Number(integer_type);
324+
let source_type = DataType::Number(source_type);
325+
let common_type = common_super_type(
326+
integer_type.clone(),
327+
source_type.clone(),
328+
&BUILTIN_FUNCTIONS.default_cast_rules,
329+
);
330+
let Some(common_type @ DataType::Number(_)) = common_type else {
331+
return None;
332+
};
333+
let preserves_equality = classify_conversion(&integer_type, &common_type)
334+
.is_safe_for_equality_inference()
335+
&& classify_conversion(&source_type, &common_type).is_safe_for_equality_inference();
336+
preserves_equality.then_some(cast.argument.as_ref())
274337
}
275338

276339
fn direct_column(expr: &ScalarExpr) -> Option<Symbol> {
@@ -730,9 +793,10 @@ impl Join {
730793
self.equi_conditions
731794
.iter()
732795
.filter_map(|condition| {
796+
let (left, right) = condition.canonical_keys();
733797
Some(JoinConditionColumns {
734-
left: direct_column(&condition.left)?,
735-
right: direct_column(&condition.right)?,
798+
left: direct_column(left)?,
799+
right: direct_column(right)?,
736800
})
737801
})
738802
.map(|columns| side.join_column(columns))
@@ -772,28 +836,26 @@ impl Join {
772836
if estimator.has_no_matches() {
773837
break;
774838
}
839+
let (left_condition, right_condition) = condition.canonical_keys();
775840
let output_columns = match (
776-
direct_column(&condition.left),
777-
direct_column(&condition.right),
841+
direct_column(left_condition),
842+
direct_column(right_condition),
778843
) {
779844
(Some(left), Some(right)) => Some(JoinConditionColumns { left, right }),
780845
_ => None,
781846
};
782847
if drop_null_join_keys && !condition.is_null_equal {
783-
if let Some(column) = null_rejected_column(&condition.left) {
848+
if let Some(column) = null_rejected_column(left_condition) {
784849
left.clear_null_count(&mut left_join_keys, column);
785850
}
786-
if let Some(column) = null_rejected_column(&condition.right) {
851+
if let Some(column) = null_rejected_column(right_condition) {
787852
right.clear_null_count(&mut right_join_keys, column);
788853
}
789854
}
790-
let left_condition_stat = join_condition_stat(
791-
&condition.left,
792-
left_input_statistics,
793-
left_stat_cardinality,
794-
)?;
855+
let left_condition_stat =
856+
join_condition_stat(left_condition, left_input_statistics, left_stat_cardinality)?;
795857
let right_condition_stat = join_condition_stat(
796-
&condition.right,
858+
right_condition,
797859
right_input_statistics,
798860
right_stat_cardinality,
799861
)?;
@@ -809,8 +871,8 @@ impl Join {
809871
};
810872
estimator.apply_condition(
811873
output_columns,
812-
condition.left.data_type().as_ref(),
813-
condition.right.data_type().as_ref(),
874+
left_condition.data_type().as_ref(),
875+
right_condition.data_type().as_ref(),
814876
left_condition_stat.as_ref(),
815877
right_condition_stat.as_ref(),
816878
condition.is_null_equal,
@@ -1420,6 +1482,7 @@ mod tests {
14201482
use crate::Visibility;
14211483
use crate::optimizer::ir::SExpr;
14221484
use crate::plans::BoundColumnRef;
1485+
use crate::plans::CastExpr;
14231486
use crate::plans::Exchange;
14241487
use crate::plans::FunctionCall;
14251488
use crate::plans::Scan;
@@ -1465,6 +1528,27 @@ mod tests {
14651528
)
14661529
}
14671530

1531+
fn int_column_stat(min: i64, max: i64, ndv: f64) -> ColumnStat {
1532+
ColumnStat::Int {
1533+
min,
1534+
max,
1535+
ndv: NdvEstimate::exact(ndv),
1536+
null_count: StatCount::exact(0),
1537+
histogram: None,
1538+
}
1539+
}
1540+
1541+
fn stat_info(cardinality: f64, column: Symbol, stat: ColumnStat) -> Arc<StatInfo> {
1542+
Arc::new(StatInfo {
1543+
cardinality,
1544+
statistics: Statistics {
1545+
precise_cardinality: Some(cardinality as u64),
1546+
column_stats: HashMap::from([(column, stat)]),
1547+
..Default::default()
1548+
},
1549+
})
1550+
}
1551+
14681552
#[test]
14691553
fn test_anti_join_only_scales_estimated_matched_rows() {
14701554
assert_eq!(estimate_anti_join_cardinality(100.0, 100.0, None), 0.0);
@@ -1483,6 +1567,30 @@ mod tests {
14831567
assert_eq!(estimate_anti_join_cardinality(100.0, 0.0, Some(0.0)), 100.0);
14841568
}
14851569

1570+
#[test]
1571+
fn test_canonical_integer_string_keys_drive_join_stats() -> Result<()> {
1572+
let left_key = column(0, DataType::Number(NumberDataType::Int64));
1573+
let right_source = column(1, DataType::Number(NumberDataType::Int32));
1574+
let right_key = ScalarExpr::CastExpr(CastExpr {
1575+
span: None,
1576+
is_try: false,
1577+
argument: Box::new(right_source),
1578+
target_type: Box::new(DataType::String),
1579+
});
1580+
let join = Join {
1581+
join_type: JoinType::Inner,
1582+
equi_conditions: vec![JoinEquiCondition::new(left_key, right_key, false)],
1583+
..Default::default()
1584+
};
1585+
let left = stat_info(4.0, Symbol::new(0), int_column_stat(1, 4, 4.0));
1586+
let right = stat_info(3.0, Symbol::new(1), int_column_stat(0, 1, 2.0));
1587+
1588+
let stats = join.derive_join_stats(left, right)?;
1589+
1590+
assert_eq!(stats.cardinality, 3.0);
1591+
Ok(())
1592+
}
1593+
14861594
#[test]
14871595
fn test_finish_semi_join_histogram_skips_all_null_join_key() -> Result<()> {
14881596
let left_stat = ColumnStat::Int {

0 commit comments

Comments
 (0)