diff --git a/pkg/sql/opt/exec/execbuilder/testdata/join b/pkg/sql/opt/exec/execbuilder/testdata/join index 52cd6b4cf61d..411d16ade524 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/join +++ b/pkg/sql/opt/exec/execbuilder/testdata/join @@ -543,18 +543,11 @@ EXPLAIN SELECT * FROM cards LEFT OUTER JOIN customers ON customers.id = cards.cu distribution: local vectorized: true · -• merge join -│ equality: (cust) = (id) -│ right cols are key -│ -├── • scan -│ missing stats -│ table: cards@cards_cust_idx -│ spans: FULL SCAN +• render │ └── • scan missing stats - table: customers@customers_pkey + table: cards@cards_pkey spans: FULL SCAN # Tests for filter propagation through joins. @@ -2220,16 +2213,9 @@ EXPLAIN SELECT * FROM cards LEFT OUTER HASH JOIN customers ON customers.id = car distribution: local vectorized: true · -• hash join -│ equality: (cust) = (id) -│ right cols are key -│ -├── • scan -│ missing stats -│ table: cards@cards_pkey -│ spans: FULL SCAN +• render │ └── • scan missing stats - table: customers@customers_pkey + table: cards@cards_pkey spans: FULL SCAN diff --git a/pkg/sql/opt/exec/execbuilder/testdata/subquery b/pkg/sql/opt/exec/execbuilder/testdata/subquery index 756f7eefc44d..7d424ad19553 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/subquery +++ b/pkg/sql/opt/exec/execbuilder/testdata/subquery @@ -184,33 +184,24 @@ EXPLAIN (VERBOSE) SELECT a FROM abc WHERE a IN (SELECT a FROM abc WHERE b < 0) distribution: local vectorized: true · -• merge join (semi) +• project │ columns: (a) -│ estimated row count: 333 (missing stats) -│ equality: (a) = (a) -│ left cols are key -│ right cols are key -│ merge ordering: +"(a=a)" -│ -├── • scan -│ columns: (a) -│ ordering: +a -│ estimated row count: 1,000 (missing stats) -│ table: abc@abc_pkey -│ spans: FULL SCAN │ -└── • filter - │ columns: (a, b) - │ ordering: +a - │ estimated row count: 333 (missing stats) - │ filter: b < 0 +└── • render + │ columns: (a, a) + │ render a: a + │ render a: a │ - └── • scan - columns: (a, b) - ordering: +a - estimated row count: 1,000 (missing stats) - table: abc@abc_pkey - spans: FULL SCAN + └── • filter + │ columns: (a, b) + │ estimated row count: 333 (missing stats) + │ filter: b < 0 + │ + └── • scan + columns: (a, b) + estimated row count: 1,000 (missing stats) + table: abc@abc_pkey + spans: FULL SCAN query T EXPLAIN SELECT * FROM (SELECT * FROM (VALUES (1, 8, 8), (3, 1, 1), (2, 4, 4)) AS moo (moo1, moo2, moo3) ORDER BY moo2) as foo (foo1) ORDER BY foo1 diff --git a/pkg/sql/opt/exec/execbuilder/testdata/upsert b/pkg/sql/opt/exec/execbuilder/testdata/upsert index 8ddd03c4ed09..88857033c9e9 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/upsert +++ b/pkg/sql/opt/exec/execbuilder/testdata/upsert @@ -107,12 +107,11 @@ vectorized: true │ auto commit │ arbiter indexes: kv_pkey │ -└── • lookup join (inner) +└── • render │ columns: (k, v_default, k) - │ estimated row count: 2 (missing stats) - │ table: kv@kv_pkey - │ equality: (k) = (k) - │ equality cols are key + │ render k: k + │ render k: k + │ render v_default: v_default │ └── • distinct │ columns: (v_default, k) diff --git a/pkg/sql/opt/memo/testdata/logprops/join b/pkg/sql/opt/memo/testdata/logprops/join index ef5e5cacc0d2..d2ef3a87543a 100644 --- a/pkg/sql/opt/memo/testdata/logprops/join +++ b/pkg/sql/opt/memo/testdata/logprops/join @@ -2427,11 +2427,11 @@ INNER JOIN (SELECT xysd.x, a.x AS t FROM xysd INNER JOIN xysd AS a ON xysd.x = a ---- inner-join (hash) ├── columns: k:1(int!null) v:2(int) r1:3(int!null) r2:4(int) x:7(int!null) t:13(int!null) - ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-more) + ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-more) ├── key: (1) ├── fd: (1)-->(2-4), (7)==(3,13), (13)==(3,7), (3)==(7,13) - ├── prune: (1,2,4) - ├── interesting orderings: (+1) (+7) (+13) + ├── prune: (1,2,4,7) + ├── interesting orderings: (+1) (+(7|13)) ├── scan fk │ ├── columns: k:1(int!null) v:2(int) r1:3(int!null) r2:4(int) │ ├── key: (1) @@ -2439,29 +2439,21 @@ inner-join (hash) │ ├── prune: (1-4) │ ├── interesting orderings: (+1) │ └── unfiltered-cols: (1-6) - ├── inner-join (hash) - │ ├── columns: xysd.x:7(int!null) a.x:13(int!null) - │ ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one) - │ ├── key: (13) + ├── project + │ ├── columns: a.x:13(int!null) xysd.x:7(int!null) + │ ├── key: (7) │ ├── fd: (7)==(13), (13)==(7) - │ ├── interesting orderings: (+7) (+13) - │ ├── unfiltered-cols: (7-18) + │ ├── prune: (7,13) + │ ├── interesting orderings: (+(7|13)) + │ ├── unfiltered-cols: (7-12) │ ├── scan xysd │ │ ├── columns: xysd.x:7(int!null) │ │ ├── key: (7) │ │ ├── prune: (7) │ │ ├── interesting orderings: (+7) │ │ └── unfiltered-cols: (7-12) - │ ├── scan xysd [as=a] - │ │ ├── columns: a.x:13(int!null) - │ │ ├── key: (13) - │ │ ├── prune: (13) - │ │ ├── interesting orderings: (+13) - │ │ └── unfiltered-cols: (13-18) - │ └── filters - │ └── eq [type=bool, outer=(7,13), constraints=(/7: (/NULL - ]; /13: (/NULL - ]), fd=(7)==(13), (13)==(7)] - │ ├── variable: xysd.x:7 [type=int] - │ └── variable: a.x:13 [type=int] + │ └── projections + │ └── variable: xysd.x:7 [as=a.x:13, type=int, outer=(7)] └── filters └── eq [type=bool, outer=(3,13), constraints=(/3: (/NULL - ]; /13: (/NULL - ]), fd=(3)==(13), (13)==(3)] ├── variable: r1:3 [type=int] @@ -2683,11 +2675,10 @@ FROM (SELECT r1, r2, r3 FROM ref WHERE r2 IS NOT NULL) INNER JOIN abc ON (r1, r2, r3) = (a, b, c) ---- -inner-join (hash) +project ├── columns: r1:1(int!null) r2:2(int!null) r3:3(int!null) a:7(int!null) b:8(int!null) c:9(int!null) - ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-more) ├── fd: (1)==(7), (7)==(1), (2)==(8), (8)==(2), (3)==(9), (9)==(3) - ├── interesting orderings: (+7,+8,+9) + ├── prune: (1-3,7-9) ├── select │ ├── columns: r1:1(int!null) r2:2(int!null) r3:3(int!null) │ ├── prune: (1,3) @@ -2698,22 +2689,10 @@ inner-join (hash) │ └── is-not [type=bool, outer=(2), constraints=(/2: (/NULL - ]; tight)] │ ├── variable: r2:2 [type=int] │ └── null [type=unknown] - ├── scan abc - │ ├── columns: a:7(int!null) b:8(int!null) c:9(int!null) - │ ├── key: (7-9) - │ ├── prune: (7-9) - │ ├── interesting orderings: (+7,+8,+9) - │ └── unfiltered-cols: (7-11) - └── filters - ├── eq [type=bool, outer=(1,7), constraints=(/1: (/NULL - ]; /7: (/NULL - ]), fd=(1)==(7), (7)==(1)] - │ ├── variable: r1:1 [type=int] - │ └── variable: a:7 [type=int] - ├── eq [type=bool, outer=(2,8), constraints=(/2: (/NULL - ]; /8: (/NULL - ]), fd=(2)==(8), (8)==(2)] - │ ├── variable: r2:2 [type=int] - │ └── variable: b:8 [type=int] - └── eq [type=bool, outer=(3,9), constraints=(/3: (/NULL - ]; /9: (/NULL - ]), fd=(3)==(9), (9)==(3)] - ├── variable: r3:3 [type=int] - └── variable: c:9 [type=int] + └── projections + ├── variable: r1:1 [as=a:7, type=int, outer=(1)] + ├── variable: r2:2 [as=b:8, type=int, outer=(2)] + └── variable: r3:3 [as=c:9, type=int, outer=(3)] # Case with a not-null multi-column foreign key and an equality on only one of # the foreign key columns. @@ -2796,15 +2775,15 @@ limit ├── columns: t_st_id:1(int!null) t_tt_id:2(int!null) t_s_symb:3(int!null) st_id:7(int!null) tt_id:10(int!null) s_symb:13(int!null) s_st_id:14(int!null) s_ex_id:15(int!null) ex_id:18(int!null) ├── cardinality: [0 - 50] ├── fd: (13)-->(14,15), (15)==(18), (18)==(15), (1)==(7), (7)==(1), (2)==(10), (10)==(2), (3)==(13), (13)==(3) - ├── prune: (14) - ├── interesting orderings: (+7) (+10) (+13) (+18) + ├── prune: (14,15,18) + ├── interesting orderings: (+7) (+10) (+13) ├── inner-join (hash) │ ├── columns: t_st_id:1(int!null) t_tt_id:2(int!null) t_s_symb:3(int!null) st_id:7(int!null) tt_id:10(int!null) s_symb:13(int!null) s_st_id:14(int!null) s_ex_id:15(int!null) ex_id:18(int!null) │ ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-more) │ ├── fd: (13)-->(14,15), (15)==(18), (18)==(15), (1)==(7), (7)==(1), (2)==(10), (10)==(2), (3)==(13), (13)==(3) │ ├── limit hint: 50.00 - │ ├── prune: (14) - │ ├── interesting orderings: (+7) (+10) (+13) (+18) + │ ├── prune: (14,15,18) + │ ├── interesting orderings: (+7) (+10) (+13) │ ├── scan trade │ │ ├── columns: t_st_id:1(int!null) t_tt_id:2(int!null) t_s_symb:3(int!null) │ │ ├── prune: (1-3) @@ -2814,8 +2793,8 @@ limit │ │ ├── multiplicity: left-rows(zero-or-more), right-rows(one-or-more) │ │ ├── key: (7,10,13) │ │ ├── fd: (13)-->(14,15), (15)==(18), (18)==(15) - │ │ ├── prune: (7,10,13,14) - │ │ ├── interesting orderings: (+7) (+10) (+13) (+18) + │ │ ├── prune: (7,10,13-15,18) + │ │ ├── interesting orderings: (+7) (+10) (+13) │ │ ├── scan status_type │ │ │ ├── columns: st_id:7(int!null) │ │ │ ├── key: (7) @@ -2826,21 +2805,20 @@ limit │ │ │ ├── columns: tt_id:10(int!null) s_symb:13(int!null) s_st_id:14(int!null) s_ex_id:15(int!null) ex_id:18(int!null) │ │ │ ├── key: (10,13) │ │ │ ├── fd: (13)-->(14,15), (15)==(18), (18)==(15) - │ │ │ ├── prune: (10,13,14) - │ │ │ ├── interesting orderings: (+10) (+13) (+18) + │ │ │ ├── prune: (10,13-15,18) + │ │ │ ├── interesting orderings: (+10) (+13) │ │ │ ├── scan trade_type │ │ │ │ ├── columns: tt_id:10(int!null) │ │ │ │ ├── key: (10) │ │ │ │ ├── prune: (10) │ │ │ │ ├── interesting orderings: (+10) │ │ │ │ └── unfiltered-cols: (10-12) - │ │ │ ├── inner-join (hash) - │ │ │ │ ├── columns: s_symb:13(int!null) s_st_id:14(int!null) s_ex_id:15(int!null) ex_id:18(int!null) - │ │ │ │ ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-more) + │ │ │ ├── project + │ │ │ │ ├── columns: ex_id:18(int!null) s_symb:13(int!null) s_st_id:14(int!null) s_ex_id:15(int!null) │ │ │ │ ├── key: (13) │ │ │ │ ├── fd: (13)-->(14,15), (15)==(18), (18)==(15) - │ │ │ │ ├── prune: (13,14) - │ │ │ │ ├── interesting orderings: (+13) (+18) + │ │ │ │ ├── prune: (13-15,18) + │ │ │ │ ├── interesting orderings: (+13) │ │ │ │ ├── unfiltered-cols: (13-17) │ │ │ │ ├── scan security │ │ │ │ │ ├── columns: s_symb:13(int!null) s_st_id:14(int!null) s_ex_id:15(int!null) @@ -2849,16 +2827,8 @@ limit │ │ │ │ │ ├── prune: (13-15) │ │ │ │ │ ├── interesting orderings: (+13) │ │ │ │ │ └── unfiltered-cols: (13-17) - │ │ │ │ ├── scan exchange - │ │ │ │ │ ├── columns: ex_id:18(int!null) - │ │ │ │ │ ├── key: (18) - │ │ │ │ │ ├── prune: (18) - │ │ │ │ │ ├── interesting orderings: (+18) - │ │ │ │ │ └── unfiltered-cols: (18-20) - │ │ │ │ └── filters - │ │ │ │ └── eq [type=bool, outer=(15,18), constraints=(/15: (/NULL - ]; /18: (/NULL - ]), fd=(15)==(18), (18)==(15)] - │ │ │ │ ├── variable: ex_id:18 [type=int] - │ │ │ │ └── variable: s_ex_id:15 [type=int] + │ │ │ │ └── projections + │ │ │ │ └── variable: s_ex_id:15 [as=ex_id:18, type=int, outer=(15)] │ │ │ └── filters (true) │ │ └── filters (true) │ └── filters diff --git a/pkg/sql/opt/norm/general_funcs.go b/pkg/sql/opt/norm/general_funcs.go index 81ee94ae4453..006d6bf099a7 100644 --- a/pkg/sql/opt/norm/general_funcs.go +++ b/pkg/sql/opt/norm/general_funcs.go @@ -305,6 +305,18 @@ func (c *CustomFuncs) MakeBoolCol() opt.ColumnID { return c.mem.Metadata().AddColumn("", types.Bool) } +// CanRemapCols returns true if it's possible to remap every column in the +// "from" set to a column in the "to" set using the given FDs. +func (c *CustomFuncs) CanRemapCols(from, to opt.ColSet, fds *props.FuncDepSet) bool { + for col, ok := from.Next(0); ok; col, ok = from.Next(col + 1) { + if !fds.ComputeEquivGroup(col).Intersects(to) { + // It is not possible to remap this column to one from the "to" set. + return false + } + } + return true +} + // ---------------------------------------------------------------------- // // Outer column functions @@ -567,6 +579,11 @@ func (c *CustomFuncs) sharedProps(e opt.Expr) *props.Shared { } } +// FuncDeps retrieves the FuncDepSet for the given expression. +func (c *CustomFuncs) FuncDeps(expr memo.RelExpr) *props.FuncDepSet { + return &expr.Relational().FuncDeps +} + // ---------------------------------------------------------------------- // // Ordering functions diff --git a/pkg/sql/opt/norm/project_funcs.go b/pkg/sql/opt/norm/project_funcs.go index ae32774e7324..258337d4eeea 100644 --- a/pkg/sql/opt/norm/project_funcs.go +++ b/pkg/sql/opt/norm/project_funcs.go @@ -15,6 +15,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/sql/opt" "github.com/cockroachdb/cockroach/pkg/sql/opt/memo" + "github.com/cockroachdb/cockroach/pkg/sql/opt/props" "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" "github.com/cockroachdb/cockroach/pkg/sql/types" "github.com/cockroachdb/cockroach/pkg/util/intsets" @@ -849,3 +850,53 @@ func (c *CustomFuncs) IsStaticTuple(expr opt.ScalarExpr) bool { } return false } + +// ProjectRemappedCols creates a projection for each column in the "from" set +// that is not in the "to" set, mapping it to an equivalent column in the "to" +// set. ProjectRemappedCols panics if this is not possible. +func (c *CustomFuncs) ProjectRemappedCols( + from, to opt.ColSet, fds *props.FuncDepSet, +) (projections memo.ProjectionsExpr) { + for col, ok := from.Next(0); ok; col, ok = from.Next(col + 1) { + if !to.Contains(col) { + candidates := fds.ComputeEquivGroup(col) + candidates.IntersectionWith(to) + if candidates.Empty() { + panic(errors.AssertionFailedf("cannot remap column %v", col)) + } + toCol, _ := candidates.Next(0) + projections = append( + projections, + c.f.ConstructProjectionsItem(c.f.ConstructVariable(toCol), col), + ) + } + } + return projections +} + +// RemapProjectionCols remaps column references in the given projections to +// refer to the "to" set. +func (c *CustomFuncs) RemapProjectionCols( + projections memo.ProjectionsExpr, to opt.ColSet, fds *props.FuncDepSet, +) memo.ProjectionsExpr { + getReplacement := func(col opt.ColumnID) opt.ColumnID { + candidates := fds.ComputeEquivGroup(col) + candidates.IntersectionWith(to) + if candidates.Empty() { + panic(errors.AssertionFailedf("cannot remap column")) + } + replacement, _ := candidates.Next(0) + return replacement + } + + // Replace any references to the "from" columns in the projections. + var replace ReplaceFunc + replace = func(e opt.Expr) opt.Expr { + if v, ok := e.(*memo.VariableExpr); ok && !to.Contains(v.Col) { + // This variable needs to be remapped. + return c.f.ConstructVariable(getReplacement(v.Col)) + } + return c.f.Replace(e, replace) + } + return *(replace(&projections).(*memo.ProjectionsExpr)) +} diff --git a/pkg/sql/opt/norm/rules/groupby.opt b/pkg/sql/opt/norm/rules/groupby.opt index 196aceabdc93..33245f655f50 100644 --- a/pkg/sql/opt/norm/rules/groupby.opt +++ b/pkg/sql/opt/norm/rules/groupby.opt @@ -58,6 +58,10 @@ # EliminateJoinUnderGroupByLeft should stay at the top of the file so that it # has a chance to fire before rules like EliminateDistinctOn that might prevent # matching. +# +# Similar to the join-elimination rules that match on Project operators, +# EliminateJoinUnderGroupByLeft can remap references to the right input of the +# join to refer to equivalent columns on the left input. [EliminateJoinUnderGroupByLeft, Normalize] (GroupBy | ScalarGroupBy | DistinctOn $input:(InnerJoin | LeftJoin $left:*) @@ -67,18 +71,23 @@ $ordering $leftCols:(OutputCols $left) ) & - (ColsAreSubset - (UnionCols + (CanRemapCols + $toRemap:(UnionCols $groupingCols (AggregationOuterCols $aggs) ) $leftCols + $fds:(FuncDeps $input) ) & (CanEliminateJoinUnderGroupByLeft $input $aggs) ) => ((OpName) - $left + (Project + $left + (ProjectRemappedCols $toRemap $leftCols $fds) + $leftCols + ) $aggs (MakeGrouping $groupingCols @@ -97,18 +106,23 @@ $ordering $rightCols:(OutputCols $right) ) & - (ColsAreSubset - (UnionCols + (CanRemapCols + $toRemap:(UnionCols $groupingCols (AggregationOuterCols $aggs) ) $rightCols + $fds:(FuncDeps $input) ) & (CanEliminateJoinUnderGroupByRight $input $aggs) ) => ((OpName) - $right + (Project + $right + (ProjectRemappedCols $toRemap $rightCols $fds) + $rightCols + ) $aggs (MakeGrouping $groupingCols diff --git a/pkg/sql/opt/norm/rules/project.opt b/pkg/sql/opt/norm/rules/project.opt index 7f102bc6db01..aba9b4c114f1 100644 --- a/pkg/sql/opt/norm/rules/project.opt +++ b/pkg/sql/opt/norm/rules/project.opt @@ -10,20 +10,36 @@ # # Note: EliminateJoinUnderProjectLeft should stay above EliminateProject so that # it has a chance to fire before the Project can be removed. +# +# It is possible for references to the right input of the join to be replaced by +# equivalent columns from the left input. This is handled by adding projections +# that map the left column to the equivalent right column (leftCol AS rightCol). [EliminateJoinUnderProjectLeft, Normalize] (Project $join:(InnerJoin | LeftJoin $left:* $right:*) & (JoinDoesNotDuplicateLeftRows $join) & (JoinPreservesLeftRows $join) - $projections:* & - ^(AreProjectionsCorrelated - $projections - $rightCols:(OutputCols $right) + $projections:* + $passthrough:* & + (CanRemapCols + (UnionCols + $passthrough + (ProjectionOuterCols $projections) + ) + $leftCols:(OutputCols $left) + $fds:(FuncDeps $join) ) - $passthrough:* & ^(ColsIntersect $passthrough $rightCols) ) => -(Project $left $projections $passthrough) +(Project + $left + (MergeProjections + (RemapProjectionCols $projections $leftCols $fds) + (ProjectRemappedCols $passthrough $leftCols $fds) + $passthrough + ) + (DifferenceCols $passthrough (OutputCols $right)) +) # EliminateJoinUnderProjectRight mirrors EliminateJoinUnderProjectLeft, except # that it only matches InnerJoins. @@ -32,15 +48,27 @@ $join:(InnerJoin $left:* $right:*) & (JoinDoesNotDuplicateRightRows $join) & (JoinPreservesRightRows $join) - $projections:* & - ^(AreProjectionsCorrelated - $projections - $leftCols:(OutputCols $left) + $projections:* + $passthrough:* & + (CanRemapCols + (UnionCols + $passthrough + (ProjectionOuterCols $projections) + ) + $rightCols:(OutputCols $right) + $fds:(FuncDeps $join) ) - $passthrough:* & ^(ColsIntersect $passthrough $leftCols) ) => -(Project $right $projections $passthrough) +(Project + $right + (MergeProjections + (RemapProjectionCols $projections $rightCols $fds) + (ProjectRemappedCols $passthrough $rightCols $fds) + $passthrough + ) + (DifferenceCols $passthrough (OutputCols $left)) +) # EliminateProject discards a Project operator which is not adding or removing # columns. diff --git a/pkg/sql/opt/norm/testdata/rules/project b/pkg/sql/opt/norm/testdata/rules/project index ce57a23bcb20..166298d40ac9 100644 --- a/pkg/sql/opt/norm/testdata/rules/project +++ b/pkg/sql/opt/norm/testdata/rules/project @@ -113,6 +113,21 @@ project └── projections └── 1 [as="?column?":12] +# The right column can be remapped to a left column. +norm expect=EliminateJoinUnderProjectLeft +SELECT b.j, b1.x FROM b INNER JOIN b AS b1 ON b.x = b1.x +---- +project + ├── columns: j:3 x:6!null + ├── key: (6) + ├── fd: (6)-->(3) + ├── scan b + │ ├── columns: b.x:1!null b.j:3 + │ ├── key: (1) + │ └── fd: (1)-->(3) + └── projections + └── b.x:1 [as=b1.x:6, outer=(1)] + # No-op case because the cross join may duplicate left rows. norm expect-not=EliminateJoinUnderProjectLeft SELECT 1 FROM b LEFT JOIN a ON True @@ -219,6 +234,38 @@ project └── filters └── x:1 = r1:10 [outer=(1,10), constraints=(/1: (/NULL - ]; /10: (/NULL - ]), fd=(1)==(10), (10)==(1)] +# Regression test for #102614 - parent.id should be mapped to child.id to allow +# join elimination to proceed. +exec-ddl +CREATE TABLE parent ( + id INT PRIMARY KEY +) +---- + +exec-ddl +CREATE TABLE child ( + id INT PRIMARY KEY, + parent_id INT NOT NULL REFERENCES parent(id) +) +---- + +opt +SELECT + id, + (SELECT child.parent_id FROM parent WHERE id = child.parent_id) +FROM child +---- +project + ├── columns: id:1!null parent_id:9!null + ├── key: (1) + ├── fd: (1)-->(9) + ├── scan child + │ ├── columns: child.id:1!null child.parent_id:2!null + │ ├── key: (1) + │ └── fd: (1)-->(2) + └── projections + └── child.parent_id:2 [as=parent_id:9, outer=(2)] + # -------------------------------------------------- # EliminateJoinUnderProjectRight # -------------------------------------------------- @@ -241,24 +288,43 @@ scan fks ├── key: (7) └── fd: (7)-->(8) +# The left column can be remapped to a right column. +norm expect=EliminateJoinUnderProjectRight +SELECT b.x, b1.j FROM b INNER JOIN b AS b1 ON b.x = b1.x +---- +project + ├── columns: x:1!null j:8 + ├── key: (1) + ├── fd: (1)-->(8) + ├── scan b [as=b1] + │ ├── columns: b1.x:6!null b1.j:8 + │ ├── key: (6) + │ └── fd: (6)-->(8) + └── projections + └── b1.x:6 [as=b.x:1, outer=(6)] + # No-op case because columns from the right side of a LeftJoin are being # projected. norm expect-not=EliminateJoinUnderProjectRight -SELECT b.x, b1.x FROM b LEFT JOIN b AS b1 ON b.x = b1.x +SELECT b.j, b1.j FROM b LEFT JOIN b AS b1 ON b.x = b1.x ---- -inner-join (hash) - ├── columns: x:1!null x:6!null - ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one) - ├── key: (6) - ├── fd: (1)==(6), (6)==(1) - ├── scan b - │ ├── columns: b.x:1!null - │ └── key: (1) - ├── scan b [as=b1] - │ ├── columns: b1.x:6!null - │ └── key: (6) - └── filters - └── b.x:1 = b1.x:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)] +project + ├── columns: j:3 j:8 + └── inner-join (hash) + ├── columns: b.x:1!null b.j:3 b1.x:6!null b1.j:8 + ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one) + ├── key: (6) + ├── fd: (1)-->(3), (6)-->(8), (1)==(6), (6)==(1) + ├── scan b + │ ├── columns: b.x:1!null b.j:3 + │ ├── key: (1) + │ └── fd: (1)-->(3) + ├── scan b [as=b1] + │ ├── columns: b1.x:6!null b1.j:8 + │ ├── key: (6) + │ └── fd: (6)-->(8) + └── filters + └── b.x:1 = b1.x:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)] # -------------------------------------------------- # EliminateProject diff --git a/pkg/sql/opt/norm/testdata/rules/prune_cols b/pkg/sql/opt/norm/testdata/rules/prune_cols index bdea6869741e..d9a3ccd17487 100644 --- a/pkg/sql/opt/norm/testdata/rules/prune_cols +++ b/pkg/sql/opt/norm/testdata/rules/prune_cols @@ -5313,7 +5313,7 @@ CREATE TABLE c100478 ( # A join which doesn't prune columns which could potentially appear in derived # ON clause conditions should not result in infinite rule recursion. -norm expect=(EliminateGroupByProject,PruneJoinLeftCols,PruneJoinRightCols) +norm expect=(EliminateGroupByProject,PruneJoinLeftCols,PruneJoinRightCols) disable=(EliminateJoinUnderProjectRight,EliminateJoinUnderGroupByRight) SELECT p.id FROM p100478 p JOIN c100478 ON p_id = p.id GROUP BY p.id ---- distinct-on diff --git a/pkg/sql/opt/norm/testdata/rules/scalar b/pkg/sql/opt/norm/testdata/rules/scalar index 8d710fe008a8..6b56dded28d8 100644 --- a/pkg/sql/opt/norm/testdata/rules/scalar +++ b/pkg/sql/opt/norm/testdata/rules/scalar @@ -1747,21 +1747,17 @@ project ├── group-by (hash) │ ├── columns: b.k:1!null a.k:8!null array_agg:17!null │ ├── grouping columns: b.k:1!null - │ ├── key: (8) - │ ├── fd: (1)==(8), (8)==(1), (8)-->(17), (1)-->(8,17) - │ ├── inner-join (hash) - │ │ ├── columns: b.k:1!null a.k:8!null - │ │ ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one) - │ │ ├── key: (8) + │ ├── key: (1) + │ ├── fd: (1)==(8), (8)==(1), (1)-->(8,17) + │ ├── project + │ │ ├── columns: a.k:8!null b.k:1!null + │ │ ├── key: (1) │ │ ├── fd: (1)==(8), (8)==(1) │ │ ├── scan a [as=b] │ │ │ ├── columns: b.k:1!null │ │ │ └── key: (1) - │ │ ├── scan a - │ │ │ ├── columns: a.k:8!null - │ │ │ └── key: (8) - │ │ └── filters - │ │ └── a.k:8 = b.k:1 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)] + │ │ └── projections + │ │ └── b.k:1 [as=a.k:8, outer=(1)] │ └── aggregations │ ├── array-agg [as=array_agg:17, outer=(8)] │ │ └── a.k:8 @@ -1869,31 +1865,22 @@ project │ ├── grouping columns: c.k:1!null │ ├── key: (1) │ ├── fd: ()-->(25), (1)-->(25,26) - │ ├── inner-join (hash) - │ │ ├── columns: c.k:1!null b.k:8!null array:22 canary:25!null - │ │ ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one) - │ │ ├── key: (8) - │ │ ├── fd: ()-->(25), (8)-->(22), (1)==(8), (8)==(1) - │ │ ├── scan a [as=c] - │ │ │ ├── columns: c.k:1!null - │ │ │ └── key: (1) - │ │ ├── project - │ │ │ ├── columns: canary:25!null array:22 b.k:8!null - │ │ │ ├── key: (8) - │ │ │ ├── fd: ()-->(25), (8)-->(22) - │ │ │ ├── scan a [as=b] - │ │ │ │ ├── columns: b.k:8!null - │ │ │ │ └── key: (8) - │ │ │ └── projections - │ │ │ ├── true [as=canary:25] - │ │ │ └── indirection [as=array:22, subquery] - │ │ │ ├── array-flatten - │ │ │ │ └── scan a - │ │ │ │ ├── columns: k:15!null - │ │ │ │ └── key: (15) - │ │ │ └── 1 - │ │ └── filters - │ │ └── b.k:8 = c.k:1 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)] + │ ├── project + │ │ ├── columns: c.k:1!null canary:25!null array:22 + │ │ ├── key: (1,22) + │ │ ├── fd: ()-->(25) + │ │ ├── scan a [as=b] + │ │ │ ├── columns: b.k:8!null + │ │ │ └── key: (8) + │ │ └── projections + │ │ ├── b.k:8 [as=c.k:1, outer=(8)] + │ │ ├── true [as=canary:25] + │ │ └── indirection [as=array:22, subquery] + │ │ ├── array-flatten + │ │ │ └── scan a + │ │ │ ├── columns: k:15!null + │ │ │ └── key: (15) + │ │ └── 1 │ └── aggregations │ ├── array-agg [as=array_agg:26, outer=(22)] │ │ └── array:22 @@ -1932,13 +1919,12 @@ project │ │ │ │ ├── cardinality: [0 - 1] │ │ │ │ ├── key: () │ │ │ │ ├── fd: ()-->(15,24) - │ │ │ │ ├── inner-join (hash) - │ │ │ │ │ ├── columns: b.k:8!null a.k:15!null + │ │ │ │ ├── project + │ │ │ │ │ ├── columns: a.k:15!null │ │ │ │ │ ├── outer: (1) │ │ │ │ │ ├── cardinality: [0 - 1] - │ │ │ │ │ ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one) │ │ │ │ │ ├── key: () - │ │ │ │ │ ├── fd: ()-->(8,15), (15)==(8), (8)==(15) + │ │ │ │ │ ├── fd: ()-->(15) │ │ │ │ │ ├── select │ │ │ │ │ │ ├── columns: b.k:8!null │ │ │ │ │ │ ├── outer: (1) @@ -1950,11 +1936,8 @@ project │ │ │ │ │ │ │ └── key: (8) │ │ │ │ │ │ └── filters │ │ │ │ │ │ └── b.k:8 = c.k:1 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)] - │ │ │ │ │ ├── scan a - │ │ │ │ │ │ ├── columns: a.k:15!null - │ │ │ │ │ │ └── key: (15) - │ │ │ │ │ └── filters - │ │ │ │ │ └── a.k:15 = b.k:8 [outer=(8,15), constraints=(/8: (/NULL - ]; /15: (/NULL - ]), fd=(8)==(15), (15)==(8)] + │ │ │ │ │ └── projections + │ │ │ │ │ └── b.k:8 [as=a.k:15, outer=(8)] │ │ │ │ └── aggregations │ │ │ │ ├── array-agg [as=array_agg:24, outer=(15)] │ │ │ │ │ └── a.k:15 diff --git a/pkg/sql/opt/testutils/opttester/forcing_opt.go b/pkg/sql/opt/testutils/opttester/forcing_opt.go index 7ff4b7023385..1e591093d4ff 100644 --- a/pkg/sql/opt/testutils/opttester/forcing_opt.go +++ b/pkg/sql/opt/testutils/opttester/forcing_opt.go @@ -73,15 +73,15 @@ func newForcingOptimizer( fo.o.Factory().SetDisabledRules(tester.Flags.DisableRules) fo.o.NotifyOnMatchedRule(func(ruleName opt.RuleName) bool { + if tester.Flags.DisableRules.Contains(int(ruleName)) { + return false + } if ignoreNormRules && ruleName.IsNormalize() { return true } if fo.remaining == 0 { return false } - if tester.Flags.DisableRules.Contains(int(ruleName)) { - return false - } fo.remaining-- fo.lastMatched = ruleName return true diff --git a/pkg/sql/opt/xform/testdata/external/customer b/pkg/sql/opt/xform/testdata/external/customer index c98bba84bb14..d228f6d65abd 100644 --- a/pkg/sql/opt/xform/testdata/external/customer +++ b/pkg/sql/opt/xform/testdata/external/customer @@ -33,7 +33,7 @@ CREATE TABLE edges ( ) ---- -opt +opt disable=EliminateJoinUnderProjectRight select nodes.id,dst from nodes join edges on edges.dst=nodes.id ---- inner-join (merge) @@ -50,6 +50,18 @@ inner-join (merge) │ └── ordering: +6 └── filters (true) +# The join is actually not necessary because of the foreign-key relation. +opt +select nodes.id,dst from nodes join edges on edges.dst=nodes.id +---- +project + ├── columns: id:1!null dst:6!null + ├── fd: (1)==(6), (6)==(1) + ├── scan edges@edges_auto_index_fk_dst_ref_nodes + │ └── columns: dst:6!null + └── projections + └── dst:6 [as=id:1, outer=(6)] + # ------------------------------------------------------------------------------ # Github Issues 16313/16426: Ensure that STORING index is used to filter unread # articles before index joining to the primary index. diff --git a/pkg/sql/opt/xform/testdata/external/trading b/pkg/sql/opt/xform/testdata/external/trading index 5ad353b93d1a..675e24234807 100644 --- a/pkg/sql/opt/xform/testdata/external/trading +++ b/pkg/sql/opt/xform/testdata/external/trading @@ -964,11 +964,11 @@ sort │ ├── stable │ ├── stats: [rows=139576.9, distinct(45)=82395.9, null(45)=0] │ ├── inner-join (hash) - │ │ ├── columns: transactiondetails.dealerid:1!null transactiondetails.isbuy:2!null transactiondate:3!null transactiondetails.cardid:4!null quantity:5!null transactiondetails.sellprice:6!null transactiondetails.buyprice:7!null transactions.dealerid:11!null transactions.isbuy:12!null date:13!null accountname:14!null customername:15!null id:20!null cardsinfo.dealerid:28!null cardsinfo.cardid:29!null + │ │ ├── columns: transactiondetails.dealerid:1!null transactiondetails.isbuy:2!null transactiondate:3!null transactiondetails.cardid:4!null quantity:5!null transactiondetails.sellprice:6!null transactiondetails.buyprice:7!null transactions.dealerid:11!null transactions.isbuy:12!null date:13!null accountname:14!null customername:15!null id:20!null │ │ ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-more) │ │ ├── stats: [rows=139576.9, distinct(3)=82395.9, null(3)=0, distinct(4)=37420.4, null(4)=0, distinct(20)=37420.4, null(20)=0] - │ │ ├── key: (5,13,29) - │ │ ├── fd: ()-->(1,2,11,12,28), (3-5)-->(6,7), (13)-->(14,15), (3)==(13), (13)==(3), (20)==(4,29), (29)==(4,20), (4)==(20,29) + │ │ ├── key: (5,13,20) + │ │ ├── fd: ()-->(1,2,11,12), (3-5)-->(6,7), (13)-->(14,15), (3)==(13), (13)==(3), (4)==(20), (20)==(4) │ │ ├── inner-join (merge) │ │ │ ├── columns: transactiondetails.dealerid:1!null transactiondetails.isbuy:2!null transactiondate:3!null transactiondetails.cardid:4!null quantity:5!null transactiondetails.sellprice:6!null transactiondetails.buyprice:7!null transactions.dealerid:11!null transactions.isbuy:12!null date:13!null accountname:14!null customername:15!null │ │ │ ├── left ordering: +3 @@ -1000,24 +1000,18 @@ sort │ │ │ │ ├── accountname:14 != 'someaccount' [outer=(14), constraints=(/14: (/NULL - /'someaccount') [/e'someaccount\x00' - ]; tight)] │ │ │ │ └── customername:15 != 'somecustomer' [outer=(15), constraints=(/15: (/NULL - /'somecustomer') [/e'somecustomer\x00' - ]; tight)] │ │ │ └── filters (true) - │ │ ├── inner-join (hash) - │ │ │ ├── columns: id:20!null cardsinfo.dealerid:28!null cardsinfo.cardid:29!null - │ │ │ ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one) - │ │ │ ├── stats: [rows=58333.33, distinct(20)=37420.4, null(20)=0, distinct(28)=1, null(28)=0, distinct(29)=37420.4, null(29)=0] - │ │ │ ├── key: (29) - │ │ │ ├── fd: ()-->(28), (20)==(29), (29)==(20) + │ │ ├── project + │ │ │ ├── columns: id:20!null + │ │ │ ├── stats: [rows=58333.33, distinct(20)=37420.4, null(20)=0] + │ │ │ ├── key: (20) │ │ │ ├── scan cardsinfo@cardsinfoversionindex │ │ │ │ ├── columns: cardsinfo.dealerid:28!null cardsinfo.cardid:29!null │ │ │ │ ├── constraint: /28/36: [/1 - /1] │ │ │ │ ├── stats: [rows=58333.33, distinct(28)=1, null(28)=0, distinct(29)=37420.4, null(29)=0] │ │ │ │ ├── key: (29) │ │ │ │ └── fd: ()-->(28) - │ │ │ ├── scan cards@cardsnamesetnumber - │ │ │ │ ├── columns: id:20!null - │ │ │ │ ├── stats: [rows=57000, distinct(20)=57000, null(20)=0] - │ │ │ │ └── key: (20) - │ │ │ └── filters - │ │ │ └── id:20 = cardsinfo.cardid:29 [outer=(20,29), constraints=(/20: (/NULL - ]; /29: (/NULL - ]), fd=(20)==(29), (29)==(20)] + │ │ │ └── projections + │ │ │ └── cardsinfo.cardid:29 [as=id:20, outer=(29)] │ │ └── filters │ │ └── id:20 = transactiondetails.cardid:4 [outer=(4,20), constraints=(/4: (/NULL - ]; /20: (/NULL - ]), fd=(4)==(20), (20)==(4)] │ └── projections diff --git a/pkg/sql/opt/xform/testdata/external/trading-mutation b/pkg/sql/opt/xform/testdata/external/trading-mutation index a518cf30ee45..371b80e7e612 100644 --- a/pkg/sql/opt/xform/testdata/external/trading-mutation +++ b/pkg/sql/opt/xform/testdata/external/trading-mutation @@ -968,11 +968,11 @@ sort │ ├── stable │ ├── stats: [rows=139576.9, distinct(53)=82395.9, null(53)=0] │ ├── inner-join (hash) - │ │ ├── columns: transactiondetails.dealerid:1!null transactiondetails.isbuy:2!null transactiondate:3!null transactiondetails.cardid:4!null quantity:5!null transactiondetails.sellprice:6!null transactiondetails.buyprice:7!null transactions.dealerid:13!null transactions.isbuy:14!null date:15!null accountname:16!null customername:17!null id:24!null cardsinfo.dealerid:32!null cardsinfo.cardid:33!null + │ │ ├── columns: transactiondetails.dealerid:1!null transactiondetails.isbuy:2!null transactiondate:3!null transactiondetails.cardid:4!null quantity:5!null transactiondetails.sellprice:6!null transactiondetails.buyprice:7!null transactions.dealerid:13!null transactions.isbuy:14!null date:15!null accountname:16!null customername:17!null id:24!null │ │ ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-more) │ │ ├── stats: [rows=139576.9, distinct(3)=82395.9, null(3)=0, distinct(4)=37420.4, null(4)=0, distinct(24)=37420.4, null(24)=0] - │ │ ├── key: (5,15,33) - │ │ ├── fd: ()-->(1,2,13,14,32), (3-5)-->(6,7), (15)-->(16,17), (3)==(15), (15)==(3), (24)==(4,33), (33)==(4,24), (4)==(24,33) + │ │ ├── key: (5,15,24) + │ │ ├── fd: ()-->(1,2,13,14), (3-5)-->(6,7), (15)-->(16,17), (3)==(15), (15)==(3), (4)==(24), (24)==(4) │ │ ├── inner-join (merge) │ │ │ ├── columns: transactiondetails.dealerid:1!null transactiondetails.isbuy:2!null transactiondate:3!null transactiondetails.cardid:4!null quantity:5!null transactiondetails.sellprice:6!null transactiondetails.buyprice:7!null transactions.dealerid:13!null transactions.isbuy:14!null date:15!null accountname:16!null customername:17!null │ │ │ ├── left ordering: +3 @@ -1004,24 +1004,18 @@ sort │ │ │ │ ├── accountname:16 != 'someaccount' [outer=(16), constraints=(/16: (/NULL - /'someaccount') [/e'someaccount\x00' - ]; tight)] │ │ │ │ └── customername:17 != 'somecustomer' [outer=(17), constraints=(/17: (/NULL - /'somecustomer') [/e'somecustomer\x00' - ]; tight)] │ │ │ └── filters (true) - │ │ ├── inner-join (hash) - │ │ │ ├── columns: id:24!null cardsinfo.dealerid:32!null cardsinfo.cardid:33!null - │ │ │ ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one) - │ │ │ ├── stats: [rows=58333.33, distinct(24)=37420.4, null(24)=0, distinct(32)=1, null(32)=0, distinct(33)=37420.4, null(33)=0] - │ │ │ ├── key: (33) - │ │ │ ├── fd: ()-->(32), (24)==(33), (33)==(24) + │ │ ├── project + │ │ │ ├── columns: id:24!null + │ │ │ ├── stats: [rows=58333.33, distinct(24)=37420.4, null(24)=0] + │ │ │ ├── key: (24) │ │ │ ├── scan cardsinfo@cardsinfoversionindex │ │ │ │ ├── columns: cardsinfo.dealerid:32!null cardsinfo.cardid:33!null │ │ │ │ ├── constraint: /32/40: [/1 - /1] │ │ │ │ ├── stats: [rows=58333.33, distinct(32)=1, null(32)=0, distinct(33)=37420.4, null(33)=0] │ │ │ │ ├── key: (33) │ │ │ │ └── fd: ()-->(32) - │ │ │ ├── scan cards@cardsnamesetnumber - │ │ │ │ ├── columns: id:24!null - │ │ │ │ ├── stats: [rows=57000, distinct(24)=57000, null(24)=0] - │ │ │ │ └── key: (24) - │ │ │ └── filters - │ │ │ └── id:24 = cardsinfo.cardid:33 [outer=(24,33), constraints=(/24: (/NULL - ]; /33: (/NULL - ]), fd=(24)==(33), (33)==(24)] + │ │ │ └── projections + │ │ │ └── cardsinfo.cardid:33 [as=id:24, outer=(33)] │ │ └── filters │ │ └── id:24 = transactiondetails.cardid:4 [outer=(4,24), constraints=(/4: (/NULL - ]; /24: (/NULL - ]), fd=(4)==(24), (24)==(4)] │ └── projections diff --git a/pkg/sql/opt/xform/testdata/rules/join b/pkg/sql/opt/xform/testdata/rules/join index 9ce11a9aa21c..3d269029b927 100644 --- a/pkg/sql/opt/xform/testdata/rules/join +++ b/pkg/sql/opt/xform/testdata/rules/join @@ -1911,7 +1911,7 @@ inner-join (lookup xyz@xy) └── y:8 = 1 [outer=(8), constraints=(/8: [/1 - /1]; tight), fd=()-->(8)] # Verify case where we generate multiple merge-joins. -memo +memo disable=(EliminateJoinUnderProjectLeft,EliminateJoinUnderProjectRight) SELECT * FROM stu AS l JOIN stu AS r ON (l.s, l.t, l.u) = (r.s, r.t, r.u) ---- memo (optimized, ~19KB, required=[presentation: s:1,t:2,u:3,s:6,t:7,u:8]) @@ -1951,7 +1951,7 @@ memo (optimized, ~19KB, required=[presentation: s:1,t:2,u:3,s:6,t:7,u:8]) ├── G13: (variable l.u) └── G14: (variable r.u) -exploretrace rule=GenerateMergeJoins +exploretrace rule=GenerateMergeJoins disable=(EliminateJoinUnderProjectLeft,EliminateJoinUnderProjectRight) SELECT * FROM stu AS l JOIN stu AS r ON (l.s, l.t, l.u) = (r.s, r.t, r.u) ---- ---- diff --git a/pkg/sql/opt/xform/testdata/rules/join_order b/pkg/sql/opt/xform/testdata/rules/join_order index 138dfe4f49db..b10bbce9ba0b 100644 --- a/pkg/sql/opt/xform/testdata/rules/join_order +++ b/pkg/sql/opt/xform/testdata/rules/join_order @@ -752,7 +752,7 @@ exec-ddl CREATE TABLE a (id INT8 PRIMARY KEY) ---- -opt set=reorder_joins_limit=3 +opt set=reorder_joins_limit=3 disable=(EliminateJoinUnderProjectLeft,EliminateJoinUnderProjectRight) SELECT 1 FROM