Skip to content

Commit 9f193b7

Browse files
committed
test(sql): cover cluster key optimizer statistics
1 parent 4cc56e0 commit 9f193b7

8 files changed

Lines changed: 498 additions & 17 deletions

File tree

src/query/service/tests/it/storages/fuse/operations/table_analyze.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ async fn check_column_ndv_statistics(
194194
let stat = provider.column_statistics(*i);
195195
assert!(stat.is_some());
196196
// Safe to unwrap: FuseTable's ndv is not None.
197-
assert_eq!(stat.unwrap().ndv.unwrap(), *num);
197+
assert_eq!(stat.unwrap().ndv_value().unwrap(), *num);
198198
}
199199

200200
Ok(())

src/query/sql/src/planner/optimizer/optimizers/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,5 @@ pub use eliminate_self_join::EliminateSelfJoinOptimizer;
2929
pub use hyper_dp::DPhpyOptimizer;
3030
pub use operator::CleanupUnusedCTEOptimizer;
3131
pub use sync_materialized_cte_ref::SyncMaterializedCTERefOptimizer;
32+
33+
pub use crate::optimizer::statistics::CollectStatisticsOptimizer;

src/query/sql/test-support/src/optimizer/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ use databend_common_sql::plans::Statistics;
4343
use databend_common_statistics::Datum;
4444
use databend_common_statistics::Histogram;
4545
use databend_common_statistics::HistogramBucket;
46+
use databend_common_statistics::NdvEstimate;
4647
use goldenfile::Mint;
4748
use serde::Deserialize;
4849
use serde::Serialize;
@@ -481,6 +482,7 @@ impl SExprVisitor for StatsApplier<'_> {
481482
table_stats: Some(table_stats),
482483
column_stats,
483484
histograms,
485+
cluster_key_orders: Default::default(),
484486
});
485487

486488
return Ok(VisitAction::Replace(
@@ -517,7 +519,7 @@ impl StatsApplier<'_> {
517519
.or_else(|| default_min_datum(&column.data_type())),
518520
max: to_datum(&stats.max)
519521
.or_else(|| default_max_datum(&column.data_type())),
520-
ndv: stats.ndv,
522+
ndv: stats.ndv.map(|ndv| NdvEstimate::exact(ndv as f64)),
521523
null_count: stats.null_count.unwrap_or(0),
522524
in_memory_size: 0,
523525
}),

src/query/sql/tests/it/framework/lite_context.rs

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ use databend_common_sql::resolve_type_name;
9595
use databend_common_sql_test_support::configure_optimizer_settings;
9696
use databend_common_statistics::Datum;
9797
use databend_common_statistics::Histogram;
98+
use databend_common_statistics::NdvEstimate;
9899
use databend_common_storage::DataOperator;
99100
use databend_common_storage::FileStatus;
100101
use databend_common_storage::StageFileInfo;
@@ -110,6 +111,7 @@ use databend_meta_runtime::DatabendRuntime;
110111
use databend_storages_common_session::SessionState;
111112
use databend_storages_common_session::TxnManager;
112113
use databend_storages_common_session::TxnManagerRef;
114+
use databend_storages_common_table_meta::meta::ClusterKey;
113115
use databend_storages_common_table_meta::meta::TableMetaTimestamps;
114116
use databend_storages_common_table_meta::meta::TableSnapshot;
115117
use databend_storages_common_table_meta::table::ChangeType;
@@ -307,6 +309,10 @@ impl Table for FakeTable {
307309
fn support_prewhere(&self) -> bool {
308310
true
309311
}
312+
313+
fn cluster_key_meta(&self) -> Option<ClusterKey> {
314+
self.table_info.cluster_key()
315+
}
310316
}
311317

