Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
55961: opt: transform InnerJoin + Values to Project r=rytaft a=erikgrinaker

This commit adds an optimizer normalization rule that transforms
inner joins with single-row `Values` into a projection, for
decorrelating subqueries that only reference variables from the outer
query.
    
It also adds the optgen helper `MakeProjectionsFromValues` to convert a
single-row `Values` into `Projections`.
    
Release note (performance improvement): The optimizer now converts
inner joins with single-row values expressions into projections. This
allows decorrelation of subqueries that only reference variables from
the outer query, such as `SELECT (SELECT value + 10) FROM table`.

Resolves cockroachdb#45853.

I would appreciate an extra pair of eyes on the new test case plans - the changes appear to make sense to me, but I'm not too familiar with the query engine just yet.

Co-authored-by: Erik Grinaker <[email protected]>
  • Loading branch information
craig[bot] and erikgrinaker committed Nov 12, 2020
2 parents 64592c3 + 2c4f604 commit 1c17ca0
Show file tree
Hide file tree
Showing 11 changed files with 421 additions and 399 deletions.
67 changes: 26 additions & 41 deletions pkg/sql/opt/memo/testdata/logprops/join
Original file line number Diff line number Diff line change
Expand Up @@ -75,54 +75,39 @@ SELECT (SELECT (VALUES (x), (y))) FROM xysd
project
├── columns: column1:8(int)
├── prune: (8)
├── inner-join-apply
│ ├── columns: x:1(int!null) y:2(int) column1:6(int) column1:7(int)
├── ensure-distinct-on
│ ├── columns: x:1(int!null) column1:6(int)
│ ├── grouping columns: x:1(int!null)
│ ├── error: "more than one row returned by a subquery used as an expression"
│ ├── key: (1)
│ ├── fd: (1)-->(2,6,7)
│ ├── prune: (7)
│ ├── interesting orderings: (+1)
│ ├── scan xysd
│ │ ├── columns: x:1(int!null) y:2(int)
│ │ ├── key: (1)
│ │ ├── fd: (1)-->(2)
│ │ ├── prune: (1,2)
│ │ └── interesting orderings: (+1)
│ ├── fd: (1)-->(6)
│ ├── prune: (6)
│ ├── inner-join-apply
│ │ ├── columns: column1:6(int) column1:7(int)
│ │ ├── outer: (1,2)
│ │ ├── cardinality: [1 - 1]
│ │ ├── key: ()
│ │ ├── fd: ()-->(6,7)
│ │ ├── prune: (7)
│ │ ├── max1-row
│ │ ├── columns: x:1(int!null) y:2(int) column1:6(int)
│ │ ├── fd: (1)-->(2)
│ │ ├── prune: (6)
│ │ ├── interesting orderings: (+1)
│ │ ├── scan xysd
│ │ │ ├── columns: x:1(int!null) y:2(int)
│ │ │ ├── key: (1)
│ │ │ ├── fd: (1)-->(2)
│ │ │ ├── prune: (1,2)
│ │ │ └── interesting orderings: (+1)
│ │ ├── values
│ │ │ ├── columns: column1:6(int)
│ │ │ ├── error: "more than one row returned by a subquery used as an expression"
│ │ │ ├── outer: (1,2)
│ │ │ ├── cardinality: [1 - 1]
│ │ │ ├── key: ()
│ │ │ ├── fd: ()-->(6)
│ │ │ └── values
│ │ │ ├── columns: column1:6(int)
│ │ │ ├── outer: (1,2)
│ │ │ ├── cardinality: [2 - 2]
│ │ │ ├── prune: (6)
│ │ │ ├── tuple [type=tuple{int}]
│ │ │ │ └── variable: x:1 [type=int]
│ │ │ └── tuple [type=tuple{int}]
│ │ │ └── variable: y:2 [type=int]
│ │ ├── values
│ │ │ ├── columns: column1:7(int)
│ │ │ ├── outer: (6)
│ │ │ ├── cardinality: [1 - 1]
│ │ │ ├── key: ()
│ │ │ ├── fd: ()-->(7)
│ │ │ ├── prune: (7)
│ │ │ ├── cardinality: [2 - 2]
│ │ │ ├── prune: (6)
│ │ │ ├── tuple [type=tuple{int}]
│ │ │ │ └── variable: x:1 [type=int]
│ │ │ └── tuple [type=tuple{int}]
│ │ │ └── variable: column1:6 [type=int]
│ │ │ └── variable: y:2 [type=int]
│ │ └── filters (true)
│ └── filters (true)
│ └── aggregations
│ └── const-agg [as=column1:6, type=int, outer=(6)]
│ └── variable: column1:6 [type=int]
└── projections
└── variable: column1:7 [as=column1:8, type=int, outer=(7)]
└── variable: column1:6 [as=column1:8, type=int, outer=(6)]

