From 095120950dfacda1b57347355f8c369ff9dd195c Mon Sep 17 00:00:00 2001 From: Marcus Gartner Date: Mon, 28 Mar 2022 19:31:11 -0400 Subject: [PATCH] opt: do not cross-join input of semi-join This commit fixes a logical correctness bug caused when `GenerateLookupJoins` cross-joins the input of a semi-join with a set of constant values to constrain the prefix columns of the lookup index. The cross-join is an invalid transformation because it increases the size of the join's input and can increase the size of the join's output. We already avoid these cross-joins for left and anti-joins (see #59646). When addressing those cases, the semi-join case was incorrectly assumed to be safe. Fixes #78681 Release note (bug fix): A bug has been fixed which caused the optimizer to generate invalid query plans which could result in incorrect query results. The bug, which has been present since version 21.1.0, can appear if all of the following conditions are true: 1) the query contains a semi-join, such as queries in the form: `SELECT * FROM t1 WHERE EXISTS (SELECT * FROM t2 WHERE t1.a = t2.a);`, 2) the inner table has an index containing the equality column, like `t2.a` in the example query, 3) the index contains one or more columns that prefix the equality column, and 4) the prefix columns are `NOT NULL` and are constrained to a set of constant values via a `CHECK` constraint or an `IN` condition in the filter. --- .../testdata/logic_test/regional_by_row | 352 ++++++----- .../logictest/testdata/logic_test/lookup_join | 30 + pkg/sql/opt/exec/execbuilder/testdata/unique | 598 ++++++++++-------- pkg/sql/opt/xform/join_funcs.go | 14 +- pkg/sql/opt/xform/testdata/rules/join | 77 ++- 5 files changed, 608 insertions(+), 463 deletions(-) diff --git a/pkg/ccl/logictestccl/testdata/logic_test/regional_by_row b/pkg/ccl/logictestccl/testdata/logic_test/regional_by_row index b3d3eb9c2504..ae3643aff27e 100644 --- a/pkg/ccl/logictestccl/testdata/logic_test/regional_by_row +++ b/pkg/ccl/logictestccl/testdata/logic_test/regional_by_row @@ -1025,20 +1025,23 @@ vectorized: true │ │ │ └── • error if rows │ │ -│ └── • lookup join (semi) -│ │ table: child@primary -│ │ equality: (lookup_join_const_col_@12, column1) = (crdb_region,c_id) -│ │ equality cols are key -│ │ pred: column8 != crdb_region +│ └── • limit +│ │ count: 1 │ │ -│ └── • cross join -│ │ estimated row count: 3 -│ │ -│ ├── • values -│ │ size: 1 column, 3 rows +│ └── • lookup join +│ │ table: child@primary +│ │ equality: (lookup_join_const_col_@12, column1) = (crdb_region,c_id) +│ │ equality cols are key +│ │ pred: column8 != crdb_region │ │ -│ └── • scan buffer -│ label: buffer 1 +│ └── • cross join +│ │ estimated row count: 3 +│ │ +│ ├── • values +│ │ size: 1 column, 3 rows +│ │ +│ └── • scan buffer +│ label: buffer 1 │ └── • constraint-check │ @@ -1143,15 +1146,10 @@ vectorized: true │ └── • lookup join (semi) │ table: child@child_c_p_id_idx - │ equality: (lookup_join_const_col_@12, p_id) = (crdb_region,c_p_id) + │ lookup condition: (p_id = c_p_id) AND (crdb_region IN ('ap-southeast-2', 'ca-central-1', 'us-east-1')) │ - └── • cross join - │ - ├── • values - │ size: 1 column, 3 rows - │ - └── • scan buffer - label: buffer 1 + └── • scan buffer + label: buffer 1 # Tests creating a index and a unique constraint on a REGIONAL BY ROW table. statement ok @@ -1183,39 +1181,45 @@ vectorized: true │ │ │ └── • error if rows │ │ -│ └── • lookup join (semi) -│ │ table: regional_by_row_table@primary -│ │ equality: (lookup_join_const_col_@23, column1) = (crdb_region,pk) -│ │ equality cols are key -│ │ pred: column15 != crdb_region +│ └── • limit +│ │ count: 1 │ │ -│ └── • cross join -│ │ estimated row count: 3 -│ │ -│ ├── • values -│ │ size: 1 column, 3 rows +│ └── • lookup join +│ │ table: regional_by_row_table@primary +│ │ equality: (lookup_join_const_col_@23, column1) = (crdb_region,pk) +│ │ equality cols are key +│ │ pred: column15 != crdb_region │ │ -│ └── • scan buffer -│ label: buffer 1 +│ └── • cross join +│ │ estimated row count: 3 +│ │ +│ ├── • values +│ │ size: 1 column, 3 rows +│ │ +│ └── • scan buffer +│ label: buffer 1 │ ├── • constraint-check │ │ │ └── • error if rows │ │ -│ └── • lookup join (semi) -│ │ table: regional_by_row_table@regional_by_row_table_b_key -│ │ equality: (lookup_join_const_col_@38, column4) = (crdb_region,b) -│ │ equality cols are key -│ │ pred: (column1 != pk) OR (column15 != crdb_region) +│ └── • limit +│ │ count: 1 │ │ -│ └── • cross join -│ │ estimated row count: 3 -│ │ -│ ├── • values -│ │ size: 1 column, 3 rows +│ └── • lookup join +│ │ table: regional_by_row_table@regional_by_row_table_b_key +│ │ equality: (lookup_join_const_col_@38, column4) = (crdb_region,b) +│ │ equality cols are key +│ │ pred: (column1 != pk) OR (column15 != crdb_region) │ │ -│ └── • scan buffer -│ label: buffer 1 +│ └── • cross join +│ │ estimated row count: 3 +│ │ +│ ├── • values +│ │ size: 1 column, 3 rows +│ │ +│ └── • scan buffer +│ label: buffer 1 │ ├── • constraint-check │ │ @@ -1223,40 +1227,37 @@ vectorized: true │ │ │ └── • lookup join (semi) │ │ table: regional_by_row_table@uniq_idx (partial index) -│ │ equality: (lookup_join_const_col_@53, column3) = (crdb_region,a) +│ │ lookup condition: (column3 = a) AND (crdb_region IN ('ap-southeast-2', 'ca-central-1', 'us-east-1')) │ │ pred: (column1 != pk) OR (column15 != crdb_region) │ │ -│ └── • cross join -│ │ estimated row count: 3 +│ └── • filter +│ │ estimated row count: 1 +│ │ filter: column4 > 0 │ │ -│ ├── • values -│ │ size: 1 column, 3 rows -│ │ -│ └── • filter -│ │ estimated row count: 1 -│ │ filter: column4 > 0 -│ │ -│ └── • scan buffer -│ label: buffer 1 +│ └── • scan buffer +│ label: buffer 1 │ └── • constraint-check │ └── • error if rows │ - └── • lookup join (semi) - │ table: regional_by_row_table@new_idx - │ equality: (lookup_join_const_col_@68, column3, column4) = (crdb_region,a,b) - │ equality cols are key - │ pred: (column1 != pk) OR (column15 != crdb_region) + └── • limit + │ count: 1 │ - └── • cross join - │ estimated row count: 3 - │ - ├── • values - │ size: 1 column, 3 rows + └── • lookup join + │ table: regional_by_row_table@new_idx + │ equality: (lookup_join_const_col_@68, column3, column4) = (crdb_region,a,b) + │ equality cols are key + │ pred: (column1 != pk) OR (column15 != crdb_region) │ - └── • scan buffer - label: buffer 1 + └── • cross join + │ estimated row count: 3 + │ + ├── • values + │ size: 1 column, 3 rows + │ + └── • scan buffer + label: buffer 1 statement error pq: duplicate key value violates unique constraint "regional_by_row_table_b_key"\nDETAIL: Key \(b\)=\(3\) already exists\. INSERT INTO regional_by_row_table (crdb_region, pk, pk2, a, b) VALUES ('us-east-1', 2, 3, 2, 3) @@ -1302,19 +1303,22 @@ vectorized: true │ │ │ └── • error if rows │ │ -│ └── • lookup join (semi) -│ │ table: regional_by_row_table@regional_by_row_table_b_key -│ │ equality: (lookup_join_const_col_@35, column5) = (crdb_region,b) -│ │ equality cols are key -│ │ pred: (upsert_pk != pk) OR (column1 != crdb_region) +│ └── • limit +│ │ count: 1 │ │ -│ └── • cross join -│ │ -│ ├── • values -│ │ size: 1 column, 3 rows +│ └── • lookup join +│ │ table: regional_by_row_table@regional_by_row_table_b_key +│ │ equality: (lookup_join_const_col_@35, column5) = (crdb_region,b) +│ │ equality cols are key +│ │ pred: (upsert_pk != pk) OR (column1 != crdb_region) │ │ -│ └── • scan buffer -│ label: buffer 1 +│ └── • cross join +│ │ +│ ├── • values +│ │ size: 1 column, 3 rows +│ │ +│ └── • scan buffer +│ label: buffer 1 │ ├── • constraint-check │ │ @@ -1322,37 +1326,35 @@ vectorized: true │ │ │ └── • lookup join (semi) │ │ table: regional_by_row_table@uniq_idx (partial index) -│ │ equality: (lookup_join_const_col_@50, column4) = (crdb_region,a) +│ │ lookup condition: (column4 = a) AND (crdb_region IN ('ap-southeast-2', 'ca-central-1', 'us-east-1')) │ │ pred: (upsert_pk != pk) OR (column1 != crdb_region) │ │ -│ └── • cross join +│ └── • filter +│ │ filter: column5 > 0 │ │ -│ ├── • values -│ │ size: 1 column, 3 rows -│ │ -│ └── • filter -│ │ filter: column5 > 0 -│ │ -│ └── • scan buffer -│ label: buffer 1 +│ └── • scan buffer +│ label: buffer 1 │ └── • constraint-check │ └── • error if rows │ - └── • lookup join (semi) - │ table: regional_by_row_table@new_idx - │ equality: (lookup_join_const_col_@65, column4, column5) = (crdb_region,a,b) - │ equality cols are key - │ pred: (upsert_pk != pk) OR (column1 != crdb_region) + └── • limit + │ count: 1 │ - └── • cross join - │ - ├── • values - │ size: 1 column, 3 rows + └── • lookup join + │ table: regional_by_row_table@new_idx + │ equality: (lookup_join_const_col_@65, column4, column5) = (crdb_region,a,b) + │ equality cols are key + │ pred: (upsert_pk != pk) OR (column1 != crdb_region) │ - └── • scan buffer - label: buffer 1 + └── • cross join + │ + ├── • values + │ size: 1 column, 3 rows + │ + └── • scan buffer + label: buffer 1 query T EXPLAIN UPSERT INTO regional_by_row_table (crdb_region, pk, pk2, a, b) @@ -1388,19 +1390,24 @@ vectorized: true │ │ │ └── • error if rows │ │ -│ └── • lookup join (semi) -│ │ table: regional_by_row_table@regional_by_row_table_b_key -│ │ equality: (lookup_join_const_col_@35, column5) = (crdb_region,b) -│ │ equality cols are key -│ │ pred: (upsert_pk != pk) OR (column1 != crdb_region) +│ └── • distinct +│ │ distinct on: rownum │ │ -│ └── • cross join -│ │ -│ ├── • values -│ │ size: 1 column, 3 rows +│ └── • lookup join +│ │ table: regional_by_row_table@regional_by_row_table_b_key +│ │ equality: (lookup_join_const_col_@35, column5) = (crdb_region,b) +│ │ equality cols are key +│ │ pred: (upsert_pk != pk) OR (column1 != crdb_region) │ │ -│ └── • scan buffer -│ label: buffer 1 +│ └── • cross join +│ │ +│ ├── • values +│ │ size: 1 column, 3 rows +│ │ +│ └── • ordinality +│ │ +│ └── • scan buffer +│ label: buffer 1 │ ├── • constraint-check │ │ @@ -1408,37 +1415,37 @@ vectorized: true │ │ │ └── • lookup join (semi) │ │ table: regional_by_row_table@uniq_idx (partial index) -│ │ equality: (lookup_join_const_col_@50, column4) = (crdb_region,a) +│ │ lookup condition: (column4 = a) AND (crdb_region IN ('ap-southeast-2', 'ca-central-1', 'us-east-1')) │ │ pred: (upsert_pk != pk) OR (column1 != crdb_region) │ │ -│ └── • cross join -│ │ -│ ├── • values -│ │ size: 1 column, 3 rows +│ └── • filter +│ │ filter: column5 > 0 │ │ -│ └── • filter -│ │ filter: column5 > 0 -│ │ -│ └── • scan buffer -│ label: buffer 1 +│ └── • scan buffer +│ label: buffer 1 │ └── • constraint-check │ └── • error if rows │ - └── • lookup join (semi) - │ table: regional_by_row_table@new_idx - │ equality: (lookup_join_const_col_@65, column4, column5) = (crdb_region,a,b) - │ equality cols are key - │ pred: (upsert_pk != pk) OR (column1 != crdb_region) + └── • distinct + │ distinct on: rownum │ - └── • cross join - │ - ├── • values - │ size: 1 column, 3 rows + └── • lookup join + │ table: regional_by_row_table@new_idx + │ equality: (lookup_join_const_col_@65, column4, column5) = (crdb_region,a,b) + │ equality cols are key + │ pred: (upsert_pk != pk) OR (column1 != crdb_region) │ - └── • scan buffer - label: buffer 1 + └── • cross join + │ + ├── • values + │ size: 1 column, 3 rows + │ + └── • ordinality + │ + └── • scan buffer + label: buffer 1 query TIIIIIIIIT colnames SELECT * FROM (VALUES ('us-east-1', 23, 24, 25, 26), ('ca-central-1', 30, 30, 31, 32)) AS v(crdb_region, pk, pk2, a, b) @@ -1738,20 +1745,23 @@ vectorized: true │ └── • error if rows │ - └── • lookup join (semi) - │ table: regional_by_row_table_as@regional_by_row_table_as_b_key - │ equality: (lookup_join_const_col_@21, column3) = (crdb_region_col,b) - │ equality cols are key - │ pred: (column1 != pk) OR (column10 != crdb_region_col) + └── • limit + │ count: 1 │ - └── • cross join - │ estimated row count: 3 - │ - ├── • values - │ size: 1 column, 3 rows + └── • lookup join + │ table: regional_by_row_table_as@regional_by_row_table_as_b_key + │ equality: (lookup_join_const_col_@21, column3) = (crdb_region_col,b) + │ equality cols are key + │ pred: (column1 != pk) OR (column10 != crdb_region_col) │ - └── • scan buffer - label: buffer 1 + └── • cross join + │ estimated row count: 3 + │ + ├── • values + │ size: 1 column, 3 rows + │ + └── • scan buffer + label: buffer 1 statement error pq: duplicate key value violates unique constraint "primary"\nDETAIL: Key \(pk\)=\(1\) already exists\. INSERT INTO regional_by_row_table_as (pk, a, b) VALUES (1, 1, 1) @@ -2694,20 +2704,23 @@ SELECT * FROM [EXPLAIN INSERT INTO regional_by_row_table_virt (pk, a, b) VALUES │ │ │ └── • error if rows │ │ -│ └── • lookup join (semi) -│ │ table: regional_by_row_table_virt@primary -│ │ equality: (lookup_join_const_col_@21, column1) = (crdb_region,pk) -│ │ equality cols are key -│ │ pred: column12 != crdb_region +│ └── • limit +│ │ count: 1 │ │ -│ └── • cross join -│ │ estimated row count: 3 -│ │ -│ ├── • values -│ │ size: 1 column, 3 rows +│ └── • lookup join +│ │ table: regional_by_row_table_virt@primary +│ │ equality: (lookup_join_const_col_@21, column1) = (crdb_region,pk) +│ │ equality cols are key +│ │ pred: column12 != crdb_region │ │ -│ └── • scan buffer -│ label: buffer 1 +│ └── • cross join +│ │ estimated row count: 3 +│ │ +│ ├── • values +│ │ size: 1 column, 3 rows +│ │ +│ └── • scan buffer +│ label: buffer 1 │ ├── • constraint-check │ │ @@ -2880,20 +2893,23 @@ SELECT * FROM [EXPLAIN INSERT INTO regional_by_row_table_virt_partial (pk, a, b) │ │ │ └── • error if rows │ │ -│ └── • lookup join (semi) -│ │ table: regional_by_row_table_virt_partial@primary -│ │ equality: (lookup_join_const_col_@23, column1) = (crdb_region,pk) -│ │ equality cols are key -│ │ pred: column12 != crdb_region +│ └── • limit +│ │ count: 1 │ │ -│ └── • cross join -│ │ estimated row count: 3 -│ │ -│ ├── • values -│ │ size: 1 column, 3 rows +│ └── • lookup join +│ │ table: regional_by_row_table_virt_partial@primary +│ │ equality: (lookup_join_const_col_@23, column1) = (crdb_region,pk) +│ │ equality cols are key +│ │ pred: column12 != crdb_region │ │ -│ └── • scan buffer -│ label: buffer 1 +│ └── • cross join +│ │ estimated row count: 3 +│ │ +│ ├── • values +│ │ size: 1 column, 3 rows +│ │ +│ └── • scan buffer +│ label: buffer 1 │ ├── • constraint-check │ │ diff --git a/pkg/sql/logictest/testdata/logic_test/lookup_join b/pkg/sql/logictest/testdata/logic_test/lookup_join index 6f4b945497df..466cc579563f 100644 --- a/pkg/sql/logictest/testdata/logic_test/lookup_join +++ b/pkg/sql/logictest/testdata/logic_test/lookup_join @@ -618,6 +618,36 @@ SELECT * FROM (VALUES (1), (2)) AS u(y) WHERE NOT EXISTS ( 1 2 +# Regression test for #78681. Ensure that invalid lookup joins are not created +# for semi joins. +statement ok +CREATE TABLE t78681 ( + x INT NOT NULL CHECK (x in (1, 3)), + y INT NOT NULL, + PRIMARY KEY (x, y) +) + +# Insert stats so that a lookup semi-join is selected. +statement ok +ALTER TABLE t78681 INJECT STATISTICS '[ + { + "columns": ["x"], + "created_at": "2018-05-01 1:00:00.00000+00:00", + "row_count": 10000000, + "distinct_count": 2 + } +]' + +statement ok +INSERT INTO t78681 VALUES (1, 1), (3, 1) + +query I rowsort +SELECT * FROM (VALUES (1), (2)) AS u(y) WHERE EXISTS ( + SELECT * FROM t78681 t WHERE u.y = t.y +) +---- +1 + statement ok CREATE TABLE lookup_expr ( r STRING NOT NULL CHECK (r IN ('east', 'west')), diff --git a/pkg/sql/opt/exec/execbuilder/testdata/unique b/pkg/sql/opt/exec/execbuilder/testdata/unique index 43ab68e9a910..c94700d22431 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/unique +++ b/pkg/sql/opt/exec/execbuilder/testdata/unique @@ -751,31 +751,47 @@ vectorized: true │ │ columns: (column1, column3) │ │ estimated row count: 1 (missing stats) │ │ -│ └── • lookup join (semi) -│ │ columns: ("lookup_join_const_col_@12", column1, column3) -│ │ table: uniq_enum@primary -│ │ equality: (lookup_join_const_col_@12, column3) = (r,i) -│ │ equality cols are key -│ │ pred: column1 != r +│ └── • distinct +│ │ columns: (column1, column3, rownum) +│ │ estimated row count: 2 (missing stats) +│ │ distinct on: rownum │ │ -│ └── • cross join (inner) -│ │ columns: ("lookup_join_const_col_@12", column1, column3) -│ │ estimated row count: 6 -│ │ -│ ├── • values -│ │ columns: ("lookup_join_const_col_@12") -│ │ size: 1 column, 3 rows -│ │ row 0, expr 0: 'us-east' -│ │ row 1, expr 0: 'us-west' -│ │ row 2, expr 0: 'eu-west' +│ └── • project +│ │ columns: (column1, column3, rownum) │ │ │ └── • project -│ │ columns: (column1, column3) -│ │ estimated row count: 2 +│ │ columns: (r, i, column1, column3, rownum) +│ │ estimated row count: 7 (missing stats) │ │ -│ └── • scan buffer -│ columns: (column1, column2, column3, column4, check1) -│ label: buffer 1 +│ └── • lookup join (inner) +│ │ columns: ("lookup_join_const_col_@12", column1, column3, rownum, r, i) +│ │ table: uniq_enum@primary +│ │ equality: (lookup_join_const_col_@12, column3) = (r,i) +│ │ equality cols are key +│ │ pred: column1 != r +│ │ +│ └── • cross join (inner) +│ │ columns: ("lookup_join_const_col_@12", column1, column3, rownum) +│ │ estimated row count: 6 +│ │ +│ ├── • values +│ │ columns: ("lookup_join_const_col_@12") +│ │ size: 1 column, 3 rows +│ │ row 0, expr 0: 'us-east' +│ │ row 1, expr 0: 'us-west' +│ │ row 2, expr 0: 'eu-west' +│ │ +│ └── • ordinality +│ │ columns: (column1, column3, rownum) +│ │ estimated row count: 2 +│ │ +│ └── • project +│ │ columns: (column1, column3) +│ │ estimated row count: 2 +│ │ +│ └── • scan buffer +│ columns: (column1, column2, column3, column4, check1) +│ label: buffer 1 │ └── • constraint-check │ @@ -790,31 +806,47 @@ vectorized: true │ columns: (column1, column2, column3, column4) │ estimated row count: 1 (missing stats) │ - └── • lookup join (semi) - │ columns: ("lookup_join_const_col_@22", column1, column2, column3, column4) - │ table: uniq_enum@uniq_enum_r_s_j_key - │ equality: (lookup_join_const_col_@22, column2, column4) = (r,s,j) - │ equality cols are key - │ pred: (column1 != r) OR (column3 != i) + └── • distinct + │ columns: (column1, column2, column3, column4, rownum) + │ estimated row count: 0 (missing stats) + │ distinct on: rownum │ - └── • cross join (inner) - │ columns: ("lookup_join_const_col_@22", column1, column2, column3, column4) - │ estimated row count: 6 - │ - ├── • values - │ columns: ("lookup_join_const_col_@22") - │ size: 1 column, 3 rows - │ row 0, expr 0: 'us-east' - │ row 1, expr 0: 'us-west' - │ row 2, expr 0: 'eu-west' + └── • project + │ columns: (column1, column2, column3, column4, rownum) │ └── • project - │ columns: (column1, column2, column3, column4) - │ estimated row count: 2 + │ columns: (r, s, i, j, column1, column2, column3, column4, rownum) + │ estimated row count: 0 (missing stats) │ - └── • scan buffer - columns: (column1, column2, column3, column4, check1) - label: buffer 1 + └── • lookup join (inner) + │ columns: ("lookup_join_const_col_@22", column1, column2, column3, column4, rownum, r, s, i, j) + │ table: uniq_enum@uniq_enum_r_s_j_key + │ equality: (lookup_join_const_col_@22, column2, column4) = (r,s,j) + │ equality cols are key + │ pred: (column1 != r) OR (column3 != i) + │ + └── • cross join (inner) + │ columns: ("lookup_join_const_col_@22", column1, column2, column3, column4, rownum) + │ estimated row count: 6 + │ + ├── • values + │ columns: ("lookup_join_const_col_@22") + │ size: 1 column, 3 rows + │ row 0, expr 0: 'us-east' + │ row 1, expr 0: 'us-west' + │ row 2, expr 0: 'eu-west' + │ + └── • ordinality + │ columns: (column1, column2, column3, column4, rownum) + │ estimated row count: 2 + │ + └── • project + │ columns: (column1, column2, column3, column4) + │ estimated row count: 2 + │ + └── • scan buffer + columns: (column1, column2, column3, column4, check1) + label: buffer 1 # Test that we use the index when available for the insert checks. This uses # the default value for columns r and j. @@ -874,31 +906,47 @@ vectorized: true │ columns: (column9, column2) │ estimated row count: 1 (missing stats) │ - └── • lookup join (semi) - │ columns: ("lookup_join_const_col_@12", column9, column2) - │ table: uniq_enum@primary - │ equality: (lookup_join_const_col_@12, column2) = (r,i) - │ equality cols are key - │ pred: column9 != r + └── • distinct + │ columns: (column9, column2, rownum) + │ estimated row count: 2 (missing stats) + │ distinct on: rownum │ - └── • cross join (inner) - │ columns: ("lookup_join_const_col_@12", column9, column2) - │ estimated row count: 6 - │ - ├── • values - │ columns: ("lookup_join_const_col_@12") - │ size: 1 column, 3 rows - │ row 0, expr 0: 'us-east' - │ row 1, expr 0: 'us-west' - │ row 2, expr 0: 'eu-west' + └── • project + │ columns: (column9, column2, rownum) │ └── • project - │ columns: (column9, column2) - │ estimated row count: 2 + │ columns: (r, i, column9, column2, rownum) + │ estimated row count: 7 (missing stats) │ - └── • scan buffer - columns: (column9, column1, column2, column10, check1) - label: buffer 1 + └── • lookup join (inner) + │ columns: ("lookup_join_const_col_@12", column9, column2, rownum, r, i) + │ table: uniq_enum@primary + │ equality: (lookup_join_const_col_@12, column2) = (r,i) + │ equality cols are key + │ pred: column9 != r + │ + └── • cross join (inner) + │ columns: ("lookup_join_const_col_@12", column9, column2, rownum) + │ estimated row count: 6 + │ + ├── • values + │ columns: ("lookup_join_const_col_@12") + │ size: 1 column, 3 rows + │ row 0, expr 0: 'us-east' + │ row 1, expr 0: 'us-west' + │ row 2, expr 0: 'eu-west' + │ + └── • ordinality + │ columns: (column9, column2, rownum) + │ estimated row count: 2 + │ + └── • project + │ columns: (column9, column2) + │ estimated row count: 2 + │ + └── • scan buffer + columns: (column9, column1, column2, column10, check1) + label: buffer 1 # Test that we use the index when available for de-duplicating INSERT ON # CONFLICT DO NOTHING rows before inserting. @@ -1344,39 +1392,25 @@ vectorized: true │ columns: (column3) │ estimated row count: 1 │ - └── • project + └── • lookup join (semi) │ columns: (column1, column2, column3, column4) │ estimated row count: 1 + │ table: uniq_partial_enum@uniq_partial_enum_r_b_idx (partial index) + │ lookup condition: (column3 = b) AND (r IN ('us-east', 'us-west', 'eu-west')) + │ pred: (column1 != r) OR (column2 != a) │ - └── • lookup join (semi) - │ columns: ("lookup_join_const_col_@13", column1, column2, column3, column4) - │ table: uniq_partial_enum@uniq_partial_enum_r_b_idx (partial index) - │ equality: (lookup_join_const_col_@13, column3) = (r,b) - │ pred: (column1 != r) OR (column2 != a) + └── • filter + │ columns: (column1, column2, column3, column4) + │ estimated row count: 2 + │ filter: column4 IN ('bar', 'baz', 'foo') │ - └── • cross join (inner) - │ columns: ("lookup_join_const_col_@13", column1, column2, column3, column4) - │ estimated row count: 6 - │ - ├── • values - │ columns: ("lookup_join_const_col_@13") - │ size: 1 column, 3 rows - │ row 0, expr 0: 'us-east' - │ row 1, expr 0: 'us-west' - │ row 2, expr 0: 'eu-west' + └── • project + │ columns: (column1, column2, column3, column4) + │ estimated row count: 2 │ - └── • filter - │ columns: (column1, column2, column3, column4) - │ estimated row count: 2 - │ filter: column4 IN ('bar', 'baz', 'foo') - │ - └── • project - │ columns: (column1, column2, column3, column4) - │ estimated row count: 2 - │ - └── • scan buffer - columns: (column1, column2, column3, column4, check1, partial_index_put1) - label: buffer 1 + └── • scan buffer + columns: (column1, column2, column3, column4, check1, partial_index_put1) + label: buffer 1 # Test that we use the partial index when available for de-duplicating INSERT ON # CONFLICT DO NOTHING rows before inserting. @@ -2203,31 +2237,47 @@ vectorized: true │ │ columns: (r_new, i_new) │ │ estimated row count: 3 (missing stats) │ │ -│ └── • lookup join (semi) -│ │ columns: (r_new, i_new, "lookup_join_const_col_@17") -│ │ table: uniq_enum@primary -│ │ equality: (lookup_join_const_col_@17, i_new) = (r,i) -│ │ equality cols are key -│ │ pred: r_new != r +│ └── • distinct +│ │ columns: (r_new, i_new, rownum) +│ │ estimated row count: 10 (missing stats) +│ │ distinct on: rownum │ │ -│ └── • cross join (inner) -│ │ columns: (r_new, i_new, "lookup_join_const_col_@17") -│ │ estimated row count: 30 (missing stats) -│ │ -│ ├── • project -│ │ │ columns: (r_new, i_new) -│ │ │ estimated row count: 10 (missing stats) -│ │ │ -│ │ └── • scan buffer -│ │ columns: (r, s, i, j, r_new, s_new, i_new, check1) -│ │ label: buffer 1 +│ └── • project +│ │ columns: (r_new, i_new, rownum) │ │ -│ └── • values -│ columns: ("lookup_join_const_col_@17") -│ size: 1 column, 3 rows -│ row 0, expr 0: 'us-east' -│ row 1, expr 0: 'us-west' -│ row 2, expr 0: 'eu-west' +│ └── • project +│ │ columns: (r, i, r_new, i_new, rownum) +│ │ estimated row count: 33 (missing stats) +│ │ +│ └── • lookup join (inner) +│ │ columns: (r_new, i_new, rownum, "lookup_join_const_col_@17", r, i) +│ │ table: uniq_enum@primary +│ │ equality: (lookup_join_const_col_@17, i_new) = (r,i) +│ │ equality cols are key +│ │ pred: r_new != r +│ │ +│ └── • cross join (inner) +│ │ columns: (r_new, i_new, rownum, "lookup_join_const_col_@17") +│ │ estimated row count: 30 (missing stats) +│ │ +│ ├── • ordinality +│ │ │ columns: (r_new, i_new, rownum) +│ │ │ estimated row count: 10 (missing stats) +│ │ │ +│ │ └── • project +│ │ │ columns: (r_new, i_new) +│ │ │ estimated row count: 10 (missing stats) +│ │ │ +│ │ └── • scan buffer +│ │ columns: (r, s, i, j, r_new, s_new, i_new, check1) +│ │ label: buffer 1 +│ │ +│ └── • values +│ columns: ("lookup_join_const_col_@17") +│ size: 1 column, 3 rows +│ row 0, expr 0: 'us-east' +│ row 1, expr 0: 'us-west' +│ row 2, expr 0: 'eu-west' │ └── • constraint-check │ @@ -2242,31 +2292,47 @@ vectorized: true │ columns: (r_new, s_new, i_new, j) │ estimated row count: 3 (missing stats) │ - └── • lookup join (semi) - │ columns: (r_new, s_new, i_new, j, "lookup_join_const_col_@27") - │ table: uniq_enum@uniq_enum_r_s_j_key - │ equality: (lookup_join_const_col_@27, s_new, j) = (r,s,j) - │ equality cols are key - │ pred: (r_new != r) OR (i_new != i) + └── • distinct + │ columns: (r_new, s_new, i_new, j, rownum) + │ estimated row count: 0 (missing stats) + │ distinct on: rownum │ - └── • cross join (inner) - │ columns: (r_new, s_new, i_new, j, "lookup_join_const_col_@27") - │ estimated row count: 30 (missing stats) - │ - ├── • project - │ │ columns: (r_new, s_new, i_new, j) - │ │ estimated row count: 10 (missing stats) - │ │ - │ └── • scan buffer - │ columns: (r, s, i, j, r_new, s_new, i_new, check1) - │ label: buffer 1 + └── • project + │ columns: (r_new, s_new, i_new, j, rownum) │ - └── • values - columns: ("lookup_join_const_col_@27") - size: 1 column, 3 rows - row 0, expr 0: 'us-east' - row 1, expr 0: 'us-west' - row 2, expr 0: 'eu-west' + └── • project + │ columns: (r, s, i, j, r_new, s_new, i_new, j, rownum) + │ estimated row count: 0 (missing stats) + │ + └── • lookup join (inner) + │ columns: (r_new, s_new, i_new, j, rownum, "lookup_join_const_col_@27", r, s, i, j) + │ table: uniq_enum@uniq_enum_r_s_j_key + │ equality: (lookup_join_const_col_@27, s_new, j) = (r,s,j) + │ equality cols are key + │ pred: (r_new != r) OR (i_new != i) + │ + └── • cross join (inner) + │ columns: (r_new, s_new, i_new, j, rownum, "lookup_join_const_col_@27") + │ estimated row count: 30 (missing stats) + │ + ├── • ordinality + │ │ columns: (r_new, s_new, i_new, j, rownum) + │ │ estimated row count: 10 (missing stats) + │ │ + │ └── • project + │ │ columns: (r_new, s_new, i_new, j) + │ │ estimated row count: 10 (missing stats) + │ │ + │ └── • scan buffer + │ columns: (r, s, i, j, r_new, s_new, i_new, check1) + │ label: buffer 1 + │ + └── • values + columns: ("lookup_join_const_col_@27") + size: 1 column, 3 rows + row 0, expr 0: 'us-east' + row 1, expr 0: 'us-west' + row 2, expr 0: 'eu-west' # None of the updated values have nulls. query T @@ -2521,39 +2587,25 @@ vectorized: true │ columns: (b_new) │ estimated row count: 0 │ - └── • project + └── • lookup join (semi) │ columns: (r, a, b_new, c) │ estimated row count: 0 + │ table: uniq_partial_enum@uniq_partial_enum_r_b_idx (partial index) + │ lookup condition: (b_new = b) AND (r IN ('us-east', 'us-west', 'eu-west')) + │ pred: (r != r) OR (a != a) │ - └── • lookup join (semi) - │ columns: ("lookup_join_const_col_@16", r, a, b_new, c) - │ table: uniq_partial_enum@uniq_partial_enum_r_b_idx (partial index) - │ equality: (lookup_join_const_col_@16, b_new) = (r,b) - │ pred: (r != r) OR (a != a) + └── • filter + │ columns: (r, a, b_new, c) + │ estimated row count: 1 + │ filter: c IN ('bar', 'baz', 'foo') │ - └── • cross join (inner) - │ columns: ("lookup_join_const_col_@16", r, a, b_new, c) - │ estimated row count: 3 - │ - ├── • values - │ columns: ("lookup_join_const_col_@16") - │ size: 1 column, 3 rows - │ row 0, expr 0: 'us-east' - │ row 1, expr 0: 'us-west' - │ row 2, expr 0: 'eu-west' + └── • project + │ columns: (r, a, b_new, c) + │ estimated row count: 1 │ - └── • filter - │ columns: (r, a, b_new, c) - │ estimated row count: 1 - │ filter: c IN ('bar', 'baz', 'foo') - │ - └── • project - │ columns: (r, a, b_new, c) - │ estimated row count: 1 - │ - └── • scan buffer - columns: (r, a, b, b_new, partial_index_put1, partial_index_put1, c) - label: buffer 1 + └── • scan buffer + columns: (r, a, b, b_new, partial_index_put1, partial_index_put1, c) + label: buffer 1 # By default, we do not require checks on UUID columns set to gen_random_uuid(), # but we do for UUID columns set to other values. @@ -3421,31 +3473,47 @@ vectorized: true │ │ columns: (upsert_r, upsert_i) │ │ estimated row count: 1 (missing stats) │ │ -│ └── • lookup join (semi) -│ │ columns: ("lookup_join_const_col_@20", upsert_r, upsert_i) -│ │ table: uniq_enum@primary -│ │ equality: (lookup_join_const_col_@20, upsert_i) = (r,i) -│ │ equality cols are key -│ │ pred: upsert_r != r +│ └── • distinct +│ │ columns: (upsert_r, upsert_i, rownum) +│ │ estimated row count: 2 (missing stats) +│ │ distinct on: rownum │ │ -│ └── • cross join (inner) -│ │ columns: ("lookup_join_const_col_@20", upsert_r, upsert_i) -│ │ estimated row count: 6 (missing stats) -│ │ -│ ├── • values -│ │ columns: ("lookup_join_const_col_@20") -│ │ size: 1 column, 3 rows -│ │ row 0, expr 0: 'us-east' -│ │ row 1, expr 0: 'us-west' -│ │ row 2, expr 0: 'eu-west' +│ └── • project +│ │ columns: (upsert_r, upsert_i, rownum) │ │ │ └── • project -│ │ columns: (upsert_r, upsert_i) -│ │ estimated row count: 2 (missing stats) +│ │ columns: (r, i, upsert_r, upsert_i, rownum) +│ │ estimated row count: 7 (missing stats) │ │ -│ └── • scan buffer -│ columns: (column1, column2, column3, column4, r, s, i, j, column2, column4, r, check1, upsert_r, upsert_i) -│ label: buffer 1 +│ └── • lookup join (inner) +│ │ columns: ("lookup_join_const_col_@20", upsert_r, upsert_i, rownum, r, i) +│ │ table: uniq_enum@primary +│ │ equality: (lookup_join_const_col_@20, upsert_i) = (r,i) +│ │ equality cols are key +│ │ pred: upsert_r != r +│ │ +│ └── • cross join (inner) +│ │ columns: ("lookup_join_const_col_@20", upsert_r, upsert_i, rownum) +│ │ estimated row count: 6 (missing stats) +│ │ +│ ├── • values +│ │ columns: ("lookup_join_const_col_@20") +│ │ size: 1 column, 3 rows +│ │ row 0, expr 0: 'us-east' +│ │ row 1, expr 0: 'us-west' +│ │ row 2, expr 0: 'eu-west' +│ │ +│ └── • ordinality +│ │ columns: (upsert_r, upsert_i, rownum) +│ │ estimated row count: 2 (missing stats) +│ │ +│ └── • project +│ │ columns: (upsert_r, upsert_i) +│ │ estimated row count: 2 (missing stats) +│ │ +│ └── • scan buffer +│ columns: (column1, column2, column3, column4, r, s, i, j, column2, column4, r, check1, upsert_r, upsert_i) +│ label: buffer 1 │ └── • constraint-check │ @@ -3460,31 +3528,47 @@ vectorized: true │ columns: (upsert_r, column2, upsert_i, column4) │ estimated row count: 1 (missing stats) │ - └── • lookup join (semi) - │ columns: ("lookup_join_const_col_@30", upsert_r, column2, upsert_i, column4) - │ table: uniq_enum@uniq_enum_r_s_j_key - │ equality: (lookup_join_const_col_@30, column2, column4) = (r,s,j) - │ equality cols are key - │ pred: (upsert_r != r) OR (upsert_i != i) + └── • distinct + │ columns: (upsert_r, column2, upsert_i, column4, rownum) + │ estimated row count: 0 (missing stats) + │ distinct on: rownum │ - └── • cross join (inner) - │ columns: ("lookup_join_const_col_@30", upsert_r, column2, upsert_i, column4) - │ estimated row count: 6 (missing stats) - │ - ├── • values - │ columns: ("lookup_join_const_col_@30") - │ size: 1 column, 3 rows - │ row 0, expr 0: 'us-east' - │ row 1, expr 0: 'us-west' - │ row 2, expr 0: 'eu-west' + └── • project + │ columns: (upsert_r, column2, upsert_i, column4, rownum) │ └── • project - │ columns: (upsert_r, column2, upsert_i, column4) - │ estimated row count: 2 (missing stats) + │ columns: (r, s, i, j, upsert_r, column2, upsert_i, column4, rownum) + │ estimated row count: 0 (missing stats) │ - └── • scan buffer - columns: (column1, column2, column3, column4, r, s, i, j, column2, column4, r, check1, upsert_r, upsert_i) - label: buffer 1 + └── • lookup join (inner) + │ columns: ("lookup_join_const_col_@30", upsert_r, column2, upsert_i, column4, rownum, r, s, i, j) + │ table: uniq_enum@uniq_enum_r_s_j_key + │ equality: (lookup_join_const_col_@30, column2, column4) = (r,s,j) + │ equality cols are key + │ pred: (upsert_r != r) OR (upsert_i != i) + │ + └── • cross join (inner) + │ columns: ("lookup_join_const_col_@30", upsert_r, column2, upsert_i, column4, rownum) + │ estimated row count: 6 (missing stats) + │ + ├── • values + │ columns: ("lookup_join_const_col_@30") + │ size: 1 column, 3 rows + │ row 0, expr 0: 'us-east' + │ row 1, expr 0: 'us-west' + │ row 2, expr 0: 'eu-west' + │ + └── • ordinality + │ columns: (upsert_r, column2, upsert_i, column4, rownum) + │ estimated row count: 2 (missing stats) + │ + └── • project + │ columns: (upsert_r, column2, upsert_i, column4) + │ estimated row count: 2 (missing stats) + │ + └── • scan buffer + columns: (column1, column2, column3, column4, r, s, i, j, column2, column4, r, check1, upsert_r, upsert_i) + label: buffer 1 # Test that we use the index when available for the ON CONFLICT checks. query T @@ -3572,31 +3656,47 @@ vectorized: true │ columns: (upsert_r, upsert_i) │ estimated row count: 1 (missing stats) │ - └── • lookup join (semi) - │ columns: ("lookup_join_const_col_@23", upsert_r, upsert_i) - │ table: uniq_enum@primary - │ equality: (lookup_join_const_col_@23, upsert_i) = (r,i) - │ equality cols are key - │ pred: upsert_r != r + └── • distinct + │ columns: (upsert_r, upsert_i, rownum) + │ estimated row count: 2 (missing stats) + │ distinct on: rownum │ - └── • cross join (inner) - │ columns: ("lookup_join_const_col_@23", upsert_r, upsert_i) - │ estimated row count: 6 (missing stats) - │ - ├── • values - │ columns: ("lookup_join_const_col_@23") - │ size: 1 column, 3 rows - │ row 0, expr 0: 'us-east' - │ row 1, expr 0: 'us-west' - │ row 2, expr 0: 'eu-west' + └── • project + │ columns: (upsert_r, upsert_i, rownum) │ └── • project - │ columns: (upsert_r, upsert_i) - │ estimated row count: 2 (missing stats) + │ columns: (r, i, upsert_r, upsert_i, rownum) + │ estimated row count: 7 (missing stats) │ - └── • scan buffer - columns: (column1, column2, column3, column4, r, s, i, j, upsert_i, r, check1, upsert_r) - label: buffer 1 + └── • lookup join (inner) + │ columns: ("lookup_join_const_col_@23", upsert_r, upsert_i, rownum, r, i) + │ table: uniq_enum@primary + │ equality: (lookup_join_const_col_@23, upsert_i) = (r,i) + │ equality cols are key + │ pred: upsert_r != r + │ + └── • cross join (inner) + │ columns: ("lookup_join_const_col_@23", upsert_r, upsert_i, rownum) + │ estimated row count: 6 (missing stats) + │ + ├── • values + │ columns: ("lookup_join_const_col_@23") + │ size: 1 column, 3 rows + │ row 0, expr 0: 'us-east' + │ row 1, expr 0: 'us-west' + │ row 2, expr 0: 'eu-west' + │ + └── • ordinality + │ columns: (upsert_r, upsert_i, rownum) + │ estimated row count: 2 (missing stats) + │ + └── • project + │ columns: (upsert_r, upsert_i) + │ estimated row count: 2 (missing stats) + │ + └── • scan buffer + columns: (column1, column2, column3, column4, r, s, i, j, upsert_i, r, check1, upsert_r) + label: buffer 1 # None of the upserted values have nulls. query T @@ -4097,39 +4197,25 @@ vectorized: true │ columns: (column3) │ estimated row count: 1 │ - └── • project + └── • lookup join (semi) │ columns: (upsert_r, upsert_a, column3, column4) │ estimated row count: 1 + │ table: uniq_partial_enum@uniq_partial_enum_r_b_idx (partial index) + │ lookup condition: (column3 = b) AND (r IN ('us-east', 'us-west', 'eu-west')) + │ pred: (upsert_r != r) OR (upsert_a != a) │ - └── • lookup join (semi) - │ columns: ("lookup_join_const_col_@22", upsert_r, upsert_a, column3, column4) - │ table: uniq_partial_enum@uniq_partial_enum_r_b_idx (partial index) - │ equality: (lookup_join_const_col_@22, column3) = (r,b) - │ pred: (upsert_r != r) OR (upsert_a != a) + └── • filter + │ columns: (upsert_r, upsert_a, column3, column4) + │ estimated row count: 2 + │ filter: column4 IN ('bar', 'baz', 'foo') │ - └── • cross join (inner) - │ columns: ("lookup_join_const_col_@22", upsert_r, upsert_a, column3, column4) - │ estimated row count: 6 - │ - ├── • values - │ columns: ("lookup_join_const_col_@22") - │ size: 1 column, 3 rows - │ row 0, expr 0: 'us-east' - │ row 1, expr 0: 'us-west' - │ row 2, expr 0: 'eu-west' + └── • project + │ columns: (upsert_r, upsert_a, column3, column4) + │ estimated row count: 2 │ - └── • filter - │ columns: (upsert_r, upsert_a, column3, column4) - │ estimated row count: 2 - │ filter: column4 IN ('bar', 'baz', 'foo') - │ - └── • project - │ columns: (upsert_r, upsert_a, column3, column4) - │ estimated row count: 2 - │ - └── • scan buffer - columns: (column1, column2, column3, column4, r, a, b, c, column3, column4, r, check1, partial_index_put1, partial_index_del1, upsert_r, upsert_a) - label: buffer 1 + └── • scan buffer + columns: (column1, column2, column3, column4, r, a, b, c, column3, column4, r, check1, partial_index_put1, partial_index_del1, upsert_r, upsert_a) + label: buffer 1 # Test that we use the partial index when available for de-duplicating INSERT ON # CONFLICT DO UPDATE rows before inserting. diff --git a/pkg/sql/opt/xform/join_funcs.go b/pkg/sql/opt/xform/join_funcs.go index e1777436f267..15a01e635ee7 100644 --- a/pkg/sql/opt/xform/join_funcs.go +++ b/pkg/sql/opt/xform/join_funcs.go @@ -371,12 +371,14 @@ func (c *CustomFuncs) generateLookupJoinsImpl( break } - if len(foundVals) > 1 && (joinType == opt.LeftJoinOp || joinType == opt.AntiJoinOp) { - // We cannot use the method constructJoinWithConstants to create a cross - // join for left or anti joins, because constructing a cross join with - // foundVals will increase the size of the input. As a result, - // non-matching input rows will show up more than once in the output, - // which is incorrect (see #59615). + if len(foundVals) > 1 && (joinType == opt.LeftJoinOp || + joinType == opt.SemiJoinOp || joinType == opt.AntiJoinOp) { + // We cannot use the method constructJoinWithConstants to create + // a cross join for left, semi, or anti joins, because + // constructing a cross join with foundVals will increase the + // size of the input. As a result, non-matching input rows will + // show up more than once in the output, which is incorrect (see + // #59615 and #78685). shouldBuildMultiSpanLookupJoin = true break } diff --git a/pkg/sql/opt/xform/testdata/rules/join b/pkg/sql/opt/xform/testdata/rules/join index cb151498cc5d..55ed026b72c1 100644 --- a/pkg/sql/opt/xform/testdata/rules/join +++ b/pkg/sql/opt/xform/testdata/rules/join @@ -3130,8 +3130,8 @@ anti-join (hash) ├── m:1 = a:5 [outer=(1,5), constraints=(/1: (/NULL - ]; /5: (/NULL - ]), fd=(1)==(5), (5)==(1)] └── n:2 = c:7 [outer=(2,7), constraints=(/2: (/NULL - ]; /7: (/NULL - ]), fd=(2)==(7), (7)==(2)] -# Regression test for #59615. Ensure that invalid lookup joins are not created -# for left and anti joins. +# Regression test for #59615 and #78681. Ensure that invalid lookup joins are +# not created for left, semi, and anti joins. exec-ddl CREATE TABLE t59615 ( x INT NOT NULL CHECK (x in (1, 3)), @@ -3159,6 +3159,26 @@ left-join (lookup t59615 [as=t]) │ └── (2,) └── filters (true) +# Regression test for #78681. +opt expect=GenerateLookupJoins +SELECT * FROM (VALUES (1), (2)) AS u(y) WHERE EXISTS ( + SELECT * FROM t59615 t WHERE u.y = t.y +) +---- +semi-join (lookup t59615 [as=t]) + ├── columns: y:1!null + ├── lookup expression + │ └── filters + │ ├── column1:1 = y:3 [outer=(1,3), constraints=(/1: (/NULL - ]; /3: (/NULL - ]), fd=(1)==(3), (3)==(1)] + │ └── x:2 IN (1, 3) [outer=(2), constraints=(/2: [/1 - /1] [/3 - /3]; tight)] + ├── cardinality: [0 - 2] + ├── values + │ ├── columns: column1:1!null + │ ├── cardinality: [2 - 2] + │ ├── (1,) + │ └── (2,) + └── filters (true) + opt expect=GenerateLookupJoins SELECT * FROM (VALUES (1), (2)) AS u(y) WHERE NOT EXISTS ( SELECT * FROM t59615 t WHERE u.y = t.y @@ -9324,44 +9344,35 @@ SELECT * FROM def_part WHERE EXISTS (SELECT * FROM abc_part WHERE e = a) AND d = ---- semi-join (lookup abc_part) ├── columns: r:1!null d:2!null e:3 f:4 - ├── key columns: [21 3] = [6 7] + ├── lookup expression + │ └── filters + │ ├── e:3 = a:7 [outer=(3,7), constraints=(/3: (/NULL - ]; /7: (/NULL - ]), fd=(3)==(7), (7)==(3)] + │ └── abc_part.r:6 IN ('central', 'east', 'west') [outer=(6), constraints=(/6: [/'central' - /'central'] [/'east' - /'east'] [/'west' - /'west']; tight)] ├── lookup columns are key ├── cardinality: [0 - 1] ├── key: () ├── fd: ()-->(1-4) - ├── inner-join (cross) - │ ├── columns: def_part.r:1!null d:2!null e:3 f:4 "lookup_join_const_col_@6":21!null - │ ├── cardinality: [0 - 3] - │ ├── multiplicity: left-rows(zero-or-one), right-rows(one-or-more) + ├── locality-optimized-search + │ ├── columns: def_part.r:1!null d:2!null e:3 f:4 + │ ├── left columns: def_part.r:11 d:12 e:13 f:14 + │ ├── right columns: def_part.r:16 d:17 e:18 f:19 + │ ├── cardinality: [0 - 1] + │ ├── key: () │ ├── fd: ()-->(1-4) - │ ├── values - │ │ ├── columns: "lookup_join_const_col_@6":21!null - │ │ ├── cardinality: [3 - 3] - │ │ ├── ('central',) - │ │ ├── ('east',) - │ │ └── ('west',) - │ ├── locality-optimized-search - │ │ ├── columns: def_part.r:1!null d:2!null e:3 f:4 - │ │ ├── left columns: def_part.r:11 d:12 e:13 f:14 - │ │ ├── right columns: def_part.r:16 d:17 e:18 f:19 + │ ├── scan def_part + │ │ ├── columns: def_part.r:11!null d:12!null e:13 f:14 + │ │ ├── constraint: /11/12: [/'central'/1 - /'central'/1] │ │ ├── cardinality: [0 - 1] │ │ ├── key: () - │ │ ├── fd: ()-->(1-4) - │ │ ├── scan def_part - │ │ │ ├── columns: def_part.r:11!null d:12!null e:13 f:14 - │ │ │ ├── constraint: /11/12: [/'central'/1 - /'central'/1] - │ │ │ ├── cardinality: [0 - 1] - │ │ │ ├── key: () - │ │ │ └── fd: ()-->(11-14) - │ │ └── scan def_part - │ │ ├── columns: def_part.r:16!null d:17!null e:18 f:19 - │ │ ├── constraint: /16/17 - │ │ │ ├── [/'east'/1 - /'east'/1] - │ │ │ └── [/'west'/1 - /'west'/1] - │ │ ├── cardinality: [0 - 1] - │ │ ├── key: () - │ │ └── fd: ()-->(16-19) - │ └── filters (true) + │ │ └── fd: ()-->(11-14) + │ └── scan def_part + │ ├── columns: def_part.r:16!null d:17!null e:18 f:19 + │ ├── constraint: /16/17 + │ │ ├── [/'east'/1 - /'east'/1] + │ │ └── [/'west'/1 - /'west'/1] + │ ├── cardinality: [0 - 1] + │ ├── key: () + │ └── fd: ()-->(16-19) └── filters (true) # A multi-column IN query must be able to become a lookup join.