use rudb_common::Result;
use rudb_plan::{BuildSide, ExprRef, JoinKind, Node, NodeRef, Plan};
use crate::estimate::{self, Facts, Side};
use crate::pass::{Context, Pass};
use crate::tables::{TableSet, Tables, produced};
use crate::walk;
#[derive(Debug, Clone, Copy)]
pub struct JoinOrder;
impl Pass for JoinOrder {
fn name(&self) -> &'static str {
"join_order"
}
fn run(&self, plan: &mut Plan, context: &Context) -> Result<()> {
reorder(plan, context.facts());
Ok(())
}
}
pub fn reorder(plan: &mut Plan, stats: &Facts) {
let mut tables = Tables::new();
let root = rebuild(plan, plan.root(), &mut tables, stats);
plan.set_root(root);
}
struct Part {
build: usize,
tables: TableSet,
side: Side,
}
enum Build {
Leaf(NodeRef),
Pair { left: usize, right: usize, conditions: Vec<ExprRef> },
}
fn rebuild(plan: &mut Plan, at: NodeRef, tables: &mut Tables, stats: &Facts) -> NodeRef {
if joining(plan, at) {
let mut leaves = Vec::new();
let mut conditions = Vec::new();
gather(plan, at, &mut leaves, &mut conditions);
let rebuilt: Vec<NodeRef> =
leaves.iter().map(|&leaf| rebuild(plan, leaf, tables, stats)).collect();
let chosen = match leaves.len() {
0..=2 => None,
_ => order(plan, at, &rebuilt, &conditions, tables, stats),
};
if let Some(chosen) = chosen {
return chosen;
}
if rebuilt == leaves {
return at;
}
return restack(plan, at, &leaves, &rebuilt);
}
let children: Vec<NodeRef> = plan.node(at).children().into_iter().flatten().collect();
let rebuilt: Vec<NodeRef> =
children.iter().map(|&child| rebuild(plan, child, tables, stats)).collect();
if rebuilt == children {
return at;
}
let mut node = plan.node(at).clone();
walk::replace_children(&mut node, &rebuilt);
plan.add_node(node)
}
fn joining(plan: &Plan, at: NodeRef) -> bool {
matches!(*plan.node(at), Node::CrossProduct { .. } | Node::Join { kind: JoinKind::Inner, .. })
}
fn gather(plan: &Plan, at: NodeRef, leaves: &mut Vec<NodeRef>, conditions: &mut Vec<ExprRef>) {
match *plan.node(at) {
Node::CrossProduct { left, right } => {
gather(plan, left, leaves, conditions);
gather(plan, right, leaves, conditions);
}
Node::Join { left, right, kind: JoinKind::Inner, conditions: list, .. } => {
conditions.extend_from_slice(plan.expr_list(list));
gather(plan, left, leaves, conditions);
gather(plan, right, leaves, conditions);
}
_ => leaves.push(at),
}
}
fn restack(plan: &mut Plan, at: NodeRef, leaves: &[NodeRef], rebuilt: &[NodeRef]) -> NodeRef {
if let Some(found) = leaves.iter().position(|&leaf| leaf == at) {
return rebuilt[found];
}
let children: Vec<NodeRef> = plan.node(at).children().into_iter().flatten().collect();
let children: Vec<NodeRef> =
children.into_iter().map(|child| restack(plan, child, leaves, rebuilt)).collect();
let mut node = plan.node(at).clone();
walk::replace_children(&mut node, &children);
plan.add_node(node)
}
fn order(
plan: &mut Plan,
at: NodeRef,
leaves: &[NodeRef],
conditions: &[ExprRef],
tables: &mut Tables,
stats: &Facts,
) -> Option<NodeRef> {
let mut builds: Vec<Build> = leaves.iter().map(|&leaf| Build::Leaf(leaf)).collect();
let mut parts = Vec::with_capacity(leaves.len());
for (build, &leaf) in leaves.iter().enumerate() {
parts.push(Part {
build,
tables: produced(plan, leaf),
side: estimate::side(plan, leaf, stats)?,
});
}
let mut whole = TableSet::new();
for part in &parts {
whole.extend(&part.tables);
}
let mut pending: Vec<(ExprRef, TableSet)> = Vec::with_capacity(conditions.len());
for &condition in conditions {
let reads = tables.of(plan, condition);
if !reads.is_subset_of(&whole) || parts.iter().any(|part| reads.is_subset_of(&part.tables))
{
return None;
}
pending.push((condition, reads));
}
let (_, before, was) = cost(plan, at, stats)?;
let mut after = 0u64;
let mut built = 0usize;
while parts.len() > 1 {
let (left, right, side) = cheapest(plan, &parts, &pending, stats);
let mut union = parts[left].tables.clone();
union.extend(&parts[right].tables);
let conditions: Vec<ExprRef> = pending
.iter()
.filter(|(_, reads)| reads.is_subset_of(&union))
.map(|(condition, _)| *condition)
.collect();
pending.retain(|(_, reads)| !reads.is_subset_of(&union));
let right = parts.remove(right);
let left = parts.remove(left);
built += usize::from(conditions.is_empty());
builds.push(Build::Pair { left: left.build, right: right.build, conditions });
after = after.saturating_add(side.rows);
parts.push(Part { build: builds.len() - 1, tables: union, side });
}
if built > was || after >= before {
return None;
}
Some(put(plan, &builds, parts[0].build))
}
fn put(plan: &mut Plan, builds: &[Build], at: usize) -> NodeRef {
match &builds[at] {
Build::Leaf(node) => *node,
Build::Pair { left, right, conditions } => {
let conditions = conditions.clone();
let left = put(plan, builds, *left);
let right = put(plan, builds, *right);
if conditions.is_empty() {
return plan.add_node(Node::CrossProduct { left, right });
}
let conditions = plan.add_expr_list(&conditions);
plan.add_node(Node::Join {
left,
right,
kind: JoinKind::Inner,
conditions,
build: BuildSide::default(),
})
}
}
}
fn cheapest(
plan: &Plan,
parts: &[Part],
pending: &[(ExprRef, TableSet)],
stats: &Facts,
) -> (usize, usize, Side) {
let mut best: Option<Pick> = None;
for left in 0..parts.len() {
for right in left + 1..parts.len() {
let mut union = parts[left].tables.clone();
union.extend(&parts[right].tables);
let testable: Vec<ExprRef> = pending
.iter()
.filter(|(_, reads)| reads.is_subset_of(&union))
.map(|(condition, _)| *condition)
.collect();
let linked = !testable.is_empty();
let (this, that) = (parts[left].side, parts[right].side);
let side = if linked {
let keys = estimate::keyspace_of(plan, &testable, stats);
estimate::matched_sides(this, that, keys)
} else {
Side {
rows: this.rows.saturating_mul(that.rows),
base: this.base.saturating_mul(that.base),
}
};
let order = (!linked, side.rows, this.rows.saturating_add(that.rows));
if best.is_none_or(|held| order < held.order) {
best = Some(Pick { left, right, side, order });
}
}
}
let best = best.expect("a region has at least two parts");
(best.left, best.right, best.side)
}
#[derive(Clone, Copy)]
struct Pick {
left: usize,
right: usize,
side: Side,
order: (bool, u64, u64),
}
fn cost(plan: &Plan, at: NodeRef, stats: &Facts) -> Option<(Side, u64, usize)> {
let (left, right, testable) = match *plan.node(at) {
Node::CrossProduct { left, right } => (left, right, Vec::new()),
Node::Join { left, right, kind: JoinKind::Inner, conditions, .. } => {
(left, right, plan.expr_list(conditions).to_vec())
}
_ => return Some((estimate::side(plan, at, stats)?, 0, 0)),
};
let linked = !testable.is_empty();
let (left, under_left, crossed_left) = cost(plan, left, stats)?;
let (right, under_right, crossed_right) = cost(plan, right, stats)?;
let side = if linked {
let keys = estimate::keyspace_of(plan, &testable, stats);
estimate::matched_sides(left, right, keys)
} else {
Side {
rows: left.rows.saturating_mul(right.rows),
base: left.base.saturating_mul(right.base),
}
};
Some((
side,
under_left.saturating_add(under_right).saturating_add(side.rows),
crossed_left + crossed_right + usize::from(!linked),
))
}
#[cfg(test)]
mod tests {
use rudb_common::stat::Provenance;
use rudb_plan::Plan;
use crate::estimate::Facts;
use super::reorder;
fn ordered(text: &str) -> String {
let mut counts = Facts::new();
for (table, rows) in [("t", 1000), ("u", 10), ("v", 100), ("w", 100_000)] {
counts.record("memory", "main", table, rows);
}
let mut plan =
Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
reorder(&mut plan, &counts);
plan.validate().unwrap_or_else(|error| panic!("{text} did not stay valid: {error}"));
plan.to_string()
}
fn counted(text: &str, columns: &[(&str, &str, u64)]) -> String {
let mut counts = Facts::new();
for (table, rows) in [("t", 1000), ("u", 10), ("v", 100), ("w", 100_000)] {
counts.record("memory", "main", table, rows);
}
for (table, column, distinct) in columns {
counts.record_distinct(
"memory",
"main",
table,
column,
*distinct,
Provenance::Dictionary,
);
}
let mut plan =
Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
reorder(&mut plan, &counts);
plan.validate().unwrap_or_else(|error| panic!("{text} did not stay valid: {error}"));
plan.to_string()
}
#[test]
fn a_cross_product_the_conditions_can_avoid_is_not_built() {
assert_eq!(
ordered(concat!(
"Join INNER on=[(#1.0::BIGINT = #0.0::BIGINT)::BOOLEAN, ",
"(#1.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" CrossProduct\n",
" Get memory.main.t AS t #0 [a::BIGINT]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
" Get memory.main.u AS u #1 [b::BIGINT]\n",
)),
concat!(
"Join INNER on=[(#1.0::BIGINT = #0.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.t AS t #0 [a::BIGINT]\n",
" Join INNER on=[(#1.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
" Get memory.main.u AS u #1 [b::BIGINT]\n",
)
);
}
#[test]
fn an_order_the_search_does_not_improve_on_is_left_exactly_as_it_was() {
let text = concat!(
"Join INNER on=[(#3.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.w AS w #3 [d::BIGINT]\n",
" Join INNER on=[(#1.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.u AS u #1 [b::BIGINT]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
);
assert_eq!(ordered(text), text);
}
#[test]
fn a_region_with_a_leaf_nobody_counted_is_left_alone() {
let text = concat!(
"Join INNER on=[(#1.0::BIGINT = #0.0::BIGINT)::BOOLEAN, ",
"(#1.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" CrossProduct\n",
" Get memory.main.t AS t #0 [a::BIGINT]\n",
" Get memory.main.x AS x #2 [c::BIGINT]\n",
" Get memory.main.u AS u #1 [b::BIGINT]\n",
);
assert_eq!(ordered(text), text);
}
#[test]
fn a_condition_that_reads_one_leaf_stops_the_search() {
let text = concat!(
"Join INNER on=[(#1.0::BIGINT = #0.0::BIGINT)::BOOLEAN, ",
"(#2.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" CrossProduct\n",
" Get memory.main.t AS t #0 [a::BIGINT]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
" Get memory.main.u AS u #1 [b::BIGINT]\n",
);
assert_eq!(ordered(text), text);
}
#[test]
fn an_outer_join_is_not_part_of_a_region() {
let text = concat!(
"Join INNER on=[(#1.0::BIGINT = #0.0::BIGINT)::BOOLEAN]\n",
" Join LEFT on=[(#0.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.t AS t #0 [a::BIGINT]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
" Get memory.main.u AS u #1 [b::BIGINT]\n",
);
assert_eq!(ordered(text), text);
}
#[test]
fn two_small_parts_with_no_condition_between_them_are_not_joined_to_each_other() {
assert_eq!(
ordered(concat!(
"Join INNER on=[(#0.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" Join INNER on=[(#3.0::BIGINT = #0.0::BIGINT)::BOOLEAN, ",
"(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" CrossProduct\n",
" Get memory.main.w AS w #3 [d::BIGINT]\n",
" Get memory.main.u AS u #1 [b::BIGINT]\n",
" Get memory.main.t AS t #0 [a::BIGINT]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
)),
concat!(
"Join INNER on=[(#3.0::BIGINT = #0.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.w AS w #3 [d::BIGINT]\n",
" Join INNER on=[(#0.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
" Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.u AS u #1 [b::BIGINT]\n",
" Get memory.main.t AS t #0 [a::BIGINT]\n",
)
);
}
#[test]
fn a_region_with_no_path_between_any_of_it_still_builds_the_smallest_middle() {
assert_eq!(
ordered(concat!(
"CrossProduct\n",
" CrossProduct\n",
" Get memory.main.w AS w #3 [d::BIGINT]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
" Get memory.main.u AS u #1 [b::BIGINT]\n",
)),
concat!(
"CrossProduct\n",
" Get memory.main.w AS w #3 [d::BIGINT]\n",
" CrossProduct\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
" Get memory.main.u AS u #1 [b::BIGINT]\n",
)
);
}
#[test]
fn an_order_the_search_reaches_and_does_not_beat_leaves_the_region_alone() {
let text = concat!(
"CrossProduct\n",
" Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.t AS t #0 [a::BIGINT]\n",
" Get memory.main.u AS u #1 [b::BIGINT]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
);
assert_eq!(ordered(text), text);
}
#[test]
fn a_join_on_a_column_with_two_values_in_it_is_left_until_the_sides_have_been_cut_down() {
let text = concat!(
"Join INNER on=[(#0.0::BIGINT = #3.1::BIGINT)::BOOLEAN]\n",
" Join INNER on=[(#1.0::BIGINT = #3.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.u AS u #1 [b::BIGINT]\n",
" Get memory.main.w AS w #3 [d::BIGINT, e::BIGINT]\n",
" Get memory.main.t AS t #0 [a::BIGINT]\n",
);
assert_eq!(ordered(text), text);
assert_eq!(
counted(text, &[("u", "b", 2), ("w", "d", 2), ("t", "a", 1000), ("w", "e", 100_000)]),
concat!(
"Join INNER on=[(#1.0::BIGINT = #3.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.u AS u #1 [b::BIGINT]\n",
" Join INNER on=[(#0.0::BIGINT = #3.1::BIGINT)::BOOLEAN]\n",
" Get memory.main.w AS w #3 [d::BIGINT, e::BIGINT]\n",
" Get memory.main.t AS t #0 [a::BIGINT]\n",
)
);
}
#[test]
fn a_region_under_something_else_is_reordered_and_what_is_above_it_is_rebuilt() {
assert_eq!(
ordered(concat!(
"Project #4 [#0.0::BIGINT AS a]\n",
" Join INNER on=[(#1.0::BIGINT = #0.0::BIGINT)::BOOLEAN, ",
"(#1.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" CrossProduct\n",
" Get memory.main.t AS t #0 [a::BIGINT]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
" Get memory.main.u AS u #1 [b::BIGINT]\n",
)),
concat!(
"Project #4 [#0.0::BIGINT AS a]\n",
" Join INNER on=[(#1.0::BIGINT = #0.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.t AS t #0 [a::BIGINT]\n",
" Join INNER on=[(#1.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
" Get memory.main.u AS u #1 [b::BIGINT]\n",
)
);
}
#[test]
fn the_join_that_removes_rows_runs_before_the_join_that_removes_none() {
assert_eq!(
ordered(concat!(
"Join INNER on=[(#0.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.w AS w #0 [d::BIGINT]\n",
" Get memory.main.u AS u #1 [b::BIGINT]\n",
" Filter (#2.0::BIGINT = 3::BIGINT)::BOOLEAN\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
)),
concat!(
"Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.u AS u #1 [b::BIGINT]\n",
" Join INNER on=[(#0.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.w AS w #0 [d::BIGINT]\n",
" Filter (#2.0::BIGINT = 3::BIGINT)::BOOLEAN\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
)
);
}
#[test]
fn with_nothing_filtering_either_side_the_same_region_is_left_as_it_was() {
let text = concat!(
"Join INNER on=[(#0.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.w AS w #0 [d::BIGINT]\n",
" Get memory.main.u AS u #1 [b::BIGINT]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
);
assert_eq!(ordered(text), text);
}
}