Skip to content

Commit 4cc56e0

Browse files
committed
feat(sql): cost clustered probe joins in DPhyp
1 parent e3e218d commit 4cc56e0

3 files changed

Lines changed: 183 additions & 6 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ pub use optimizer::optimize;
2828
pub use optimizer::optimize_query;
2929
pub use optimizer_api::Optimizer;
3030
pub use optimizer_context::OptimizerContext;
31+
pub use optimizer_context::OptimizerTraceEvent;

src/query/sql/src/planner/optimizer/optimizer_context.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ use crate::MetadataRef;
2626
use crate::optimizer::optimizers::rule::RuleID;
2727
use crate::planner::QueryExecutor;
2828

29+
pub struct OptimizerTraceEvent {
30+
pub optimizer: String,
31+
pub event: String,
32+
pub detail: String,
33+
}
34+
2935
#[derive(Educe)]
3036
#[educe(Debug)]
3137
pub struct OptimizerContext {
@@ -49,6 +55,8 @@ pub struct OptimizerContext {
4955
// Optimizer state flags
5056
#[educe(Debug(ignore))]
5157
flags: RwLock<HashMap<String, bool>>,
58+
#[educe(Debug(ignore))]
59+
optimizer_trace: RwLock<Vec<OptimizerTraceEvent>>,
5260

5361
// Enable optimizer tracing
5462
#[educe(Debug(ignore))]
@@ -85,6 +93,7 @@ impl OptimizerContext {
8593
skip_list_str,
8694
grouping_sets_to_union,
8795
flags: RwLock::new(HashMap::new()),
96+
optimizer_trace: RwLock::new(Vec::new()),
8897
enable_trace: RwLock::new(false),
8998
})
9099
}
@@ -164,6 +173,27 @@ impl OptimizerContext {
164173
*flags.get(name).unwrap_or(&false)
165174
}
166175

176+
pub fn add_optimizer_trace(
177+
&self,
178+
optimizer: impl Into<String>,
179+
event: impl Into<String>,
180+
detail: impl Into<String>,
181+
) {
182+
self.optimizer_trace.write().push(OptimizerTraceEvent {
183+
optimizer: optimizer.into(),
184+
event: event.into(),
185+
detail: detail.into(),
186+
});
187+
}
188+
189+
pub fn clear_optimizer_trace(&self) {
190+
self.optimizer_trace.write().clear();
191+
}
192+
193+
pub fn take_optimizer_trace(&self) -> Vec<OptimizerTraceEvent> {
194+
std::mem::take(&mut *self.optimizer_trace.write())
195+
}
196+
167197
pub fn get_enable_trace(&self) -> bool {
168198
*self.enable_trace.read()
169199
}

src/query/sql/src/planner/optimizer/optimizers/hyper_dp/dphyp.rs

Lines changed: 152 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
use std::collections::BTreeMap;
1516
use std::collections::HashMap;
1617
use std::collections::HashSet;
1718
use std::sync::Arc;
@@ -20,10 +21,13 @@ use databend_common_base::runtime::spawn;
2021
use databend_common_catalog::table_context::TableContext;
2122
use databend_common_exception::ErrorCode;
2223
use databend_common_exception::Result;
24+
use databend_common_expression::Expr;
2325

2426
use crate::IndexType;
27+
use crate::Metadata;
2528
use crate::MetadataRef;
2629
use crate::ScalarExpr;
30+
use crate::Symbol;
2731
use crate::optimizer::Optimizer;
2832
use crate::optimizer::OptimizerContext;
2933
use crate::optimizer::ir::SExpr;
@@ -42,6 +46,8 @@ use crate::plans::RelOperator;
4246

4347
const EMIT_THRESHOLD: usize = 10000;
4448
const RELATION_THRESHOLD: usize = 10;
49+
const CLUSTERED_PROBE_COLUMN_FACTOR: f64 = 0.85;
50+
const CLUSTERED_PROBE_EXPRESSION_FACTOR: f64 = 0.95;
4551

