1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
// Copyright Materialize, Inc. and contributors. All rights reserved.
//
// Use of this software is governed by the Business Source License
// included in the LICENSE file.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0.
//! Fuses multiple `Join` operators into one `Join` operator.
//!
//! Multiway join planning relies on a broad view of the involved relations,
//! and chains of binary joins can make this challenging to reason about.
//! Collecting multiple joins together with their constraints improves
//! our ability to plan these joins, and reason about other operators' motion
//! around them.
//!
//! Also removes unit collections from joins, and joins with fewer than two inputs.
//!
//! Unit collections have no columns and a count of one, and a join with such
//! a collection act as the identity operator on collections. Once removed,
//! we may find joins with zero or one input, which can be further simplified.
use crate::TransformArgs;
use expr::{MirRelationExpr, MirScalarExpr};
use repr::RelationType;
/// Fuses multiple `Join` operators into one `Join` operator.
///
/// Removes unit collections from joins, and joins with fewer than two inputs.
/// Filters on top of nested joins are lifted so the nested joins can be fused.
#[derive(Debug)]
pub struct Join;
impl crate::Transform for Join {
fn transform(
&self,
relation: &mut MirRelationExpr,
_: TransformArgs,
) -> Result<(), crate::TransformError> {
relation.try_visit_mut_post(&mut |e| Ok(self.action(e)))
}
}
impl Join {
/// Fuses multiple `Join` operators into one `Join` operator.
pub fn action(&self, relation: &mut MirRelationExpr) {
if let MirRelationExpr::Join {
inputs,
equivalences,
..
} = relation
{
let mut join_builder = JoinBuilder::new(equivalences);
// We scan through each input, digesting any joins that we find and updating their equivalence classes.
// We retain any existing equivalence classes, as they are already with respect to the cross product.
for input in inputs.drain(..) {
match input {
MirRelationExpr::Join {
inputs,
equivalences,
..
} => {
// Merge the inputs into the new join being built.
join_builder.add_subjoin(inputs, equivalences, None);
}
MirRelationExpr::Filter { input, predicates } => {
if let MirRelationExpr::Join {
inputs,
equivalences,
..
} = *input
{
// Merge the inputs and the predicates into the new join being built.
join_builder.add_subjoin(inputs, equivalences, Some(predicates));
} else {
// Retain the input.
let input = input.filter(predicates);
join_builder.add_input(input);
}
}
_ => {
// Retain the input.
join_builder.add_input(input);
}
}
}
*relation = join_builder.build();
}
}
}
/// Helper builder for fusing the inputs of nested joins into a single Join expression.
struct JoinBuilder {
inputs: Vec<MirRelationExpr>,
equivalences: Vec<Vec<MirScalarExpr>>,
num_columns: usize,
/// Predicates that will be evaluated on top of the join, if any.
predicates: Vec<MirScalarExpr>,
}
impl JoinBuilder {
fn new(equivalences: &mut Vec<Vec<MirScalarExpr>>) -> Self {
Self {
inputs: Vec::new(),
equivalences: equivalences.drain(..).collect(),
num_columns: 0,
predicates: Vec::new(),
}
}
fn add_input(&mut self, input: MirRelationExpr) {
// Filter join identities out of the inputs.
// The join identity is a single 0-ary row constant expression.
let insert = {
if let MirRelationExpr::Constant {
rows: Ok(rows),
typ,
} = &input
{
!(rows.len() == 1 && typ.column_types.len() == 0 && rows[0].1 == 1)
} else {
true
}
};
if insert {
self.num_columns += input.arity();
self.inputs.push(input);
}
}
fn add_subjoin<I>(
&mut self,
inputs: I,
mut equivalences: Vec<Vec<MirScalarExpr>>,
predicates: Option<Vec<MirScalarExpr>>,
) where
I: IntoIterator<Item = MirRelationExpr>,
{
// Update and push all of the variables.
for mut equivalence in equivalences.drain(..) {
for expr in equivalence.iter_mut() {
expr.visit_mut_post(&mut |e| {
if let MirScalarExpr::Column(c) = e {
*c += self.num_columns;
}
});
}
self.equivalences.push(equivalence);
}
if let Some(mut predicates) = predicates {
for mut expr in predicates.drain(..) {
expr.visit_mut_post(&mut |e| {
if let MirScalarExpr::Column(c) = e {
*c += self.num_columns;
}
});
self.predicates.push(expr);
}
}
// Add all of the inputs.
for input in inputs {
self.add_input(input);
}
}
fn build(mut self) -> MirRelationExpr {
expr::canonicalize::canonicalize_equivalence_classes(&mut self.equivalences);
// If `inputs` is now empty or a singleton (without constraints),
// we can remove the join.
let mut join = match self.inputs.len() {
0 => {
// The identity for join is the collection containing a single 0-ary row.
MirRelationExpr::constant(vec![vec![]], RelationType::empty())
}
1 if self.equivalences.is_empty() => {
// if there are constraints, they probably should have
// been pushed down by predicate pushdown, but .. let's
// not re-write that code here.
self.inputs.pop().unwrap()
}
_ => MirRelationExpr::Join {
inputs: self.inputs,
equivalences: self.equivalences,
implementation: expr::JoinImplementation::Unimplemented,
},
};
if !self.predicates.is_empty() {
join = join.filter(self.predicates);
}
join
}
}