1use crate::sqljoin::Strategy;
48use crate::sqlselect::JoinKind;
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub enum Stage {
53 Scan {
55 table: String,
56 binding: String,
60 rows: usize,
61 },
62 Join {
64 kind: JoinKind,
65 table: String,
66 binding: String,
67 strategy: Strategy,
68 keys: usize,
72 left_rows: usize,
73 right_rows: usize,
74 out_rows: usize,
75 early_stopped: bool,
77 post_filter_removed: Option<usize>,
86 },
87 Filter { in_rows: usize, out_rows: usize },
89 Project { columns: usize, out_rows: usize },
91 Distinct { in_rows: usize, out_rows: usize },
93 Sort { keys: usize, rows: usize },
95 Prefilter {
100 binding: String,
101 predicates: usize,
102 in_rows: usize,
103 out_rows: usize,
104 },
105 Limit {
107 limit: Option<usize>,
108 offset: Option<usize>,
109 in_rows: usize,
110 out_rows: usize,
111 },
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
132pub enum PlanTree {
133 Leaf(Stage),
134 Unary { stage: Stage, input: Box<PlanTree> },
135 Binary { stage: Stage, left: Box<PlanTree>, right: Box<PlanTree> },
136}
137
138impl PlanTree {
139 pub fn stage(&self) -> &Stage {
140 match self {
141 PlanTree::Leaf(s) => s,
142 PlanTree::Unary { stage, .. } => stage,
143 PlanTree::Binary { stage, .. } => stage,
144 }
145 }
146
147 pub fn children(&self) -> Vec<&PlanTree> {
149 match self {
150 PlanTree::Leaf(_) => vec![],
151 PlanTree::Unary { input, .. } => vec![input],
152 PlanTree::Binary { left, right, .. } => vec![left, right],
153 }
154 }
155
156 pub fn size(&self) -> usize {
158 1 + self.children().iter().map(|c| c.size()).sum::<usize>()
159 }
160
161 pub fn depth(&self) -> usize {
165 1 + self.children().iter().map(|c| c.depth()).max().unwrap_or(0)
166 }
167}
168
169#[derive(Debug, Clone, Default, PartialEq, Eq)]
171pub struct Plan {
172 pub stages: Vec<Stage>,
173 pub budget: Option<usize>,
175 pub refusals: Vec<String>,
179}
180
181impl Plan {
182 pub fn push(&mut self, s: Stage) {
183 self.stages.push(s);
184 }
185
186 pub fn joins(&self) -> Vec<&Stage> {
191 self.stages
192 .iter()
193 .filter(|s| matches!(s, Stage::Join { .. }))
194 .collect()
195 }
196
197 pub fn join_strategy(&self, n: usize) -> Option<Strategy> {
199 match self.joins().get(n) {
200 Some(Stage::Join { strategy, .. }) => Some(*strategy),
201 _ => None,
202 }
203 }
204
205 pub fn join_strategies(&self) -> Vec<Strategy> {
207 self.stages
208 .iter()
209 .filter_map(|s| match s {
210 Stage::Join { strategy, .. } => Some(*strategy),
211 _ => None,
212 })
213 .collect()
214 }
215
216 pub fn join_keys(&self, n: usize) -> Option<usize> {
218 match self.joins().get(n) {
219 Some(Stage::Join { keys, .. }) => Some(*keys),
220 _ => None,
221 }
222 }
223
224 pub fn tree(&self) -> Option<PlanTree> {
230 let mut it = self.stages.iter();
231 let mut node = PlanTree::Leaf(it.next()?.clone());
232 let rest: Vec<&Stage> = it.collect();
233 let mut i = 0usize;
234 while i < rest.len() {
235 let right_len = match (rest.get(i), rest.get(i + 1), rest.get(i + 2)) {
241 (Some(Stage::Scan { .. }), Some(Stage::Join { .. }), _) => Some(1),
242 (
243 Some(Stage::Scan { .. }),
244 Some(Stage::Prefilter { .. }),
245 Some(Stage::Join { .. }),
246 ) => Some(2),
247 _ => None,
248 };
249 if let Some(n) = right_len {
250 let mut right = PlanTree::Leaf(rest[i].clone());
251 if n == 2 {
252 right = PlanTree::Unary {
253 stage: rest[i + 1].clone(),
254 input: Box::new(right),
255 };
256 }
257 node = PlanTree::Binary {
258 stage: rest[i + n].clone(),
259 left: Box::new(node),
260 right: Box::new(right),
261 };
262 i += n + 1;
263 } else {
264 node = PlanTree::Unary {
265 stage: rest[i].clone(),
266 input: Box::new(node),
267 };
268 i += 1;
269 }
270 }
271 Some(node)
272 }
273
274 pub fn render(&self) -> Vec<String> {
281 let mut out = vec![];
282 if let Some(t) = self.tree() {
283 render_node(&t, 0, &mut out);
284 }
285
286 for r in &self.refusals {
287 out.push(r.clone());
288 }
289 if let Some(b) = self.budget {
290 out.push(format!(
291 "Row budget: {b} — the join was allowed to stop once this many \
292 rows existed"
293 ));
294 }
295 out.push(
296 "NEDB reports ACTUAL rows, never estimates: it has no statistics to \
297 estimate from, and a guess printed as a number is worse than the truth."
298 .to_string(),
299 );
300 out
301 }
302}
303
304fn render_node(n: &PlanTree, depth: usize, out: &mut Vec<String>) {
313 let indent = " ".repeat(depth);
314 let arrow = if depth == 0 { String::new() } else { format!("{indent}-> ") };
315 let line = match n.stage() {
316 Stage::Scan { table, binding, rows } => {
317 format!("{arrow}Seq Scan on {} (actual rows={rows})", named(table, binding))
318 }
319 Stage::Join {
320 kind,
321 table,
322 binding,
323 strategy,
324 keys,
325 left_rows,
326 right_rows,
327 out_rows,
328 early_stopped,
329 post_filter_removed,
330 } => {
331 let k = match keys {
332 0 => "no equality key".to_string(),
333 1 => "1 hash key".to_string(),
334 n => format!("{n} hash keys"),
335 };
336 let stop = if *early_stopped { ", stopped early" } else { "" };
337 let filt = match post_filter_removed {
338 Some(n) => format!(", post-join filter removed {n}"),
339 None => String::new(),
340 };
341 format!(
342 "{arrow}{strategy} {} Join on {} \
343 ({k}, left={left_rows}, right={right_rows}{stop}{filt}) \
344 (actual rows={out_rows})",
345 kind_name(*kind),
346 named(table, binding)
347 )
348 }
349 Stage::Filter { in_rows, out_rows } => format!(
350 "{arrow}Filter (removed {}) (actual rows={out_rows})",
351 in_rows.saturating_sub(*out_rows)
352 ),
353 Stage::Project { columns, out_rows } => {
354 format!("{arrow}Project ({columns} columns) (actual rows={out_rows})")
355 }
356 Stage::Distinct { in_rows, out_rows } => format!(
357 "{arrow}Unique (removed {}) (actual rows={out_rows})",
358 in_rows.saturating_sub(*out_rows)
359 ),
360 Stage::Sort { keys, rows } => {
361 format!("{arrow}Sort ({keys} key(s)) (actual rows={rows})")
362 }
363 Stage::Prefilter { binding, predicates, in_rows, out_rows } => format!(
364 "{arrow}Prefilter on {binding} ({predicates} pushed, removed {}) \
365 (actual rows={out_rows})",
366 in_rows.saturating_sub(*out_rows)
367 ),
368 Stage::Limit { limit, offset, in_rows, out_rows } => {
369 let l = limit.map(|n| n.to_string()).unwrap_or_else(|| "ALL".into());
370 let o = offset.map(|n| format!(", offset {n}")).unwrap_or_default();
371 format!("{arrow}Limit ({l}{o}, from {in_rows}) (actual rows={out_rows})")
372 }
373 };
374 out.push(line);
375 for c in n.children() {
376 render_node(c, depth + 1, out);
377 }
378}
379
380fn named(table: &str, binding: &str) -> String {
382 if table == binding {
383 table.to_string()
384 } else {
385 format!("{table} {binding}")
386 }
387}
388
389fn kind_name(k: JoinKind) -> &'static str {
390 match k {
391 JoinKind::Inner => "Inner",
392 JoinKind::Left => "Left",
393 JoinKind::Right => "Right",
394 JoinKind::Full => "Full",
395 JoinKind::Cross => "Cross",
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 fn scan(t: &str, rows: usize) -> Stage {
404 Stage::Scan { table: t.into(), binding: t.into(), rows }
405 }
406
407 #[test]
408 fn an_empty_plan_still_explains_itself() {
409 let p = Plan::default();
410 let r = p.render();
411 assert_eq!(r.len(), 1);
412 assert!(r[0].contains("ACTUAL rows"));
413 }
414
415 #[test]
416 fn a_scan_renders_with_actual_rows() {
417 let mut p = Plan::default();
418 p.push(scan("orders", 1000));
419 let r = p.render();
420 assert!(r[0].starts_with("Seq Scan on orders"), "{:?}", r[0]);
421 assert!(r[0].contains("actual rows=1000"));
422 }
423
424 #[test]
425 fn an_alias_is_shown_but_a_redundant_one_is_not() {
426 let mut p = Plan::default();
427 p.push(Stage::Scan { table: "orders".into(), binding: "o".into(), rows: 1 });
428 assert!(p.render()[0].contains("orders o"));
429
430 let mut p = Plan::default();
431 p.push(scan("orders", 1));
432 assert!(!p.render()[0].contains("orders orders"));
433 }
434
435 #[test]
436 fn the_outermost_stage_is_printed_first() {
437 let mut p = Plan::default();
438 p.push(scan("orders", 100));
439 p.push(Stage::Limit { limit: Some(5), offset: None, in_rows: 100, out_rows: 5 });
440 let r = p.render();
441 assert!(r[0].starts_with("Limit"), "{r:?}");
442 assert!(r[1].contains("Seq Scan"), "{r:?}");
443 assert!(r[1].starts_with(" -> "), "{:?}", r[1]);
445 }
446
447 #[test]
448 fn a_join_names_its_strategy_and_key_count() {
449 let mut p = Plan::default();
450 p.push(scan("orders", 1000));
451 p.push(Stage::Join {
452 kind: JoinKind::Inner,
453 table: "customers".into(),
454 binding: "c".into(),
455 strategy: Strategy::Hash,
456 keys: 2,
457 left_rows: 1000,
458 right_rows: 500,
459 out_rows: 1922,
460 early_stopped: false,
461 post_filter_removed: None,
462 });
463 let r = p.render();
464 assert!(r[0].contains("Hash Join"), "{:?}", r[0]);
465 assert!(r[0].contains("Inner"));
466 assert!(r[0].contains("2 hash keys"));
467 assert!(r[0].contains("customers c"));
468 assert!(r[0].contains("actual rows=1922"));
469 }
470
471 #[test]
472 fn a_join_with_no_key_says_so_because_that_is_why_it_is_slow() {
473 let mut p = Plan::default();
474 p.push(Stage::Join {
475 kind: JoinKind::Inner,
476 table: "customers".into(),
477 binding: "customers".into(),
478 strategy: Strategy::NestedLoop,
479 keys: 0,
480 left_rows: 1000,
481 right_rows: 500,
482 out_rows: 2500,
483 early_stopped: false,
484 post_filter_removed: None,
485 });
486 let r = p.render();
487 assert!(r[0].contains("Nested Loop"));
488 assert!(r[0].contains("no equality key"), "{:?}", r[0]);
489 }
490
491 #[test]
492 fn early_termination_is_visible() {
493 let mut p = Plan::default();
494 p.push(Stage::Join {
495 kind: JoinKind::Inner,
496 table: "c".into(),
497 binding: "c".into(),
498 strategy: Strategy::Hash,
499 keys: 1,
500 left_rows: 1000,
501 right_rows: 500,
502 out_rows: 20,
503 early_stopped: true,
504 post_filter_removed: None,
505 });
506 p.budget = Some(20);
507 let r = p.render();
508 assert!(r[0].contains("stopped early"), "{:?}", r[0]);
509 assert!(r.iter().any(|l| l.contains("Row budget: 20")));
510 }
511
512 #[test]
513 fn filter_and_unique_report_what_they_removed() {
514 let mut p = Plan::default();
515 p.push(Stage::Filter { in_rows: 1000, out_rows: 117 });
516 p.push(Stage::Distinct { in_rows: 117, out_rows: 4 });
517 let r = p.render();
518 assert!(r.iter().any(|l| l.contains("Unique") && l.contains("removed 113")), "{r:?}");
519 assert!(r.iter().any(|l| l.contains("Filter") && l.contains("removed 883")), "{r:?}");
520 }
521
522 #[test]
523 fn a_joins_two_inputs_are_siblings_not_nested() {
524 let mut p = Plan::default();
528 p.push(scan("orders", 10));
529 p.push(scan("customers", 5));
530 p.push(Stage::Join {
531 kind: JoinKind::Inner,
532 table: "customers".into(),
533 binding: "customers".into(),
534 strategy: Strategy::Hash,
535 keys: 1,
536 left_rows: 10,
537 right_rows: 5,
538 out_rows: 7,
539 early_stopped: false,
540 post_filter_removed: None,
541 });
542 let r = p.render();
543 assert!(r[0].contains("Hash Join"), "{r:?}");
544 let orders = r.iter().find(|l| l.contains("orders")).expect("orders scanned");
545 let custs = r.iter().find(|l| l.contains("customers (actual")).expect("customers");
546 let depth = |l: &str| l.len() - l.trim_start().len();
547 assert_eq!(
548 depth(orders), depth(custs),
549 "the two inputs of a join must be at the same depth\n{r:#?}"
550 );
551 assert!(depth(orders) > depth(&r[0]), "both are nested under the join");
552 }
553
554 fn join_stage(table: &str) -> Stage {
557 Stage::Join {
558 kind: JoinKind::Inner,
559 table: table.into(),
560 binding: table.into(),
561 strategy: Strategy::Hash,
562 keys: 1,
563 left_rows: 1,
564 right_rows: 1,
565 out_rows: 1,
566 early_stopped: false,
567 post_filter_removed: None,
568 }
569 }
570
571 #[test]
572 fn a_join_node_has_exactly_two_children() {
573 let mut p = Plan::default();
578 p.push(scan("a", 1));
579 p.push(scan("b", 1));
580 p.push(join_stage("b"));
581
582 let t = p.tree().expect("a tree");
583 assert!(matches!(t, PlanTree::Binary { .. }), "a join is binary");
584 assert_eq!(t.children().len(), 2, "two inputs, not one nested in the other");
585 assert_eq!(t.size(), 3, "join + two scans");
586 assert_eq!(t.depth(), 2, "the inputs are siblings\n{t:#?}");
588 for c in t.children() {
589 assert!(matches!(c, PlanTree::Leaf(Stage::Scan { .. })));
590 assert_eq!(c.children().len(), 0, "a scan consumes nothing");
591 }
592 }
593
594 #[test]
595 fn the_outer_side_is_the_left_child() {
596 let mut p = Plan::default();
600 p.push(scan("a", 10));
601 p.push(scan("b", 5));
602 p.push(join_stage("b"));
603 let t = p.tree().unwrap();
604 let kids = t.children();
605 assert_eq!(kids[0].stage(), &scan("a", 10), "outer side first");
606 assert_eq!(kids[1].stage(), &scan("b", 5), "inner side second");
607 }
608
609 #[test]
610 fn a_chained_join_nests_on_the_left() {
611 let mut p = Plan::default();
614 p.push(scan("a", 1));
615 p.push(scan("b", 1));
616 p.push(join_stage("b"));
617 p.push(scan("c", 1));
618 p.push(join_stage("c"));
619
620 let t = p.tree().unwrap();
621 assert_eq!(t.size(), 5, "3 scans + 2 joins");
622 assert_eq!(t.depth(), 3, "left-deep: join -> join -> scan");
623 let kids = t.children();
624 assert!(matches!(kids[0], PlanTree::Binary { .. }), "outer side is the first join");
625 assert!(matches!(kids[1], PlanTree::Leaf(_)), "inner side is c");
626 assert_eq!(kids[0].children().len(), 2);
627 }
628
629 #[test]
630 fn unary_stages_wrap_the_whole_tree_below_them() {
631 let mut p = Plan::default();
632 p.push(scan("a", 100));
633 p.push(scan("b", 5));
634 p.push(join_stage("b"));
635 p.push(Stage::Filter { in_rows: 100, out_rows: 7 });
636 p.push(Stage::Limit { limit: Some(2), offset: None, in_rows: 7, out_rows: 2 });
637
638 let t = p.tree().unwrap();
639 assert!(matches!(t.stage(), Stage::Limit { .. }), "the last stage is outermost");
640 assert_eq!(t.children().len(), 1, "a unary stage has one input");
641 let filter = t.children()[0];
642 assert!(matches!(filter.stage(), Stage::Filter { .. }));
643 assert_eq!(filter.children().len(), 1);
644 let join = filter.children()[0];
645 assert_eq!(join.children().len(), 2, "and the join below still has two");
646 assert_eq!(t.size(), 5);
647 }
648
649 #[test]
650 fn a_plan_with_no_stages_has_no_tree() {
651 assert_eq!(Plan::default().tree(), None);
653 }
654
655 #[test]
656 fn the_rendered_depth_agrees_with_the_tree_depth() {
657 let mut p = Plan::default();
660 p.push(scan("a", 1));
661 p.push(scan("b", 1));
662 p.push(join_stage("b"));
663 p.push(scan("c", 1));
664 p.push(join_stage("c"));
665 let t = p.tree().unwrap();
666
667 let lines = p.render();
668 let plan_lines: Vec<&String> = lines
669 .iter()
670 .filter(|l| !l.starts_with("NEDB reports") && !l.starts_with("Row budget"))
671 .collect();
672 assert_eq!(plan_lines.len(), t.size(), "every node is rendered once");
673
674 let max_indent = plan_lines
675 .iter()
676 .map(|l| (l.len() - l.trim_start().len()) / 2)
677 .max()
678 .unwrap();
679 assert_eq!(max_indent + 1, t.depth(), "rendered nesting matches the tree");
680 }
681
682 #[test]
683 fn joins_and_join_strategy_read_the_same_report() {
684 let mut p = Plan::default();
685 p.push(scan("a", 1));
686 for s in [Strategy::NestedLoop, Strategy::Hash] {
687 p.push(Stage::Join {
688 kind: JoinKind::Left,
689 table: "b".into(),
690 binding: "b".into(),
691 strategy: s,
692 keys: 1,
693 left_rows: 1,
694 right_rows: 1,
695 out_rows: 1,
696 early_stopped: false,
697 post_filter_removed: None,
698 });
699 }
700 assert_eq!(p.joins().len(), 2);
701 assert_eq!(p.join_strategy(0), Some(Strategy::NestedLoop));
702 assert_eq!(p.join_strategy(1), Some(Strategy::Hash));
703 assert_eq!(p.join_strategy(2), None);
704 }
705}