4652
/// The join reorder algorithm follows the paper: Dynamic Programming Strikes Back
4753
/// See the paper for more details.
@@ -81,6 +87,120 @@ impl DPhpyOptimizer {
8187
self.opt_ctx.get_metadata()
8288
}
8389

90+
fn clustered_probe_join_cost_factor(
91+
&self,
92+
probe_node: &JoinNode,
93+
join_conditions: &[(ScalarExpr, ScalarExpr)],
94+
) -> Result<f64> {
95+
let probe_join_keys = {
96+
let mut probe_join_keys = Vec::with_capacity(join_conditions.len());
97+
for (probe_key, _) in join_conditions {
98+
probe_join_keys.push(probe_key.as_symbol_expr()?);
99+
}
100+
probe_join_keys
101+
};
102+
if probe_join_keys.is_empty() {
103+
return Ok(1.0);
104+
}
105+
106+
let probe_expr = probe_node
107+
.s_expr
108+
.clone()
109+
.unwrap_or_else(|| probe_node.s_expr(&self.join_relations));
110+
let stat_info = probe_expr.derive_cardinality()?;
111+
Ok(
112+
Self::best_cluster_key_candidate(&stat_info.statistics.cluster_keys, &probe_join_keys)
113+
.unwrap_or(1.0),
114+
)
115+
}
116+
117+
fn best_cluster_key_candidate(
118+
cluster_keys: &BTreeMap<IndexType, Vec<Expr<Symbol>>>,
119+
probe_join_keys: &[Expr<Symbol>],
120+
) -> Option<f64> {
121+
cluster_keys
122+
.iter()
123+
.filter_map(|(_, cluster_key)| {
124+
let factor = Self::cluster_key_prefix_cost_factor(cluster_key, probe_join_keys);
125+
(factor < 1.0).then_some(factor)
126+
})
127+
.min_by(|left, right| left.total_cmp(right))
128+
}
129+
130+
fn cluster_key_prefix_cost_factor(
131+
cluster_key: &[Expr<Symbol>],
132+
probe_join_keys: &[Expr<Symbol>],
133+
) -> f64 {
134+
let mut matched_join_keys = vec![false; probe_join_keys.len()];
135+
let mut factor = 1.0;
136+
137+
for cluster_key_expr in cluster_key {
138+
let mut matched = false;
139+
for (idx, join_key_expr) in probe_join_keys.iter().enumerate() {
140+
if matched_join_keys[idx] {
141+
continue;
142+
}
143+
if let Some(key_factor) =
144+
Self::cluster_key_match_factor(cluster_key_expr, join_key_expr)
145+
{
146+
factor *= key_factor;
147+
matched_join_keys[idx] = true;
148+
matched = true;
149+
break;
150+
}
151+
}
152+
if !matched {
153+
return factor;
154+
}
155+
}
156+
157+
factor
158+
}
159+
160+
fn cluster_key_match_factor(
161+
cluster_key_expr: &Expr<Symbol>,
162+
join_key_expr: &Expr<Symbol>,
163+
) -> Option<f64> {
164+
match cluster_key_expr {
165+
Expr::ColumnRef(cluster_key)
166+
if let Expr::ColumnRef(join_key) = join_key_expr
167+
&& cluster_key.id == join_key.id =>
168+
{
169+
Some(CLUSTERED_PROBE_COLUMN_FACTOR)
170+
}
171+
_ if cluster_key_expr == join_key_expr => Some(CLUSTERED_PROBE_EXPRESSION_FACTOR),
172+
_ => None,
173+
}
174+
}
175+
176+
fn relation_set_label(&self, relation_set: &[IndexType]) -> String {
177+
let metadata = self.metadata();
178+
let metadata = metadata.read();
179+
let mut names = Vec::new();
180+
for relation_idx in relation_set {
181+
if let Some(relation) = self.join_relations.get(*relation_idx) {
182+
Self::collect_scan_table_names(&metadata, &relation.s_expr(), &mut names);
183+
}
184+
}
185+
names.sort();
186+
if names.is_empty() {
187+
format!("{relation_set:?}")
188+
} else {
189+
format!("[{}]", names.join(", "))
190+
}
191+
}
192+
193+
fn collect_scan_table_names(metadata: &Metadata, expr: &SExpr, names: &mut Vec<String>) {
194+
if let RelOperator::Scan(scan) = expr.plan() {
195+
names.push(metadata.table(scan.table_index).name().to_string());
196+
return;
197+
}
198+
199+
for child in expr.children() {
200+
Self::collect_scan_table_names(metadata, child, names);
201+
}
202+
}
203+
84204
/// Process children of a node in parallel
85205
async fn new_children(&mut self, s_expr: &SExpr) -> Result<SExpr> {
86206
// Parallel process children: start a new dphyp for each child.
@@ -807,7 +927,7 @@ impl DPhpyOptimizer {
807927
right_cardinality: f64,
808928
left_join: JoinNode,
809929
right_join: JoinNode,
810-
) -> Result<JoinNode> {
930+
) -> Result<(JoinNode, f64)> {
811931
let parent_set = union(left, right);
812932

813933
if !join_conditions.is_empty() {
@@ -826,13 +946,18 @@ impl DPhpyOptimizer {
826946
};
827947

828948
// Calculate cost for inner join
829-
let cost = join_node.cardinality(&self.join_relations).await?
949+
let probe_factor = self.clustered_probe_join_cost_factor(
950+
&join_node.children[0],
951+
join_node.join_conditions.as_ref(),
952+
)?;
953+
let cardinality = join_node.cardinality(&self.join_relations).await?;
954+
let cost = cardinality * probe_factor
830955
+ join_node.children[0].cost
831956
+ join_node.children[1].cost;
832957

833958
let mut result = join_node;
834959
result.set_cost(cost);
835-
Ok(result)
960+
Ok((result, probe_factor))
836961
} else {
837962
// Create cross join
838963
let join_node = JoinNode {
@@ -849,7 +974,7 @@ impl DPhpyOptimizer {
849974
s_expr: None,
850975
};
851976

852-
Ok(join_node)
977+
Ok((join_node, 1.0))
853978
}
854979
}
855980

@@ -878,7 +1003,7 @@ impl DPhpyOptimizer {
8781003
}
8791004

8801005
// Create join node
881-
let join_node = self
1006+
let (join_node, probe_factor) = self
8821007
.create_join_node(
8831008
left,
8841009
right,
@@ -904,7 +1029,28 @@ impl DPhpyOptimizer {
9041029
};
9051030

9061031
// Update `dp_table` if we found a better plan
907-
if parent_node.is_none() || parent_node.unwrap().cost > join_node.cost {
1032+
let selected = parent_node.is_none() || parent_node.unwrap().cost > join_node.cost;
1033+
if self.opt_ctx.get_flag("explain_memo") {
1034+
let previous_best = parent_node
1035+
.map(|node| format!("{:.3}", node.cost))
1036+
.unwrap_or_else(|| "-".to_string());
1037+
self.opt_ctx.add_optimizer_trace(
1038+
self.name(),
1039+
"join_order_candidate",
1040+
format!(
1041+
"parent: {}, left: {}, right: {}, cost: {:.3}, previous best: {}, probe factor: {:.3}, selected: {}",
1042+
self.relation_set_label(&parent_set),
1043+
self.relation_set_label(left),
1044+
self.relation_set_label(right),
1045+
join_node.cost,
1046+
previous_best,
1047+
probe_factor,
1048+
selected,
1049+
),
1050+
);
1051+
}
1052+
1053+
if selected {
9081054
self.dp_table.insert(parent_set, join_node);
9091055
}
9101056

0 commit comments

Comments
 (0)