use crate::sqljoin::Strategy;
use crate::sqlselect::JoinKind;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Stage {
Scan {
table: String,
binding: String,
rows: usize,
},
Join {
kind: JoinKind,
table: String,
binding: String,
strategy: Strategy,
keys: usize,
left_rows: usize,
right_rows: usize,
out_rows: usize,
early_stopped: bool,
post_filter_removed: Option<usize>,
},
Filter { in_rows: usize, out_rows: usize },
Project { columns: usize, out_rows: usize },
Distinct { in_rows: usize, out_rows: usize },
Sort { keys: usize, rows: usize },
Prefilter {
binding: String,
predicates: usize,
in_rows: usize,
out_rows: usize,
},
Limit {
limit: Option<usize>,
offset: Option<usize>,
in_rows: usize,
out_rows: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PlanTree {
Leaf(Stage),
Unary { stage: Stage, input: Box<PlanTree> },
Binary { stage: Stage, left: Box<PlanTree>, right: Box<PlanTree> },
}
impl PlanTree {
pub fn stage(&self) -> &Stage {
match self {
PlanTree::Leaf(s) => s,
PlanTree::Unary { stage, .. } => stage,
PlanTree::Binary { stage, .. } => stage,
}
}
pub fn children(&self) -> Vec<&PlanTree> {
match self {
PlanTree::Leaf(_) => vec![],
PlanTree::Unary { input, .. } => vec![input],
PlanTree::Binary { left, right, .. } => vec![left, right],
}
}
pub fn size(&self) -> usize {
1 + self.children().iter().map(|c| c.size()).sum::<usize>()
}
pub fn depth(&self) -> usize {
1 + self.children().iter().map(|c| c.depth()).max().unwrap_or(0)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Plan {
pub stages: Vec<Stage>,
pub budget: Option<usize>,
pub refusals: Vec<String>,
}
impl Plan {
pub fn push(&mut self, s: Stage) {
self.stages.push(s);
}
pub fn joins(&self) -> Vec<&Stage> {
self.stages
.iter()
.filter(|s| matches!(s, Stage::Join { .. }))
.collect()
}
pub fn join_strategy(&self, n: usize) -> Option<Strategy> {
match self.joins().get(n) {
Some(Stage::Join { strategy, .. }) => Some(*strategy),
_ => None,
}
}
pub fn join_strategies(&self) -> Vec<Strategy> {
self.stages
.iter()
.filter_map(|s| match s {
Stage::Join { strategy, .. } => Some(*strategy),
_ => None,
})
.collect()
}
pub fn join_keys(&self, n: usize) -> Option<usize> {
match self.joins().get(n) {
Some(Stage::Join { keys, .. }) => Some(*keys),
_ => None,
}
}
pub fn tree(&self) -> Option<PlanTree> {
let mut it = self.stages.iter();
let mut node = PlanTree::Leaf(it.next()?.clone());
let rest: Vec<&Stage> = it.collect();
let mut i = 0usize;
while i < rest.len() {
let right_len = match (rest.get(i), rest.get(i + 1), rest.get(i + 2)) {
(Some(Stage::Scan { .. }), Some(Stage::Join { .. }), _) => Some(1),
(
Some(Stage::Scan { .. }),
Some(Stage::Prefilter { .. }),
Some(Stage::Join { .. }),
) => Some(2),
_ => None,
};
if let Some(n) = right_len {
let mut right = PlanTree::Leaf(rest[i].clone());
if n == 2 {
right = PlanTree::Unary {
stage: rest[i + 1].clone(),
input: Box::new(right),
};
}
node = PlanTree::Binary {
stage: rest[i + n].clone(),
left: Box::new(node),
right: Box::new(right),
};
i += n + 1;
} else {
node = PlanTree::Unary {
stage: rest[i].clone(),
input: Box::new(node),
};
i += 1;
}
}
Some(node)
}
pub fn render(&self) -> Vec<String> {
let mut out = vec![];
if let Some(t) = self.tree() {
render_node(&t, 0, &mut out);
}
for r in &self.refusals {
out.push(r.clone());
}
if let Some(b) = self.budget {
out.push(format!(
"Row budget: {b} — the join was allowed to stop once this many \
rows existed"
));
}
out.push(
"NEDB reports ACTUAL rows, never estimates: it has no statistics to \
estimate from, and a guess printed as a number is worse than the truth."
.to_string(),
);
out
}
}
fn render_node(n: &PlanTree, depth: usize, out: &mut Vec<String>) {
let indent = " ".repeat(depth);
let arrow = if depth == 0 { String::new() } else { format!("{indent}-> ") };
let line = match n.stage() {
Stage::Scan { table, binding, rows } => {
format!("{arrow}Seq Scan on {} (actual rows={rows})", named(table, binding))
}
Stage::Join {
kind,
table,
binding,
strategy,
keys,
left_rows,
right_rows,
out_rows,
early_stopped,
post_filter_removed,
} => {
let k = match keys {
0 => "no equality key".to_string(),
1 => "1 hash key".to_string(),
n => format!("{n} hash keys"),
};
let stop = if *early_stopped { ", stopped early" } else { "" };
let filt = match post_filter_removed {
Some(n) => format!(", post-join filter removed {n}"),
None => String::new(),
};
format!(
"{arrow}{strategy} {} Join on {} \
({k}, left={left_rows}, right={right_rows}{stop}{filt}) \
(actual rows={out_rows})",
kind_name(*kind),
named(table, binding)
)
}
Stage::Filter { in_rows, out_rows } => format!(
"{arrow}Filter (removed {}) (actual rows={out_rows})",
in_rows.saturating_sub(*out_rows)
),
Stage::Project { columns, out_rows } => {
format!("{arrow}Project ({columns} columns) (actual rows={out_rows})")
}
Stage::Distinct { in_rows, out_rows } => format!(
"{arrow}Unique (removed {}) (actual rows={out_rows})",
in_rows.saturating_sub(*out_rows)
),
Stage::Sort { keys, rows } => {
format!("{arrow}Sort ({keys} key(s)) (actual rows={rows})")
}
Stage::Prefilter { binding, predicates, in_rows, out_rows } => format!(
"{arrow}Prefilter on {binding} ({predicates} pushed, removed {}) \
(actual rows={out_rows})",
in_rows.saturating_sub(*out_rows)
),
Stage::Limit { limit, offset, in_rows, out_rows } => {
let l = limit.map(|n| n.to_string()).unwrap_or_else(|| "ALL".into());
let o = offset.map(|n| format!(", offset {n}")).unwrap_or_default();
format!("{arrow}Limit ({l}{o}, from {in_rows}) (actual rows={out_rows})")
}
};
out.push(line);
for c in n.children() {
render_node(c, depth + 1, out);
}
}
fn named(table: &str, binding: &str) -> String {
if table == binding {
table.to_string()
} else {
format!("{table} {binding}")
}
}
fn kind_name(k: JoinKind) -> &'static str {
match k {
JoinKind::Inner => "Inner",
JoinKind::Left => "Left",
JoinKind::Right => "Right",
JoinKind::Full => "Full",
JoinKind::Cross => "Cross",
}
}
#[cfg(test)]
mod tests {
use super::*;
fn scan(t: &str, rows: usize) -> Stage {
Stage::Scan { table: t.into(), binding: t.into(), rows }
}
#[test]
fn an_empty_plan_still_explains_itself() {
let p = Plan::default();
let r = p.render();
assert_eq!(r.len(), 1);
assert!(r[0].contains("ACTUAL rows"));
}
#[test]
fn a_scan_renders_with_actual_rows() {
let mut p = Plan::default();
p.push(scan("orders", 1000));
let r = p.render();
assert!(r[0].starts_with("Seq Scan on orders"), "{:?}", r[0]);
assert!(r[0].contains("actual rows=1000"));
}
#[test]
fn an_alias_is_shown_but_a_redundant_one_is_not() {
let mut p = Plan::default();
p.push(Stage::Scan { table: "orders".into(), binding: "o".into(), rows: 1 });
assert!(p.render()[0].contains("orders o"));
let mut p = Plan::default();
p.push(scan("orders", 1));
assert!(!p.render()[0].contains("orders orders"));
}
#[test]
fn the_outermost_stage_is_printed_first() {
let mut p = Plan::default();
p.push(scan("orders", 100));
p.push(Stage::Limit { limit: Some(5), offset: None, in_rows: 100, out_rows: 5 });
let r = p.render();
assert!(r[0].starts_with("Limit"), "{r:?}");
assert!(r[1].contains("Seq Scan"), "{r:?}");
assert!(r[1].starts_with(" -> "), "{:?}", r[1]);
}
#[test]
fn a_join_names_its_strategy_and_key_count() {
let mut p = Plan::default();
p.push(scan("orders", 1000));
p.push(Stage::Join {
kind: JoinKind::Inner,
table: "customers".into(),
binding: "c".into(),
strategy: Strategy::Hash,
keys: 2,
left_rows: 1000,
right_rows: 500,
out_rows: 1922,
early_stopped: false,
post_filter_removed: None,
});
let r = p.render();
assert!(r[0].contains("Hash Join"), "{:?}", r[0]);
assert!(r[0].contains("Inner"));
assert!(r[0].contains("2 hash keys"));
assert!(r[0].contains("customers c"));
assert!(r[0].contains("actual rows=1922"));
}
#[test]
fn a_join_with_no_key_says_so_because_that_is_why_it_is_slow() {
let mut p = Plan::default();
p.push(Stage::Join {
kind: JoinKind::Inner,
table: "customers".into(),
binding: "customers".into(),
strategy: Strategy::NestedLoop,
keys: 0,
left_rows: 1000,
right_rows: 500,
out_rows: 2500,
early_stopped: false,
post_filter_removed: None,
});
let r = p.render();
assert!(r[0].contains("Nested Loop"));
assert!(r[0].contains("no equality key"), "{:?}", r[0]);
}
#[test]
fn early_termination_is_visible() {
let mut p = Plan::default();
p.push(Stage::Join {
kind: JoinKind::Inner,
table: "c".into(),
binding: "c".into(),
strategy: Strategy::Hash,
keys: 1,
left_rows: 1000,
right_rows: 500,
out_rows: 20,
early_stopped: true,
post_filter_removed: None,
});
p.budget = Some(20);
let r = p.render();
assert!(r[0].contains("stopped early"), "{:?}", r[0]);
assert!(r.iter().any(|l| l.contains("Row budget: 20")));
}
#[test]
fn filter_and_unique_report_what_they_removed() {
let mut p = Plan::default();
p.push(Stage::Filter { in_rows: 1000, out_rows: 117 });
p.push(Stage::Distinct { in_rows: 117, out_rows: 4 });
let r = p.render();
assert!(r.iter().any(|l| l.contains("Unique") && l.contains("removed 113")), "{r:?}");
assert!(r.iter().any(|l| l.contains("Filter") && l.contains("removed 883")), "{r:?}");
}
#[test]
fn a_joins_two_inputs_are_siblings_not_nested() {
let mut p = Plan::default();
p.push(scan("orders", 10));
p.push(scan("customers", 5));
p.push(Stage::Join {
kind: JoinKind::Inner,
table: "customers".into(),
binding: "customers".into(),
strategy: Strategy::Hash,
keys: 1,
left_rows: 10,
right_rows: 5,
out_rows: 7,
early_stopped: false,
post_filter_removed: None,
});
let r = p.render();
assert!(r[0].contains("Hash Join"), "{r:?}");
let orders = r.iter().find(|l| l.contains("orders")).expect("orders scanned");
let custs = r.iter().find(|l| l.contains("customers (actual")).expect("customers");
let depth = |l: &str| l.len() - l.trim_start().len();
assert_eq!(
depth(orders), depth(custs),
"the two inputs of a join must be at the same depth\n{r:#?}"
);
assert!(depth(orders) > depth(&r[0]), "both are nested under the join");
}
fn join_stage(table: &str) -> Stage {
Stage::Join {
kind: JoinKind::Inner,
table: table.into(),
binding: table.into(),
strategy: Strategy::Hash,
keys: 1,
left_rows: 1,
right_rows: 1,
out_rows: 1,
early_stopped: false,
post_filter_removed: None,
}
}
#[test]
fn a_join_node_has_exactly_two_children() {
let mut p = Plan::default();
p.push(scan("a", 1));
p.push(scan("b", 1));
p.push(join_stage("b"));
let t = p.tree().expect("a tree");
assert!(matches!(t, PlanTree::Binary { .. }), "a join is binary");
assert_eq!(t.children().len(), 2, "two inputs, not one nested in the other");
assert_eq!(t.size(), 3, "join + two scans");
assert_eq!(t.depth(), 2, "the inputs are siblings\n{t:#?}");
for c in t.children() {
assert!(matches!(c, PlanTree::Leaf(Stage::Scan { .. })));
assert_eq!(c.children().len(), 0, "a scan consumes nothing");
}
}
#[test]
fn the_outer_side_is_the_left_child() {
let mut p = Plan::default();
p.push(scan("a", 10));
p.push(scan("b", 5));
p.push(join_stage("b"));
let t = p.tree().unwrap();
let kids = t.children();
assert_eq!(kids[0].stage(), &scan("a", 10), "outer side first");
assert_eq!(kids[1].stage(), &scan("b", 5), "inner side second");
}
#[test]
fn a_chained_join_nests_on_the_left() {
let mut p = Plan::default();
p.push(scan("a", 1));
p.push(scan("b", 1));
p.push(join_stage("b"));
p.push(scan("c", 1));
p.push(join_stage("c"));
let t = p.tree().unwrap();
assert_eq!(t.size(), 5, "3 scans + 2 joins");
assert_eq!(t.depth(), 3, "left-deep: join -> join -> scan");
let kids = t.children();
assert!(matches!(kids[0], PlanTree::Binary { .. }), "outer side is the first join");
assert!(matches!(kids[1], PlanTree::Leaf(_)), "inner side is c");
assert_eq!(kids[0].children().len(), 2);
}
#[test]
fn unary_stages_wrap_the_whole_tree_below_them() {
let mut p = Plan::default();
p.push(scan("a", 100));
p.push(scan("b", 5));
p.push(join_stage("b"));
p.push(Stage::Filter { in_rows: 100, out_rows: 7 });
p.push(Stage::Limit { limit: Some(2), offset: None, in_rows: 7, out_rows: 2 });
let t = p.tree().unwrap();
assert!(matches!(t.stage(), Stage::Limit { .. }), "the last stage is outermost");
assert_eq!(t.children().len(), 1, "a unary stage has one input");
let filter = t.children()[0];
assert!(matches!(filter.stage(), Stage::Filter { .. }));
assert_eq!(filter.children().len(), 1);
let join = filter.children()[0];
assert_eq!(join.children().len(), 2, "and the join below still has two");
assert_eq!(t.size(), 5);
}
#[test]
fn a_plan_with_no_stages_has_no_tree() {
assert_eq!(Plan::default().tree(), None);
}
#[test]
fn the_rendered_depth_agrees_with_the_tree_depth() {
let mut p = Plan::default();
p.push(scan("a", 1));
p.push(scan("b", 1));
p.push(join_stage("b"));
p.push(scan("c", 1));
p.push(join_stage("c"));
let t = p.tree().unwrap();
let lines = p.render();
let plan_lines: Vec<&String> = lines
.iter()
.filter(|l| !l.starts_with("NEDB reports") && !l.starts_with("Row budget"))
.collect();
assert_eq!(plan_lines.len(), t.size(), "every node is rendered once");
let max_indent = plan_lines
.iter()
.map(|l| (l.len() - l.trim_start().len()) / 2)
.max()
.unwrap();
assert_eq!(max_indent + 1, t.depth(), "rendered nesting matches the tree");
}
#[test]
fn joins_and_join_strategy_read_the_same_report() {
let mut p = Plan::default();
p.push(scan("a", 1));
for s in [Strategy::NestedLoop, Strategy::Hash] {
p.push(Stage::Join {
kind: JoinKind::Left,
table: "b".into(),
binding: "b".into(),
strategy: s,
keys: 1,
left_rows: 1,
right_rows: 1,
out_rows: 1,
early_stopped: false,
post_filter_removed: None,
});
}
assert_eq!(p.joins().len(), 2);
assert_eq!(p.join_strategy(0), Some(Strategy::NestedLoop));
assert_eq!(p.join_strategy(1), Some(Strategy::Hash));
assert_eq!(p.join_strategy(2), None);
}
}