Skip to content

Commit

Permalink
Merge #30048
Browse files Browse the repository at this point in the history
30048: opt: fix join cardinality calculation r=RaduBerinde a=RaduBerinde

The calculation was incorrect because it accounted for the "outerness"
using `AtLeast` but only with the `Min` value; the `Max` value needs
to be taken into account as well.

This change fixes this and makes the following improvements:
 - `AtLeast` now takes a cardinality instead of a value;
 - `AtMost` is renamed to `Limit` because it's inconsistent with the
   new `AtLeast`;
 - the cardinality for full outer join with a false filter is
   improved;
 - extensive unit tests for makeJoinCardinality.

Release note: None

Co-authored-by: Radu Berinde <[email protected]>
  • Loading branch information
craig[bot] and RaduBerinde committed Sep 11, 2018
2 parents e876894 + bbd2eea commit bcf150d
Show file tree
Hide file tree
Showing 5 changed files with 318 additions and 94 deletions.
58 changes: 35 additions & 23 deletions pkg/sql/opt/memo/logical_props_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,9 @@ func (b *logicalPropsBuilder) buildScanProps(ev ExprView) props.Logical {
if def.Constraint != nil && def.Constraint.IsContradiction() {
relational.Cardinality = props.ZeroCardinality
} else if relational.FuncDeps.HasMax1Row() {
relational.Cardinality = relational.Cardinality.AtMost(1)
relational.Cardinality = relational.Cardinality.Limit(1)
} else if hardLimit > 0 && hardLimit < math.MaxUint32 {
relational.Cardinality = relational.Cardinality.AtMost(uint32(hardLimit))
relational.Cardinality = relational.Cardinality.Limit(uint32(hardLimit))
}

// Statistics
Expand Down Expand Up @@ -279,7 +279,7 @@ func (b *logicalPropsBuilder) buildSelectProps(ev ExprView) props.Logical {
if filter.Operator() == opt.FalseOp || filterProps.Constraints == constraint.Contradiction {
relational.Cardinality = props.ZeroCardinality
} else if relational.FuncDeps.HasMax1Row() {
relational.Cardinality = relational.Cardinality.AtMost(1)
relational.Cardinality = relational.Cardinality.Limit(1)
}

// Statistics
Expand Down Expand Up @@ -529,7 +529,7 @@ func (b *logicalPropsBuilder) buildJoinProps(ev ExprView) props.Logical {
// Calculate cardinality, depending on join type.
relational.Cardinality = b.makeJoinCardinality(leftProps.Cardinality, &h)
if relational.FuncDeps.HasMax1Row() {
relational.Cardinality = relational.Cardinality.AtMost(1)
relational.Cardinality = relational.Cardinality.Limit(1)
}

// Statistics
Expand Down Expand Up @@ -636,7 +636,7 @@ func (b *logicalPropsBuilder) buildGroupByProps(ev ExprView) props.Logical {
// will also be returned by GroupBy and DistinctOn.
relational.Cardinality = inputProps.Cardinality.AsLowAs(1)
if relational.FuncDeps.HasMax1Row() {
relational.Cardinality = relational.Cardinality.AtMost(1)
relational.Cardinality = relational.Cardinality.Limit(1)
}
}

Expand Down Expand Up @@ -867,7 +867,7 @@ func (b *logicalPropsBuilder) buildLimitProps(ev ExprView) props.Logical {
if constLimit <= 0 {
relational.Cardinality = props.ZeroCardinality
} else if constLimit < math.MaxUint32 {
relational.Cardinality = relational.Cardinality.AtMost(uint32(constLimit))
relational.Cardinality = relational.Cardinality.Limit(uint32(constLimit))
}

// Statistics
Expand Down Expand Up @@ -962,7 +962,7 @@ func (b *logicalPropsBuilder) buildMax1RowProps(ev ExprView) props.Logical {
// Cardinality
// -----------
// Max1Row ensures that zero or one row is returned from input.
relational.Cardinality = inputProps.Cardinality.AtMost(1)
relational.Cardinality = inputProps.Cardinality.Limit(1)

// Statistics
// ----------
Expand Down Expand Up @@ -1201,7 +1201,6 @@ func makeTableFuncDep(md *opt.Metadata, tabID opt.TableID) *props.FuncDepSet {
func (b *logicalPropsBuilder) makeJoinCardinality(
left props.Cardinality, h *joinPropsHelper,
) props.Cardinality {
var card props.Cardinality
switch h.joinType {
case opt.SemiJoinOp, opt.SemiJoinApplyOp, opt.AntiJoinOp, opt.AntiJoinApplyOp:
// Semi/Anti join cardinality never exceeds left input cardinality, and
Expand All @@ -1211,32 +1210,45 @@ func (b *logicalPropsBuilder) makeJoinCardinality(

// Other join types can return up to cross product of rows.
right := h.rightCardinality
card = left.Product(right)
innerJoinCard := left.Product(right)

// Apply filter to cardinality.
if !h.filterIsTrue {
if h.filterIsFalse {
card = props.ZeroCardinality
innerJoinCard = props.ZeroCardinality
} else {
card = card.AsLowAs(0)
innerJoinCard = innerJoinCard.AsLowAs(0)
}
}

// Outer joins return minimum number of rows, depending on type.
switch h.joinType {
case opt.LeftJoinOp, opt.LeftJoinApplyOp, opt.FullJoinOp, opt.FullJoinApplyOp:
card = card.AtLeast(left.Min)
}
switch h.joinType {
case opt.RightJoinOp, opt.RightJoinApplyOp, opt.FullJoinOp, opt.FullJoinApplyOp:
card = card.AtLeast(right.Min)
}
switch h.joinType {
case opt.LeftJoinOp, opt.LeftJoinApplyOp:
return innerJoinCard.AtLeast(left)

case opt.RightJoinOp, opt.RightJoinApplyOp:
return innerJoinCard.AtLeast(right)

case opt.FullJoinOp, opt.FullJoinApplyOp:
card = card.AsHighAs(left.Min + right.Min)
}
if innerJoinCard.IsZero() {
// In this case, we know that each left or right row will generate an
// output row.
return left.Add(right)
}
var c props.Cardinality
// We get at least MAX(left.Min, right.Min) rows.
c.Min = left.Min
if c.Min < right.Min {
c.Min = right.Min
}
// We could get left.Max + right.Max rows (if the filter doesn't match
// anything). We use Add here because it handles overflow.
c.Max = left.Add(right).Max
return innerJoinCard.AtLeast(c)

return card
default:
return innerJoinCard
}
}

func (b *logicalPropsBuilder) makeSetCardinality(
Expand All @@ -1251,7 +1263,7 @@ func (b *logicalPropsBuilder) makeSetCardinality(
case opt.IntersectOp, opt.IntersectAllOp:
// Use minimum of left and right Max cardinality.
card = props.Cardinality{Min: 0, Max: left.Max}
card = card.AtMost(right.Max)
card = card.Limit(right.Max)

case opt.ExceptOp, opt.ExceptAllOp:
// Use left Max cardinality.
Expand Down
209 changes: 209 additions & 0 deletions pkg/sql/opt/memo/logical_props_builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
// Copyright 2018 The Cockroach Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.

package memo

import (
"fmt"
"testing"

"github.com/cockroachdb/cockroach/pkg/sql/opt"
"github.com/cockroachdb/cockroach/pkg/sql/opt/props"
)

func TestJoinCardinality(t *testing.T) {
c := func(min, max uint32) props.Cardinality {
return props.Cardinality{Min: min, Max: max}
}

type testCase struct {
left props.Cardinality
right props.Cardinality
expected props.Cardinality
}

testCaseGroups := []struct {
joinType opt.Operator
filter string // "true", "false", or "other"
testCases []testCase
}{
{ // Inner join, true filter.
joinType: opt.InnerJoinOp,
filter: "true",
testCases: []testCase{
{left: c(0, 10), right: c(0, 10), expected: c(0, 100)},
{left: c(5, 10), right: c(0, 10), expected: c(0, 100)},
{left: c(0, 10), right: c(5, 10), expected: c(0, 100)},
{left: c(5, 10), right: c(5, 10), expected: c(25, 100)},
},
},

{ // Inner join, false filter.
joinType: opt.InnerJoinOp,
filter: "false",
testCases: []testCase{
{left: c(0, 10), right: c(0, 10), expected: c(0, 0)},
{left: c(5, 10), right: c(0, 10), expected: c(0, 0)},
{left: c(0, 10), right: c(5, 10), expected: c(0, 0)},
{left: c(5, 10), right: c(5, 10), expected: c(0, 0)},
},
},

{ // Inner join, other filter.
joinType: opt.InnerJoinOp,
filter: "other",
testCases: []testCase{
{left: c(0, 10), right: c(0, 10), expected: c(0, 100)},
{left: c(5, 10), right: c(0, 10), expected: c(0, 100)},
{left: c(0, 10), right: c(5, 10), expected: c(0, 100)},
{left: c(5, 10), right: c(5, 10), expected: c(0, 100)},
},
},

{ // Left join, true filter.
joinType: opt.LeftJoinOp,
filter: "true",
testCases: []testCase{
{left: c(0, 10), right: c(0, 10), expected: c(0, 100)},
{left: c(5, 10), right: c(0, 10), expected: c(5, 100)},
{left: c(0, 10), right: c(5, 10), expected: c(0, 100)},
{left: c(5, 10), right: c(5, 10), expected: c(25, 100)},
},
},

{ // Left join, false filter.
joinType: opt.LeftJoinOp,
filter: "false",
testCases: []testCase{
{left: c(0, 10), right: c(0, 10), expected: c(0, 10)},
{left: c(5, 10), right: c(0, 10), expected: c(5, 10)},
{left: c(0, 10), right: c(5, 10), expected: c(0, 10)},
{left: c(5, 10), right: c(5, 10), expected: c(5, 10)},
},
},

{ // Left join, other filter.
joinType: opt.LeftJoinOp,
filter: "other",
testCases: []testCase{
{left: c(0, 10), right: c(0, 10), expected: c(0, 100)},
{left: c(5, 10), right: c(0, 10), expected: c(5, 100)},
{left: c(0, 10), right: c(5, 10), expected: c(0, 100)},
{left: c(5, 10), right: c(5, 10), expected: c(5, 100)},
},
},

{ // Right join, true filter.
joinType: opt.RightJoinOp,
filter: "true",
testCases: []testCase{
{left: c(0, 10), right: c(0, 10), expected: c(0, 100)},
{left: c(5, 10), right: c(0, 10), expected: c(0, 100)},
{left: c(0, 10), right: c(5, 10), expected: c(5, 100)},
{left: c(5, 10), right: c(5, 10), expected: c(25, 100)},
},
},

{ // Right join, false filter.
joinType: opt.RightJoinOp,
filter: "false",
testCases: []testCase{
{left: c(0, 10), right: c(0, 10), expected: c(0, 10)},
{left: c(5, 10), right: c(0, 10), expected: c(0, 10)},
{left: c(0, 10), right: c(5, 10), expected: c(5, 10)},
{left: c(5, 10), right: c(5, 10), expected: c(5, 10)},
},
},

{ // Right join, other filter.
joinType: opt.RightJoinOp,
filter: "other",
testCases: []testCase{
{left: c(0, 10), right: c(0, 10), expected: c(0, 100)},
{left: c(5, 10), right: c(0, 10), expected: c(0, 100)},
{left: c(0, 10), right: c(5, 10), expected: c(5, 100)},
{left: c(5, 10), right: c(5, 10), expected: c(5, 100)},
},
},

{ // Full join, true filter.
joinType: opt.FullJoinOp,
filter: "true",
testCases: []testCase{
{left: c(0, 1), right: c(0, 1), expected: c(0, 2)},
{left: c(1, 1), right: c(1, 1), expected: c(1, 2)},
{left: c(0, 10), right: c(0, 10), expected: c(0, 100)},
{left: c(5, 10), right: c(0, 10), expected: c(5, 100)},
{left: c(0, 10), right: c(5, 10), expected: c(5, 100)},
{left: c(5, 10), right: c(5, 10), expected: c(25, 100)},
{left: c(7, 10), right: c(8, 10), expected: c(56, 100)},
{left: c(8, 10), right: c(7, 10), expected: c(56, 100)},
},
},

{ // Full join, false filter.
joinType: opt.FullJoinOp,
filter: "false",
testCases: []testCase{
{left: c(0, 1), right: c(0, 1), expected: c(0, 2)},
{left: c(1, 1), right: c(1, 1), expected: c(2, 2)},
{left: c(2, 5), right: c(3, 8), expected: c(5, 13)},
{left: c(0, 10), right: c(0, 10), expected: c(0, 20)},
{left: c(5, 10), right: c(0, 10), expected: c(5, 20)},
{left: c(0, 10), right: c(5, 10), expected: c(5, 20)},
{left: c(5, 10), right: c(5, 10), expected: c(10, 20)},
{left: c(7, 10), right: c(8, 10), expected: c(15, 20)},
{left: c(8, 10), right: c(7, 10), expected: c(15, 20)},
},
},

{ // Full join, other filter.
joinType: opt.FullJoinOp,
filter: "other",
testCases: []testCase{
{left: c(0, 1), right: c(0, 1), expected: c(0, 2)},
{left: c(1, 1), right: c(1, 1), expected: c(1, 2)},
{left: c(2, 5), right: c(3, 8), expected: c(3, 40)},
{left: c(0, 10), right: c(0, 10), expected: c(0, 100)},
{left: c(5, 10), right: c(0, 10), expected: c(5, 100)},
{left: c(0, 10), right: c(5, 10), expected: c(5, 100)},
{left: c(5, 10), right: c(5, 10), expected: c(5, 100)},
{left: c(7, 10), right: c(8, 10), expected: c(8, 100)},
{left: c(8, 10), right: c(7, 10), expected: c(8, 100)},
},
},
}

for _, group := range testCaseGroups {
t.Run(fmt.Sprintf("%s/%s", group.joinType, group.filter), func(t *testing.T) {
for i, tc := range group.testCases {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
b := &logicalPropsBuilder{}
h := &joinPropsHelper{}
h.rightCardinality = tc.right
h.joinType = group.joinType
h.filterIsTrue = (group.filter == "true")
h.filterIsFalse = (group.filter == "false")

res := b.makeJoinCardinality(tc.left, h)
if res != tc.expected {
t.Errorf(
"left=%s right=%s: expected %s, got %s\n", tc.left, tc.right, tc.expected, res,
)
}
})
}
})
}
}
4 changes: 2 additions & 2 deletions pkg/sql/opt/memo/testdata/logprops/join
Original file line number Diff line number Diff line change
Expand Up @@ -1026,13 +1026,13 @@ full-join
└── filters [type=bool]
└── true [type=bool]

# Calculate full-join cardinality with filter.
# Calculate full-join cardinality with false filter.
build
SELECT * FROM (VALUES (NULL), (NULL)) a FULL JOIN (VALUES (NULL), (NULL)) b ON a.column1=b.column1
----
full-join
├── columns: column1:1(unknown) column1:2(unknown)
├── cardinality: [2 - 4]
├── cardinality: [4 - 4]
├── prune: (1,2)
├── reject-nulls: (1,2)
├── values
Expand Down
Loading

0 comments on commit bcf150d

Please sign in to comment.