From e5d499a462297e9ab540bc704fc2e5109d971d7c Mon Sep 17 00:00:00 2001 From: Marcus Gartner Date: Sat, 20 Feb 2021 10:37:01 -0800 Subject: [PATCH] opt: support UPDATE with partial UNIQUE WITHOUT INDEX constraints This commit add uniqueness checks for partial `UNIQUE WITHOUT INDEX` constraints during `UPDATE` statements. As partial of this change, I discovered that #60535 introduced a regression where columns not required by uniqueness checks are not pruned. I've left TODOs in the column pruning tests and plan on fixing this in a follow-up PR. There is no release note because these constraints are gated behind the experimental_enable_unique_without_index_constraints session variable. Release note: None --- pkg/sql/logictest/testdata/logic_test/unique | 65 ++- pkg/sql/opt/exec/execbuilder/testdata/unique | 492 +++++++++++++++--- pkg/sql/opt/norm/testdata/rules/prune_cols | 86 +++ pkg/sql/opt/optbuilder/mutation_builder.go | 12 +- .../opt/optbuilder/mutation_builder_unique.go | 20 +- .../optbuilder/testdata/unique-checks-update | 447 +++++++++++++++- 6 files changed, 1031 insertions(+), 91 deletions(-) diff --git a/pkg/sql/logictest/testdata/logic_test/unique b/pkg/sql/logictest/testdata/logic_test/unique index 6abbf7863fa7..3de7a80548ba 100644 --- a/pkg/sql/logictest/testdata/logic_test/unique +++ b/pkg/sql/logictest/testdata/logic_test/unique @@ -86,13 +86,6 @@ CREATE TABLE uniq_enum ( UNIQUE WITHOUT INDEX (s, j) ) -statement ok -CREATE TABLE uniq_partial_index ( - i INT, - UNIQUE WITHOUT INDEX (i), - UNIQUE INDEX (i) WHERE i > 0 -) - statement ok CREATE TABLE other (k INT, v INT, w INT NOT NULL, x INT, y INT) @@ -437,6 +430,51 @@ r s i j eu-west bar 2 2 us-west foo 1 1 +# Set a to the same value it already has. +statement ok +UPDATE uniq_partial SET a = 1 WHERE a = 1 AND b = 1 + +# Set a to an existing value. +statement error pgcode 23505 pq: duplicate key value violates unique constraint "unique_a"\nDETAIL: Key \(a\)=\(1\) already exists\. +UPDATE uniq_partial SET a = 1 WHERE a = 2 + +# Make b of (1, -1) positive so that it conflicts with (1, 1) +statement error pgcode 23505 pq: duplicate key value violates unique constraint "unique_a"\nDETAIL: Key \(a\)=\(1\) already exists\. +UPDATE uniq_partial SET b = 10 WHERE a = 1 AND b = -1 + +# Set a to NULL. +statement ok +UPDATE uniq_partial SET a = NULL, b = 10 WHERE a = 1 AND b = -1 + +# Update two existing, non-conflicting rows resulting in a conflict. +statement error pgcode 23505 pq: duplicate key value violates unique constraint "unique_a"\nDETAIL: Key \(a\)=\(10\) already exists\. +UPDATE uniq_partial SET a = 10 WHERE a IS NULL AND b = 5 + +# Set a to a non-existing value. +statement ok +UPDATE uniq_partial SET a = 10 WHERE a = 9 AND b = 9 + +# Set a to a value that would conflict if it was a non-partial unique constraint. +statement ok +UPDATE uniq_partial SET a = 1 WHERE b = -7 + +query II colnames,rowsort +SELECT * FROM uniq_partial +---- +a b +1 1 +1 -3 +1 -7 +2 2 +5 5 +6 6 +7 7 +9 -9 +10 9 +NULL 5 +NULL 5 +NULL 10 + # -- Tests with UPSERT -- subtest Upsert @@ -616,11 +654,18 @@ eu-west bar 2 2 # Ensure that we do not choose a partial index as the arbiter when there is a # UNIQUE WITHOUT INDEX constraint. statement ok -INSERT INTO uniq_partial_index VALUES (-1) ON CONFLICT (i) WHERE i > 0 DO UPDATE SET i = 1; -INSERT INTO uniq_partial_index VALUES (-1) ON CONFLICT (i) WHERE i > 0 DO UPDATE SET i = 2 +CREATE TABLE uniq_partial_index_and_constraint ( + i INT, + UNIQUE WITHOUT INDEX (i), + UNIQUE INDEX (i) WHERE i > 0 +) + +statement ok +INSERT INTO uniq_partial_index_and_constraint VALUES (-1) ON CONFLICT (i) WHERE i > 0 DO UPDATE SET i = 1; +INSERT INTO uniq_partial_index_and_constraint VALUES (-1) ON CONFLICT (i) WHERE i > 0 DO UPDATE SET i = 2 query I colnames -SELECT * FROM uniq_partial_index +SELECT * FROM uniq_partial_index_and_constraint ---- i 2 diff --git a/pkg/sql/opt/exec/execbuilder/testdata/unique b/pkg/sql/opt/exec/execbuilder/testdata/unique index d5bb809c5ac6..539b288c00ed 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/unique +++ b/pkg/sql/opt/exec/execbuilder/testdata/unique @@ -131,16 +131,77 @@ CREATE TABLE uniq_enum ( statement ok CREATE TABLE uniq_partial_enum ( r region DEFAULT CASE (random()*3)::int WHEN 0 THEN 'us-east' WHEN 1 THEN 'us-west' ELSE 'eu-west' END, - i INT, - s STRING, - PRIMARY KEY (r, i), - UNIQUE WITHOUT INDEX (i) WHERE s IN ('foo', 'bar', 'baz'), - INDEX (r, i) WHERE s IN ('foo', 'bar', 'baz'), + a INT, + b INT, + c STRING, + PRIMARY KEY (r, a), + UNIQUE WITHOUT INDEX (b) WHERE c IN ('foo', 'bar', 'baz'), + INDEX (r, b) WHERE c IN ('foo', 'bar', 'baz'), FAMILY (r), - FAMILY (s), - FAMILY (i) + FAMILY (a), + FAMILY (b), + FAMILY (c) ) +statement ok +ALTER TABLE uniq_partial_enum INJECT STATISTICS '[ + { + "columns": ["r"], + "created_at": "2018-01-01 1:00:00.00000+00:00", + "row_count": 1000, + "distinct_count": 3, + "histo_col_type": "region", + "histo_buckets": [ + {"num_eq": 333, "num_range": 0, "distinct_range": 0, "upper_bound": "eu-west"}, + {"num_eq": 333, "num_range": 0, "distinct_range": 0, "upper_bound": "us-east"}, + {"num_eq": 334, "num_range": 0, "distinct_range": 0, "upper_bound": "us-west"} + ] + }, + { + "columns": ["a"], + "created_at": "2018-01-01 1:00:00.00000+00:00", + "row_count": 1000, + "distinct_count": 1000, + "histo_col_type": "int", + "histo_buckets": [ + {"num_eq": 1, "num_range": 0, "distinct_range": 0, "upper_bound": "0"}, + {"num_eq": 1, "num_range": 199, "distinct_range": 199, "upper_bound": "200"}, + {"num_eq": 1, "num_range": 199, "distinct_range": 199, "upper_bound": "400"}, + {"num_eq": 1, "num_range": 199, "distinct_range": 199, "upper_bound": "600"}, + {"num_eq": 1, "num_range": 199, "distinct_range": 199, "upper_bound": "800"}, + {"num_eq": 1, "num_range": 198, "distinct_range": 198, "upper_bound": "999"} + ] + }, + { + "columns": ["b"], + "created_at": "2018-01-01 1:00:00.00000+00:00", + "row_count": 1000, + "distinct_count": 1000, + "histo_col_type": "int", + "histo_buckets": [ + {"num_eq": 1, "num_range": 0, "distinct_range": 0, "upper_bound": "0"}, + {"num_eq": 1, "num_range": 199, "distinct_range": 199, "upper_bound": "200"}, + {"num_eq": 1, "num_range": 199, "distinct_range": 199, "upper_bound": "400"}, + {"num_eq": 1, "num_range": 199, "distinct_range": 199, "upper_bound": "600"}, + {"num_eq": 1, "num_range": 199, "distinct_range": 199, "upper_bound": "800"}, + {"num_eq": 1, "num_range": 198, "distinct_range": 198, "upper_bound": "999"} + ] + }, + { + "columns": ["c"], + "created_at": "2018-01-01 1:00:00.00000+00:00", + "row_count": 1000, + "distinct_count": 4, + "histo_col_type": "string", + "histo_buckets": [ + {"num_eq": 200, "num_range": 0, "distinct_range": 0, "upper_bound": "bar"}, + {"num_eq": 200, "num_range": 0, "distinct_range": 0, "upper_bound": "baz"}, + {"num_eq": 200, "num_range": 0, "distinct_range": 0, "upper_bound": "foo"}, + {"num_eq": 400, "num_range": 0, "distinct_range": 0, "upper_bound": "fud"} + ] + } +]' + statement ok CREATE TABLE other (k INT, v INT, w INT NOT NULL, x INT, y INT) @@ -1389,7 +1450,7 @@ vectorized: true # Test that we use the partial index when available for the insert checks. query T -EXPLAIN (VERBOSE) INSERT INTO uniq_partial_enum VALUES ('us-west', 1, 'foo'), ('us-east', 2, 'bar') +EXPLAIN (VERBOSE) INSERT INTO uniq_partial_enum VALUES ('us-west', 1, 1, 'foo'), ('us-east', 2, 2, 'bar') ---- distribution: local vectorized: true @@ -1400,30 +1461,33 @@ vectorized: true ├── • insert │ │ columns: () │ │ estimated row count: 0 (missing stats) -│ │ into: uniq_partial_enum(r, i, s) +│ │ into: uniq_partial_enum(r, a, b, c) │ │ │ └── • buffer -│ │ columns: (column1, column2, column3, check1, partial_index_put1) +│ │ columns: (column1, column2, column3, column4, check1, partial_index_put1) │ │ label: buffer 1 │ │ │ └── • render -│ │ columns: (column1, column2, column3, check1, partial_index_put1) +│ │ columns: (column1, column2, column3, column4, check1, partial_index_put1) │ │ estimated row count: 2 -│ │ render partial_index_put1: column3 IN ('bar', 'baz', 'foo') +│ │ render partial_index_put1: column4 IN ('bar', 'baz', 'foo') │ │ render check1: column1 IN ('us-east', 'us-west', 'eu-west') │ │ render column1: column1 │ │ render column2: column2 │ │ render column3: column3 +│ │ render column4: column4 │ │ │ └── • values -│ columns: (column1, column2, column3) -│ size: 3 columns, 2 rows +│ columns: (column1, column2, column3, column4) +│ size: 4 columns, 2 rows │ row 0, expr 0: 'us-west' │ row 0, expr 1: 1 -│ row 0, expr 2: 'foo' +│ row 0, expr 2: 1 +│ row 0, expr 3: 'foo' │ row 1, expr 0: 'us-east' │ row 1, expr 1: 2 -│ row 1, expr 2: 'bar' +│ row 1, expr 2: 2 +│ row 1, expr 3: 'bar' │ └── • constraint-check │ @@ -1431,44 +1495,43 @@ vectorized: true │ columns: () │ └── • project - │ columns: (column1, column2, column3) - │ estimated row count: 1 (missing stats) + │ columns: (column1, column2, column3, column4) + │ estimated row count: 1 │ └── • lookup join (semi) - │ columns: ("lookup_join_const_col_@11", column1, column2, column3) - │ table: uniq_partial_enum@uniq_partial_enum_r_i_idx (partial index) - │ equality: (lookup_join_const_col_@11, column2) = (r,i) - │ equality cols are key - │ pred: column1 != r + │ 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) │ └── • cross join (inner) - │ columns: ("lookup_join_const_col_@11", column1, column2, column3) + │ columns: ("lookup_join_const_col_@13", column1, column2, column3, column4) │ estimated row count: 6 │ ├── • values - │ columns: ("lookup_join_const_col_@11") + │ 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' │ └── • filter - │ columns: (column1, column2, column3) + │ columns: (column1, column2, column3, column4) │ estimated row count: 2 - │ filter: column3 IN ('bar', 'baz', 'foo') + │ filter: column4 IN ('bar', 'baz', 'foo') │ └── • project - │ columns: (column1, column2, column3) + │ columns: (column1, column2, column3, column4) │ estimated row count: 2 │ └── • scan buffer - columns: (column1, column2, column3, check1, partial_index_put1) + 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. query T -EXPLAIN (VERBOSE) INSERT INTO uniq_partial_enum VALUES ('us-west', 1, 'foo'), ('us-east', 2, 'bar') +EXPLAIN (VERBOSE) INSERT INTO uniq_partial_enum VALUES ('us-west', 1, 1, 'foo'), ('us-east', 2, 2, 'bar') ON CONFLICT DO NOTHING ---- distribution: local @@ -1480,60 +1543,64 @@ vectorized: true ├── • insert │ │ columns: () │ │ estimated row count: 0 (missing stats) -│ │ into: uniq_partial_enum(r, i, s) +│ │ into: uniq_partial_enum(r, a, b, c) │ │ arbiter indexes: primary -│ │ arbiter constraints: unique_i +│ │ arbiter constraints: unique_b │ │ │ └── • buffer -│ │ columns: (column1, column2, column3, check1, partial_index_put1) +│ │ columns: (column1, column2, column3, column4, check1, partial_index_put1) │ │ label: buffer 1 │ │ │ └── • render -│ │ columns: (column1, column2, column3, check1, partial_index_put1) -│ │ estimated row count: 0 (missing stats) -│ │ render partial_index_put1: column3 IN ('bar', 'baz', 'foo') +│ │ columns: (column1, column2, column3, column4, check1, partial_index_put1) +│ │ estimated row count: 0 +│ │ render partial_index_put1: column4 IN ('bar', 'baz', 'foo') │ │ render check1: column1 IN ('us-east', 'us-west', 'eu-west') │ │ render column1: column1 │ │ render column2: column2 │ │ render column3: column3 +│ │ render column4: column4 │ │ │ └── • distinct -│ │ columns: (arbiter_unique_i_distinct, column1, column2, column3) -│ │ estimated row count: 0 (missing stats) -│ │ distinct on: arbiter_unique_i_distinct, column2 +│ │ columns: (arbiter_unique_b_distinct, column1, column2, column3, column4) +│ │ estimated row count: 0 +│ │ distinct on: arbiter_unique_b_distinct, column3 │ │ nulls are distinct │ │ │ └── • render -│ │ columns: (arbiter_unique_i_distinct, column1, column2, column3) -│ │ estimated row count: 0 (missing stats) -│ │ render arbiter_unique_i_distinct: (column3 IN ('bar', 'baz', 'foo')) OR CAST(NULL AS BOOL) +│ │ columns: (arbiter_unique_b_distinct, column1, column2, column3, column4) +│ │ estimated row count: 0 +│ │ render arbiter_unique_b_distinct: (column4 IN ('bar', 'baz', 'foo')) OR CAST(NULL AS BOOL) │ │ render column1: column1 │ │ render column2: column2 │ │ render column3: column3 +│ │ render column4: column4 │ │ │ └── • lookup join (anti) -│ │ columns: (column1, column2, column3) -│ │ estimated row count: 0 (missing stats) -│ │ table: uniq_partial_enum@uniq_partial_enum_r_i_idx (partial index) -│ │ lookup condition: (column2 = i) AND (r IN ('us-east', 'us-west', 'eu-west')) -│ │ pred: column3 IN ('bar', 'baz', 'foo') +│ │ columns: (column1, column2, column3, column4) +│ │ estimated row count: 0 +│ │ 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: column4 IN ('bar', 'baz', 'foo') │ │ │ └── • lookup join (anti) -│ │ columns: (column1, column2, column3) -│ │ estimated row count: 0 (missing stats) +│ │ columns: (column1, column2, column3, column4) +│ │ estimated row count: 0 │ │ table: uniq_partial_enum@primary -│ │ equality: (column1, column2) = (r,i) +│ │ equality: (column1, column2) = (r,a) │ │ equality cols are key │ │ │ └── • values -│ columns: (column1, column2, column3) -│ size: 3 columns, 2 rows +│ columns: (column1, column2, column3, column4) +│ size: 4 columns, 2 rows │ row 0, expr 0: 'us-west' │ row 0, expr 1: 1 -│ row 0, expr 2: 'foo' +│ row 0, expr 2: 1 +│ row 0, expr 3: 'foo' │ row 1, expr 0: 'us-east' │ row 1, expr 1: 2 -│ row 1, expr 2: 'bar' +│ row 1, expr 2: 2 +│ row 1, expr 3: 'bar' │ └── • constraint-check │ @@ -1541,38 +1608,37 @@ vectorized: true │ columns: () │ └── • project - │ columns: (column1, column2, column3) - │ estimated row count: 0 (missing stats) + │ columns: (column1, column2, column3, column4) + │ estimated row count: 0 │ └── • lookup join (semi) - │ columns: ("lookup_join_const_col_@22", column1, column2, column3) - │ table: uniq_partial_enum@uniq_partial_enum_r_i_idx (partial index) - │ equality: (lookup_join_const_col_@22, column2) = (r,i) - │ equality cols are key - │ pred: column1 != r + │ columns: ("lookup_join_const_col_@26", column1, column2, column3, column4) + │ table: uniq_partial_enum@uniq_partial_enum_r_b_idx (partial index) + │ equality: (lookup_join_const_col_@26, column3) = (r,b) + │ pred: (column1 != r) OR (column2 != a) │ └── • cross join (inner) - │ columns: ("lookup_join_const_col_@22", column1, column2, column3) - │ estimated row count: 0 (missing stats) + │ columns: ("lookup_join_const_col_@26", column1, column2, column3, column4) + │ estimated row count: 0 │ ├── • values - │ columns: ("lookup_join_const_col_@22") + │ columns: ("lookup_join_const_col_@26") │ size: 1 column, 3 rows │ row 0, expr 0: 'us-east' │ row 1, expr 0: 'us-west' │ row 2, expr 0: 'eu-west' │ └── • filter - │ columns: (column1, column2, column3) - │ estimated row count: 0 (missing stats) - │ filter: column3 IN ('bar', 'baz', 'foo') + │ columns: (column1, column2, column3, column4) + │ estimated row count: 0 + │ filter: column4 IN ('bar', 'baz', 'foo') │ └── • project - │ columns: (column1, column2, column3) - │ estimated row count: 0 (missing stats) + │ columns: (column1, column2, column3, column4) + │ estimated row count: 0 │ └── • scan buffer - columns: (column1, column2, column3, check1, partial_index_put1) + columns: (column1, column2, column3, column4, check1, partial_index_put1) label: buffer 1 # -- Tests with UPDATE -- @@ -1946,7 +2012,7 @@ vectorized: true spans: FULL SCAN # Combine unique checks with foreign keys. -# There is no uniquness check since column c is not updated. +# There is no uniqueness check since column c is not updated. query T EXPLAIN UPDATE uniq_fk_child SET a = 1, b = 2 ---- @@ -2179,6 +2245,290 @@ vectorized: true row 1, expr 0: 'us-west' row 2, expr 0: 'eu-west' +# None of the updated values have nulls. +query T +EXPLAIN UPDATE uniq_partial SET a = 1, b = 2 +---- +distribution: local +vectorized: true +· +• root +│ +├── • update +│ │ table: uniq_partial +│ │ set: a, b +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • render +│ │ +│ └── • scan +│ missing stats +│ table: uniq_partial@primary +│ spans: FULL SCAN +│ locking strength: for update +│ +├── • constraint-check +│ │ +│ └── • error if rows +│ │ +│ └── • hash join (semi) +│ │ equality: (a_new) = (a) +│ │ pred: k != k +│ │ +│ ├── • filter +│ │ │ filter: b_new > 0 +│ │ │ +│ │ └── • scan buffer +│ │ label: buffer 1 +│ │ +│ └── • filter +│ │ filter: b > 0 +│ │ +│ └── • scan +│ missing stats +│ table: uniq_partial@primary +│ spans: FULL SCAN +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • hash join (semi) + │ equality: (b_new) = (b) + │ pred: k != k + │ + ├── • filter + │ │ filter: b_new > 0 + │ │ + │ └── • scan buffer + │ label: buffer 1 + │ + └── • filter + │ filter: b > 0 + │ + └── • scan + missing stats + table: uniq_partial@primary + spans: FULL SCAN + +# No need to plan checks for a since a is always null. +# Also update the primary key. +query T +EXPLAIN UPDATE uniq_partial SET k = 1, a = NULL, b = 2 +---- +distribution: local +vectorized: true +· +• root +│ +├── • update +│ │ table: uniq_partial +│ │ set: k, a, b +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • render +│ │ +│ └── • scan +│ missing stats +│ table: uniq_partial@primary +│ spans: FULL SCAN +│ locking strength: for update +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • hash join (semi) + │ equality: (b_new) = (b) + │ pred: k_new != k + │ + ├── • filter + │ │ filter: b_new > 0 + │ │ + │ └── • scan buffer + │ label: buffer 1 + │ + └── • filter + │ filter: b > 0 + │ + └── • scan + missing stats + table: uniq_partial@primary + spans: FULL SCAN + +# No need to plan checks since none of the columns requiring checks are updated. +query T +EXPLAIN UPDATE uniq_partial SET k = 1 +---- +distribution: local +vectorized: true +· +• update +│ table: uniq_partial +│ set: k +│ auto commit +│ +└── • render + │ + └── • scan + missing stats + table: uniq_partial@primary + spans: FULL SCAN + locking strength: for update + +# Plan checks for a since b is in the partial predicate and is updated. +query T +EXPLAIN UPDATE uniq_partial SET b = 2 +---- +distribution: local +vectorized: true +· +• root +│ +├── • update +│ │ table: uniq_partial +│ │ set: b +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • render +│ │ +│ └── • scan +│ missing stats +│ table: uniq_partial@primary +│ spans: FULL SCAN +│ locking strength: for update +│ +├── • constraint-check +│ │ +│ └── • error if rows +│ │ +│ └── • hash join (semi) +│ │ equality: (a) = (a) +│ │ pred: k != k +│ │ +│ ├── • filter +│ │ │ filter: b_new > 0 +│ │ │ +│ │ └── • scan buffer +│ │ label: buffer 1 +│ │ +│ └── • filter +│ │ filter: b > 0 +│ │ +│ └── • scan +│ missing stats +│ table: uniq_partial@primary +│ spans: FULL SCAN +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • hash join (semi) + │ equality: (b_new) = (b) + │ pred: k != k + │ + ├── • filter + │ │ filter: b_new > 0 + │ │ + │ └── • scan buffer + │ label: buffer 1 + │ + └── • filter + │ filter: b > 0 + │ + └── • scan + missing stats + table: uniq_partial@primary + spans: FULL SCAN + +# Test that we use the index when available for the update checks. +query T +EXPLAIN (VERBOSE) UPDATE uniq_partial_enum SET b = 20 WHERE a = 2 +---- +distribution: local +vectorized: true +· +• root +│ columns: () +│ +├── • update +│ │ columns: () +│ │ estimated row count: 0 (missing stats) +│ │ table: uniq_partial_enum +│ │ set: b +│ │ +│ └── • buffer +│ │ columns: (r, a, b, b_new, partial_index_put1, partial_index_put1, c) +│ │ label: buffer 1 +│ │ +│ └── • project +│ │ columns: (r, a, b, b_new, partial_index_put1, partial_index_put1, c) +│ │ +│ └── • render +│ │ columns: (partial_index_put1, b_new, r, a, b, c) +│ │ estimated row count: 1 +│ │ render partial_index_put1: c IN ('bar', 'baz', 'foo') +│ │ render b_new: 20 +│ │ render r: r +│ │ render a: a +│ │ render b: b +│ │ render c: c +│ │ +│ └── • scan +│ columns: (r, a, b, c) +│ estimated row count: 1 (0.10% of the table) +│ table: uniq_partial_enum@primary +│ spans: /"@"/2/0-/"@"/2/1 /"@"/2/2/1-/"@"/2/3/2 /"\x80"/2/0-/"\x80"/2/1 /"\x80"/2/2/1-/"\x80"/2/3/2 /"\xc0"/2/0-/"\xc0"/2/1 /"\xc0"/2/2/1-/"\xc0"/2/3/2 +│ parallel +│ locking strength: for update +│ +└── • constraint-check + │ + └── • error if rows + │ columns: () + │ + └── • project + │ columns: (r, a, b_new, c) + │ estimated row count: 0 + │ + └── • 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) + │ + └── • 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' + │ + └── • 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 + + # -- Tests with UPSERT -- subtest Upsert diff --git a/pkg/sql/opt/norm/testdata/rules/prune_cols b/pkg/sql/opt/norm/testdata/rules/prune_cols index 6431de0c0f3f..6d4d935295a6 100644 --- a/pkg/sql/opt/norm/testdata/rules/prune_cols +++ b/pkg/sql/opt/norm/testdata/rules/prune_cols @@ -89,6 +89,20 @@ CREATE TABLE uniq ( ) ---- +exec-ddl +CREATE TABLE uniq_partial ( + k INT PRIMARY KEY, + v INT, + w INT, + x INT, + UNIQUE WITHOUT INDEX (v) WHERE w > 0, + FAMILY (k), + FAMILY (v), + FAMILY (w), + FAMILY (x) +) +---- + exec-ddl CREATE TABLE uniq_fk_parent ( k INT PRIMARY KEY, @@ -3578,6 +3592,8 @@ upsert checks └── column2:7 > upsert_b:16 [as=check3:19, outer=(7,16)] # Do not prune columns from updates that are needed for unique checks. +# TODO(mgartner): v and z can be pruned because they are not updated and not +# needed for uniqueness checks. norm expect=PruneMutationInputCols UPDATE uniq SET w = 1, x = 2 WHERE k = 3 ---- @@ -3662,6 +3678,76 @@ update uniq ├── y:41 = uniq.y:34 [outer=(34,41), constraints=(/34: (/NULL - ]; /41: (/NULL - ]), fd=(34)==(41), (41)==(34)] └── k:37 != uniq.k:30 [outer=(30,37), constraints=(/30: (/NULL - ]; /37: (/NULL - ])] +# Do not prune columns from updates that are needed for partial unique checks. +# TODO(mgartner): x can be pruned because it is not updated and not needed for +# uniqueness checks. +norm expect=PruneMutationInputCols +UPDATE uniq_partial SET v = 1 WHERE k = 3 +---- +update uniq_partial + ├── columns: + ├── fetch columns: uniq_partial.k:6 uniq_partial.v:7 + ├── update-mapping: + │ └── v_new:11 => uniq_partial.v:2 + ├── input binding: &1 + ├── cardinality: [0 - 0] + ├── volatile, mutations + ├── project + │ ├── columns: v_new:11!null uniq_partial.k:6!null uniq_partial.v:7 uniq_partial.w:8 uniq_partial.x:9 + │ ├── cardinality: [0 - 1] + │ ├── key: () + │ ├── fd: ()-->(6-9,11) + │ ├── select + │ │ ├── columns: uniq_partial.k:6!null uniq_partial.v:7 uniq_partial.w:8 uniq_partial.x:9 + │ │ ├── cardinality: [0 - 1] + │ │ ├── key: () + │ │ ├── fd: ()-->(6-9) + │ │ ├── scan uniq_partial + │ │ │ ├── columns: uniq_partial.k:6!null uniq_partial.v:7 uniq_partial.w:8 uniq_partial.x:9 + │ │ │ ├── key: (6) + │ │ │ └── fd: (6)-->(7-9) + │ │ └── filters + │ │ └── uniq_partial.k:6 = 3 [outer=(6), constraints=(/6: [/3 - /3]; tight), fd=()-->(6)] + │ └── projections + │ └── 1 [as=v_new:11] + └── unique-checks + └── unique-checks-item: uniq_partial(v) + └── semi-join (hash) + ├── columns: k:17!null v:18!null w:19!null x:20 + ├── cardinality: [0 - 1] + ├── key: () + ├── fd: ()-->(17-20) + ├── select + │ ├── columns: k:17!null v:18!null w:19!null x:20 + │ ├── cardinality: [0 - 1] + │ ├── key: () + │ ├── fd: ()-->(17-20) + │ ├── with-scan &1 + │ │ ├── columns: k:17!null v:18!null w:19 x:20 + │ │ ├── mapping: + │ │ │ ├── uniq_partial.k:6 => k:17 + │ │ │ ├── v_new:11 => v:18 + │ │ │ ├── uniq_partial.w:8 => w:19 + │ │ │ └── uniq_partial.x:9 => x:20 + │ │ ├── cardinality: [0 - 1] + │ │ ├── key: () + │ │ └── fd: ()-->(17-20) + │ └── filters + │ └── w:19 > 0 [outer=(19), constraints=(/19: [/1 - ]; tight)] + ├── select + │ ├── columns: uniq_partial.k:12!null uniq_partial.v:13 uniq_partial.w:14!null + │ ├── key: (12) + │ ├── fd: (12)-->(13,14) + │ ├── scan uniq_partial + │ │ ├── columns: uniq_partial.k:12!null uniq_partial.v:13 uniq_partial.w:14 + │ │ ├── key: (12) + │ │ └── fd: (12)-->(13,14) + │ └── filters + │ └── uniq_partial.w:14 > 0 [outer=(14), constraints=(/14: [/1 - ]; tight)] + └── filters + ├── v:18 = uniq_partial.v:13 [outer=(13,18), constraints=(/13: (/NULL - ]; /18: (/NULL - ]), fd=(13)==(18), (18)==(13)] + └── k:17 != uniq_partial.k:12 [outer=(12,17), constraints=(/12: (/NULL - ]; /17: (/NULL - ])] + # Do not prune columns that are needed for foreign key checks or cascades. norm expect=PruneMutationInputCols INSERT INTO uniq_fk_parent VALUES (2, 1) ON CONFLICT (k) DO UPDATE SET c = 1 diff --git a/pkg/sql/opt/optbuilder/mutation_builder.go b/pkg/sql/opt/optbuilder/mutation_builder.go index 7e4aec570c3a..03e3843dd1b0 100644 --- a/pkg/sql/opt/optbuilder/mutation_builder.go +++ b/pkg/sql/opt/optbuilder/mutation_builder.go @@ -1206,12 +1206,12 @@ func (mb *mutationBuilder) parsePartialIndexPredicateExpr(idx cat.IndexOrdinal) // parseUniqueConstraintPredicateExpr parses the predicate of the given partial // unique constraint and caches it for reuse. This function panics if the unique // constraint at the given ordinal is not partial. -func (mb *mutationBuilder) parseUniqueConstraintPredicateExpr(idx cat.UniqueOrdinal) tree.Expr { - uniqueConstraint := mb.tab.Unique(idx) +func (mb *mutationBuilder) parseUniqueConstraintPredicateExpr(uniq cat.UniqueOrdinal) tree.Expr { + uniqueConstraint := mb.tab.Unique(uniq) predStr, isPartial := uniqueConstraint.Predicate() if !isPartial { - panic(errors.AssertionFailedf("unique constraint at ordinal %d is not a partial unique constraint", idx)) + panic(errors.AssertionFailedf("unique constraint at ordinal %d is not a partial unique constraint", uniq)) } if mb.parsedUniqueConstraintExprs == nil { @@ -1219,8 +1219,8 @@ func (mb *mutationBuilder) parseUniqueConstraintPredicateExpr(idx cat.UniqueOrdi } // Return expression from the cache, if it was already parsed previously. - if mb.parsedUniqueConstraintExprs[idx] != nil { - return mb.parsedUniqueConstraintExprs[idx] + if mb.parsedUniqueConstraintExprs[uniq] != nil { + return mb.parsedUniqueConstraintExprs[uniq] } expr, err := parser.ParseExpr(predStr) @@ -1228,7 +1228,7 @@ func (mb *mutationBuilder) parseUniqueConstraintPredicateExpr(idx cat.UniqueOrdi panic(err) } - mb.parsedUniqueConstraintExprs[idx] = expr + mb.parsedUniqueConstraintExprs[uniq] = expr return expr } diff --git a/pkg/sql/opt/optbuilder/mutation_builder_unique.go b/pkg/sql/opt/optbuilder/mutation_builder_unique.go index 5cedbbc68253..492875cff950 100644 --- a/pkg/sql/opt/optbuilder/mutation_builder_unique.go +++ b/pkg/sql/opt/optbuilder/mutation_builder_unique.go @@ -107,14 +107,32 @@ func (mb *mutationBuilder) hasUniqueWithoutIndexConstraints() bool { } // uniqueColsUpdated returns true if any of the columns for a unique -// constraint are being updated (according to updateColIDs). +// constraint are being updated (according to updateColIDs). When the unique +// constraint has a partial predicate, it also returns true if the predicate +// references any of the columns being updated. func (mb *mutationBuilder) uniqueColsUpdated(uniqueOrdinal int) bool { uc := mb.tab.Unique(uniqueOrdinal) + for i, n := 0, uc.ColumnCount(); i < n; i++ { if ord := uc.ColumnOrdinal(mb.tab, i); mb.updateColIDs[ord] != 0 { return true } } + + if _, isPartial := uc.Predicate(); isPartial { + pred := mb.parseUniqueConstraintPredicateExpr(uniqueOrdinal) + typedPred := mb.fetchScope.resolveAndRequireType(pred, types.Bool) + + var predCols opt.ColSet + mb.b.buildScalar(typedPred, mb.fetchScope, nil, nil, &predCols) + for colID, ok := predCols.Next(0); ok; colID, ok = predCols.Next(colID + 1) { + ord := mb.md.ColumnMeta(colID).Table.ColumnOrdinal(colID) + if mb.updateColIDs[ord] != 0 { + return true + } + } + } + return false } diff --git a/pkg/sql/opt/optbuilder/testdata/unique-checks-update b/pkg/sql/opt/optbuilder/testdata/unique-checks-update index 77ced5f530b6..a1627c60cd21 100644 --- a/pkg/sql/opt/optbuilder/testdata/unique-checks-update +++ b/pkg/sql/opt/optbuilder/testdata/unique-checks-update @@ -62,7 +62,7 @@ update uniq ├── y:36 = uniq.y:30 └── k:32 != uniq.k:26 -# No need to plan checks for w since it's aways null. +# No need to plan checks for w since it's always null. build UPDATE uniq SET w = NULL, x = 1 ---- @@ -99,7 +99,7 @@ update uniq ├── y:25 = uniq.y:19 └── k:21 != uniq.k:15 -# No need to plan checks for x,y since x is aways null. +# No need to plan checks for x,y since x is always null. # Also update the primary key. build UPDATE uniq SET k = 1, w = 2, x = NULL @@ -138,7 +138,7 @@ update uniq ├── w:24 = uniq.w:18 └── k:22 != uniq.k:16 -# No need to plan checks for x,y since y is aways null. +# No need to plan checks for x,y since y is always null. build UPDATE uniq SET w = 1, y = NULL WHERE k = 1 ---- @@ -570,3 +570,444 @@ update uniq_hidden_pk └── filters ├── a:37 = uniq_hidden_pk.a:31 └── rowid:41 != uniq_hidden_pk.rowid:35 + +exec-ddl +CREATE TABLE uniq_partial ( + k INT PRIMARY KEY, + a INT, + b INT, + c INT, + UNIQUE WITHOUT INDEX (a) WHERE b > 0 +) +---- + +# None of the updated values have nulls. +build +UPDATE uniq_partial SET a = 1 +---- +update uniq_partial + ├── columns: + ├── fetch columns: uniq_partial.k:6 uniq_partial.a:7 uniq_partial.b:8 uniq_partial.c:9 + ├── update-mapping: + │ └── a_new:11 => uniq_partial.a:2 + ├── input binding: &1 + ├── project + │ ├── columns: a_new:11!null uniq_partial.k:6!null uniq_partial.a:7 uniq_partial.b:8 uniq_partial.c:9 crdb_internal_mvcc_timestamp:10 + │ ├── scan uniq_partial + │ │ └── columns: uniq_partial.k:6!null uniq_partial.a:7 uniq_partial.b:8 uniq_partial.c:9 crdb_internal_mvcc_timestamp:10 + │ └── projections + │ └── 1 [as=a_new:11] + └── unique-checks + └── unique-checks-item: uniq_partial(a) + └── semi-join (hash) + ├── columns: k:17!null a:18!null b:19 c:20 + ├── with-scan &1 + │ ├── columns: k:17!null a:18!null b:19 c:20 + │ └── mapping: + │ ├── uniq_partial.k:6 => k:17 + │ ├── a_new:11 => a:18 + │ ├── uniq_partial.b:8 => b:19 + │ └── uniq_partial.c:9 => c:20 + ├── scan uniq_partial + │ └── columns: uniq_partial.k:12!null uniq_partial.a:13 uniq_partial.b:14 uniq_partial.c:15 + └── filters + ├── a:18 = uniq_partial.a:13 + ├── b:19 > 0 + ├── uniq_partial.b:14 > 0 + └── k:17 != uniq_partial.k:12 + +# Plan a check when a column in the predicate is updated. +build +UPDATE uniq_partial SET b = 1 +---- +update uniq_partial + ├── columns: + ├── fetch columns: uniq_partial.k:6 uniq_partial.a:7 uniq_partial.b:8 uniq_partial.c:9 + ├── update-mapping: + │ └── b_new:11 => uniq_partial.b:3 + ├── input binding: &1 + ├── project + │ ├── columns: b_new:11!null uniq_partial.k:6!null uniq_partial.a:7 uniq_partial.b:8 uniq_partial.c:9 crdb_internal_mvcc_timestamp:10 + │ ├── scan uniq_partial + │ │ └── columns: uniq_partial.k:6!null uniq_partial.a:7 uniq_partial.b:8 uniq_partial.c:9 crdb_internal_mvcc_timestamp:10 + │ └── projections + │ └── 1 [as=b_new:11] + └── unique-checks + └── unique-checks-item: uniq_partial(a) + └── semi-join (hash) + ├── columns: k:17!null a:18 b:19!null c:20 + ├── with-scan &1 + │ ├── columns: k:17!null a:18 b:19!null c:20 + │ └── mapping: + │ ├── uniq_partial.k:6 => k:17 + │ ├── uniq_partial.a:7 => a:18 + │ ├── b_new:11 => b:19 + │ └── uniq_partial.c:9 => c:20 + ├── scan uniq_partial + │ └── columns: uniq_partial.k:12!null uniq_partial.a:13 uniq_partial.b:14 uniq_partial.c:15 + └── filters + ├── a:18 = uniq_partial.a:13 + ├── b:19 > 0 + ├── uniq_partial.b:14 > 0 + └── k:17 != uniq_partial.k:12 + +# No need to plan checks for a since it's always null. +build +UPDATE uniq_partial SET a = NULL, b = 1 +---- +update uniq_partial + ├── columns: + ├── fetch columns: k:6 a:7 b:8 c:9 + ├── update-mapping: + │ ├── a_new:11 => a:2 + │ └── b_new:12 => b:3 + └── project + ├── columns: a_new:11 b_new:12!null k:6!null a:7 b:8 c:9 crdb_internal_mvcc_timestamp:10 + ├── scan uniq_partial + │ └── columns: k:6!null a:7 b:8 c:9 crdb_internal_mvcc_timestamp:10 + └── projections + ├── NULL::INT8 [as=a_new:11] + └── 1 [as=b_new:12] + +# No need to plan checks for a since it's always null. +# Also update the primary key. +build +UPDATE uniq_partial SET k = 1, a = NULL, b = 1 +---- +update uniq_partial + ├── columns: + ├── fetch columns: k:6 a:7 b:8 c:9 + ├── update-mapping: + │ ├── k_new:11 => k:1 + │ ├── a_new:12 => a:2 + │ └── k_new:11 => b:3 + └── project + ├── columns: k_new:11!null a_new:12 k:6!null a:7 b:8 c:9 crdb_internal_mvcc_timestamp:10 + ├── scan uniq_partial + │ └── columns: k:6!null a:7 b:8 c:9 crdb_internal_mvcc_timestamp:10 + └── projections + ├── 1 [as=k_new:11] + └── NULL::INT8 [as=a_new:12] + +# No need to plan checks since none of the columns in the unique constraint or +# its predicate are updated. +build +UPDATE uniq_partial SET c = 2 +---- +update uniq_partial + ├── columns: + ├── fetch columns: k:6 a:7 b:8 c:9 + ├── update-mapping: + │ └── c_new:11 => c:4 + └── project + ├── columns: c_new:11!null k:6!null a:7 b:8 c:9 crdb_internal_mvcc_timestamp:10 + ├── scan uniq_partial + │ └── columns: k:6!null a:7 b:8 c:9 crdb_internal_mvcc_timestamp:10 + └── projections + └── 2 [as=c_new:11] + +# Update with non-constant input. +build +UPDATE uniq_partial SET a = other.w, b = other.x FROM other +---- +update uniq_partial + ├── columns: + ├── fetch columns: uniq_partial.k:6 uniq_partial.a:7 uniq_partial.b:8 uniq_partial.c:9 + ├── update-mapping: + │ ├── w:13 => uniq_partial.a:2 + │ └── x:14 => uniq_partial.b:3 + ├── input binding: &1 + ├── distinct-on + │ ├── columns: uniq_partial.k:6!null uniq_partial.a:7 uniq_partial.b:8 uniq_partial.c:9 uniq_partial.crdb_internal_mvcc_timestamp:10 other.k:11 v:12 w:13!null x:14 y:15 rowid:16!null other.crdb_internal_mvcc_timestamp:17 + │ ├── grouping columns: uniq_partial.k:6!null + │ ├── inner-join (cross) + │ │ ├── columns: uniq_partial.k:6!null uniq_partial.a:7 uniq_partial.b:8 uniq_partial.c:9 uniq_partial.crdb_internal_mvcc_timestamp:10 other.k:11 v:12 w:13!null x:14 y:15 rowid:16!null other.crdb_internal_mvcc_timestamp:17 + │ │ ├── scan uniq_partial + │ │ │ └── columns: uniq_partial.k:6!null uniq_partial.a:7 uniq_partial.b:8 uniq_partial.c:9 uniq_partial.crdb_internal_mvcc_timestamp:10 + │ │ ├── scan other + │ │ │ └── columns: other.k:11 v:12 w:13!null x:14 y:15 rowid:16!null other.crdb_internal_mvcc_timestamp:17 + │ │ └── filters (true) + │ └── aggregations + │ ├── first-agg [as=uniq_partial.a:7] + │ │ └── uniq_partial.a:7 + │ ├── first-agg [as=uniq_partial.b:8] + │ │ └── uniq_partial.b:8 + │ ├── first-agg [as=uniq_partial.c:9] + │ │ └── uniq_partial.c:9 + │ ├── first-agg [as=uniq_partial.crdb_internal_mvcc_timestamp:10] + │ │ └── uniq_partial.crdb_internal_mvcc_timestamp:10 + │ ├── first-agg [as=other.k:11] + │ │ └── other.k:11 + │ ├── first-agg [as=v:12] + │ │ └── v:12 + │ ├── first-agg [as=w:13] + │ │ └── w:13 + │ ├── first-agg [as=x:14] + │ │ └── x:14 + │ ├── first-agg [as=y:15] + │ │ └── y:15 + │ ├── first-agg [as=rowid:16] + │ │ └── rowid:16 + │ └── first-agg [as=other.crdb_internal_mvcc_timestamp:17] + │ └── other.crdb_internal_mvcc_timestamp:17 + └── unique-checks + └── unique-checks-item: uniq_partial(a) + └── semi-join (hash) + ├── columns: k:23!null a:24!null b:25 c:26 + ├── with-scan &1 + │ ├── columns: k:23!null a:24!null b:25 c:26 + │ └── mapping: + │ ├── uniq_partial.k:6 => k:23 + │ ├── w:13 => a:24 + │ ├── x:14 => b:25 + │ └── uniq_partial.c:9 => c:26 + ├── scan uniq_partial + │ └── columns: uniq_partial.k:18!null uniq_partial.a:19 uniq_partial.b:20 uniq_partial.c:21 + └── filters + ├── a:24 = uniq_partial.a:19 + ├── b:25 > 0 + ├── uniq_partial.b:20 > 0 + └── k:23 != uniq_partial.k:18 + +exec-ddl +CREATE TABLE uniq_partial_overlaps_pk ( + a INT, + b INT, + c INT, + d INT, + PRIMARY KEY (a, b), + UNIQUE WITHOUT INDEX (c) WHERE d > 0, + UNIQUE WITHOUT INDEX (a) WHERE d > 0, + UNIQUE WITHOUT INDEX (a, b) WHERE d > 0, + UNIQUE WITHOUT INDEX (b, c) WHERE d > 0, + UNIQUE WITHOUT INDEX (a, b, c) WHERE d > 0 +) +---- + +# Update with constant input. +# Do not build uniqueness checks when the primary key columns are a subset of +# the partial unique constraint columns. +# Add inequality filters for the primary key columns that are not part of each +# unique constraint to prevent rows from matching themselves in the semi join. +build +UPDATE uniq_partial_overlaps_pk SET a = 1, b = 2, c = 3, d = 4 WHERE a = 5 +---- +update uniq_partial_overlaps_pk + ├── columns: + ├── fetch columns: uniq_partial_overlaps_pk.a:6 uniq_partial_overlaps_pk.b:7 uniq_partial_overlaps_pk.c:8 uniq_partial_overlaps_pk.d:9 + ├── update-mapping: + │ ├── a_new:11 => uniq_partial_overlaps_pk.a:1 + │ ├── b_new:12 => uniq_partial_overlaps_pk.b:2 + │ ├── c_new:13 => uniq_partial_overlaps_pk.c:3 + │ └── d_new:14 => uniq_partial_overlaps_pk.d:4 + ├── input binding: &1 + ├── project + │ ├── columns: a_new:11!null b_new:12!null c_new:13!null d_new:14!null uniq_partial_overlaps_pk.a:6!null uniq_partial_overlaps_pk.b:7!null uniq_partial_overlaps_pk.c:8 uniq_partial_overlaps_pk.d:9 crdb_internal_mvcc_timestamp:10 + │ ├── select + │ │ ├── columns: uniq_partial_overlaps_pk.a:6!null uniq_partial_overlaps_pk.b:7!null uniq_partial_overlaps_pk.c:8 uniq_partial_overlaps_pk.d:9 crdb_internal_mvcc_timestamp:10 + │ │ ├── scan uniq_partial_overlaps_pk + │ │ │ └── columns: uniq_partial_overlaps_pk.a:6!null uniq_partial_overlaps_pk.b:7!null uniq_partial_overlaps_pk.c:8 uniq_partial_overlaps_pk.d:9 crdb_internal_mvcc_timestamp:10 + │ │ └── filters + │ │ └── uniq_partial_overlaps_pk.a:6 = 5 + │ └── projections + │ ├── 1 [as=a_new:11] + │ ├── 2 [as=b_new:12] + │ ├── 3 [as=c_new:13] + │ └── 4 [as=d_new:14] + └── unique-checks + ├── unique-checks-item: uniq_partial_overlaps_pk(c) + │ └── semi-join (hash) + │ ├── columns: a:20!null b:21!null c:22!null d:23!null + │ ├── with-scan &1 + │ │ ├── columns: a:20!null b:21!null c:22!null d:23!null + │ │ └── mapping: + │ │ ├── a_new:11 => a:20 + │ │ ├── b_new:12 => b:21 + │ │ ├── c_new:13 => c:22 + │ │ └── d_new:14 => d:23 + │ ├── scan uniq_partial_overlaps_pk + │ │ └── columns: uniq_partial_overlaps_pk.a:15!null uniq_partial_overlaps_pk.b:16!null uniq_partial_overlaps_pk.c:17 uniq_partial_overlaps_pk.d:18 + │ └── filters + │ ├── c:22 = uniq_partial_overlaps_pk.c:17 + │ ├── d:23 > 0 + │ ├── uniq_partial_overlaps_pk.d:18 > 0 + │ └── (a:20 != uniq_partial_overlaps_pk.a:15) OR (b:21 != uniq_partial_overlaps_pk.b:16) + ├── unique-checks-item: uniq_partial_overlaps_pk(a) + │ └── semi-join (hash) + │ ├── columns: a:29!null b:30!null c:31!null d:32!null + │ ├── with-scan &1 + │ │ ├── columns: a:29!null b:30!null c:31!null d:32!null + │ │ └── mapping: + │ │ ├── a_new:11 => a:29 + │ │ ├── b_new:12 => b:30 + │ │ ├── c_new:13 => c:31 + │ │ └── d_new:14 => d:32 + │ ├── scan uniq_partial_overlaps_pk + │ │ └── columns: uniq_partial_overlaps_pk.a:24!null uniq_partial_overlaps_pk.b:25!null uniq_partial_overlaps_pk.c:26 uniq_partial_overlaps_pk.d:27 + │ └── filters + │ ├── a:29 = uniq_partial_overlaps_pk.a:24 + │ ├── d:32 > 0 + │ ├── uniq_partial_overlaps_pk.d:27 > 0 + │ └── b:30 != uniq_partial_overlaps_pk.b:25 + └── unique-checks-item: uniq_partial_overlaps_pk(b,c) + └── semi-join (hash) + ├── columns: a:38!null b:39!null c:40!null d:41!null + ├── with-scan &1 + │ ├── columns: a:38!null b:39!null c:40!null d:41!null + │ └── mapping: + │ ├── a_new:11 => a:38 + │ ├── b_new:12 => b:39 + │ ├── c_new:13 => c:40 + │ └── d_new:14 => d:41 + ├── scan uniq_partial_overlaps_pk + │ └── columns: uniq_partial_overlaps_pk.a:33!null uniq_partial_overlaps_pk.b:34!null uniq_partial_overlaps_pk.c:35 uniq_partial_overlaps_pk.d:36 + └── filters + ├── b:39 = uniq_partial_overlaps_pk.b:34 + ├── c:40 = uniq_partial_overlaps_pk.c:35 + ├── d:41 > 0 + ├── uniq_partial_overlaps_pk.d:36 > 0 + └── a:38 != uniq_partial_overlaps_pk.a:33 + +# Update with non-constant input. +# Do not build uniqueness checks when the primary key columns are a subset of +# the partial unique constraint columns. +# No need to add a check for b,c since those columns weren't updated. +# Add inequality filters for the primary key columns that are not part of each +# unique constraint to prevent rows from matching themselves in the semi join. +build +UPDATE uniq_partial_overlaps_pk SET a = k FROM other +---- +update uniq_partial_overlaps_pk + ├── columns: + ├── fetch columns: uniq_partial_overlaps_pk.a:6 uniq_partial_overlaps_pk.b:7 uniq_partial_overlaps_pk.c:8 uniq_partial_overlaps_pk.d:9 + ├── update-mapping: + │ └── k:11 => uniq_partial_overlaps_pk.a:1 + ├── input binding: &1 + ├── distinct-on + │ ├── columns: uniq_partial_overlaps_pk.a:6!null uniq_partial_overlaps_pk.b:7!null uniq_partial_overlaps_pk.c:8 uniq_partial_overlaps_pk.d:9 uniq_partial_overlaps_pk.crdb_internal_mvcc_timestamp:10 k:11 v:12 w:13!null x:14 y:15 rowid:16!null other.crdb_internal_mvcc_timestamp:17 + │ ├── grouping columns: uniq_partial_overlaps_pk.a:6!null uniq_partial_overlaps_pk.b:7!null + │ ├── inner-join (cross) + │ │ ├── columns: uniq_partial_overlaps_pk.a:6!null uniq_partial_overlaps_pk.b:7!null uniq_partial_overlaps_pk.c:8 uniq_partial_overlaps_pk.d:9 uniq_partial_overlaps_pk.crdb_internal_mvcc_timestamp:10 k:11 v:12 w:13!null x:14 y:15 rowid:16!null other.crdb_internal_mvcc_timestamp:17 + │ │ ├── scan uniq_partial_overlaps_pk + │ │ │ └── columns: uniq_partial_overlaps_pk.a:6!null uniq_partial_overlaps_pk.b:7!null uniq_partial_overlaps_pk.c:8 uniq_partial_overlaps_pk.d:9 uniq_partial_overlaps_pk.crdb_internal_mvcc_timestamp:10 + │ │ ├── scan other + │ │ │ └── columns: k:11 v:12 w:13!null x:14 y:15 rowid:16!null other.crdb_internal_mvcc_timestamp:17 + │ │ └── filters (true) + │ └── aggregations + │ ├── first-agg [as=uniq_partial_overlaps_pk.c:8] + │ │ └── uniq_partial_overlaps_pk.c:8 + │ ├── first-agg [as=uniq_partial_overlaps_pk.d:9] + │ │ └── uniq_partial_overlaps_pk.d:9 + │ ├── first-agg [as=uniq_partial_overlaps_pk.crdb_internal_mvcc_timestamp:10] + │ │ └── uniq_partial_overlaps_pk.crdb_internal_mvcc_timestamp:10 + │ ├── first-agg [as=k:11] + │ │ └── k:11 + │ ├── first-agg [as=v:12] + │ │ └── v:12 + │ ├── first-agg [as=w:13] + │ │ └── w:13 + │ ├── first-agg [as=x:14] + │ │ └── x:14 + │ ├── first-agg [as=y:15] + │ │ └── y:15 + │ ├── first-agg [as=rowid:16] + │ │ └── rowid:16 + │ └── first-agg [as=other.crdb_internal_mvcc_timestamp:17] + │ └── other.crdb_internal_mvcc_timestamp:17 + └── unique-checks + └── unique-checks-item: uniq_partial_overlaps_pk(a) + └── semi-join (hash) + ├── columns: a:23 b:24!null c:25 d:26 + ├── with-scan &1 + │ ├── columns: a:23 b:24!null c:25 d:26 + │ └── mapping: + │ ├── k:11 => a:23 + │ ├── uniq_partial_overlaps_pk.b:7 => b:24 + │ ├── uniq_partial_overlaps_pk.c:8 => c:25 + │ └── uniq_partial_overlaps_pk.d:9 => d:26 + ├── scan uniq_partial_overlaps_pk + │ └── columns: uniq_partial_overlaps_pk.a:18!null uniq_partial_overlaps_pk.b:19!null uniq_partial_overlaps_pk.c:20 uniq_partial_overlaps_pk.d:21 + └── filters + ├── a:23 = uniq_partial_overlaps_pk.a:18 + ├── d:26 > 0 + ├── uniq_partial_overlaps_pk.d:21 > 0 + └── b:24 != uniq_partial_overlaps_pk.b:19 + +exec-ddl +CREATE TABLE uniq_partial_hidden_pk ( + a INT, + b INT, + UNIQUE WITHOUT INDEX (a) WHERE b > 0 +) +---- + +# Update with constant input. +# Add inequality filters for the hidden primary key column. +build +UPDATE uniq_partial_hidden_pk SET a = 1 +---- +update uniq_partial_hidden_pk + ├── columns: + ├── fetch columns: uniq_partial_hidden_pk.a:5 uniq_partial_hidden_pk.b:6 uniq_partial_hidden_pk.rowid:7 + ├── update-mapping: + │ └── a_new:9 => uniq_partial_hidden_pk.a:1 + ├── input binding: &1 + ├── project + │ ├── columns: a_new:9!null uniq_partial_hidden_pk.a:5 uniq_partial_hidden_pk.b:6 uniq_partial_hidden_pk.rowid:7!null crdb_internal_mvcc_timestamp:8 + │ ├── scan uniq_partial_hidden_pk + │ │ └── columns: uniq_partial_hidden_pk.a:5 uniq_partial_hidden_pk.b:6 uniq_partial_hidden_pk.rowid:7!null crdb_internal_mvcc_timestamp:8 + │ └── projections + │ └── 1 [as=a_new:9] + └── unique-checks + └── unique-checks-item: uniq_partial_hidden_pk(a) + └── semi-join (hash) + ├── columns: a:14!null b:15 rowid:16!null + ├── with-scan &1 + │ ├── columns: a:14!null b:15 rowid:16!null + │ └── mapping: + │ ├── a_new:9 => a:14 + │ ├── uniq_partial_hidden_pk.b:6 => b:15 + │ └── uniq_partial_hidden_pk.rowid:7 => rowid:16 + ├── scan uniq_partial_hidden_pk + │ └── columns: uniq_partial_hidden_pk.a:10 uniq_partial_hidden_pk.b:11 uniq_partial_hidden_pk.rowid:12!null + └── filters + ├── a:14 = uniq_partial_hidden_pk.a:10 + ├── b:15 > 0 + ├── uniq_partial_hidden_pk.b:11 > 0 + └── rowid:16 != uniq_partial_hidden_pk.rowid:12 + +# Update with non-constant input. +# Add inequality filters for the hidden primary key column. +build +UPDATE uniq_partial_hidden_pk SET a = k FROM other +---- +update uniq_partial_hidden_pk + ├── columns: + ├── fetch columns: uniq_partial_hidden_pk.a:5 uniq_partial_hidden_pk.b:6 uniq_partial_hidden_pk.rowid:7 + ├── update-mapping: + │ └── k:9 => uniq_partial_hidden_pk.a:1 + ├── input binding: &1 + ├── inner-join (cross) + │ ├── columns: uniq_partial_hidden_pk.a:5 uniq_partial_hidden_pk.b:6 uniq_partial_hidden_pk.rowid:7!null uniq_partial_hidden_pk.crdb_internal_mvcc_timestamp:8 k:9 v:10 w:11!null x:12 y:13 other.rowid:14!null other.crdb_internal_mvcc_timestamp:15 + │ ├── scan uniq_partial_hidden_pk + │ │ └── columns: uniq_partial_hidden_pk.a:5 uniq_partial_hidden_pk.b:6 uniq_partial_hidden_pk.rowid:7!null uniq_partial_hidden_pk.crdb_internal_mvcc_timestamp:8 + │ ├── scan other + │ │ └── columns: k:9 v:10 w:11!null x:12 y:13 other.rowid:14!null other.crdb_internal_mvcc_timestamp:15 + │ └── filters (true) + └── unique-checks + └── unique-checks-item: uniq_partial_hidden_pk(a) + └── semi-join (hash) + ├── columns: a:20 b:21 rowid:22!null + ├── with-scan &1 + │ ├── columns: a:20 b:21 rowid:22!null + │ └── mapping: + │ ├── k:9 => a:20 + │ ├── uniq_partial_hidden_pk.b:6 => b:21 + │ └── uniq_partial_hidden_pk.rowid:7 => rowid:22 + ├── scan uniq_partial_hidden_pk + │ └── columns: uniq_partial_hidden_pk.a:16 uniq_partial_hidden_pk.b:17 uniq_partial_hidden_pk.rowid:18!null + └── filters + ├── a:20 = uniq_partial_hidden_pk.a:16 + ├── b:21 > 0 + ├── uniq_partial_hidden_pk.b:17 > 0 + └── rowid:22 != uniq_partial_hidden_pk.rowid:18