# Inner-join-apply nested in inner-join-apply with outer column references to
# each parent.
Expand Down
41 changes: 27 additions & 14 deletions pkg/sql/opt/memo/testdata/stats/join
Original file line number Diff line number Diff line change
Expand Up @@ -1175,27 +1175,40 @@ left-join (cross)
└── filters (true)

norm
SELECT * FROM (SELECT 1) FULL JOIN (VALUES (1), (2)) ON true
SELECT * FROM (SELECT 1 UNION SELECT 2) FULL JOIN (VALUES (1), (2)) ON true
----
inner-join (cross)
├── columns: "?column?":1(int!null) column1:2(int!null)
├── cardinality: [2 - 2]
├── multiplicity: left-rows(exactly-one), right-rows(one-or-more)
├── stats: [rows=2]
├── fd: ()-->(1)
├── columns: "?column?":3(int!null) column1:4(int!null)
├── cardinality: [2 - 4]
├── multiplicity: left-rows(one-or-more), right-rows(one-or-more)
├── stats: [rows=4]
├── values
│ ├── columns: column1:2(int!null)
│ ├── columns: column1:4(int!null)
│ ├── cardinality: [2 - 2]
│ ├── stats: [rows=2]
│ ├── (1,) [type=tuple{int}]
│ └── (2,) [type=tuple{int}]
├── values
│ ├── columns: "?column?":1(int!null)
│ ├── cardinality: [1 - 1]
│ ├── stats: [rows=1]
│ ├── key: ()
│ ├── fd: ()-->(1)
│ └── (1,) [type=tuple{int}]
├── union
│ ├── columns: "?column?":3(int!null)
│ ├── left columns: "?column?":1(int)
│ ├── right columns: "?column?":2(int)
│ ├── cardinality: [1 - 2]
│ ├── stats: [rows=2, distinct(3)=2, null(3)=0]
│ ├── key: (3)
│ ├── values
│ │ ├── columns: "?column?":1(int!null)
│ │ ├── cardinality: [1 - 1]
│ │ ├── stats: [rows=1, distinct(1)=1, null(1)=0]
│ │ ├── key: ()
│ │ ├── fd: ()-->(1)
│ │ └── (1,) [type=tuple{int}]
│ └── values
│ ├── columns: "?column?":2(int!null)
│ ├── cardinality: [1 - 1]
│ ├── stats: [rows=1, distinct(2)=1, null(2)=0]
│ ├── key: ()
│ ├── fd: ()-->(2)
│ └── (2,) [type=tuple{int}]
└── filters (true)

exec-ddl
Expand Down
15 changes: 15 additions & 0 deletions pkg/sql/opt/norm/join_funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -583,3 +583,18 @@ func (c *CustomFuncs) CommuteJoinFlags(p *memo.JoinPrivate) *memo.JoinPrivate {
res.Flags = f
return &res
}

// MakeProjectionsFromValues converts single-row values into projections, for
// use when transforming inner joins with a values operator into a projection.
func (c *CustomFuncs) MakeProjectionsFromValues(values *memo.ValuesExpr) memo.ProjectionsExpr {
if len(values.Rows) != 1 {
panic(errors.AssertionFailedf("MakeProjectionsFromValues expects 1 row, got %d",
len(values.Rows)))
}
projections := make(memo.ProjectionsExpr, 0, len(values.Cols))
elems := values.Rows[0].(*memo.TupleExpr).Elems
for i, col := range values.Cols {
projections = append(projections, c.f.ConstructProjectionsItem(elems[i], col))
}
return projections
}
22 changes: 22 additions & 0 deletions pkg/sql/opt/norm/rules/join.opt
Original file line number Diff line number Diff line change
Expand Up @@ -769,3 +769,25 @@ $left
)
=>
((OpName) $left $right (RemoveFiltersItem $on $item) $private)

# ProjectInnerJoinValues transforms an inner join with a single-row Values
# operator to a Project operator. This allows decorrelation of e.g.:
#
# SELECT (SELECT CASE WHEN ord.approved THEN 'Approved' ELSE '---' END)
# FROM (VALUES (1, true), (2, false)) ord(id, approved)
#
[ProjectInnerJoinValues, Normalize]
(InnerJoin | InnerJoinApply
$left:*
$right:(Values) & (HasOneRow $right)
$on:*
)
=>
(Select
(Project
$left
(MakeProjectionsFromValues $right)
(OutputCols $left)
)
$on
)
Loading

0 comments on commit 1c17ca0

Please sign in to comment.