312318
#[async_trait::async_trait]
@@ -712,6 +718,7 @@ impl LiteTableContext {
712718
database: &str,
713719
table_name: &str,
714720
fields: Vec<TableField>,
721+
cluster_key: Option<String>,
715722
table_stats: Option<TableStatistics>,
716723
column_stats: ColumnStatsMap,
717724
histograms: HistogramStatsMap,
@@ -744,6 +751,7 @@ impl LiteTableContext {
744751
name: table_name.to_string(),
745752
meta: TableMeta {
746753
schema,
754+
cluster_key_v2: cluster_key.map(|key| (0, key)),
747755
..Default::default()
748756
},
749757
catalog_info: self.default_catalog.info(),
@@ -910,11 +918,33 @@ impl LiteTableContext {
910918
table_stats: Option<TableStatistics>,
911919
column_stats: ColumnStatsMap,
912920
histograms: HistogramStatsMap,
921+
) -> Result<()> {
922+
self.register_table_with_cluster_key_stats_and_histograms(
923+
database,
924+
table_name,
925+
fields,
926+
None,
927+
table_stats,
928+
column_stats,
929+
histograms,
930+
)
931+
}
932+
933+
fn register_table_with_cluster_key_stats_and_histograms(
934+
self: &Arc<Self>,
935+
database: &str,
936+
table_name: &str,
937+
fields: Vec<TableField>,
938+
cluster_key: Option<String>,
939+
table_stats: Option<TableStatistics>,
940+
column_stats: ColumnStatsMap,
941+
histograms: HistogramStatsMap,
913942
) -> Result<()> {
914943
let table = self.build_fake_table(
915944
database,
916945
table_name,
917946
fields,
947+
cluster_key,
918948
table_stats,
919949
column_stats,
920950
histograms,
@@ -1059,7 +1089,7 @@ impl LiteTableContext {
10591089
BasicColumnStatistics {
10601090
min: Some(Datum::UInt(0)),
10611091
max: Some(Datum::UInt(num_rows.saturating_sub(1))),
1062-
ndv: Some(num_rows),
1092+
ndv: Some(NdvEstimate::exact(num_rows as f64)),
10631093
null_count: 0,
10641094
in_memory_size: num_rows.saturating_mul(8),
10651095
},
@@ -1078,11 +1108,20 @@ impl LiteTableContext {
10781108
}
10791109
}
10801110
};
1081-
1082-
self.register_table_with_stats_and_histograms(
1111+
let cluster_key = stmt.cluster_by.as_ref().map(|cluster_by| {
1112+
let cluster_exprs = cluster_by
1113+
.cluster_exprs
1114+
.iter()
1115+
.map(|expr| format!("{expr:#}"))
1116+
.collect::<Vec<_>>();
1117+
format!("({})", cluster_exprs.join(", "))
1118+
});
1119+
1120+
self.register_table_with_cluster_key_stats_and_histograms(
10831121
&database,
10841122
&table_name,
10851123
fields,
1124+
cluster_key,
10861125
table_stats,
10871126
column_stats,
10881127
histograms,
@@ -1109,6 +1148,10 @@ impl LiteTableContext {
11091148
pub async fn optimize_plan(self: &Arc<Self>, plan: Plan) -> Result<Plan> {
11101149
let metadata = match &plan {
11111150
Plan::Query { metadata, .. } => metadata.clone(),
1151+
Plan::Explain { plan, .. } => match plan.as_ref() {
1152+
Plan::Query { metadata, .. } => metadata.clone(),
1153+
_ => Arc::new(RwLock::new(Metadata::default())),
1154+
},
11121155
_ => Arc::new(RwLock::new(Metadata::default())),
11131156
};
11141157
let settings = self.get_settings();
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
// Copyright 2021 Datafuse Labs
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use std::collections::BTreeMap;
16+
use std::collections::HashMap;
17+
use std::io::Write;
18+
19+
use databend_common_catalog::BasicColumnStatistics;
20+
use databend_common_catalog::TableStatistics;
21+
use databend_common_catalog::table_context::TableContextSettings;
22+
use databend_common_exception::Result;
23+
use databend_common_expression::stat_distribution::NdvEstimate;
24+
use databend_common_sql::optimizer::OptimizerContext;
25+
use databend_common_sql::optimizer::OptimizerTraceEvent;
26+
use databend_common_sql::optimizer::optimizers::CascadesOptimizer;
27+
use databend_common_sql::optimizer::optimizers::CollectStatisticsOptimizer;
28+
use databend_common_sql::optimizer::optimizers::DPhpyOptimizer;
29+
use databend_common_sql::optimizer::optimizers::operator::PullUpFilterOptimizer;
30+
use databend_common_sql::optimizer::optimizers::operator::RuleStatsAggregateOptimizer;
31+
use databend_common_sql::optimizer::optimizers::operator::SubqueryDecorrelatorOptimizer;
32+
use databend_common_sql::optimizer::optimizers::recursive::RecursiveRuleOptimizer;
33+
use databend_common_sql::optimizer::optimizers::rule::DEFAULT_REWRITE_RULES;
34+
use databend_common_sql::optimizer::optimizers::rule::RuleID;
35+
use databend_common_sql::optimizer::pipeline::OptimizerPipeline;
36+
use databend_common_sql::plans::Plan;
37+
use databend_common_statistics::Datum;
38+
39+
use crate::framework::LiteTableContext;
40+
use crate::framework::golden::open_golden_file;
41+
use crate::framework::golden::write_case_title;
42+
43+
struct JoinMemoCase<'a> {
44+
name: &'a str,
45+
description: &'a str,
46+
cluster_by: &'a str,
47+
sql: &'a str,
48+
}
49+
50+
fn table_statistics(rows: u64) -> TableStatistics {
51+
TableStatistics {
52+
num_rows: Some(rows),
53+
data_size: Some(rows.saturating_mul(24)),
54+
data_size_compressed: None,
55+
index_size: None,
56+
bloom_index_size: None,
57+
ngram_index_size: None,
58+
inverted_index_size: None,
59+
vector_index_size: None,
60+
virtual_column_size: None,
61+
number_of_blocks: Some(1),
62+
number_of_segments: Some(1),
63+
}
64+
}
65+
66+
fn column_statistics(rows: u64) -> HashMap<String, BasicColumnStatistics> {
67+
["k1", "k2", "v"]
68+
.into_iter()
69+
.map(|column| {
70+
(column.to_string(), BasicColumnStatistics {
71+
min: Some(Datum::Int(0)),
72+
max: Some(Datum::Int(rows as i64)),
73+
ndv: Some(NdvEstimate::exact(rows as f64)),
74+
null_count: 0,
75+
in_memory_size: rows.saturating_mul(8),
76+
})
77+
})
78+
.collect()
79+
}
80+
81+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
82+
async fn test_cluster_key_order_join_memo_golden() -> Result<()> {
83+
let mut file = open_golden_file("optimizer", "cluster_key_join_order.txt")?;
84+
85+
for case in [
86+
JoinMemoCase {
87+
name: "k1_k2_prefix",
88+
description: "Full memo output when the clustered probe can first match a.k1.",
89+
cluster_by: "CLUSTER BY (k1, k2)",
90+
sql: "
91+
SELECT *
92+
FROM a
93+
JOIN b ON a.k1 = b.k1
94+
JOIN c ON a.k2 = c.k2
95+
",
96+
},
97+
JoinMemoCase {
98+
name: "k2_k1_prefix",
99+
description: "Full memo output when the clustered probe can first match a.k2.",
100+
cluster_by: "CLUSTER BY (k2, k1)",
101+
sql: "
102+
SELECT *
103+
FROM a
104+
JOIN b ON a.k1 = b.k1
105+
JOIN c ON a.k2 = c.k2
106+
",
107+
},
108+
JoinMemoCase {
109+
name: "filter_preserves_cluster_keys",
110+
description: "Cluster keys still affect join order after a filter on the clustered table.",
111+
cluster_by: "CLUSTER BY (k1, k2)",
112+
sql: "
113+
SELECT *
114+
FROM (SELECT * FROM a WHERE v >= 0) a
115+
JOIN b ON a.k1 = b.k1
116+
JOIN c ON a.k2 = c.k2
117+
",
118+
},
119+
JoinMemoCase {
120+
name: "limit_and_join_preserve_cluster_keys",
121+
description: "Cluster keys still affect join order after a limit subquery and a partial join.",
122+
cluster_by: "CLUSTER BY (k1, k2)",
123+
sql: "
124+
SELECT *
125+
FROM (SELECT * FROM a LIMIT 1000) a
126+
JOIN b ON a.k1 = b.k1
127+
JOIN c ON a.k2 = c.k2
128+
",
129+
},
130+
] {
131+
write_cluster_key_join_order_memo(&mut file, case).await?;
132+
}
133+
134+
Ok(())
135+
}
136+
137+
async fn write_cluster_key_join_order_memo(
138+
file: &mut impl Write,
139+
case: JoinMemoCase<'_>,
140+
) -> Result<()> {
141+
let ctx = LiteTableContext::create().await?;
142+
ctx.configure_for_optimizer_case(true)?;
143+
ctx.set_cluster_node_num(1);
144+
145+
write_case_title(file, case.name, case.description)?;
146+
for table in ["a", "b", "c"] {
147+
let table_cluster_by = if table == "a" { case.cluster_by } else { "" };
148+
let setup_sql = match table_cluster_by {
149+
"" => format!("CREATE TABLE {table}(k1 BIGINT, k2 BIGINT, v BIGINT)"),
150+
_ => {
151+
format!("CREATE TABLE {table}(k1 BIGINT, k2 BIGINT, v BIGINT) {table_cluster_by}")
152+
}
153+
};
154+
writeln!(file, "setup: {setup_sql}")?;
155+
ctx.register_table_sql_with_stats(
156+
&setup_sql,
157+
Some(table_statistics(1000)),
158+
column_statistics(1000),
159+
)
160+
.await?;
161+
}
162+
163+
let sql = case
164+
.sql
165+
.lines()
166+
.map(str::trim)
167+
.filter(|line| !line.is_empty())
168+
.collect::<Vec<_>>()
169+
.join("\n");
170+
writeln!(file, "sql: {sql}")?;
171+
writeln!(file, "memo:")?;
172+
writeln!(file, "{}", explain_memo(&ctx, &sql).await?)?;
173+
writeln!(file)?;
174+
Ok(())
175+
}
176+
177+
async fn explain_memo(ctx: &std::sync::Arc<LiteTableContext>, sql: &str) -> Result<String> {
178+
let Plan::Query {
179+
s_expr, metadata, ..
180+
} = ctx.bind_sql(sql).await?
181+
else {
182+
unreachable!("SELECT should bind to a query plan");
183+
};
184+
185+
let settings = ctx.get_settings();
186+
let opt_ctx = OptimizerContext::new(ctx.clone(), metadata)
187+
.with_settings(&settings)?
188+
.set_enable_distributed_optimization(true)
189+
.clone();
190+
opt_ctx.set_flag("explain_memo", true);
191+
opt_ctx.clear_optimizer_trace();
192+
193+
let mut pipeline = OptimizerPipeline::new(opt_ctx.clone(), *s_expr)
194+
.await?
195+
.add(SubqueryDecorrelatorOptimizer::new(opt_ctx.clone(), None))
196+
.add(RuleStatsAggregateOptimizer::new(opt_ctx.clone()))
197+
.add(CollectStatisticsOptimizer::new(opt_ctx.clone()))
198+
.add(PullUpFilterOptimizer::new(opt_ctx.clone()))
199+
.add(RecursiveRuleOptimizer::new(
200+
opt_ctx.clone(),
201+
&DEFAULT_REWRITE_RULES,
202+
))
203+
.add(RecursiveRuleOptimizer::new(opt_ctx.clone(), &[
204+
RuleID::SplitAggregate,
205+
]))
206+
.add(DPhpyOptimizer::new(opt_ctx.clone()))
207+
.add(CascadesOptimizer::new(opt_ctx.clone())?);
208+
209+
let _s_expr = pipeline.execute().await?;
210+
211+
let mut sections = Vec::new();
212+
sections.extend(format_optimizer_trace(opt_ctx.take_optimizer_trace()));
213+
sections.push(pipeline.memo().display()?);
214+
Ok(sections.join("\n\n"))
215+
}
216+
217+
fn format_optimizer_trace(traces: Vec<OptimizerTraceEvent>) -> Vec<String> {
218+
let mut groups = BTreeMap::<String, BTreeMap<String, Vec<String>>>::new();
219+
for trace in traces {
220+
groups
221+
.entry(trace.optimizer)
222+
.or_default()
223+
.entry(trace.event)
224+
.or_default()
225+
.push(trace.detail);
226+
}
227+
228+
groups
229+
.into_iter()
230+
.map(|(optimizer, events)| {
231+
let mut lines = vec![format!("{optimizer}:")];
232+
for (event, details) in events {
233+
lines.push(format!("{event}:"));
234+
lines.extend(details.into_iter().map(|detail| format!("- {detail}")));
235+
}
236+
lines.join("\n")
237+
})
238+
.collect()
239+
}

0 commit comments

Comments
 (0)