Skip to content

Commit

Permalink
query_graph: Classify join preds for LEFT JOINs
Browse files Browse the repository at this point in the history
Unlike inner joins, non-join-key predicates in left joins can't just be
considered as part of other query predicates in the WHERE clause -
semantically, those predicates need to be executed either above the
join, or as part of the join itself - specifically so that rows
which *don't* satisfy those predicates get NULLs for all columns from
the right-hand side of the join.

In the real world, many queries have "local" predicates in the ON clause
of the join - predicates which mention only columns on one side of the
join. These predicates can be compiled as regular filter nodes just
above the join on either side, without having to modify the left join
operator in dataflow to execute arbitrary filter expressions during join
clause evaluation.

To pave the way to compiling these queries, this commit classifies
the (non join-predicate) conditions in the ON clause of LEFT JOINs as
either local to the left, local to the right, global, or parameters.
This still returns an error whenever these lists are non-empty, but
coming up we can compile left-local and right-local preds as filter
nodes during MIR lowering.

Change-Id: I55f91a8172b1611fb636470a0f365942901c4e05
Reviewed-on: https://gerrit.readyset.name/c/readyset/+/6224
Tested-by: Buildkite CI
Reviewed-by: Luke Osborne <luke@readyset.io>
  • Loading branch information
glittershark committed Oct 20, 2023
1 parent b254e94 commit 9fa3923
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 7 deletions.
16 changes: 13 additions & 3 deletions readyset-server/src/controller/sql/mir/join.rs
Expand Up @@ -47,9 +47,19 @@ pub(super) fn make_joins(
for jref in qg.join_order.iter() {
let (mut join_kind, jps) = match &qg.edges[&(jref.src.clone(), jref.dst.clone())] {
QueryGraphEdge::Join { on } => (JoinKind::Inner, on),
QueryGraphEdge::LeftJoin { on, extra_preds } => {
if !extra_preds.is_empty() {
unsupported!("Non-equal predicates not (yet) supported in left joins");
QueryGraphEdge::LeftJoin {
on,
left_local_preds,
right_local_preds,
global_preds,
params,
} => {
if !(left_local_preds.is_empty()
&& right_local_preds.is_empty()
&& global_preds.is_empty()
&& params.is_empty())
{
unsupported!("Non equal-join predicates not (yet) supported in left joins");
}
(JoinKind::Left, on)
}
Expand Down
82 changes: 80 additions & 2 deletions readyset-server/src/controller/sql/query_graph.rs
Expand Up @@ -219,7 +219,14 @@ pub enum QueryGraphEdge {
},
LeftJoin {
on: Vec<JoinPredicate>,
extra_preds: Vec<Expr>,
/// Predicates which are local to the left-hand side of the left join.
left_local_preds: Vec<Expr>,
/// Predicates which are local to the right-hand side of the left join.
right_local_preds: Vec<Expr>,
/// Global predicates mentioned in the ON clause of the join
global_preds: Vec<Expr>,
/// Parameters mentioned in the ON clause of the join
params: Vec<Parameter>,
},
}

Expand Down Expand Up @@ -1083,7 +1090,32 @@ pub fn to_query_graph(stmt: SelectStatement) -> ReadySetResult<QueryGraph> {
{
e.insert(match jc.operator {
JoinOperator::LeftJoin | JoinOperator::LeftOuterJoin => {
QueryGraphEdge::LeftJoin { on, extra_preds }
let mut local_preds = HashMap::new();
let mut global_preds = vec![];
let mut params = vec![];
for pred in &extra_preds {
classify_conditionals(
pred,
&mut local_preds,
&mut global_preds,
&mut params,
)?;
}

let left_local_preds = local_preds.remove(&left_table).unwrap_or_default();
let right_local_preds = local_preds.remove(&right_table).unwrap_or_default();

// Anything that isn't local to the left or the right is actually a global
// predicate in disguise
global_predicates.extend(local_preds.into_values().flatten());

QueryGraphEdge::LeftJoin {
on,
left_local_preds,
right_local_preds,
global_preds,
params,
}
}
JoinOperator::Join | JoinOperator::InnerJoin => {
for pred in &extra_preds {
Expand Down Expand Up @@ -1642,6 +1674,52 @@ mod tests {
)
}

#[test]
fn local_pred_in_left_join() {
let qg = make_query_graph(
"SELECT t1.x FROM t1 LEFT JOIN t2 ON t2.x = 4 AND t1.y = 7 AND t1.z = t2.z",
);

let join = qg.edges.get(&("t1".into(), "t2".into())).unwrap();

match join {
QueryGraphEdge::LeftJoin {
on,
left_local_preds,
right_local_preds,
global_preds,
params,
} => {
assert_eq!(
*on,
vec![JoinPredicate {
left: Column::from("t1.z"),
right: Column::from("t2.z")
}]
);
assert_eq!(
*left_local_preds,
vec![Expr::BinaryOp {
lhs: Box::new(Expr::Column("t1.y".into())),
op: BinaryOperator::Equal,
rhs: Box::new(Expr::Literal(7.into()))
}]
);
assert_eq!(
*right_local_preds,
vec![Expr::BinaryOp {
lhs: Box::new(Expr::Column("t2.x".into())),
op: BinaryOperator::Equal,
rhs: Box::new(Expr::Literal(4.into()))
}]
);
assert_eq!(*global_preds, vec![]);
assert_eq!(*params, vec![]);
}
QueryGraphEdge::Join { .. } => panic!("Expected left join, got {join:?}"),
}
}

mod view_key {
use super::*;

Expand Down
12 changes: 10 additions & 2 deletions readyset-server/src/controller/sql/query_signature.rs
Expand Up @@ -80,13 +80,21 @@ impl Signature for QueryGraph {
.flat_map(|p| vec![&p.left, &p.right])
.for_each(&mut record_column);
}
QueryGraphEdge::LeftJoin { on, extra_preds } => {
QueryGraphEdge::LeftJoin {
on,
left_local_preds,
right_local_preds,
global_preds,
..
} => {
on.iter()
.flat_map(|p| vec![&p.left, &p.right])
.for_each(&mut record_column);

extra_preds
left_local_preds
.iter()
.chain(right_local_preds)
.chain(global_preds)
.flat_map(|expr| expr.referred_columns())
.for_each(&mut record_column);
}
Expand Down

0 comments on commit 9fa3923

Please sign in to comment.