Skip to content

Commit

Permalink
feat: optimise outer joins
Browse files Browse the repository at this point in the history
Allows for pushing projection and LIMIT to the
RHS of ApplyJoins

Signed-off-by: Andres Taylor <andres@planetscale.com>
  • Loading branch information
systay committed May 4, 2024
1 parent f27d287 commit 4a2b269
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 115 deletions.
12 changes: 8 additions & 4 deletions go/vt/vtgate/planbuilder/operators/joins.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ type JoinOp interface {
AddJoinPredicate(ctx *plancontext.PlanningContext, expr sqlparser.Expr)
}

func IsOuter(outer JoinOp) bool {
return !outer.IsInner()
}

func AddPredicate(
ctx *plancontext.PlanningContext,
join JoinOp,
Expand All @@ -50,11 +54,11 @@ func AddPredicate(
case deps.IsSolvedBy(TableID(join.GetRHS())):
// if we are dealing with an outer join, always start by checking if this predicate can turn
// the join into an inner join
if !joinPredicates && !join.IsInner() && canConvertToInner(ctx, expr, TableID(join.GetRHS())) {
if !joinPredicates && IsOuter(join) && canConvertToInner(ctx, expr, TableID(join.GetRHS())) {
join.MakeInner()
}

if !joinPredicates && !join.IsInner() {
if !joinPredicates && IsOuter(join) {
// if we still are dealing with an outer join
// we need to filter after the join has been evaluated
return newFilter(join, expr)
Expand All @@ -68,11 +72,11 @@ func AddPredicate(
case deps.IsSolvedBy(TableID(join)):
// if we are dealing with an outer join, always start by checking if this predicate can turn
// the join into an inner join
if !joinPredicates && !join.IsInner() && canConvertToInner(ctx, expr, TableID(join.GetRHS())) {
if !joinPredicates && IsOuter(join) && canConvertToInner(ctx, expr, TableID(join.GetRHS())) {
join.MakeInner()
}

if !joinPredicates && !join.IsInner() {
if !joinPredicates && IsOuter(join) {
// if we still are dealing with an outer join
// we need to filter after the join has been evaluated
return newFilter(join, expr)
Expand Down
6 changes: 2 additions & 4 deletions go/vt/vtgate/planbuilder/operators/projection.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,16 +201,14 @@ func createSimpleProjection(ctx *plancontext.PlanningContext, selExprs []sqlpars
// been settled. Once they have settled, we know where to push the projection, but if we push too early
// the projection can end up in the wrong branch of joins
func (p *Projection) canPush(ctx *plancontext.PlanningContext) bool {
if reachedPhase(ctx, subquerySettling) {
return true
}
subQSettled := reachedPhase(ctx, subquerySettling)
ap, ok := p.Columns.(AliasedProjections)
if !ok {
// we can't mix subqueries and unexpanded stars, so we know this does not contain any subqueries
return true
}
for _, projection := range ap {
if _, ok := projection.Info.(SubQueryExpression); ok {
if _, ok := projection.Info.(SubQueryExpression); ok && !subQSettled {
return false
}
}
Expand Down
21 changes: 20 additions & 1 deletion go/vt/vtgate/planbuilder/operators/projection_pushing.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,16 @@ func pushProjectionToOuterContainer(ctx *plancontext.PlanningContext, p *Project
return src, Rewrote("push projection into outer side of subquery container")
}

// nullInNullOutExpr returns true if the expression will return NULL if any of its inputs are NULL
func nullInNullOutExpr(expr sqlparser.Expr) bool {
switch expr.(type) {
case *sqlparser.ColName:
return true
default:
return false
}
}

// pushProjectionInApplyJoin optimizes the ApplyJoin operation by pushing down the projection operation into it. This function works as follows:
//
// 1. It traverses each input column of the projection operation.
Expand Down Expand Up @@ -219,10 +229,19 @@ func pushProjectionInApplyJoin(
src *ApplyJoin,
) (Operator, *ApplyResult) {
ap, err := p.GetAliasedProjections()
if !src.IsInner() || err != nil {
if err != nil {
// we can't push down expression evaluation to the rhs if we are not sure if it will even be executed
return p, NoRewrite
}
if IsOuter(src) {
// for outer joins, we have to check that we can send down the projection to the rhs
for _, expr := range ap.GetColumns() {
if !nullInNullOutExpr(expr.Expr) {
return p, NoRewrite
}
}
}

lhs, rhs := &projector{}, &projector{}
if p.DT != nil && len(p.DT.Columns) > 0 {
lhs.explicitColumnAliases = true
Expand Down
11 changes: 11 additions & 0 deletions go/vt/vtgate/planbuilder/operators/query_planning.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,17 @@ func tryPushLimit(in *Limit) (Operator, *ApplyResult) {
return tryPushingDownLimitInRoute(in, src)
case *Aggregator:
return in, NoRewrite
case *ApplyJoin:
if in.Pushed {
return in, NoRewrite
}
src.RHS = &Limit{
Source: src.RHS,
AST: in.AST,
Pushed: true,
}
in.Pushed = true
return in, Rewrote("push limit to RHS of apply join")
default:
return setUpperLimit(in)
}
Expand Down
28 changes: 17 additions & 11 deletions go/vt/vtgate/planbuilder/testdata/aggr_cases.json
Original file line number Diff line number Diff line change
Expand Up @@ -3474,20 +3474,26 @@
"Name": "user",
"Sharded": true
},
"FieldQuery": "select `user`.id from `user` where 1 != 1",
"Query": "select `user`.id from `user`",
"FieldQuery": "select x.`user.id` from (select `user`.id as `user.id` from `user` where 1 != 1) as x where 1 != 1",
"Query": "select x.`user.id` from (select `user`.id as `user.id` from `user`) as x",
"Table": "`user`"
},
{
"OperatorType": "Route",
"Variant": "Scatter",
"Keyspace": {
"Name": "user",
"Sharded": true
},
"FieldQuery": "select user_extra.col from user_extra where 1 != 1",
"Query": "select user_extra.col from user_extra where user_extra.id = :user_id",
"Table": "user_extra"
"OperatorType": "Limit",
"Count": "10",
"Inputs": [
{
"OperatorType": "Route",
"Variant": "Scatter",
"Keyspace": {
"Name": "user",
"Sharded": true
},
"FieldQuery": "select x.col from (select user_extra.col as col from user_extra where 1 != 1) as x where 1 != 1",
"Query": "select x.col from (select user_extra.col as col from user_extra where user_extra.id = :user_id) as x",
"Table": "user_extra"
}
]
}
]
}
Expand Down
52 changes: 32 additions & 20 deletions go/vt/vtgate/planbuilder/testdata/cte_cases.json
Original file line number Diff line number Diff line change
Expand Up @@ -287,20 +287,26 @@
"Name": "user",
"Sharded": true
},
"FieldQuery": "select `user`.id from `user` where 1 != 1",
"Query": "select `user`.id from `user`",
"FieldQuery": "select x.`user.id` from (select `user`.id as `user.id` from `user` where 1 != 1) as x where 1 != 1",
"Query": "select x.`user.id` from (select `user`.id as `user.id` from `user`) as x",
"Table": "`user`"
},
{
"OperatorType": "Route",
"Variant": "Scatter",
"Keyspace": {
"Name": "user",
"Sharded": true
},
"FieldQuery": "select user_extra.col from user_extra where 1 != 1",
"Query": "select user_extra.col from user_extra where user_extra.id = :user_id",
"Table": "user_extra"
"OperatorType": "Limit",
"Count": "10",
"Inputs": [
{
"OperatorType": "Route",
"Variant": "Scatter",
"Keyspace": {
"Name": "user",
"Sharded": true
},
"FieldQuery": "select x.col from (select user_extra.col as col from user_extra where 1 != 1) as x where 1 != 1",
"Query": "select x.col from (select user_extra.col as col from user_extra where user_extra.id = :user_id) as x",
"Table": "user_extra"
}
]
}
]
}
Expand Down Expand Up @@ -1758,15 +1764,21 @@
]
},
{
"OperatorType": "Route",
"Variant": "Scatter",
"Keyspace": {
"Name": "user",
"Sharded": true
},
"FieldQuery": "select 1 from user_extra as ue where 1 != 1",
"Query": "select 1 from user_extra as ue",
"Table": "user_extra"
"OperatorType": "Limit",
"Count": "1",
"Inputs": [
{
"OperatorType": "Route",
"Variant": "Scatter",
"Keyspace": {
"Name": "user",
"Sharded": true
},
"FieldQuery": "select 1 from user_extra as ue where 1 != 1",
"Query": "select 1 from user_extra as ue",
"Table": "user_extra"
}
]
}
]
}
Expand Down
24 changes: 15 additions & 9 deletions go/vt/vtgate/planbuilder/testdata/from_cases.json
Original file line number Diff line number Diff line change
Expand Up @@ -3520,15 +3520,21 @@
]
},
{
"OperatorType": "Route",
"Variant": "Scatter",
"Keyspace": {
"Name": "user",
"Sharded": true
},
"FieldQuery": "select 1 from user_extra as ue where 1 != 1",
"Query": "select 1 from user_extra as ue",
"Table": "user_extra"
"OperatorType": "Limit",
"Count": "1",
"Inputs": [
{
"OperatorType": "Route",
"Variant": "Scatter",
"Keyspace": {
"Name": "user",
"Sharded": true
},
"FieldQuery": "select 1 from user_extra as ue where 1 != 1",
"Query": "select 1 from user_extra as ue",
"Table": "user_extra"
}
]
}
]
}
Expand Down
24 changes: 15 additions & 9 deletions go/vt/vtgate/planbuilder/testdata/postprocess_cases.json
Original file line number Diff line number Diff line change
Expand Up @@ -1145,15 +1145,21 @@
"Table": "`user`"
},
{
"OperatorType": "Route",
"Variant": "Scatter",
"Keyspace": {
"Name": "user",
"Sharded": true
},
"FieldQuery": "select 1 from user_extra where 1 != 1",
"Query": "select 1 from user_extra",
"Table": "user_extra"
"OperatorType": "Limit",
"Count": "1",
"Inputs": [
{
"OperatorType": "Route",
"Variant": "Scatter",
"Keyspace": {
"Name": "user",
"Sharded": true
},
"FieldQuery": "select 1 from user_extra where 1 != 1",
"Query": "select 1 from user_extra",
"Table": "user_extra"
}
]
}
]
}
Expand Down
65 changes: 26 additions & 39 deletions go/vt/vtgate/planbuilder/testdata/select_cases.json
Original file line number Diff line number Diff line change
Expand Up @@ -5149,48 +5149,35 @@
"QueryType": "SELECT",
"Original": "select name as t0, name as t1 from user left outer join user_extra on user.cola = user_extra.cola",
"Instructions": {
"OperatorType": "SimpleProjection",
"ColumnNames": [
"t0",
"t1"
],
"Columns": [
0,
0
],
"OperatorType": "Join",
"Variant": "LeftJoin",
"JoinColumnIndexes": "L:0,L:0",
"JoinVars": {
"user_cola": 2
},
"TableName": "`user`_user_extra",
"Inputs": [
{
"OperatorType": "Join",
"Variant": "LeftJoin",
"JoinColumnIndexes": "L:0",
"JoinVars": {
"user_cola": 1
"OperatorType": "Route",
"Variant": "Scatter",
"Keyspace": {
"Name": "user",
"Sharded": true
},
"TableName": "`user`_user_extra",
"Inputs": [
{
"OperatorType": "Route",
"Variant": "Scatter",
"Keyspace": {
"Name": "user",
"Sharded": true
},
"FieldQuery": "select `name`, `user`.cola from `user` where 1 != 1",
"Query": "select `name`, `user`.cola from `user`",
"Table": "`user`"
},
{
"OperatorType": "Route",
"Variant": "Scatter",
"Keyspace": {
"Name": "user",
"Sharded": true
},
"FieldQuery": "select 1 from user_extra where 1 != 1",
"Query": "select 1 from user_extra where user_extra.cola = :user_cola",
"Table": "user_extra"
}
]
"FieldQuery": "select `name` as t0, `name` as t1, `user`.cola from `user` where 1 != 1",
"Query": "select `name` as t0, `name` as t1, `user`.cola from `user`",
"Table": "`user`"
},
{
"OperatorType": "Route",
"Variant": "Scatter",
"Keyspace": {
"Name": "user",
"Sharded": true
},
"FieldQuery": "select 1 from user_extra where 1 != 1",
"Query": "select 1 from user_extra where user_extra.cola = :user_cola",
"Table": "user_extra"
}
]
},
Expand Down
Loading

0 comments on commit 4a2b269

Please sign in to comment.