use crate::{JoinType, LogicalPlan};
use super::OptPass;
#[derive(Debug, Clone, PartialEq)]
pub enum JoinAlgoHint {
HashJoin,
MergeJoin,
NestedLoop,
}
pub struct JoinAlgorithmPass;
impl OptPass for JoinAlgorithmPass {
fn name(&self) -> &'static str {
"join_algorithm"
}
fn apply(&self, plan: LogicalPlan) -> LogicalPlan {
annotate_join(plan)
}
}
fn annotate_join(plan: LogicalPlan) -> LogicalPlan {
match plan {
LogicalPlan::Join {
left,
right,
on,
join_type,
algo_hint: _,
} => {
let left = Box::new(annotate_join(*left));
let right = Box::new(annotate_join(*right));
let hint = choose_hint(&on, &join_type, &left, &right);
LogicalPlan::Join {
left,
right,
on,
join_type,
algo_hint: Some(hint),
}
}
LogicalPlan::Filter { input, predicate } => LogicalPlan::Filter {
input: Box::new(annotate_join(*input)),
predicate,
},
LogicalPlan::Project { input, columns } => LogicalPlan::Project {
input: Box::new(annotate_join(*input)),
columns,
},
LogicalPlan::Aggregate {
input,
group_by,
aggregates,
} => LogicalPlan::Aggregate {
input: Box::new(annotate_join(*input)),
group_by,
aggregates,
},
LogicalPlan::Sort { input, order_by } => LogicalPlan::Sort {
input: Box::new(annotate_join(*input)),
order_by,
},
LogicalPlan::Limit {
input,
count,
offset,
} => LogicalPlan::Limit {
input: Box::new(annotate_join(*input)),
count,
offset,
},
LogicalPlan::Window { input, functions } => LogicalPlan::Window {
input: Box::new(annotate_join(*input)),
functions,
},
LogicalPlan::Cte {
name,
query,
recursive,
} => LogicalPlan::Cte {
name,
query: Box::new(annotate_join(*query)),
recursive,
},
LogicalPlan::SetOp {
op,
all,
left,
right,
} => LogicalPlan::SetOp {
op,
all,
left: Box::new(annotate_join(*left)),
right: Box::new(annotate_join(*right)),
},
LogicalPlan::Subquery { query, alias } => LogicalPlan::Subquery {
query: Box::new(annotate_join(*query)),
alias,
},
LogicalPlan::Exists { subquery, negated } => LogicalPlan::Exists {
subquery: Box::new(annotate_join(*subquery)),
negated,
},
LogicalPlan::InSubquery {
expr,
subquery,
negated,
} => LogicalPlan::InSubquery {
expr,
subquery: Box::new(annotate_join(*subquery)),
negated,
},
LogicalPlan::Compute { input, bindings } => LogicalPlan::Compute {
input: Box::new(annotate_join(*input)),
bindings,
},
leaf => leaf,
}
}
fn choose_hint(
on: &str,
join_type: &JoinType,
left: &LogicalPlan,
right: &LogicalPlan,
) -> JoinAlgoHint {
if matches!(join_type, JoinType::Cross) || on.is_empty() {
return JoinAlgoHint::NestedLoop;
}
let is_equi = on.contains('=') && !on.contains('<') && !on.contains('>');
if !is_equi {
return JoinAlgoHint::NestedLoop;
}
if sides_sorted_on_join_key(on, left) && sides_sorted_on_join_key(on, right) {
return JoinAlgoHint::MergeJoin;
}
JoinAlgoHint::HashJoin
}
fn sides_sorted_on_join_key(on: &str, plan: &LogicalPlan) -> bool {
if let LogicalPlan::Sort { order_by, .. } = plan {
if let Some(first) = order_by.first() {
let col = first.column.rsplit('.').next().unwrap_or(&first.column);
return on.contains(col);
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{parse_one, plan_statement, JoinType, LogicalPlan, Optimizer};
fn equi_join_plan() -> LogicalPlan {
LogicalPlan::Join {
left: Box::new(LogicalPlan::Scan {
table: "a".to_string(),
alias: None,
limit: None,
}),
right: Box::new(LogicalPlan::Scan {
table: "b".to_string(),
alias: None,
limit: None,
}),
on: "a.id = b.id".to_string(),
join_type: JoinType::Inner,
algo_hint: None,
}
}
fn cross_join_plan() -> LogicalPlan {
LogicalPlan::Join {
left: Box::new(LogicalPlan::Scan {
table: "a".to_string(),
alias: None,
limit: None,
}),
right: Box::new(LogicalPlan::Scan {
table: "b".to_string(),
alias: None,
limit: None,
}),
on: String::new(),
join_type: JoinType::Cross,
algo_hint: None,
}
}
#[test]
fn test_equi_join_gets_hash_hint() {
let plan = equi_join_plan();
let result = JoinAlgorithmPass.apply(plan);
match result {
LogicalPlan::Join { algo_hint, .. } => {
assert_eq!(algo_hint, Some(JoinAlgoHint::HashJoin));
}
other => panic!("expected Join, got {other:?}"),
}
}
#[test]
fn test_cross_join_gets_nested_loop() {
let plan = cross_join_plan();
let result = JoinAlgorithmPass.apply(plan);
match result {
LogicalPlan::Join { algo_hint, .. } => {
assert_eq!(algo_hint, Some(JoinAlgoHint::NestedLoop));
}
other => panic!("expected Join, got {other:?}"),
}
}
#[test]
fn test_join_algorithm_pass_name() {
assert_eq!(JoinAlgorithmPass.name(), "join_algorithm");
}
#[test]
fn test_optimizer_pipeline_annotates_join() {
let stmt = parse_one("SELECT * FROM a JOIN b ON a.id = b.id").expect("parse");
let plan = plan_statement(&stmt).expect("plan");
let result = Optimizer::new().optimize(plan);
fn find_join_hint(p: &LogicalPlan) -> Option<Option<JoinAlgoHint>> {
match p {
LogicalPlan::Join {
algo_hint,
left,
right,
..
} => {
let _ = (left, right); Some(algo_hint.clone())
}
LogicalPlan::Filter { input, .. }
| LogicalPlan::Project { input, .. }
| LogicalPlan::Sort { input, .. }
| LogicalPlan::Limit { input, .. }
| LogicalPlan::Aggregate { input, .. } => find_join_hint(input),
_ => None,
}
}
let hint = find_join_hint(&result);
assert!(
matches!(hint, Some(Some(JoinAlgoHint::HashJoin))),
"expected HashJoin hint in optimized plan, got {hint:?}"
);
}
}