From 8212bf24b749d5e6375f5476ab1c3019688235c3 Mon Sep 17 00:00:00 2001 From: coldWater Date: Fri, 31 Jul 2026 20:13:46 +0800 Subject: [PATCH 1/2] fix(query): improve selectivity estimation for boolean expressions --- .../planner/optimizer/ir/stats/selectivity.rs | 40 ++++++++-- .../sql/tests/it/optimizer/selectivity.rs | 33 ++++++++ .../it/optimizer/selectivity_logical.txt | 80 ++++++++++++++++++- .../explain/selectivity/boolean.test | 33 ++++++++ 4 files changed, 176 insertions(+), 10 deletions(-) create mode 100644 tests/sqllogictests/suites/mode/standalone/explain/selectivity/boolean.test diff --git a/src/query/sql/src/planner/optimizer/ir/stats/selectivity.rs b/src/query/sql/src/planner/optimizer/ir/stats/selectivity.rs index 9f234769b4125..20f7351c19897 100644 --- a/src/query/sql/src/planner/optimizer/ir/stats/selectivity.rs +++ b/src/query/sql/src/planner/optimizer/ir/stats/selectivity.rs @@ -52,6 +52,7 @@ use crate::plans::ScalarExpr; pub const DEFAULT_SELECTIVITY: f64 = 1f64 / 5f64; pub const UNKNOWN_COL_STATS_FILTER_SEL_LOWER_BOUND: f64 = 0.5_f64; pub const MAX_SELECTIVITY: f64 = 1f64; +const BOOLEAN_VALUE_SELECTIVITY: f64 = 0.5; const HISTOGRAM_ROW_COUNT_TOLERANCE: f64 = 1e-9; /// Some constants for like predicate selectivity estimation. @@ -633,14 +634,23 @@ impl SelectivityVisitor<'_> { (Expr::ColumnRef(column_ref), Expr::Constant(constant)) | (Expr::Constant(constant), Expr::ColumnRef(column_ref)) => { let column_index = column_ref.id.index; + let op = if left.is_constant() { op.reverse() } else { op }; if !self.column_stats.contains_key(&column_index) { + if matches!(column_ref.data_type.remove_nullable(), DataType::Boolean) + && let Scalar::Boolean(value) = constant.scalar + { + return if column_ref.data_type.is_nullable() { + Ok(Selectivity::Unknown) + } else { + Ok(boolean_comparison_selectivity(op, value)) + }; + } // The column is derived column, give a small selectivity currently. // Need to improve it later. // Another case: column is from system table, such as numbers. We shouldn't use numbers() table to test cardinality estimation. return Ok(Selectivity::LowerBound); } let column_stat = &self.column_stats[&column_index]; - let op = if left.is_constant() { op.reverse() } else { op }; let can_apply_constant_constraint = { use DataType::*; @@ -972,8 +982,13 @@ impl SelectivityVisitor<'_> { .unwrap_or(Selectivity::Unknown); Ok(()) } - Expr::ColumnRef(_) => { - self.selectivity = Selectivity::LowerBound; + Expr::ColumnRef(column_ref) => { + self.selectivity = + if matches!(column_ref.data_type.remove_nullable(), DataType::Boolean) { + Selectivity::N(BOOLEAN_VALUE_SELECTIVITY) + } else { + Selectivity::LowerBound + }; Ok(()) } Expr::Cast(cast) => self.visit_expr(&cast.expr), @@ -1029,16 +1044,14 @@ impl SelectivityVisitor<'_> { self.selectivity = if has_zero { Selectivity::Zero - } else if !has_unknown && !has_lower_bound && !has_n { - Selectivity::All - } else if (!has_unknown && !has_lower_bound) || acc < DEFAULT_SELECTIVITY { + } else if has_n { Selectivity::N(acc) } else if has_unknown { Selectivity::Unknown } else if has_lower_bound { Selectivity::LowerBound } else { - Selectivity::Unknown + Selectivity::All }; } @@ -1124,3 +1137,16 @@ impl SelectivityVisitor<'_> { Ok(()) } } + +fn boolean_comparison_selectivity(op: ComparisonOp, constant: bool) -> Selectivity { + let selectivity = match (op, constant) { + (ComparisonOp::Equal | ComparisonOp::NotEqual, _) => BOOLEAN_VALUE_SELECTIVITY, + (ComparisonOp::GT, false) + | (ComparisonOp::LT, true) + | (ComparisonOp::GTE, true) + | (ComparisonOp::LTE, false) => BOOLEAN_VALUE_SELECTIVITY, + (ComparisonOp::GT, true) | (ComparisonOp::LT, false) => 0.0, + (ComparisonOp::GTE, false) | (ComparisonOp::LTE, true) => MAX_SELECTIVITY, + }; + Selectivity::N(selectivity) +} diff --git a/src/query/sql/tests/it/optimizer/selectivity.rs b/src/query/sql/tests/it/optimizer/selectivity.rs index 634bbe9f9b5e2..d6eeae434eda1 100644 --- a/src/query/sql/tests/it/optimizer/selectivity.rs +++ b/src/query/sql/tests/it/optimizer/selectivity.rs @@ -1196,6 +1196,39 @@ fn test_selectivity_logical_outcomes() -> Result<()> { StatCardinality::estimate(100.0), )?; + write_case_title( + &mut file, + "missing_stats_logical_predicates", + "Boolean predicates should use an equal true/false distribution, while numeric estimates should take priority over AND fallbacks.", + )?; + let partial_stats = ColumnStatSet::from_iter([(Symbol::new(1), ColumnStat { + min: Datum::UInt(0), + max: Datum::UInt(3), + ndv: NdvEstimate::exact(4.0), + null_count: StatCount::exact(0), + histogram: None, + })]); + let partial_columns = [ + ("flag", BooleanType::data_type()), + ("number", UInt64Type::data_type()), + ("missing", UInt64Type::data_type()), + ("nullable_missing", UInt64Type::data_type().wrap_nullable()), + ("nullable_flag", BooleanType::data_type().wrap_nullable()), + ]; + for expr in [ + "and_filters(flag, number = 1)", + "and_filters(flag = true, number = 1)", + "or_filters(flag, number = 1)", + "or_filters(flag = true, number = 1)", + "flag > true", + "flag >= false", + "nullable_flag >= false", + "and_filters(missing = 1, number = 1)", + "and_filters(is_not_null(nullable_missing), number = 1)", + ] { + run_case(&mut file, expr, &partial_columns, partial_stats.clone())?; + } + write_case_title( &mut file, "histogram_logical_predicates", diff --git a/src/query/sql/tests/it/optimizer/selectivity_logical.txt b/src/query/sql/tests/it/optimizer/selectivity_logical.txt index 64ff3283ca619..d61977812d672 100644 --- a/src/query/sql/tests/it/optimizer/selectivity_logical.txt +++ b/src/query/sql/tests/it/optimizer/selectivity_logical.txt @@ -110,6 +110,80 @@ out stats : 0 ColumnStat { min: UInt(0), max: UInt(9), ndv: 0.0, null_count: 0, histogram: None } 1 ColumnStat { min: UInt(0), max: UInt(9), ndv: 0.0, null_count: 0, histogram: None } +=== missing_stats_logical_predicates === +description: Boolean predicates should use an equal true/false distribution, while numeric estimates should take priority over AND fallbacks. +expr : and_filters(flag, number = 1) +cardinality : 100 +estimated : 25 +in stats : +1 ColumnStat { min: UInt(0), max: UInt(3), ndv: 4.0, null_count: 0, histogram: None } +out stats : +1 ColumnStat { min: UInt(1), max: UInt(1), ndv: 1.0, null_count: 0, histogram: None } + +expr : and_filters(flag = true, number = 1) +cardinality : 100 +estimated : 25 +in stats : +1 ColumnStat { min: UInt(0), max: UInt(3), ndv: 4.0, null_count: 0, histogram: None } +out stats : +1 ColumnStat { min: UInt(1), max: UInt(1), ndv: 1.0, null_count: 0, histogram: None } + +expr : or_filters(flag, number = 1) +cardinality : 100 +estimated : 62.5 +in stats : +1 ColumnStat { min: UInt(0), max: UInt(3), ndv: 4.0, null_count: 0, histogram: None } +out stats : +1 ColumnStat { min: UInt(0), max: UInt(3), ndv: ~3.99999999991029[..4.0], null_count: 0, histogram: None } + +expr : or_filters(flag = true, number = 1) +cardinality : 100 +estimated : 62.5 +in stats : +1 ColumnStat { min: UInt(0), max: UInt(3), ndv: 4.0, null_count: 0, histogram: None } +out stats : +1 ColumnStat { min: UInt(0), max: UInt(3), ndv: ~3.99999999991029[..4.0], null_count: 0, histogram: None } + +expr : flag > true +cardinality : 100 +estimated : 0 +in stats : +1 ColumnStat { min: UInt(0), max: UInt(3), ndv: 4.0, null_count: 0, histogram: None } +out stats : +1 ColumnStat { min: UInt(0), max: UInt(3), ndv: 4.0, null_count: 0, histogram: None } + +expr : flag >= false +cardinality : 100 +estimated : 100 +in stats : +1 ColumnStat { min: UInt(0), max: UInt(3), ndv: 4.0, null_count: 0, histogram: None } +out stats : +1 ColumnStat { min: UInt(0), max: UInt(3), ndv: 4.0, null_count: 0, histogram: None } + +expr : nullable_flag >= false +cardinality : 100 +estimated : 20 +in stats : +1 ColumnStat { min: UInt(0), max: UInt(3), ndv: 4.0, null_count: 0, histogram: None } +out stats : +1 ColumnStat { min: UInt(0), max: UInt(3), ndv: ~3.984888427254817[..4.0], null_count: 0, histogram: None } + +expr : and_filters(missing = 1, number = 1) +cardinality : 100 +estimated : 25 +in stats : +1 ColumnStat { min: UInt(0), max: UInt(3), ndv: 4.0, null_count: 0, histogram: None } +out stats : +1 ColumnStat { min: UInt(1), max: UInt(1), ndv: 1.0, null_count: 0, histogram: None } + +expr : and_filters(is_not_null(nullable_missing), number = 1) +cardinality : 100 +estimated : 25 +in stats : +1 ColumnStat { min: UInt(0), max: UInt(3), ndv: 4.0, null_count: 0, histogram: None } +out stats : +1 ColumnStat { min: UInt(1), max: UInt(1), ndv: 1.0, null_count: 0, histogram: None } + === histogram_logical_predicates === description: AND constraints should be visible to later predicates, while OR and NOT should only affect final selectivity. expr : a > 3, a > 4 @@ -194,13 +268,13 @@ out stats : expr : and_filters(a > 4, a + b > 10) cardinality : 100 -estimated : 20 +estimated : 50 in stats : 0 ColumnStat { min: UInt(0), max: UInt(9), ndv: 10.0, null_count: 0, histogram: Some(UInt(TypedHistogram { accuracy: true, buckets: [TypedHistogramBucket { lower_bound: 0, upper_bound: 9, num_values: 100.0, num_distinct: 10.0 }], avg_spacing: None })) } 1 ColumnStat { min: UInt(0), max: UInt(4), ndv: 5.0, null_count: 0, histogram: Some(UInt(TypedHistogram { accuracy: true, buckets: [TypedHistogramBucket { lower_bound: 0, upper_bound: 4, num_values: 50.0, num_distinct: 5.0 }], avg_spacing: None })) } out stats : -0 ColumnStat { min: UInt(5), max: UInt(9), ndv: ~4.969766912[..5.0], null_count: 0, histogram: Some(UInt(TypedHistogram { accuracy: false, row_scale: 0.4, buckets: [TypedHistogramBucket { lower_bound: 5, upper_bound: 9, num_values: 50.0, num_distinct: 5.0 }], avg_spacing: None })) } -1 ColumnStat { min: UInt(0), max: UInt(4), ndv: ~4.463129088[..5.0], null_count: 0, histogram: Some(UInt(TypedHistogram { accuracy: false, row_scale: 0.2, buckets: [TypedHistogramBucket { lower_bound: 0, upper_bound: 4, num_values: 50.0, num_distinct: 5.0 }], avg_spacing: None })) } +0 ColumnStat { min: UInt(5), max: UInt(9), ndv: 5.0, null_count: 0, histogram: Some(UInt(TypedHistogram { accuracy: true, buckets: [TypedHistogramBucket { lower_bound: 5, upper_bound: 9, num_values: 50.0, num_distinct: 5.0 }], avg_spacing: None })) } +1 ColumnStat { min: UInt(0), max: UInt(4), ndv: ~4.9951171875[..5.0], null_count: 0, histogram: Some(UInt(TypedHistogram { accuracy: false, row_scale: 0.5, buckets: [TypedHistogramBucket { lower_bound: 0, upper_bound: 4, num_values: 50.0, num_distinct: 5.0 }], avg_spacing: None })) } === constant_logical_predicates === description: Constant predicates should preserve Zero and All through logical composition. diff --git a/tests/sqllogictests/suites/mode/standalone/explain/selectivity/boolean.test b/tests/sqllogictests/suites/mode/standalone/explain/selectivity/boolean.test new file mode 100644 index 0000000000000..7e9afeaa9b0e8 --- /dev/null +++ b/tests/sqllogictests/suites/mode/standalone/explain/selectivity/boolean.test @@ -0,0 +1,33 @@ +statement ok +settings (ddl_column_type_nullable=0) CREATE OR REPLACE TABLE t1 AS +SELECT + number % 5 AS c39, + number % 2 = 0 AS c13, + if(number % 40 = 0, NULL, number % 40) AS c40, + number AS c45 +FROM numbers(1000) + +statement ok +ANALYZE TABLE t1 + +query T +EXPLAIN SELECT c39, c13, c40, c45 +FROM t1 +WHERE c39 != 0 + AND c13 = FALSE + AND (c40 = 1 OR c40 IS NULL OR c40 = 2); +---- +TableScan +├── table: default.default.t1 +├── scan id: 0 +├── output columns: [c39 (#0), c13 (#1), c40 (#2), c45 (#3)] +├── read rows: 1000 +├── read size: 1.91 KiB +├── partitions total: 1 +├── partitions scanned: 1 +├── pruning stats: [segments: , blocks: ] +├── push downs: [filters: [t1.c39 (#0) <> 0 and t1.c13 (#1) = false and (t1.c40 (#2) = 1 or NOT is_not_null(t1.c40 (#2)) or t1.c40 (#2) = 2)], limit: NONE] +└── estimated rows: 73.14 + +statement ok +DROP TABLE t1 From d58cf3973061e26abbfb04fa882dbf30771677aa Mon Sep 17 00:00:00 2001 From: coldWater Date: Fri, 31 Jul 2026 23:34:30 +0800 Subject: [PATCH 2/2] fix --- .../planner/optimizer/ir/stats/selectivity.rs | 8 +++- .../sql/tests/it/optimizer/selectivity.rs | 5 ++- .../it/optimizer/selectivity_logical.txt | 32 +++++++++++++-- .../tests/it/optimizer/selectivity_smoke.rs | 39 ------------------- .../common_subexpression_optimizer.test | 16 ++++---- .../push_down_filter_project_set.test | 14 +++---- .../explain/selectivity/boolean.test | 2 +- 7 files changed, 54 insertions(+), 62 deletions(-) diff --git a/src/query/sql/src/planner/optimizer/ir/stats/selectivity.rs b/src/query/sql/src/planner/optimizer/ir/stats/selectivity.rs index 20f7351c19897..b7cda10b54397 100644 --- a/src/query/sql/src/planner/optimizer/ir/stats/selectivity.rs +++ b/src/query/sql/src/planner/optimizer/ir/stats/selectivity.rs @@ -1035,7 +1035,9 @@ impl SelectivityVisitor<'_> { // the original input rows. Multiplying estimates // for predicates on different columns would assume // those columns are independent; without that proof, - // keep the narrowest single estimate. + // keep the narrowest single estimate. It takes + // priority over fallbacks only when it is below + // the lower-bound selectivity threshold. acc = acc.min(n); } } @@ -1044,12 +1046,14 @@ impl SelectivityVisitor<'_> { self.selectivity = if has_zero { Selectivity::Zero - } else if has_n { + } else if has_n && acc < UNKNOWN_COL_STATS_FILTER_SEL_LOWER_BOUND { Selectivity::N(acc) } else if has_unknown { Selectivity::Unknown } else if has_lower_bound { Selectivity::LowerBound + } else if has_n { + Selectivity::N(acc) } else { Selectivity::All }; diff --git a/src/query/sql/tests/it/optimizer/selectivity.rs b/src/query/sql/tests/it/optimizer/selectivity.rs index d6eeae434eda1..3f75f5cb70347 100644 --- a/src/query/sql/tests/it/optimizer/selectivity.rs +++ b/src/query/sql/tests/it/optimizer/selectivity.rs @@ -1199,7 +1199,7 @@ fn test_selectivity_logical_outcomes() -> Result<()> { write_case_title( &mut file, "missing_stats_logical_predicates", - "Boolean predicates should use an equal true/false distribution, while numeric estimates should take priority over AND fallbacks.", + "Boolean predicates should use an equal true/false distribution, while numeric estimates below the lower-bound threshold should take priority over AND fallbacks.", )?; let partial_stats = ColumnStatSet::from_iter([(Symbol::new(1), ColumnStat { min: Datum::UInt(0), @@ -1225,6 +1225,9 @@ fn test_selectivity_logical_outcomes() -> Result<()> { "nullable_flag >= false", "and_filters(missing = 1, number = 1)", "and_filters(is_not_null(nullable_missing), number = 1)", + "and_filters(flag, is_not_null(nullable_missing))", + "and_filters(missing = 1, number != 1)", + "and_filters(is_not_null(nullable_missing), number != 1)", ] { run_case(&mut file, expr, &partial_columns, partial_stats.clone())?; } diff --git a/src/query/sql/tests/it/optimizer/selectivity_logical.txt b/src/query/sql/tests/it/optimizer/selectivity_logical.txt index d61977812d672..b3a3f0d8cbb7f 100644 --- a/src/query/sql/tests/it/optimizer/selectivity_logical.txt +++ b/src/query/sql/tests/it/optimizer/selectivity_logical.txt @@ -111,7 +111,7 @@ out stats : 1 ColumnStat { min: UInt(0), max: UInt(9), ndv: 0.0, null_count: 0, histogram: None } === missing_stats_logical_predicates === -description: Boolean predicates should use an equal true/false distribution, while numeric estimates should take priority over AND fallbacks. +description: Boolean predicates should use an equal true/false distribution, while numeric estimates below the lower-bound threshold should take priority over AND fallbacks. expr : and_filters(flag, number = 1) cardinality : 100 estimated : 25 @@ -184,6 +184,30 @@ in stats : out stats : 1 ColumnStat { min: UInt(1), max: UInt(1), ndv: 1.0, null_count: 0, histogram: None } +expr : and_filters(flag, is_not_null(nullable_missing)) +cardinality : 100 +estimated : 20 +in stats : +1 ColumnStat { min: UInt(0), max: UInt(3), ndv: 4.0, null_count: 0, histogram: None } +out stats : +1 ColumnStat { min: UInt(0), max: UInt(3), ndv: ~3.984888427254817[..4.0], null_count: 0, histogram: None } + +expr : and_filters(missing = 1, number != 1) +cardinality : 100 +estimated : 50 +in stats : +1 ColumnStat { min: UInt(0), max: UInt(3), ndv: 4.0, null_count: 0, histogram: None } +out stats : +1 ColumnStat { min: UInt(0), max: UInt(3), ndv: ~3.9999998807907104[..4.0], null_count: 0, histogram: None } + +expr : and_filters(is_not_null(nullable_missing), number != 1) +cardinality : 100 +estimated : 20 +in stats : +1 ColumnStat { min: UInt(0), max: UInt(3), ndv: 4.0, null_count: 0, histogram: None } +out stats : +1 ColumnStat { min: UInt(0), max: UInt(3), ndv: ~3.984888427254817[..4.0], null_count: 0, histogram: None } + === histogram_logical_predicates === description: AND constraints should be visible to later predicates, while OR and NOT should only affect final selectivity. expr : a > 3, a > 4 @@ -268,13 +292,13 @@ out stats : expr : and_filters(a > 4, a + b > 10) cardinality : 100 -estimated : 50 +estimated : 20 in stats : 0 ColumnStat { min: UInt(0), max: UInt(9), ndv: 10.0, null_count: 0, histogram: Some(UInt(TypedHistogram { accuracy: true, buckets: [TypedHistogramBucket { lower_bound: 0, upper_bound: 9, num_values: 100.0, num_distinct: 10.0 }], avg_spacing: None })) } 1 ColumnStat { min: UInt(0), max: UInt(4), ndv: 5.0, null_count: 0, histogram: Some(UInt(TypedHistogram { accuracy: true, buckets: [TypedHistogramBucket { lower_bound: 0, upper_bound: 4, num_values: 50.0, num_distinct: 5.0 }], avg_spacing: None })) } out stats : -0 ColumnStat { min: UInt(5), max: UInt(9), ndv: 5.0, null_count: 0, histogram: Some(UInt(TypedHistogram { accuracy: true, buckets: [TypedHistogramBucket { lower_bound: 5, upper_bound: 9, num_values: 50.0, num_distinct: 5.0 }], avg_spacing: None })) } -1 ColumnStat { min: UInt(0), max: UInt(4), ndv: ~4.9951171875[..5.0], null_count: 0, histogram: Some(UInt(TypedHistogram { accuracy: false, row_scale: 0.5, buckets: [TypedHistogramBucket { lower_bound: 0, upper_bound: 4, num_values: 50.0, num_distinct: 5.0 }], avg_spacing: None })) } +0 ColumnStat { min: UInt(5), max: UInt(9), ndv: ~4.969766912[..5.0], null_count: 0, histogram: Some(UInt(TypedHistogram { accuracy: false, row_scale: 0.4, buckets: [TypedHistogramBucket { lower_bound: 5, upper_bound: 9, num_values: 50.0, num_distinct: 5.0 }], avg_spacing: None })) } +1 ColumnStat { min: UInt(0), max: UInt(4), ndv: ~4.463129088[..5.0], null_count: 0, histogram: Some(UInt(TypedHistogram { accuracy: false, row_scale: 0.2, buckets: [TypedHistogramBucket { lower_bound: 0, upper_bound: 4, num_values: 50.0, num_distinct: 5.0 }], avg_spacing: None })) } === constant_logical_predicates === description: Constant predicates should preserve Zero and All through logical composition. diff --git a/src/query/sql/tests/it/optimizer/selectivity_smoke.rs b/src/query/sql/tests/it/optimizer/selectivity_smoke.rs index 46afae921b9be..fb203ac6ffab9 100644 --- a/src/query/sql/tests/it/optimizer/selectivity_smoke.rs +++ b/src/query/sql/tests/it/optimizer/selectivity_smoke.rs @@ -50,9 +50,6 @@ use databend_common_sql::plans::FunctionCall as ScalarFunctionCall; use databend_common_statistics::DEFAULT_HISTOGRAM_BUCKETS; use databend_common_statistics::Datum; use databend_common_statistics::F64; -use databend_common_statistics::Histogram; -use databend_common_statistics::TypedHistogram; -use databend_common_statistics::TypedHistogramBucket; use proptest::prelude::*; fn column_binding(name: &str, index: usize, data_type: DataType) -> ColumnBinding { @@ -111,42 +108,6 @@ fn zero_cardinality_comparison_selectivity_is_finite() { assert!(estimated_rows.is_finite()); } -#[test] -fn distorted_histogram_comparison_estimate_narrows_range() { - let column_stats = ColumnStatSet::from_iter([(Symbol::new(0), ColumnStat { - min: Datum::UInt(0), - max: Datum::UInt(1000), - ndv: NdvEstimate::exact(100.0), - null_count: StatCount::exact(0), - histogram: Some(Histogram::Float(TypedHistogram { - accuracy: false, - row_scale: 1.0, - buckets: vec![TypedHistogramBucket::new( - F64::from(0.0), - F64::from(1000.0), - 100.0, - 100.0, - )], - avg_spacing: Some(1e13), - })), - })]); - let expr = comparison_expr( - "gt", - column_expr("a", 0, DataType::Number(NumberDataType::UInt64)), - constant_expr(Scalar::Number(NumberScalar::UInt64(100))), - ); - - let mut estimator = SelectivityEstimator::new(column_stats, StatCardinality::estimate(100.0)); - let estimated_rows = estimator - .apply(&[expr]) - .expect("distorted histogram comparison should estimate"); - let stat = &estimator.column_stats()[&Symbol::new(0)]; - - assert!((estimated_rows - 50.0).abs() < f64::EPSILON); - assert_eq!(stat.min, Datum::UInt(101)); - assert_eq!(stat.max, Datum::UInt(1000)); -} - #[test] fn float_full_domain_arithmetic_selectivity_is_not_empty() { let column = column_expr("a", 0, DataType::Number(NumberDataType::Float64)); diff --git a/tests/sqllogictests/suites/mode/standalone/explain/common_subexpression_optimizer.test b/tests/sqllogictests/suites/mode/standalone/explain/common_subexpression_optimizer.test index 0f141e503faeb..86c1e024a93b2 100644 --- a/tests/sqllogictests/suites/mode/standalone/explain/common_subexpression_optimizer.test +++ b/tests/sqllogictests/suites/mode/standalone/explain/common_subexpression_optimizer.test @@ -493,7 +493,7 @@ Sequence │ └── Filter │ ├── output columns: [] │ ├── filters: [is_true(time_dim.t_hour (#91) = 12), is_true(time_dim.t_minute (#92) < 30), is_true(store_sales.ss_sold_time_sk (#84) = time_dim.t_time_sk (#90)), is_true(store_sales.ss_hdemo_sk (#85) = household_demographics.hd_demo_sk (#87)), is_true(store_sales.ss_store_sk (#86) = store.s_store_sk (#93)), household_demographics.hd_dep_count (#88) = 4 and household_demographics.hd_vehicle_count (#89) <= 6 or household_demographics.hd_dep_count (#88) = 2 and household_demographics.hd_vehicle_count (#89) <= 4 or household_demographics.hd_dep_count (#88) = 0 and household_demographics.hd_vehicle_count (#89) <= 2, is_true(store.s_store_name (#94) = 'ese')] - │ ├── estimated rows: 3.30 + │ ├── estimated rows: 3.43 │ └── MaterializeCTERef │ ├── cte_name: cte_cse_0 │ ├── cte_schema: [ss_sold_time_sk (#84), ss_hdemo_sk (#85), ss_store_sk (#86), hd_demo_sk (#87), hd_dep_count (#88), hd_vehicle_count (#89), t_time_sk (#90), t_hour (#91), t_minute (#92), s_store_sk (#93), s_store_name (#94)] @@ -518,7 +518,7 @@ Sequence │ └── Filter │ ├── output columns: [] │ ├── filters: [is_true(time_dim.t_hour (#79) = 11), is_true(time_dim.t_minute (#80) >= 30), is_true(store_sales.ss_sold_time_sk (#72) = time_dim.t_time_sk (#78)), is_true(store_sales.ss_hdemo_sk (#73) = household_demographics.hd_demo_sk (#75)), is_true(store_sales.ss_store_sk (#74) = store.s_store_sk (#81)), household_demographics.hd_dep_count (#76) = 4 and household_demographics.hd_vehicle_count (#77) <= 6 or household_demographics.hd_dep_count (#76) = 2 and household_demographics.hd_vehicle_count (#77) <= 4 or household_demographics.hd_dep_count (#76) = 0 and household_demographics.hd_vehicle_count (#77) <= 2, is_true(store.s_store_name (#82) = 'ese')] - │ ├── estimated rows: 3.30 + │ ├── estimated rows: 3.43 │ └── MaterializeCTERef │ ├── cte_name: cte_cse_0 │ ├── cte_schema: [ss_sold_time_sk (#72), ss_hdemo_sk (#73), ss_store_sk (#74), hd_demo_sk (#75), hd_dep_count (#76), hd_vehicle_count (#77), t_time_sk (#78), t_hour (#79), t_minute (#80), s_store_sk (#81), s_store_name (#82)] @@ -543,7 +543,7 @@ Sequence │ └── Filter │ ├── output columns: [] │ ├── filters: [is_true(time_dim.t_hour (#67) = 11), is_true(time_dim.t_minute (#68) < 30), is_true(store_sales.ss_sold_time_sk (#60) = time_dim.t_time_sk (#66)), is_true(store_sales.ss_hdemo_sk (#61) = household_demographics.hd_demo_sk (#63)), is_true(store_sales.ss_store_sk (#62) = store.s_store_sk (#69)), household_demographics.hd_dep_count (#64) = 4 and household_demographics.hd_vehicle_count (#65) <= 6 or household_demographics.hd_dep_count (#64) = 2 and household_demographics.hd_vehicle_count (#65) <= 4 or household_demographics.hd_dep_count (#64) = 0 and household_demographics.hd_vehicle_count (#65) <= 2, is_true(store.s_store_name (#70) = 'ese')] - │ ├── estimated rows: 3.30 + │ ├── estimated rows: 3.43 │ └── MaterializeCTERef │ ├── cte_name: cte_cse_0 │ ├── cte_schema: [ss_sold_time_sk (#60), ss_hdemo_sk (#61), ss_store_sk (#62), hd_demo_sk (#63), hd_dep_count (#64), hd_vehicle_count (#65), t_time_sk (#66), t_hour (#67), t_minute (#68), s_store_sk (#69), s_store_name (#70)] @@ -568,7 +568,7 @@ Sequence │ └── Filter │ ├── output columns: [] │ ├── filters: [is_true(time_dim.t_hour (#55) = 10), is_true(time_dim.t_minute (#56) >= 30), is_true(store_sales.ss_sold_time_sk (#48) = time_dim.t_time_sk (#54)), is_true(store_sales.ss_hdemo_sk (#49) = household_demographics.hd_demo_sk (#51)), is_true(store_sales.ss_store_sk (#50) = store.s_store_sk (#57)), household_demographics.hd_dep_count (#52) = 4 and household_demographics.hd_vehicle_count (#53) <= 6 or household_demographics.hd_dep_count (#52) = 2 and household_demographics.hd_vehicle_count (#53) <= 4 or household_demographics.hd_dep_count (#52) = 0 and household_demographics.hd_vehicle_count (#53) <= 2, is_true(store.s_store_name (#58) = 'ese')] - │ ├── estimated rows: 3.30 + │ ├── estimated rows: 3.43 │ └── MaterializeCTERef │ ├── cte_name: cte_cse_0 │ ├── cte_schema: [ss_sold_time_sk (#48), ss_hdemo_sk (#49), ss_store_sk (#50), hd_demo_sk (#51), hd_dep_count (#52), hd_vehicle_count (#53), t_time_sk (#54), t_hour (#55), t_minute (#56), s_store_sk (#57), s_store_name (#58)] @@ -593,7 +593,7 @@ Sequence │ └── Filter │ ├── output columns: [] │ ├── filters: [is_true(time_dim.t_hour (#43) = 10), is_true(time_dim.t_minute (#44) < 30), is_true(store_sales.ss_sold_time_sk (#36) = time_dim.t_time_sk (#42)), is_true(store_sales.ss_hdemo_sk (#37) = household_demographics.hd_demo_sk (#39)), is_true(store_sales.ss_store_sk (#38) = store.s_store_sk (#45)), household_demographics.hd_dep_count (#40) = 4 and household_demographics.hd_vehicle_count (#41) <= 6 or household_demographics.hd_dep_count (#40) = 2 and household_demographics.hd_vehicle_count (#41) <= 4 or household_demographics.hd_dep_count (#40) = 0 and household_demographics.hd_vehicle_count (#41) <= 2, is_true(store.s_store_name (#46) = 'ese')] - │ ├── estimated rows: 3.30 + │ ├── estimated rows: 3.43 │ └── MaterializeCTERef │ ├── cte_name: cte_cse_0 │ ├── cte_schema: [ss_sold_time_sk (#36), ss_hdemo_sk (#37), ss_store_sk (#38), hd_demo_sk (#39), hd_dep_count (#40), hd_vehicle_count (#41), t_time_sk (#42), t_hour (#43), t_minute (#44), s_store_sk (#45), s_store_name (#46)] @@ -618,7 +618,7 @@ Sequence │ └── Filter │ ├── output columns: [] │ ├── filters: [is_true(time_dim.t_hour (#31) = 9), is_true(time_dim.t_minute (#32) >= 30), is_true(store_sales.ss_sold_time_sk (#24) = time_dim.t_time_sk (#30)), is_true(store_sales.ss_hdemo_sk (#25) = household_demographics.hd_demo_sk (#27)), is_true(store_sales.ss_store_sk (#26) = store.s_store_sk (#33)), household_demographics.hd_dep_count (#28) = 4 and household_demographics.hd_vehicle_count (#29) <= 6 or household_demographics.hd_dep_count (#28) = 2 and household_demographics.hd_vehicle_count (#29) <= 4 or household_demographics.hd_dep_count (#28) = 0 and household_demographics.hd_vehicle_count (#29) <= 2, is_true(store.s_store_name (#34) = 'ese')] - │ ├── estimated rows: 3.30 + │ ├── estimated rows: 3.43 │ └── MaterializeCTERef │ ├── cte_name: cte_cse_0 │ ├── cte_schema: [ss_sold_time_sk (#24), ss_hdemo_sk (#25), ss_store_sk (#26), hd_demo_sk (#27), hd_dep_count (#28), hd_vehicle_count (#29), t_time_sk (#30), t_hour (#31), t_minute (#32), s_store_sk (#33), s_store_name (#34)] @@ -643,7 +643,7 @@ Sequence │ └── Filter │ ├── output columns: [] │ ├── filters: [is_true(time_dim.t_hour (#19) = 9), is_true(time_dim.t_minute (#20) < 30), is_true(store_sales.ss_sold_time_sk (#12) = time_dim.t_time_sk (#18)), is_true(store_sales.ss_hdemo_sk (#13) = household_demographics.hd_demo_sk (#15)), is_true(store_sales.ss_store_sk (#14) = store.s_store_sk (#21)), household_demographics.hd_dep_count (#16) = 4 and household_demographics.hd_vehicle_count (#17) <= 6 or household_demographics.hd_dep_count (#16) = 2 and household_demographics.hd_vehicle_count (#17) <= 4 or household_demographics.hd_dep_count (#16) = 0 and household_demographics.hd_vehicle_count (#17) <= 2, is_true(store.s_store_name (#22) = 'ese')] - │ ├── estimated rows: 3.30 + │ ├── estimated rows: 3.43 │ └── MaterializeCTERef │ ├── cte_name: cte_cse_0 │ ├── cte_schema: [ss_sold_time_sk (#12), ss_hdemo_sk (#13), ss_store_sk (#14), hd_demo_sk (#15), hd_dep_count (#16), hd_vehicle_count (#17), t_time_sk (#18), t_hour (#19), t_minute (#20), s_store_sk (#21), s_store_name (#22)] @@ -660,7 +660,7 @@ Sequence └── Filter ├── output columns: [] ├── filters: [is_true(time_dim.t_hour (#7) = 8), is_true(time_dim.t_minute (#8) >= 30), is_true(store_sales.ss_sold_time_sk (#0) = time_dim.t_time_sk (#6)), is_true(store_sales.ss_hdemo_sk (#1) = household_demographics.hd_demo_sk (#3)), is_true(store_sales.ss_store_sk (#2) = store.s_store_sk (#9)), household_demographics.hd_dep_count (#4) = 4 and household_demographics.hd_vehicle_count (#5) <= 6 or household_demographics.hd_dep_count (#4) = 2 and household_demographics.hd_vehicle_count (#5) <= 4 or household_demographics.hd_dep_count (#4) = 0 and household_demographics.hd_vehicle_count (#5) <= 2, is_true(store.s_store_name (#10) = 'ese')] - ├── estimated rows: 3.30 + ├── estimated rows: 3.43 └── MaterializeCTERef ├── cte_name: cte_cse_0 ├── cte_schema: [ss_sold_time_sk (#0), ss_hdemo_sk (#1), ss_store_sk (#2), hd_demo_sk (#3), hd_dep_count (#4), hd_vehicle_count (#5), t_time_sk (#6), t_hour (#7), t_minute (#8), s_store_sk (#9), s_store_name (#10)] diff --git a/tests/sqllogictests/suites/mode/standalone/explain/push_down_filter/push_down_filter_project_set.test b/tests/sqllogictests/suites/mode/standalone/explain/push_down_filter/push_down_filter_project_set.test index 276dbd62e9aa1..6ba12dbdc9a50 100644 --- a/tests/sqllogictests/suites/mode/standalone/explain/push_down_filter/push_down_filter_project_set.test +++ b/tests/sqllogictests/suites/mode/standalone/explain/push_down_filter/push_down_filter_project_set.test @@ -14,14 +14,14 @@ explain select name, json_path_query(details, '$.features.*') as all_features, j EvalScalar ├── output columns: [products.name (#0), all_features (#3), first_feature (#4)] ├── expressions: [get(1)(json_path_query(products.details (#1), '$.features.*') (#2)), json_path_query_first(products.details (#1), '$.features.*')] -├── estimated rows: 0.36 +├── estimated rows: 0.60 └── Filter ├── output columns: [products.name (#0), products.details (#1), json_path_query(products.details (#1), '$.features.*') (#2)] ├── filters: [is_true(get(1)(json_path_query(products.details (#1), '$.features.*') (#2)) = '"512GB"')] - ├── estimated rows: 0.36 + ├── estimated rows: 0.60 └── ProjectSet ├── output columns: [products.name (#0), products.details (#1), json_path_query(products.details (#1), '$.features.*') (#2)] - ├── estimated rows: 1.80 + ├── estimated rows: 3.00 ├── set returning functions: json_path_query(products.details (#1), '$.features.*') └── TableScan ├── table: default.default.products @@ -33,7 +33,7 @@ EvalScalar ├── partitions scanned: 1 ├── pruning stats: [segments: , decompress cost: , range pruning: 1 to 1 cost: >, blocks: , bloom index read cost: , bloom pruning: 1 to 1 cost: >] ├── push downs: [filters: [products.name (#0) = 'Laptop' and json_path_query_first(products.details (#1), '$.features.*') = '"16GB"'], limit: NONE] - └── estimated rows: 0.60 + └── estimated rows: 1.00 query T?? select name, json_path_query(details, '$.features.*') as all_features, json_path_query_first(details, '$.features.*') as first_feature from products where name = 'Laptop' and first_feature = '16GB' and all_features = '512GB'; @@ -46,10 +46,10 @@ explain select name, json_path_query(details, '$.features.*') as all_features, j EvalScalar ├── output columns: [products.name (#0), all_features (#3), first_feature (#4)] ├── expressions: [get(1)(json_path_query(products.details (#1), '$.features.*') (#2)), json_path_query_first(products.details (#1), '$.features.*')] -├── estimated rows: 1.80 +├── estimated rows: 3.00 └── ProjectSet ├── output columns: [products.name (#0), products.details (#1), json_path_query(products.details (#1), '$.features.*') (#2)] - ├── estimated rows: 1.80 + ├── estimated rows: 3.00 ├── set returning functions: json_path_query(products.details (#1), '$.features.*') └── TableScan ├── table: default.default.products @@ -61,7 +61,7 @@ EvalScalar ├── partitions scanned: 1 ├── pruning stats: [segments: , decompress cost: , range pruning: 1 to 1 cost: >, blocks: , bloom index read cost: , bloom pruning: 1 to 1 cost: >] ├── push downs: [filters: [products.name (#0) = 'Laptop' and json_path_query_first(products.details (#1), '$.features.*') = '"16GB"'], limit: NONE] - └── estimated rows: 0.60 + └── estimated rows: 1.00 query T?? select name, json_path_query(details, '$.features.*') as all_features, json_path_query_first(details, '$.features.*') as first_feature from products where name = 'Laptop' and first_feature = '16GB'; diff --git a/tests/sqllogictests/suites/mode/standalone/explain/selectivity/boolean.test b/tests/sqllogictests/suites/mode/standalone/explain/selectivity/boolean.test index 7e9afeaa9b0e8..46e2ff7883b7c 100644 --- a/tests/sqllogictests/suites/mode/standalone/explain/selectivity/boolean.test +++ b/tests/sqllogictests/suites/mode/standalone/explain/selectivity/boolean.test @@ -25,7 +25,7 @@ TableScan ├── read size: 1.91 KiB ├── partitions total: 1 ├── partitions scanned: 1 -├── pruning stats: [segments: , blocks: ] +├── pruning stats: [segments: , decompress cost: , range pruning: 1 to 1 cost: >, blocks: , bloom index read cost: , bloom pruning: 1 to 1 cost: >] ├── push downs: [filters: [t1.c39 (#0) <> 0 and t1.c13 (#1) = false and (t1.c40 (#2) = 1 or NOT is_not_null(t1.c40 (#2)) or t1.c40 (#2) = 2)], limit: NONE] └── estimated rows: 73.14