Skip to content

Commit 0e5c06a

Browse files
committed
opt: do not remap columns with non-identical types
This commit limits the remapping of column during join elimination to only destination columns with identical types to their equivalent source columns. This prevents the optimizer from producing an incorrect query plan. See the test cases for examples. Fixes cockroachdb#107850 There is no release note because this bug does not exist in any releases. Release note: None
1 parent c3f3951 commit 0e5c06a

File tree

4 files changed

+138
-21
lines changed

4 files changed

+138
-21
lines changed

pkg/sql/logictest/testdata/logic_test/join

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1217,3 +1217,24 @@ SELECT * FROM (SELECT * FROM t106371 ORDER BY y LIMIT 1) a
12171217
JOIN (SELECT DISTINCT ON (x) * FROM (SELECT * FROM t106371 WHERE y = 2)) b ON a.x = b.x;
12181218
----
12191219
1 1 1 2
1220+
1221+
# Regression test for #107850. Do not eliminate joins and remap columns that are
1222+
# equivalent but have non-identical types.
1223+
subtest regression_107850
1224+
1225+
statement ok
1226+
CREATE TABLE t107850a (
1227+
d2 DECIMAL(10, 2) NOT NULL UNIQUE
1228+
);
1229+
CREATE TABLE t107850b (
1230+
d0 DECIMAL(10, 0) NOT NULL REFERENCES t107850a (d2)
1231+
);
1232+
INSERT INTO t107850a VALUES (1.00);
1233+
INSERT INTO t107850b VALUES (1);
1234+
1235+
query R
1236+
SELECT d2 FROM t107850a JOIN t107850b ON d2 = d0
1237+
----
1238+
1.00
1239+
1240+
subtest end

