From 25ec75a901353cf84130c9c76b7995a6dcc2fb75 Mon Sep 17 00:00:00 2001 From: Drew Kimball Date: Tue, 22 Nov 2022 21:49:52 -0800 Subject: [PATCH] opt: add rule to replace outer cols with equivalent non-outer cols This commit adds three decorrelation rules, `TryRemapJoinOuterColsRight`, `TryRemapJoinOuterColsLeft`, and `TryRemapSelectOuterCols`. These rules match joins and selects that have a correlated input and an equality filter between an outer and a non-outer column from the input. Upon matching, the rules traverse the input as far as it would be valid to push the equality filter through normal push-down rules, and replace any encountered references to the equivalent outer column. This approach avoids interactions with rules like `TryDecorrelateSelect`, which attempt to pull filters *up* the operator tree when correlation is present. Fixes #88885 Release note: None --- pkg/sql/opt/norm/decorrelate_funcs.go | 165 ++++++ pkg/sql/opt/norm/rules/decorrelate.opt | 57 ++ pkg/sql/opt/norm/testdata/rules/decorrelate | 549 ++++++++++++++++++-- 3 files changed, 733 insertions(+), 38 deletions(-) diff --git a/pkg/sql/opt/norm/decorrelate_funcs.go b/pkg/sql/opt/norm/decorrelate_funcs.go index b9d15f2a45a6..6ae9fe2d5fa7 100644 --- a/pkg/sql/opt/norm/decorrelate_funcs.go +++ b/pkg/sql/opt/norm/decorrelate_funcs.go @@ -1047,3 +1047,168 @@ func (r *subqueryHoister) constructGroupByAny( opt.ColSet{}, ) } + +// CanMaybeRemapOuterCols performs a best-effort check to minimize the cases +// where TryRemapOuterCols is called unnecessarily. +func (c *CustomFuncs) CanMaybeRemapOuterCols(input memo.RelExpr, filters memo.FiltersExpr) bool { + // The usages of ComputeEquivClosureNoCopy are ok because of the copy below. + outerCols := input.Relational().OuterCols.Copy() + equivGroup := input.Relational().FuncDeps.ComputeEquivClosureNoCopy(outerCols) + for i := range filters { + if equivGroup.Intersects(input.Relational().OutputCols) { + return true + } + equivGroup = filters[i].ScalarProps().FuncDeps.ComputeEquivClosureNoCopy(equivGroup) + } + return equivGroup.Intersects(input.Relational().OutputCols) +} + +// TryRemapOuterCols attempts to replace outer column references in the given +// expression with equivalent non-outer columns using equalities from the given +// filters. It accomplishes this by traversing the operator tree for each outer +// column with the set of equivalent non-outer columns, wherever it would be +// valid to push down a filter on those non-outer columns. If a reference to the +// outer column is discovered during this traversal, it is valid to replace it +// with one of the non-outer columns in the set. +func (c *CustomFuncs) TryRemapOuterCols( + input memo.RelExpr, filters memo.FiltersExpr, +) (remapped memo.RelExpr, succeeded bool) { + outerCols := input.Relational().OuterCols + for col, ok := outerCols.Next(0); ok; col, ok = outerCols.Next(col + 1) { + // candidateCols is the set of input columns for which it may be possible to + // push a filter constraining the column to be equal to an outer column. + candidateCols := input.Relational().FuncDeps.ComputeEquivGroup(col) + for i := range filters { + candidateCols = filters[i].ScalarProps().FuncDeps.ComputeEquivClosureNoCopy(candidateCols) + } + candidateCols.DifferenceWith(input.Relational().OuterCols) + var replaceFn ReplaceFunc + replaceFn = func(e opt.Expr) opt.Expr { + if candidateCols.Empty() { + // It is not possible to remap any references to the current outer + // column within this expression. + return e + } + if v, ok := e.(*memo.VariableExpr); ok && v.Col == col { + if replaceCol, ok := candidateCols.Next(0); ok { + // This outer-column reference can be remapped. + succeeded = true + return c.f.ConstructVariable(replaceCol) + } + } + switch t := e.(type) { + case memo.RelExpr: + if !t.Relational().OuterCols.Contains(col) { + // This expression does not reference the current outer column. + return t + } + // Modifications to candidateCols may be necessary in order to allow + // outer-column remapping within (the children of) a RelExpr. + candidateCols = c.getCandidateColsRelExpr(t, candidateCols) + case opt.ScalarExpr: + // Any candidate columns that reach a ScalarExpr are valid candidates + // for outer-column replacement. No changes to candidateCols required. + } + // If we made it here, candidateCols has been modified so that calling the + // replace function on the current expression is valid. + return c.f.Replace(e, replaceFn) + } + remapped = replaceFn(input).(memo.RelExpr) + } + return remapped, succeeded +} + +// getCandidateColsRelExpr modifies the given set of candidate columns to +// reflect the set of columns for which an equality with an outer column could +// be pushed through the given expression. The logic of getCandidateColsRelExpr +// mirrors that of the filter push-down rules in select.opt and join.opt. +// TODO(drewk): null-rejection has to push down a 'col IS NOT NULL' filter - +// we should be able to share logic. Doing so would remove the issue of rule +// cycles. Any other rules that reuse this logic should reconsider the +// simplification made in getCandidateColsSetOp. +func (c *CustomFuncs) getCandidateColsRelExpr( + expr memo.RelExpr, candidateCols opt.ColSet, +) opt.ColSet { + // Remove any columns that are not in the output of this expression. + // Non-output columns can be in candidateCols after a recursive call + // into the input of an expression that either has multiple relational + // inputs (e.g. Joins) or can synthesize columns (e.g. Projects). + candidateCols = candidateCols.Intersection(expr.Relational().OutputCols) + + // Depending on the expression, further modifications to candidateCols + // may be necessary. + switch t := expr.(type) { + case *memo.SelectExpr: + // [MergeSelects] + // No restrictions on push-down for the cols in candidateCols. + case *memo.ProjectExpr, *memo.ProjectSetExpr: + // [PushSelectIntoProject] + // [PushSelectIntoProjectSet] + // Filter push-down candidates can only reference input columns. + inputCols := t.Child(0).(memo.RelExpr).Relational().OutputCols + candidateCols = candidateCols.Intersection(inputCols) + case *memo.InnerJoinExpr, *memo.InnerJoinApplyExpr: + // [MergeSelectInnerJoin] + // [PushFilterIntoJoinLeft] + // [PushFilterIntoJoinRight] + // No restrictions on push-down for the cols in candidateCols. + case *memo.LeftJoinExpr, *memo.LeftJoinApplyExpr, *memo.SemiJoinExpr, + *memo.SemiJoinApplyExpr, *memo.AntiJoinExpr, *memo.AntiJoinApplyExpr: + // [PushSelectIntoJoinLeft] + // [PushSelectCondLeftIntoJoinLeftAndRight] + candidateCols = getCandidateColsLeftSemiAntiJoin(t, candidateCols) + case *memo.GroupByExpr, *memo.DistinctOnExpr: + // [PushSelectIntoGroupBy] + // Filters must refer only to grouping and ConstAgg columns. + private := t.Private().(*memo.GroupingPrivate) + aggs := t.Child(1).(*memo.AggregationsExpr) + candidateCols = candidateCols.Intersection(c.GroupingAndConstCols(private, *aggs)) + case *memo.UnionExpr, *memo.UnionAllExpr, *memo.IntersectExpr, + *memo.IntersectAllExpr, *memo.ExceptExpr, *memo.ExceptAllExpr: + // [PushFilterIntoSetOp] + candidateCols = getCandidateColsSetOp(t, candidateCols) + default: + // Filter push-down through this expression is not supported. + candidateCols = opt.ColSet{} + } + return candidateCols +} + +func getCandidateColsLeftSemiAntiJoin(join memo.RelExpr, candidateCols opt.ColSet) opt.ColSet { + // It is always valid to push an equality between an outer and non-outer + // left column into the left input of a LeftJoin, SemiJoin, or AntiJoin. If + // one of the join filters constrains that left column to be equal to a right + // column, it is also possible to remap and push the equality into the right + // input. See the PushSelectCondLeftIntoJoinLeftAndRight rule for more info. + // + // We can satisfy these requirements by first restricting candidateCols + // to left input columns, then extending it with right input columns + // that are held equivalent by the join filters. + left := join.Child(0).(memo.RelExpr) + on := join.Child(2).(*memo.FiltersExpr) + candidateCols = candidateCols.Intersection(left.Relational().OutputCols) + for i := range *on { + // The usage of ComputeEquivClosureNoCopy is ok because the call to + // Intersection above copies the set. + candidateCols = (*on)[i].ScalarProps().FuncDeps.ComputeEquivClosureNoCopy(candidateCols) + } + return candidateCols +} + +func getCandidateColsSetOp(set memo.RelExpr, candidateCols opt.ColSet) opt.ColSet { + // Because TryRemapOuterCols is the equivalent of pushing down an + // equality filter between an input column and an outer column, we don't + // have to worry about composite sensitivity here (see CanMapOnSetOp). + // Map the output columns contained in candidateCols to the columns from + // both inputs. We filter out non-output columns from candidateCols on + // each recursive call to TryRemapOuterCols, so we don't need to remove any + // columns here. + private := set.Private().(*memo.SetPrivate) + for i, outCol := range private.OutCols { + if candidateCols.Contains(outCol) { + candidateCols.Add(private.LeftCols[i]) + candidateCols.Add(private.RightCols[i]) + } + } + return candidateCols +} diff --git a/pkg/sql/opt/norm/rules/decorrelate.opt b/pkg/sql/opt/norm/rules/decorrelate.opt index 71c24801b3ac..29b719b19aa7 100644 --- a/pkg/sql/opt/norm/rules/decorrelate.opt +++ b/pkg/sql/opt/norm/rules/decorrelate.opt @@ -66,6 +66,63 @@ (EmptyJoinPrivate) ) +# TryRemapJoinOuterColsRight attempts to replace outer column references in the +# right input of a join with equivalent non-outer columns using the ON filters. +# It is valid to do this whenever it is possible to push the equality filter(s) +# down the tree until it holds true for the outer-column reference. Using the +# following query as an example: +# +# SELECT * FROM xy INNER JOIN LATERAL (SELECT * FROM ab WHERE a = x) ON b = x +# +# It is possible to push the 'b = x' filter down into the correlated right input +# of the join, which would allow replacing the 'a = x' filter with 'a = b', thus +# decorrelating the join. The match condition is similar to that for +# PushFilterIntoJoinRight because TryRemapJoinOuterColsRight simulates filter +# push-down when it makes the replacement. +# +# It is desirable to attempt to fire TryRemapJoinOuterColsRight before other +# decorrelation rules because it does not perform any transformations beyond the +# variable replacement. This prevents situations where decorrelation rules make +# the plan worse in their attempts to decorrelate the query. +[TryRemapJoinOuterColsRight, Normalize] +(InnerJoin | InnerJoinApply | LeftJoin | LeftJoinApply | SemiJoin + | SemiJoinApply | AntiJoin | AntiJoinApply + $left:* + $right:* & (HasOuterCols $right) + $on:* & + (CanMaybeRemapOuterCols $right $on) & + (Let ($remapped $ok):(TryRemapOuterCols $right $on) $ok) + $private:* +) +=> +((OpName) $left $remapped $on $private) + +# TryRemapJoinOuterColsLeft is similar to TryRemapJoinOuterColsRight, but it +# applies to the right input of a join. +[TryRemapJoinOuterColsLeft, Normalize] +(InnerJoin | InnerJoinApply | SemiJoin | SemiJoinApply + $left:* & (HasOuterCols $left) + $right:* + $on:* & + (CanMaybeRemapOuterCols $left $on) & + (Let ($remapped $ok):(TryRemapOuterCols $left $on) $ok) + $private:* +) +=> +((OpName) $remapped $right $on $private) + +# TryRemapSelectOuterCols is similar to TryRemapJoinOuterColsRight, but it +# applies to the input of a Select. +[TryRemapSelectOuterCols, Normalize] +(Select + $input:* & (HasOuterCols $input) + $on:* & + (CanMaybeRemapOuterCols $input $on) & + (Let ($remapped $ok):(TryRemapOuterCols $input $on) $ok) +) +=> +(Select $remapped $on) + # TryDecorrelateSelect "pushes down" the join apply into the select operator, # in order to eliminate any correlation between the select filter list and the # left side of the join, and also to keep "digging" down to find and eliminate diff --git a/pkg/sql/opt/norm/testdata/rules/decorrelate b/pkg/sql/opt/norm/testdata/rules/decorrelate index 233c58d505d0..970476940bca 100644 --- a/pkg/sql/opt/norm/testdata/rules/decorrelate +++ b/pkg/sql/opt/norm/testdata/rules/decorrelate @@ -1763,31 +1763,30 @@ group-by (hash) norm expect=TryDecorrelateGroupBy SELECT * FROM xy, uv -WHERE x=v AND u=(SELECT max(i) FROM a WHERE k=x) +WHERE x(2), (5)-->(1,2,6), (1)==(6), (6)==(1) + ├── key: (1,5) + ├── fd: (1)-->(2), (5)-->(6), (1,5)-->(2,6) └── select ├── columns: x:1!null y:2 u:5!null v:6!null max:16!null - ├── key: (5) - ├── fd: (1)-->(2), (5)-->(1,2,6), (1)==(6), (6)==(1), (5)==(16), (16)==(5) + ├── key: (1,5) + ├── fd: (1)-->(2), (5)-->(6), (1,5)-->(2,6,16), (5)==(16), (16)==(5) ├── group-by (hash) │ ├── columns: x:1!null y:2 u:5!null v:6!null max:16!null - │ ├── grouping columns: u:5!null - │ ├── key: (5) - │ ├── fd: (1)-->(2), (5)-->(1,2,6,16), (1)==(6), (6)==(1) + │ ├── grouping columns: x:1!null u:5!null + │ ├── key: (1,5) + │ ├── fd: (1)-->(2), (5)-->(6), (1,5)-->(2,6,16) │ ├── inner-join (hash) │ │ ├── columns: x:1!null y:2 u:5!null v:6!null k:9!null i:10!null │ │ ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-more) - │ │ ├── key: (5) - │ │ ├── fd: (1)-->(2), (5)-->(6), (1)==(6,9), (6)==(1,9), (9)-->(10), (9)==(1,6) - │ │ ├── inner-join (hash) + │ │ ├── key: (5,9) + │ │ ├── fd: (1)-->(2), (5)-->(6), (9)-->(10), (1)==(9), (9)==(1) + │ │ ├── inner-join (cross) │ │ │ ├── columns: x:1!null y:2 u:5!null v:6!null - │ │ │ ├── multiplicity: left-rows(zero-or-more), right-rows(zero-or-one) - │ │ │ ├── key: (5) - │ │ │ ├── fd: (1)-->(2), (5)-->(6), (1)==(6), (6)==(1) + │ │ │ ├── key: (1,5) + │ │ │ ├── fd: (1)-->(2), (5)-->(6) │ │ │ ├── scan xy │ │ │ │ ├── columns: x:1!null y:2 │ │ │ │ ├── key: (1) @@ -1797,7 +1796,7 @@ project │ │ │ │ ├── key: (5) │ │ │ │ └── fd: (5)-->(6) │ │ │ └── filters - │ │ │ └── x:1 = v:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)] + │ │ │ └── x:1 < v:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ])] │ │ ├── select │ │ │ ├── columns: k:9!null i:10!null │ │ │ ├── key: (9) @@ -1815,10 +1814,8 @@ project │ │ └── i:10 │ ├── const-agg [as=v:6, outer=(6)] │ │ └── v:6 - │ ├── const-agg [as=y:2, outer=(2)] - │ │ └── y:2 - │ └── const-agg [as=x:1, outer=(1)] - │ └── x:1 + │ └── const-agg [as=y:2, outer=(2)] + │ └── y:2 └── filters └── u:5 = max:16 [outer=(5,16), constraints=(/5: (/NULL - ]; /16: (/NULL - ]), fd=(5)==(16), (16)==(5)] @@ -1827,31 +1824,30 @@ project norm expect=TryDecorrelateGroupBy SELECT * FROM xy, uv -WHERE x=v AND (SELECT max(i) FROM a WHERE k=x) IS DISTINCT FROM u +WHERE x(2), (5)-->(1,2,6), (1)==(6), (6)==(1) + ├── key: (1,5) + ├── fd: (1)-->(2), (5)-->(6), (1,5)-->(2,6) └── select ├── columns: x:1!null y:2 u:5!null v:6!null max:16 - ├── key: (5) - ├── fd: (1)-->(2), (5)-->(1,2,6,16), (1)==(6), (6)==(1) + ├── key: (1,5) + ├── fd: (1)-->(2), (5)-->(6), (1,5)-->(2,6,16) ├── group-by (hash) │ ├── columns: x:1!null y:2 u:5!null v:6!null max:16 - │ ├── grouping columns: u:5!null - │ ├── key: (5) - │ ├── fd: (1)-->(2), (5)-->(1,2,6,16), (1)==(6), (6)==(1) + │ ├── grouping columns: x:1!null u:5!null + │ ├── key: (1,5) + │ ├── fd: (1)-->(2), (5)-->(6), (1,5)-->(2,6,16) │ ├── left-join (hash) │ │ ├── columns: x:1!null y:2 u:5!null v:6!null k:9 i:10 │ │ ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-more) - │ │ ├── key: (5) - │ │ ├── fd: (1)-->(2), (5)-->(6,9,10), (1)==(6), (6)==(1), (9)-->(10) - │ │ ├── inner-join (hash) + │ │ ├── key: (1,5) + │ │ ├── fd: (1)-->(2), (5)-->(6), (9)-->(10), (1,5)-->(9,10) + │ │ ├── inner-join (cross) │ │ │ ├── columns: x:1!null y:2 u:5!null v:6!null - │ │ │ ├── multiplicity: left-rows(zero-or-more), right-rows(zero-or-one) - │ │ │ ├── key: (5) - │ │ │ ├── fd: (1)-->(2), (5)-->(6), (1)==(6), (6)==(1) + │ │ │ ├── key: (1,5) + │ │ │ ├── fd: (1)-->(2), (5)-->(6) │ │ │ ├── scan xy │ │ │ │ ├── columns: x:1!null y:2 │ │ │ │ ├── key: (1) @@ -1861,7 +1857,7 @@ project │ │ │ │ ├── key: (5) │ │ │ │ └── fd: (5)-->(6) │ │ │ └── filters - │ │ │ └── x:1 = v:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)] + │ │ │ └── x:1 < v:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ])] │ │ ├── scan a │ │ │ ├── columns: k:9!null i:10 │ │ │ ├── key: (9) @@ -1873,10 +1869,8 @@ project │ │ └── i:10 │ ├── const-agg [as=v:6, outer=(6)] │ │ └── v:6 - │ ├── const-agg [as=y:2, outer=(2)] - │ │ └── y:2 - │ └── const-agg [as=x:1, outer=(1)] - │ └── x:1 + │ └── const-agg [as=y:2, outer=(2)] + │ └── y:2 └── filters └── u:5 IS DISTINCT FROM max:16 [outer=(5,16)] @@ -5833,3 +5827,482 @@ project │ └── xy.x:8 └── projections └── xy.x:8 [as=x:12, outer=(8)] + +# -------------------------------------------------- +# TryRemapJoinOuterColsRight +# -------------------------------------------------- + +# Case with Select. Matched join is an InnerJoin. +norm expect=TryRemapJoinOuterColsRight format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT * FROM ab WHERE b = x) ON a = x +---- +inner-join (hash) + ├── scan xy + ├── select + │ ├── scan ab + │ └── filters + │ └── b = a + └── filters + └── a = x + +# Case with Select. Matched join is a LeftJoin. +norm expect=TryRemapJoinOuterColsRight format=hide-all +SELECT * FROM xy LEFT JOIN LATERAL (SELECT * FROM ab WHERE b = x) ON a = x +---- +left-join (hash) + ├── scan xy + ├── select + │ ├── scan ab + │ └── filters + │ └── b = a + └── filters + └── a = x + +# Case with Project. +norm expect=TryRemapJoinOuterColsRight format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT a, a+x FROM ab) ON a = x +---- +inner-join (hash) + ├── scan xy + ├── project + │ ├── scan ab + │ └── projections + │ └── a + a + └── filters + └── a = x + +# Case with ProjectSet. +norm expect=TryRemapJoinOuterColsRight disable=ConvertZipArraysToValues format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT a, unnest(ARRAY[x, b]) FROM ab) ON a = x +---- +project + └── inner-join (hash) + ├── scan xy + ├── project-set + │ ├── scan ab + │ └── zip + │ └── unnest(ARRAY[a, b]) + └── filters + └── a = x + +# Case with an InnerJoin. +norm expect=TryRemapJoinOuterColsRight format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT * FROM (SELECT a, b, b+x FROM ab) INNER JOIN uv ON b = u) ON a = x +---- +inner-join (hash) + ├── scan xy + ├── inner-join (hash) + │ ├── project + │ │ ├── scan ab + │ │ └── projections + │ │ └── b + a + │ ├── scan uv + │ └── filters + │ └── b = u + └── filters + └── a = x + +# Case with an InnerJoin. The outer column is in the join filters. +norm expect=TryRemapJoinOuterColsRight format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT * FROM ab INNER JOIN uv ON b = u OR b = x) ON a = x +---- +inner-join (hash) + ├── scan xy + ├── inner-join (cross) + │ ├── scan ab + │ ├── scan uv + │ └── filters + │ └── (b = u) OR (b = a) + └── filters + └── a = x + +# Case with a LeftJoin. +norm expect=TryRemapJoinOuterColsRight format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT * FROM (SELECT a, b, b+x FROM ab) LEFT JOIN uv ON b = u) ON a = x +---- +inner-join (hash) + ├── scan xy + ├── left-join (hash) + │ ├── project + │ │ ├── scan ab + │ │ └── projections + │ │ └── b + a + │ ├── scan uv + │ └── filters + │ └── b = u + └── filters + └── a = x + +# Case with a LeftJoin. The outer column is in the join filters. +norm expect=TryRemapJoinOuterColsRight format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT * FROM ab LEFT JOIN uv ON b = u OR b = x) ON a = x +---- +inner-join (hash) + ├── scan xy + ├── left-join (cross) + │ ├── scan ab + │ ├── scan uv + │ └── filters + │ └── (b = u) OR (b = a) + └── filters + └── a = x + +# Case with a LeftJoin. The filter can be mapped to the right input. +norm expect=TryRemapJoinOuterColsRight format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT * FROM ab LEFT JOIN (SELECT u, v + x FROM uv) ON a = u) ON a = x +---- +inner-join (hash) + ├── scan xy + ├── left-join (hash) + │ ├── scan ab + │ ├── project + │ │ ├── scan uv + │ │ └── projections + │ │ └── v + u + │ └── filters + │ └── a = u + └── filters + └── a = x + +# Case with a SemiJoin. +norm expect=TryRemapJoinOuterColsRight disable=TryDecorrelateSelect format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT * FROM (SELECT a, b+x AS c FROM ab) WHERE EXISTS (SELECT * FROM uv WHERE c = u)) ON a = x +---- +inner-join (hash) + ├── scan xy + ├── semi-join-apply + │ ├── project + │ │ ├── scan ab + │ │ └── projections + │ │ └── b + a + │ ├── select + │ │ ├── scan uv + │ │ └── filters + │ │ └── c = u + │ └── filters (true) + └── filters + └── a = x + +# Case with a SemiJoin. The outer column is in the join filters. +norm expect=TryRemapJoinOuterColsRight format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT * FROM ab WHERE EXISTS (SELECT * FROM uv WHERE a = u OR v = x)) ON a = x +---- +inner-join (hash) + ├── scan xy + ├── semi-join (cross) + │ ├── scan ab + │ ├── scan uv + │ └── filters + │ └── (a = u) OR (v = a) + └── filters + └── a = x + +# Case with an AntiJoin. +norm expect=TryRemapJoinOuterColsRight format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT * FROM (SELECT a, b+x AS c FROM ab) WHERE NOT EXISTS (SELECT * FROM uv WHERE c = u)) ON a = x +---- +inner-join (hash) + ├── scan xy + ├── anti-join (hash) + │ ├── project + │ │ ├── scan ab + │ │ └── projections + │ │ └── b + a + │ ├── scan uv + │ └── filters + │ └── c = u + └── filters + └── a = x + +# Case with an AntiJoin. The outer column is in the join filters. +norm expect=TryRemapJoinOuterColsRight format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT * FROM ab WHERE NOT EXISTS (SELECT * FROM uv WHERE a = u OR v = x)) ON a = x +---- +inner-join (hash) + ├── scan xy + ├── anti-join (cross) + │ ├── scan ab + │ ├── scan uv + │ └── filters + │ └── (a = u) OR (v = a) + └── filters + └── a = x + +# Case with a GroupBy. Equality references the grouping column. +norm expect=TryRemapJoinOuterColsRight format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT a, corr(b, x) FROM ab GROUP BY a) ON a = x +---- +inner-join (hash) + ├── scan xy + ├── group-by (hash) + │ ├── project + │ │ ├── scan ab + │ │ └── projections + │ │ └── a + │ └── aggregations + │ └── corr + │ ├── b + │ └── x + └── filters + └── a = xy.x + +# Case with a GroupBy. Equality references a ConstAgg column. +norm expect=TryRemapJoinOuterColsRight format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT v, corr(u, x) FROM uv GROUP BY u, v) ON v = x +---- +project + └── inner-join (hash) + ├── scan xy + ├── group-by (hash) + │ ├── project + │ │ ├── scan uv + │ │ └── projections + │ │ └── v + │ └── aggregations + │ ├── corr + │ │ ├── u + │ │ └── x + │ └── const-agg + │ └── v + └── filters + └── v = xy.x + +# Case with a DistinctOn. +norm expect=TryRemapJoinOuterColsRight format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT DISTINCT ON (a) * FROM (SELECT *, b+x FROM ab)) ON a = x +---- +inner-join (hash) + ├── scan xy + ├── distinct-on + │ ├── project + │ │ ├── scan ab + │ │ └── projections + │ │ └── b + a + │ └── aggregations + │ ├── first-agg + │ │ └── b + │ └── first-agg + │ └── "?column?" + └── filters + └── a = x + +# Case with a Union. +norm expect=TryRemapJoinOuterColsRight format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT * FROM ab UNION (SELECT u, v+x FROM uv)) ON a = x +---- +inner-join (hash) + ├── scan xy + ├── union + │ ├── scan ab + │ └── project + │ ├── scan uv + │ └── projections + │ └── v + u + └── filters + └── a = x + +# Case with an Intersect. +norm expect=TryRemapJoinOuterColsRight format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT * FROM ab Intersect (SELECT u, v+x FROM uv)) ON a = x +---- +inner-join (hash) + ├── scan xy + ├── intersect-all + │ ├── scan ab + │ └── project + │ ├── scan uv + │ └── projections + │ └── v + u + └── filters + └── a = x + +# Case with an Except. +norm expect=TryRemapJoinOuterColsRight format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT * FROM ab Except (SELECT u, v+x FROM uv)) ON a = x +---- +inner-join (hash) + ├── scan xy + ├── except + │ ├── scan ab + │ └── project + │ ├── scan uv + │ └── projections + │ └── v + u + └── filters + └── a = x + +# No-op case with FullJoin. +norm expect-not=TryRemapJoinOuterColsRight format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT * FROM (SELECT a, b, b+x FROM ab) FULL JOIN uv ON a = x) ON True +---- +inner-join-apply + ├── scan xy + ├── full-join (cross) + │ ├── project + │ │ ├── scan ab + │ │ └── projections + │ │ └── b + x + │ ├── scan uv + │ └── filters + │ └── a = x + └── filters (true) + +# No-op case because there is no outer-non-outer column equality. +norm expect-not=TryRemapJoinOuterColsRight format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT * FROM ab WHERE b = x) ON a <> x +---- +inner-join (hash) + ├── scan xy + ├── select + │ ├── scan ab + │ └── filters + │ └── a != b + └── filters + └── b = x + +# No-op case because the equality is a disjunct. +norm expect-not=TryRemapJoinOuterColsRight format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT * FROM ab WHERE b = x) ON a = x OR x IS NULL +---- +inner-join (hash) + ├── scan xy + ├── select + │ ├── scan ab + │ └── filters + │ └── (a = b) OR (b IS NULL) + └── filters + └── b = x + +# No-op case because the outer column is equal to a synthesized column, so push +# down is not possible below the Project. +norm expect-not=TryRemapJoinOuterColsRight format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT *, a+b AS c FROM ab WHERE b = x) ON c = x +---- +project + ├── inner-join (hash) + │ ├── scan xy + │ ├── select + │ │ ├── scan ab + │ │ └── filters + │ │ └── b = (a + b) + │ └── filters + │ └── b = x + └── projections + └── a + b + +# No-op case because the outer column is equal to a synthesized column, so push +# down is not possible below the GroupBy. +norm expect-not=TryRemapJoinOuterColsRight format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT v, corr(u, v) AS w FROM (SELECT *, u + x FROM uv) GROUP BY u, v) ON w = x +---- +project + └── inner-join (cross) + ├── scan xy + ├── group-by (hash) + │ ├── scan uv + │ └── aggregations + │ ├── corr + │ │ ├── u + │ │ └── v + │ └── const-agg + │ └── v + └── filters + └── corr = x + +# -------------------------------------------------- +# TryRemapJoinOuterColsLeft +# -------------------------------------------------- + +# Case with InnerJoin. +norm expect=TryRemapJoinOuterColsLeft format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT * FROM (SELECT * FROM ab WHERE b = x) INNER JOIN uv ON a = x) ON True +---- +inner-join (cross) + ├── inner-join (hash) + │ ├── scan xy + │ ├── select + │ │ ├── scan ab + │ │ └── filters + │ │ └── b = a + │ └── filters + │ └── a = x + ├── scan uv + └── filters (true) + +# Case with SemiJoin. +norm expect=TryRemapJoinOuterColsLeft format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT * FROM (SELECT *, b+x FROM ab) WHERE EXISTS (SELECT * FROM uv WHERE a = x)) ON True +---- +inner-join-apply + ├── scan xy + ├── semi-join (cross) + │ ├── project + │ │ ├── scan ab + │ │ └── projections + │ │ └── b + a + │ ├── scan uv + │ └── filters + │ └── a = x + └── filters (true) + +# No-op case with LeftJoin. +norm expect-not=TryRemapJoinOuterColsLeft format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT * FROM (SELECT * FROM ab WHERE b = x) LEFT JOIN uv ON a = x) ON True +---- +left-join (cross) + ├── inner-join (hash) + │ ├── scan xy + │ ├── scan ab + │ └── filters + │ └── b = x + ├── scan uv + └── filters + └── a = x + +# No-op case with AntiJoin. +norm expect-not=TryRemapJoinOuterColsLeft format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT * FROM (SELECT *, b+x FROM ab) WHERE NOT EXISTS (SELECT * FROM uv WHERE a = x)) ON True +---- +inner-join-apply + ├── scan xy + ├── anti-join (cross) + │ ├── project + │ │ ├── scan ab + │ │ └── projections + │ │ └── b + x + │ ├── scan uv + │ └── filters + │ └── a = x + └── filters (true) + +# No-op case with FullJoin. +norm expect-not=TryRemapJoinOuterColsLeft format=hide-all +SELECT * FROM xy INNER JOIN LATERAL (SELECT * FROM (SELECT * FROM ab WHERE b = x) FULL JOIN uv ON a = x) ON True +---- +inner-join-apply + ├── scan xy + ├── full-join (cross) + │ ├── select + │ │ ├── scan ab + │ │ └── filters + │ │ └── b = x + │ ├── scan uv + │ └── filters + │ └── a = x + └── filters (true) + +# -------------------------------------------------- +# TryRemapSelectOuterCols +# -------------------------------------------------- + +norm expect=TryRemapSelectOuterCols format=hide-all +SELECT * FROM xy LEFT JOIN LATERAL (SELECT * FROM (SELECT *, b+x FROM ab) WHERE a = x) ON True +---- +left-join (hash) + ├── scan xy + ├── project + │ ├── scan ab + │ └── projections + │ └── b + a + └── filters + └── a = x