pkg/sql/opt/norm/general_funcs.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,15 +308,34 @@ func (c *CustomFuncs) MakeBoolCol() opt.ColumnID {
308308
// CanRemapCols returns true if it's possible to remap every column in the
309309
// "from" set to a column in the "to" set using the given FDs.
310310
func (c *CustomFuncs) CanRemapCols(from, to opt.ColSet, fds *props.FuncDepSet) bool {
311-
for col, ok := from.Next(0); ok; col, ok = from.Next(col + 1) {
312-
if !fds.ComputeEquivGroup(col).Intersects(to) {
311+
for fromCol, ok := from.Next(0); ok; fromCol, ok = from.Next(fromCol + 1) {
312+
if _, ok := c.remapColWithIdenticalType(fromCol, to, fds); !ok {
313313
// It is not possible to remap this column to one from the "to" set.
314314
return false
315315
}
316316
}
317317
return true
318318
}
319319

320+
// remapColWithIdenticalType returns a column in the "to" set that is equivalent
321+
// to "fromCol" and has the identical type. It returns ok=false if no such
322+
// column exists.
323+
func (c *CustomFuncs) remapColWithIdenticalType(
324+
fromCol opt.ColumnID, to opt.ColSet, fds *props.FuncDepSet,
325+
) (_ opt.ColumnID, ok bool) {
326+
md := c.mem.Metadata()
327+
fromColType := md.ColumnMeta(fromCol).Type
328+
equivCols := fds.ComputeEquivGroup(fromCol)
329+
equivCols.IntersectionWith(to)
330+
for equivCol, ok := equivCols.Next(0); ok; equivCol, ok = equivCols.Next(equivCol + 1) {
331+
equivColType := md.ColumnMeta(equivCol).Type
332+
if fromColType.Identical(equivColType) {
333+
return equivCol, true
334+
}
335+
}
336+
return 0, false
337+
}
338+
320339
// ----------------------------------------------------------------------
321340
//
322341
// Outer column functions

pkg/sql/opt/norm/project_funcs.go

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -859,14 +859,10 @@ func (c *CustomFuncs) ProjectRemappedCols(
859859
) (projections memo.ProjectionsExpr) {
860860
for col, ok := from.Next(0); ok; col, ok = from.Next(col + 1) {
861861
if !to.Contains(col) {
862-
// TODO(mgartner): We don't need to compute the entire equivalence group,
863-
// we only need to find the first equivalent column that's in the to set.
864-
candidates := fds.ComputeEquivGroup(col)
865-
candidates.IntersectionWith(to)
866-
if candidates.Empty() {
862+
toCol, ok := c.remapColWithIdenticalType(col, to, fds)
863+
if !ok {
867864
panic(errors.AssertionFailedf("cannot remap column %v", col))
868865
}
869-
toCol, _ := candidates.Next(0)
870866
projections = append(
871867
projections,
872868
c.f.ConstructProjectionsItem(c.f.ConstructVariable(toCol), col),
@@ -881,18 +877,6 @@ func (c *CustomFuncs) ProjectRemappedCols(
881877
func (c *CustomFuncs) RemapProjectionCols(
882878
projections memo.ProjectionsExpr, to opt.ColSet, fds *props.FuncDepSet,
883879
) memo.ProjectionsExpr {
884-
getReplacement := func(col opt.ColumnID) opt.ColumnID {
885-
// TODO(mgartner): We don't need to compute the entire equivalence group, we
886-
// only need to find the first equivalent column that's in the to set.
887-
candidates := fds.ComputeEquivGroup(col)
888-
candidates.IntersectionWith(to)
889-
if candidates.Empty() {
890-
panic(errors.AssertionFailedf("cannot remap column"))
891-
}
892-
replacement, _ := candidates.Next(0)
893-
return replacement
894-
}
895-
896880
// Use the projection outer columns to filter out columns that are synthesized
897881
// within the projections themselves (e.g. in a subquery).
898882
outerCols := c.ProjectionOuterCols(projections)
@@ -902,7 +886,11 @@ func (c *CustomFuncs) RemapProjectionCols(
902886
replace = func(e opt.Expr) opt.Expr {
903887
if v, ok := e.(*memo.VariableExpr); ok && !to.Contains(v.Col) && outerCols.Contains(v.Col) {
904888
// This variable needs to be remapped.
905-
return c.f.ConstructVariable(getReplacement(v.Col))
889+
toCol, ok := c.remapColWithIdenticalType(v.Col, to, fds)
890+
if !ok {
891+
panic(errors.AssertionFailedf("cannot remap column %v", v.Col))
892+
}
893+
return c.f.ConstructVariable(toCol)
906894
}
907895
return c.f.Replace(e, replace)
908896
}

pkg/sql/opt/norm/testdata/rules/project

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,14 @@ project
266266
└── projections
267267
└── child.parent_id:2 [as=parent_id:9, outer=(2)]
268268

269+
exec-ddl
270+
DROP TABLE child
271+
----
272+
273+
exec-ddl
274+
DROP TABLE parent
275+
----
276+
269277
# The join can be eliminated as long as it doesn't require remapping with
270278
# OptimizerUseImprovedJoinElimination disabled.
271279
norm set=optimizer_use_improved_join_elimination=false expect=EliminateJoinUnderProjectLeft
@@ -395,6 +403,87 @@ project
395403
└── projections
396404
└── NULL [as="?column?":5]
397405

406+
# Regression test for #107850 - columns should not be mapped if their types are
407+
# not identical.
408+
exec-ddl
409+
CREATE TABLE parent (
410+
id DECIMAL(10, 2) PRIMARY KEY
411+
)
412+
----
413+
414+
exec-ddl
415+
CREATE TABLE child (
416+
id INT PRIMARY KEY,
417+
parent_id DECIMAL(10, 0) NOT NULL REFERENCES parent(id),
418+
parent_id2 DECIMAL(10, 2) NOT NULL REFERENCES parent(id)
419+
)
420+
----
421+
422+
norm expect-not=EliminateJoinUnderProjectRight
423+
SELECT parent.id FROM parent JOIN child ON parent.id = child.parent_id
424+
----
425+
project
426+
├── columns: id:1!null
427+
├── immutable
428+
└── inner-join (hash)
429+
├── columns: parent.id:1!null parent_id:5!null
430+
├── multiplicity: left-rows(zero-or-more), right-rows(exactly-one)
431+
├── immutable
432+
├── fd: (1)==(5), (5)==(1)
433+
├── scan parent
434+
│ ├── columns: parent.id:1!null
435+
│ └── key: (1)
436+
├── scan child
437+
│ └── columns: parent_id:5!null
438+
└── filters
439+
└── parent.id:1 = parent_id:5 [outer=(1,5), immutable, constraints=(/1: (/NULL - ]; /5: (/NULL - ]), fd=(1)==(5), (5)==(1)]
440+
441+
# The projected column with the identical type, child.parent_id2, should be
442+
# remapped if there are multiple equivalent columns.
443+
norm
444+
SELECT parent.id FROM parent JOIN child ON parent.id = child.parent_id AND child.parent_id = child.parent_id2
445+
----
446+
project
447+
├── columns: id:1!null
448+
├── immutable
449+
├── select
450+
│ ├── columns: parent_id:5!null parent_id2:6!null
451+
│ ├── immutable
452+
│ ├── fd: (5)==(6), (6)==(5)
453+
│ ├── scan child
454+
│ │ └── columns: parent_id:5!null parent_id2:6!null
455+
│ └── filters
456+
│ └── parent_id:5 = parent_id2:6 [outer=(5,6), immutable, constraints=(/5: (/NULL - ]; /6: (/NULL - ]), fd=(5)==(6), (6)==(5)]
457+
└── projections
458+
└── parent_id2:6 [as=parent.id:1, outer=(6)]
459+
460+
# In a projection expression the column with the identical type,
461+
# child.parent_id2, should be remapped if there are multiple equivalent columns.
462+
norm
463+
SELECT 1 + parent.id FROM parent JOIN child ON parent.id = child.parent_id AND child.parent_id = child.parent_id2
464+
----
465+
project
466+
├── columns: "?column?":9!null
467+
├── immutable
468+
├── select
469+
│ ├── columns: parent_id:5!null parent_id2:6!null
470+
│ ├── immutable
471+
│ ├── fd: (5)==(6), (6)==(5)
472+
│ ├── scan child
473+
│ │ └── columns: parent_id:5!null parent_id2:6!null
474+
│ └── filters
475+
│ └── parent_id:5 = parent_id2:6 [outer=(5,6), immutable, constraints=(/5: (/NULL - ]; /6: (/NULL - ]), fd=(5)==(6), (6)==(5)]
476+
└── projections
477+
└── parent_id2:6 + 1 [as="?column?":9, outer=(6), immutable]
478+
479+
exec-ddl
480+
DROP TABLE child
481+
----
482+
483+
exec-ddl
484+
DROP TABLE parent
485+
----
486+
398487
# --------------------------------------------------
399488
# EliminateProject
400489
# --------------------------------------------------

0 commit comments

Comments
 (0)