From 20b524ab95156359ce2242b872fec937f900e989 Mon Sep 17 00:00:00 2001 From: Rebecca Taft Date: Mon, 14 Dec 2020 20:36:35 -0600 Subject: [PATCH] opt: add update checks for unique constraints This commit adds checks for unique constraints when planning updates in the optimizer. This does not yet impact anything outside of the optimizer tests, since UNIQUE WITHOUT INDEX is still not fully supported outside of the optimizer test catalog. Informs #41535 Release note: None --- pkg/sql/opt/norm/testdata/rules/prune_cols | 92 ++- .../opt/optbuilder/mutation_builder_unique.go | 70 ++- .../optbuilder/testdata/unique-checks-update | 541 ++++++++++++++++++ pkg/sql/opt/optbuilder/update.go | 2 + 4 files changed, 685 insertions(+), 20 deletions(-) create mode 100644 pkg/sql/opt/optbuilder/testdata/unique-checks-update diff --git a/pkg/sql/opt/norm/testdata/rules/prune_cols b/pkg/sql/opt/norm/testdata/rules/prune_cols index d2598229f25b..af66e47ec302 100644 --- a/pkg/sql/opt/norm/testdata/rules/prune_cols +++ b/pkg/sql/opt/norm/testdata/rules/prune_cols @@ -2585,8 +2585,96 @@ upsert checks ├── upsert_b:16 > 10 [as=check2:18, outer=(16)] └── column2:7 > upsert_b:16 [as=check3:19, outer=(7,16)] -# TODO(rytaft): test that columns needed for unique checks are not pruned -# from updates. +exec-ddl +CREATE TABLE uniq ( + k INT PRIMARY KEY, + v INT, + w INT UNIQUE WITHOUT INDEX, + x INT, + y INT, + z INT UNIQUE, + UNIQUE WITHOUT INDEX (x, y) +) +---- + +# Do not prune columns from updates that are needed for unique checks. +norm expect=PruneMutationInputCols +UPDATE uniq SET w = 1, x = 2 WHERE k = 3 +---- +update uniq + ├── columns: + ├── fetch columns: uniq.k:8 v:9 w:10 x:11 uniq.y:12 z:13 + ├── update-mapping: + │ ├── w_new:15 => w:3 + │ └── x_new:16 => x:4 + ├── input binding: &1 + ├── cardinality: [0 - 0] + ├── volatile, mutations + ├── project + │ ├── columns: w_new:15!null x_new:16!null uniq.k:8!null v:9 w:10 x:11 uniq.y:12 z:13 + │ ├── cardinality: [0 - 1] + │ ├── key: () + │ ├── fd: ()-->(8-13,15,16) + │ ├── select + │ │ ├── columns: uniq.k:8!null v:9 w:10 x:11 uniq.y:12 z:13 + │ │ ├── cardinality: [0 - 1] + │ │ ├── key: () + │ │ ├── fd: ()-->(8-13) + │ │ ├── scan uniq + │ │ │ ├── columns: uniq.k:8!null v:9 w:10 x:11 uniq.y:12 z:13 + │ │ │ ├── key: (8) + │ │ │ └── fd: (8)-->(9-13), (13)~~>(8-12) + │ │ └── filters + │ │ └── uniq.k:8 = 3 [outer=(8), constraints=(/8: [/3 - /3]; tight), fd=()-->(8)] + │ └── projections + │ ├── 1 [as=w_new:15] + │ └── 2 [as=x_new:16] + └── unique-checks + ├── unique-checks-item: uniq(w) + │ └── semi-join (hash) + │ ├── columns: w_new:17!null k:18!null + │ ├── cardinality: [0 - 1] + │ ├── key: () + │ ├── fd: ()-->(17,18) + │ ├── with-scan &1 + │ │ ├── columns: w_new:17!null k:18!null + │ │ ├── mapping: + │ │ │ ├── w_new:15 => w_new:17 + │ │ │ └── uniq.k:8 => k:18 + │ │ ├── cardinality: [0 - 1] + │ │ ├── key: () + │ │ └── fd: ()-->(17,18) + │ ├── scan uniq + │ │ ├── columns: uniq.k:19!null w:21 + │ │ ├── key: (19) + │ │ └── fd: (19)-->(21) + │ └── filters + │ ├── w_new:17 = w:21 [outer=(17,21), constraints=(/17: (/NULL - ]; /21: (/NULL - ]), fd=(17)==(21), (21)==(17)] + │ └── k:18 != uniq.k:19 [outer=(18,19), constraints=(/18: (/NULL - ]; /19: (/NULL - ])] + └── unique-checks-item: uniq(x,y) + └── semi-join (hash) + ├── columns: x_new:26!null y:27 k:28!null + ├── cardinality: [0 - 1] + ├── key: () + ├── fd: ()-->(26-28) + ├── with-scan &1 + │ ├── columns: x_new:26!null y:27 k:28!null + │ ├── mapping: + │ │ ├── x_new:16 => x_new:26 + │ │ ├── uniq.y:12 => y:27 + │ │ └── uniq.k:8 => k:28 + │ ├── cardinality: [0 - 1] + │ ├── key: () + │ └── fd: ()-->(26-28) + ├── scan uniq + │ ├── columns: uniq.k:29!null x:32 uniq.y:33 + │ ├── key: (29) + │ └── fd: (29)-->(32,33) + └── filters + ├── x_new:26 = x:32 [outer=(26,32), constraints=(/26: (/NULL - ]; /32: (/NULL - ]), fd=(26)==(32), (32)==(26)] + ├── y:27 = uniq.y:33 [outer=(27,33), constraints=(/27: (/NULL - ]; /33: (/NULL - ]), fd=(27)==(33), (33)==(27)] + └── k:28 != uniq.k:29 [outer=(28,29), constraints=(/28: (/NULL - ]; /29: (/NULL - ])] + # ------------------------------------------------------------------------------ # PruneMutationReturnCols diff --git a/pkg/sql/opt/optbuilder/mutation_builder_unique.go b/pkg/sql/opt/optbuilder/mutation_builder_unique.go index a23e6bafa5e5..af3fbb0536c6 100644 --- a/pkg/sql/opt/optbuilder/mutation_builder_unique.go +++ b/pkg/sql/opt/optbuilder/mutation_builder_unique.go @@ -21,33 +21,18 @@ import ( ) // buildUniqueChecksForInsert builds uniqueness check queries for an insert. +// These check queries are used to enforce UNIQUE WITHOUT INDEX constraints. func (mb *mutationBuilder) buildUniqueChecksForInsert() { - uniqueCount := mb.tab.UniqueCount() - if uniqueCount == 0 { - // No relevant unique checks. - return - } - // We only need to build unique checks if there is at least one unique // constraint without an index. - needChecks := false - i := 0 - for ; i < uniqueCount; i++ { - if mb.tab.Unique(i).WithoutIndex() { - needChecks = true - break - } - } - if !needChecks { + if !mb.hasUniqueWithoutIndexConstraints() { return } mb.ensureWithID() h := &mb.uniqueCheckHelper - // i is already set to the index of the first uniqueness check without an - // index, so start iterating from there. - for ; i < uniqueCount; i++ { + for i, n := 0, mb.tab.UniqueCount(); i < n; i++ { // If this constraint is already enforced by an index we don't need to plan // a check. if mb.tab.Unique(i).WithoutIndex() && h.init(mb, i) { @@ -57,6 +42,55 @@ func (mb *mutationBuilder) buildUniqueChecksForInsert() { telemetry.Inc(sqltelemetry.UniqueChecksUseCounter) } +// buildUniqueChecksForUpdate builds uniqueness check queries for an update. +// These check queries are used to enforce UNIQUE WITHOUT INDEX constraints. +func (mb *mutationBuilder) buildUniqueChecksForUpdate() { + // We only need to build unique checks if there is at least one unique + // constraint without an index. + if !mb.hasUniqueWithoutIndexConstraints() { + return + } + + mb.ensureWithID() + h := &mb.uniqueCheckHelper + + for i, n := 0, mb.tab.UniqueCount(); i < n; i++ { + // If this constraint is already enforced by an index or doesn't include + // the updated columns we don't need to plan a check. + if mb.tab.Unique(i).WithoutIndex() && mb.uniqueColsUpdated(i) && h.init(mb, i) { + // The insertion check works for updates too since it simply checks that + // the unique columns in the newly inserted or updated rows do not match + // any existing rows. The check prevents rows from matching themselves by + // adding a filter based on the primary key. + mb.uniqueChecks = append(mb.uniqueChecks, h.buildInsertionCheck()) + } + } + telemetry.Inc(sqltelemetry.UniqueChecksUseCounter) +} + +// hasUniqueWithoutIndexConstraints returns true if there are any +// UNIQUE WITHOUT INDEX constraints on the table. +func (mb *mutationBuilder) hasUniqueWithoutIndexConstraints() bool { + for i, n := 0, mb.tab.UniqueCount(); i < n; i++ { + if mb.tab.Unique(i).WithoutIndex() { + return true + } + } + return false +} + +// uniqueColsUpdated returns true if any of the columns for a unique +// constraint are being updated (according to updateColIDs). +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 + } + } + return false +} + // uniqueCheckHelper is a type associated with a single unique constraint and // is used to build the "leaves" of a unique check expression, namely the // WithScan of the mutation input and the Scan of the table. diff --git a/pkg/sql/opt/optbuilder/testdata/unique-checks-update b/pkg/sql/opt/optbuilder/testdata/unique-checks-update new file mode 100644 index 000000000000..007675fc8f92 --- /dev/null +++ b/pkg/sql/opt/optbuilder/testdata/unique-checks-update @@ -0,0 +1,541 @@ +exec-ddl +CREATE TABLE uniq ( + k INT PRIMARY KEY, + v INT UNIQUE, + w INT UNIQUE WITHOUT INDEX, + x INT, + y INT, + UNIQUE WITHOUT INDEX (x, y) +) +---- + +# None of the updated values have nulls. +build +UPDATE uniq SET w = 1, x = 2 +---- +update uniq + ├── columns: + ├── fetch columns: uniq.k:7 v:8 w:9 x:10 uniq.y:11 + ├── update-mapping: + │ ├── w_new:13 => w:3 + │ └── x_new:14 => x:4 + ├── input binding: &1 + ├── project + │ ├── columns: w_new:13!null x_new:14!null uniq.k:7!null v:8 w:9 x:10 uniq.y:11 crdb_internal_mvcc_timestamp:12 + │ ├── scan uniq + │ │ └── columns: uniq.k:7!null v:8 w:9 x:10 uniq.y:11 crdb_internal_mvcc_timestamp:12 + │ └── projections + │ ├── 1 [as=w_new:13] + │ └── 2 [as=x_new:14] + └── unique-checks + ├── unique-checks-item: uniq(w) + │ └── semi-join (hash) + │ ├── columns: w_new:15!null k:16!null + │ ├── with-scan &1 + │ │ ├── columns: w_new:15!null k:16!null + │ │ └── mapping: + │ │ ├── w_new:13 => w_new:15 + │ │ └── uniq.k:7 => k:16 + │ ├── scan uniq + │ │ └── columns: uniq.k:17!null w:19 + │ └── filters + │ ├── w_new:15 = w:19 + │ └── k:16 != uniq.k:17 + └── unique-checks-item: uniq(x,y) + └── semi-join (hash) + ├── columns: x_new:23!null y:24 k:25!null + ├── with-scan &1 + │ ├── columns: x_new:23!null y:24 k:25!null + │ └── mapping: + │ ├── x_new:14 => x_new:23 + │ ├── uniq.y:11 => y:24 + │ └── uniq.k:7 => k:25 + ├── scan uniq + │ └── columns: uniq.k:26!null x:29 uniq.y:30 + └── filters + ├── x_new:23 = x:29 + ├── y:24 = uniq.y:30 + └── k:25 != uniq.k:26 + +# No need to plan checks for w since it's aways null. +build +UPDATE uniq SET w = NULL, x = 1 +---- +update uniq + ├── columns: + ├── fetch columns: uniq.k:7 v:8 w:9 x:10 uniq.y:11 + ├── update-mapping: + │ ├── w_new:13 => w:3 + │ └── x_new:14 => x:4 + ├── input binding: &1 + ├── project + │ ├── columns: w_new:13 x_new:14!null uniq.k:7!null v:8 w:9 x:10 uniq.y:11 crdb_internal_mvcc_timestamp:12 + │ ├── scan uniq + │ │ └── columns: uniq.k:7!null v:8 w:9 x:10 uniq.y:11 crdb_internal_mvcc_timestamp:12 + │ └── projections + │ ├── NULL::INT8 [as=w_new:13] + │ └── 1 [as=x_new:14] + └── unique-checks + └── unique-checks-item: uniq(x,y) + └── semi-join (hash) + ├── columns: x_new:15!null y:16 k:17!null + ├── with-scan &1 + │ ├── columns: x_new:15!null y:16 k:17!null + │ └── mapping: + │ ├── x_new:14 => x_new:15 + │ ├── uniq.y:11 => y:16 + │ └── uniq.k:7 => k:17 + ├── scan uniq + │ └── columns: uniq.k:18!null x:21 uniq.y:22 + └── filters + ├── x_new:15 = x:21 + ├── y:16 = uniq.y:22 + └── k:17 != uniq.k:18 + +# No need to plan checks for x,y since x is aways null. +# Also update the primary key. +build +UPDATE uniq SET k = 1, w = 2, x = NULL +---- +update uniq + ├── columns: + ├── fetch columns: k:7 v:8 w:9 x:10 y:11 + ├── update-mapping: + │ ├── k_new:13 => k:1 + │ ├── w_new:14 => w:3 + │ └── x_new:15 => x:4 + ├── input binding: &1 + ├── project + │ ├── columns: k_new:13!null w_new:14!null x_new:15 k:7!null v:8 w:9 x:10 y:11 crdb_internal_mvcc_timestamp:12 + │ ├── scan uniq + │ │ └── columns: k:7!null v:8 w:9 x:10 y:11 crdb_internal_mvcc_timestamp:12 + │ └── projections + │ ├── 1 [as=k_new:13] + │ ├── 2 [as=w_new:14] + │ └── NULL::INT8 [as=x_new:15] + └── unique-checks + └── unique-checks-item: uniq(w) + └── semi-join (hash) + ├── columns: w_new:16!null k_new:17!null + ├── with-scan &1 + │ ├── columns: w_new:16!null k_new:17!null + │ └── mapping: + │ ├── w_new:14 => w_new:16 + │ └── k_new:13 => k_new:17 + ├── scan uniq + │ └── columns: k:18!null w:20 + └── filters + ├── w_new:16 = w:20 + └── k_new:17 != k:18 + +# No need to plan checks for x,y since y is aways null. +build +UPDATE uniq SET w = 1, y = NULL WHERE k = 1 +---- +update uniq + ├── columns: + ├── fetch columns: uniq.k:7 v:8 w:9 x:10 y:11 + ├── update-mapping: + │ ├── w_new:13 => w:3 + │ └── y_new:14 => y:5 + ├── input binding: &1 + ├── project + │ ├── columns: w_new:13!null y_new:14 uniq.k:7!null v:8 w:9 x:10 y:11 crdb_internal_mvcc_timestamp:12 + │ ├── select + │ │ ├── columns: uniq.k:7!null v:8 w:9 x:10 y:11 crdb_internal_mvcc_timestamp:12 + │ │ ├── scan uniq + │ │ │ └── columns: uniq.k:7!null v:8 w:9 x:10 y:11 crdb_internal_mvcc_timestamp:12 + │ │ └── filters + │ │ └── uniq.k:7 = 1 + │ └── projections + │ ├── 1 [as=w_new:13] + │ └── NULL::INT8 [as=y_new:14] + └── unique-checks + └── unique-checks-item: uniq(w) + └── semi-join (hash) + ├── columns: w_new:15!null k:16!null + ├── with-scan &1 + │ ├── columns: w_new:15!null k:16!null + │ └── mapping: + │ ├── w_new:13 => w_new:15 + │ └── uniq.k:7 => k:16 + ├── scan uniq + │ └── columns: uniq.k:17!null w:19 + └── filters + ├── w_new:15 = w:19 + └── k:16 != uniq.k:17 + +# No need to plan checks since none of the columns requiring checks are updated. +build +UPDATE uniq SET k = 1, v = 2 +---- +update uniq + ├── columns: + ├── fetch columns: k:7 v:8 w:9 x:10 y:11 + ├── update-mapping: + │ ├── k_new:13 => k:1 + │ └── v_new:14 => v:2 + └── project + ├── columns: k_new:13!null v_new:14!null k:7!null v:8 w:9 x:10 y:11 crdb_internal_mvcc_timestamp:12 + ├── scan uniq + │ └── columns: k:7!null v:8 w:9 x:10 y:11 crdb_internal_mvcc_timestamp:12 + └── projections + ├── 1 [as=k_new:13] + └── 2 [as=v_new:14] + +exec-ddl +CREATE TABLE other (k INT, v INT, w INT NOT NULL, x INT, y INT) +---- + +# Update with non-constant input. +build +UPDATE uniq SET w = other.w, x = other.x FROM other +---- +update uniq + ├── columns: + ├── fetch columns: uniq.k:7 uniq.v:8 uniq.w:9 uniq.x:10 uniq.y:11 + ├── update-mapping: + │ ├── other.w:15 => uniq.w:3 + │ └── other.x:16 => uniq.x:4 + ├── input binding: &1 + ├── distinct-on + │ ├── columns: uniq.k:7!null uniq.v:8 uniq.w:9 uniq.x:10 uniq.y:11 uniq.crdb_internal_mvcc_timestamp:12 other.k:13 other.v:14 other.w:15!null other.x:16 other.y:17 rowid:18!null other.crdb_internal_mvcc_timestamp:19 + │ ├── grouping columns: uniq.k:7!null + │ ├── inner-join (cross) + │ │ ├── columns: uniq.k:7!null uniq.v:8 uniq.w:9 uniq.x:10 uniq.y:11 uniq.crdb_internal_mvcc_timestamp:12 other.k:13 other.v:14 other.w:15!null other.x:16 other.y:17 rowid:18!null other.crdb_internal_mvcc_timestamp:19 + │ │ ├── scan uniq + │ │ │ └── columns: uniq.k:7!null uniq.v:8 uniq.w:9 uniq.x:10 uniq.y:11 uniq.crdb_internal_mvcc_timestamp:12 + │ │ ├── scan other + │ │ │ └── columns: other.k:13 other.v:14 other.w:15!null other.x:16 other.y:17 rowid:18!null other.crdb_internal_mvcc_timestamp:19 + │ │ └── filters (true) + │ └── aggregations + │ ├── first-agg [as=uniq.v:8] + │ │ └── uniq.v:8 + │ ├── first-agg [as=uniq.w:9] + │ │ └── uniq.w:9 + │ ├── first-agg [as=uniq.x:10] + │ │ └── uniq.x:10 + │ ├── first-agg [as=uniq.y:11] + │ │ └── uniq.y:11 + │ ├── first-agg [as=uniq.crdb_internal_mvcc_timestamp:12] + │ │ └── uniq.crdb_internal_mvcc_timestamp:12 + │ ├── first-agg [as=other.k:13] + │ │ └── other.k:13 + │ ├── first-agg [as=other.v:14] + │ │ └── other.v:14 + │ ├── first-agg [as=other.w:15] + │ │ └── other.w:15 + │ ├── first-agg [as=other.x:16] + │ │ └── other.x:16 + │ ├── first-agg [as=other.y:17] + │ │ └── other.y:17 + │ ├── first-agg [as=rowid:18] + │ │ └── rowid:18 + │ └── first-agg [as=other.crdb_internal_mvcc_timestamp:19] + │ └── other.crdb_internal_mvcc_timestamp:19 + └── unique-checks + ├── unique-checks-item: uniq(w) + │ └── semi-join (hash) + │ ├── columns: w:20!null k:21!null + │ ├── with-scan &1 + │ │ ├── columns: w:20!null k:21!null + │ │ └── mapping: + │ │ ├── other.w:15 => w:20 + │ │ └── uniq.k:7 => k:21 + │ ├── scan uniq + │ │ └── columns: uniq.k:22!null uniq.w:24 + │ └── filters + │ ├── w:20 = uniq.w:24 + │ └── k:21 != uniq.k:22 + └── unique-checks-item: uniq(x,y) + └── semi-join (hash) + ├── columns: x:28 y:29 k:30!null + ├── with-scan &1 + │ ├── columns: x:28 y:29 k:30!null + │ └── mapping: + │ ├── other.x:16 => x:28 + │ ├── uniq.y:11 => y:29 + │ └── uniq.k:7 => k:30 + ├── scan uniq + │ └── columns: uniq.k:31!null uniq.x:34 uniq.y:35 + └── filters + ├── x:28 = uniq.x:34 + ├── y:29 = uniq.y:35 + └── k:30 != uniq.k:31 + +exec-ddl +CREATE TABLE uniq_overlaps_pk ( + a INT, + b INT, + c INT, + d INT, + PRIMARY KEY (a, b), + UNIQUE WITHOUT INDEX (b, c), + UNIQUE WITHOUT INDEX (a, b, d), + UNIQUE WITHOUT INDEX (a), + UNIQUE WITHOUT INDEX (c, d) +) +---- + +# Update with constant input. +# 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_overlaps_pk SET a = 1, b = 2, c = 3, d = 4 WHERE a = 5 +---- +update uniq_overlaps_pk + ├── columns: + ├── fetch columns: a:6 b:7 c:8 d:9 + ├── update-mapping: + │ ├── a_new:11 => a:1 + │ ├── b_new:12 => b:2 + │ ├── c_new:13 => c:3 + │ └── d_new:14 => d:4 + ├── input binding: &1 + ├── project + │ ├── columns: a_new:11!null b_new:12!null c_new:13!null d_new:14!null a:6!null b:7!null c:8 d:9 crdb_internal_mvcc_timestamp:10 + │ ├── select + │ │ ├── columns: a:6!null b:7!null c:8 d:9 crdb_internal_mvcc_timestamp:10 + │ │ ├── scan uniq_overlaps_pk + │ │ │ └── columns: a:6!null b:7!null c:8 d:9 crdb_internal_mvcc_timestamp:10 + │ │ └── filters + │ │ └── 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_overlaps_pk(b,c) + │ └── semi-join (hash) + │ ├── columns: b_new:15!null c_new:16!null a_new:17!null + │ ├── with-scan &1 + │ │ ├── columns: b_new:15!null c_new:16!null a_new:17!null + │ │ └── mapping: + │ │ ├── b_new:12 => b_new:15 + │ │ ├── c_new:13 => c_new:16 + │ │ └── a_new:11 => a_new:17 + │ ├── scan uniq_overlaps_pk + │ │ └── columns: a:18!null b:19!null c:20 + │ └── filters + │ ├── b_new:15 = b:19 + │ ├── c_new:16 = c:20 + │ └── a_new:17 != a:18 + ├── unique-checks-item: uniq_overlaps_pk(a) + │ └── semi-join (hash) + │ ├── columns: a_new:23!null b_new:24!null + │ ├── with-scan &1 + │ │ ├── columns: a_new:23!null b_new:24!null + │ │ └── mapping: + │ │ ├── a_new:11 => a_new:23 + │ │ └── b_new:12 => b_new:24 + │ ├── scan uniq_overlaps_pk + │ │ └── columns: a:25!null b:26!null + │ └── filters + │ ├── a_new:23 = a:25 + │ └── b_new:24 != b:26 + └── unique-checks-item: uniq_overlaps_pk(c,d) + └── semi-join (hash) + ├── columns: c_new:30!null d_new:31!null a_new:32!null b_new:33!null + ├── with-scan &1 + │ ├── columns: c_new:30!null d_new:31!null a_new:32!null b_new:33!null + │ └── mapping: + │ ├── c_new:13 => c_new:30 + │ ├── d_new:14 => d_new:31 + │ ├── a_new:11 => a_new:32 + │ └── b_new:12 => b_new:33 + ├── scan uniq_overlaps_pk + │ └── columns: a:34!null b:35!null c:36 d:37 + └── filters + ├── c_new:30 = c:36 + ├── d_new:31 = d:37 + └── (a_new:32 != a:34) OR (b_new:33 != b:35) + +# Update with non-constant input. +# 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_overlaps_pk SET a = k, d = v FROM other +---- +update uniq_overlaps_pk + ├── columns: + ├── fetch columns: a:6 uniq_overlaps_pk.b:7 uniq_overlaps_pk.c:8 d:9 + ├── update-mapping: + │ ├── other.k:11 => a:1 + │ └── other.v:12 => d:4 + ├── input binding: &1 + ├── distinct-on + │ ├── columns: a:6!null uniq_overlaps_pk.b:7!null uniq_overlaps_pk.c:8 d:9 uniq_overlaps_pk.crdb_internal_mvcc_timestamp:10 other.k:11 other.v:12 w:13!null x:14 y:15 rowid:16!null other.crdb_internal_mvcc_timestamp:17 + │ ├── grouping columns: a:6!null uniq_overlaps_pk.b:7!null + │ ├── inner-join (cross) + │ │ ├── columns: a:6!null uniq_overlaps_pk.b:7!null uniq_overlaps_pk.c:8 d:9 uniq_overlaps_pk.crdb_internal_mvcc_timestamp:10 other.k:11 other.v:12 w:13!null x:14 y:15 rowid:16!null other.crdb_internal_mvcc_timestamp:17 + │ │ ├── scan uniq_overlaps_pk + │ │ │ └── columns: a:6!null uniq_overlaps_pk.b:7!null uniq_overlaps_pk.c:8 d:9 uniq_overlaps_pk.crdb_internal_mvcc_timestamp:10 + │ │ ├── scan other + │ │ │ └── columns: other.k:11 other.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_overlaps_pk.c:8] + │ │ └── uniq_overlaps_pk.c:8 + │ ├── first-agg [as=d:9] + │ │ └── d:9 + │ ├── first-agg [as=uniq_overlaps_pk.crdb_internal_mvcc_timestamp:10] + │ │ └── uniq_overlaps_pk.crdb_internal_mvcc_timestamp:10 + │ ├── first-agg [as=other.k:11] + │ │ └── other.k:11 + │ ├── first-agg [as=other.v:12] + │ │ └── other.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_overlaps_pk(a) + │ └── semi-join (hash) + │ ├── columns: k:18 b:19!null + │ ├── with-scan &1 + │ │ ├── columns: k:18 b:19!null + │ │ └── mapping: + │ │ ├── other.k:11 => k:18 + │ │ └── uniq_overlaps_pk.b:7 => b:19 + │ ├── scan uniq_overlaps_pk + │ │ └── columns: a:20!null uniq_overlaps_pk.b:21!null + │ └── filters + │ ├── k:18 = a:20 + │ └── b:19 != uniq_overlaps_pk.b:21 + └── unique-checks-item: uniq_overlaps_pk(c,d) + └── semi-join (hash) + ├── columns: c:25 v:26 k:27 b:28!null + ├── with-scan &1 + │ ├── columns: c:25 v:26 k:27 b:28!null + │ └── mapping: + │ ├── uniq_overlaps_pk.c:8 => c:25 + │ ├── other.v:12 => v:26 + │ ├── other.k:11 => k:27 + │ └── uniq_overlaps_pk.b:7 => b:28 + ├── scan uniq_overlaps_pk + │ └── columns: a:29!null uniq_overlaps_pk.b:30!null uniq_overlaps_pk.c:31 d:32 + └── filters + ├── c:25 = uniq_overlaps_pk.c:31 + ├── v:26 = d:32 + └── (k:27 != a:29) OR (b:28 != uniq_overlaps_pk.b:30) + +exec-ddl +CREATE TABLE uniq_hidden_pk ( + a INT, + b INT, + c INT, + d INT, + UNIQUE WITHOUT INDEX (b, c), + UNIQUE WITHOUT INDEX (a, b, d), + UNIQUE WITHOUT INDEX (a) +) +---- + +# Update with constant input. +# No need to add a check for b,c since those columns weren't updated. +# Add inequality filters for the hidden primary key column. +build +UPDATE uniq_hidden_pk SET a = 1 +---- +update uniq_hidden_pk + ├── columns: + ├── fetch columns: a:7 uniq_hidden_pk.b:8 c:9 uniq_hidden_pk.d:10 uniq_hidden_pk.rowid:11 + ├── update-mapping: + │ └── a_new:13 => a:1 + ├── input binding: &1 + ├── project + │ ├── columns: a_new:13!null a:7 uniq_hidden_pk.b:8 c:9 uniq_hidden_pk.d:10 uniq_hidden_pk.rowid:11!null crdb_internal_mvcc_timestamp:12 + │ ├── scan uniq_hidden_pk + │ │ └── columns: a:7 uniq_hidden_pk.b:8 c:9 uniq_hidden_pk.d:10 uniq_hidden_pk.rowid:11!null crdb_internal_mvcc_timestamp:12 + │ └── projections + │ └── 1 [as=a_new:13] + └── unique-checks + ├── unique-checks-item: uniq_hidden_pk(a,b,d) + │ └── semi-join (hash) + │ ├── columns: a_new:14!null b:15 d:16 rowid:17!null + │ ├── with-scan &1 + │ │ ├── columns: a_new:14!null b:15 d:16 rowid:17!null + │ │ └── mapping: + │ │ ├── a_new:13 => a_new:14 + │ │ ├── uniq_hidden_pk.b:8 => b:15 + │ │ ├── uniq_hidden_pk.d:10 => d:16 + │ │ └── uniq_hidden_pk.rowid:11 => rowid:17 + │ ├── scan uniq_hidden_pk + │ │ └── columns: a:18 uniq_hidden_pk.b:19 uniq_hidden_pk.d:21 uniq_hidden_pk.rowid:22!null + │ └── filters + │ ├── a_new:14 = a:18 + │ ├── b:15 = uniq_hidden_pk.b:19 + │ ├── d:16 = uniq_hidden_pk.d:21 + │ └── rowid:17 != uniq_hidden_pk.rowid:22 + └── unique-checks-item: uniq_hidden_pk(a) + └── semi-join (hash) + ├── columns: a_new:24!null rowid:25!null + ├── with-scan &1 + │ ├── columns: a_new:24!null rowid:25!null + │ └── mapping: + │ ├── a_new:13 => a_new:24 + │ └── uniq_hidden_pk.rowid:11 => rowid:25 + ├── scan uniq_hidden_pk + │ └── columns: a:26 uniq_hidden_pk.rowid:30!null + └── filters + ├── a_new:24 = a:26 + └── rowid:25 != uniq_hidden_pk.rowid:30 + +# Update with non-constant input. +# No need to add a check for b,c since those columns weren't updated. +# Add inequality filters for the hidden primary key column. +build +UPDATE uniq_hidden_pk SET a = k FROM other +---- +update uniq_hidden_pk + ├── columns: + ├── fetch columns: a:7 uniq_hidden_pk.b:8 c:9 uniq_hidden_pk.d:10 uniq_hidden_pk.rowid:11 + ├── update-mapping: + │ └── other.k:13 => a:1 + ├── input binding: &1 + ├── inner-join (cross) + │ ├── columns: a:7 uniq_hidden_pk.b:8 c:9 uniq_hidden_pk.d:10 uniq_hidden_pk.rowid:11!null uniq_hidden_pk.crdb_internal_mvcc_timestamp:12 other.k:13 v:14 w:15!null x:16 y:17 other.rowid:18!null other.crdb_internal_mvcc_timestamp:19 + │ ├── scan uniq_hidden_pk + │ │ └── columns: a:7 uniq_hidden_pk.b:8 c:9 uniq_hidden_pk.d:10 uniq_hidden_pk.rowid:11!null uniq_hidden_pk.crdb_internal_mvcc_timestamp:12 + │ ├── scan other + │ │ └── columns: other.k:13 v:14 w:15!null x:16 y:17 other.rowid:18!null other.crdb_internal_mvcc_timestamp:19 + │ └── filters (true) + └── unique-checks + ├── unique-checks-item: uniq_hidden_pk(a,b,d) + │ └── semi-join (hash) + │ ├── columns: k:20 b:21 d:22 rowid:23!null + │ ├── with-scan &1 + │ │ ├── columns: k:20 b:21 d:22 rowid:23!null + │ │ └── mapping: + │ │ ├── other.k:13 => k:20 + │ │ ├── uniq_hidden_pk.b:8 => b:21 + │ │ ├── uniq_hidden_pk.d:10 => d:22 + │ │ └── uniq_hidden_pk.rowid:11 => rowid:23 + │ ├── scan uniq_hidden_pk + │ │ └── columns: a:24 uniq_hidden_pk.b:25 uniq_hidden_pk.d:27 uniq_hidden_pk.rowid:28!null + │ └── filters + │ ├── k:20 = a:24 + │ ├── b:21 = uniq_hidden_pk.b:25 + │ ├── d:22 = uniq_hidden_pk.d:27 + │ └── rowid:23 != uniq_hidden_pk.rowid:28 + └── unique-checks-item: uniq_hidden_pk(a) + └── semi-join (hash) + ├── columns: k:30 rowid:31!null + ├── with-scan &1 + │ ├── columns: k:30 rowid:31!null + │ └── mapping: + │ ├── other.k:13 => k:30 + │ └── uniq_hidden_pk.rowid:11 => rowid:31 + ├── scan uniq_hidden_pk + │ └── columns: a:32 uniq_hidden_pk.rowid:36!null + └── filters + ├── k:30 = a:32 + └── rowid:31 != uniq_hidden_pk.rowid:36 diff --git a/pkg/sql/opt/optbuilder/update.go b/pkg/sql/opt/optbuilder/update.go index 49142920eb5e..18b3c71e30b8 100644 --- a/pkg/sql/opt/optbuilder/update.go +++ b/pkg/sql/opt/optbuilder/update.go @@ -348,6 +348,8 @@ func (mb *mutationBuilder) buildUpdate(returning tree.ReturningExprs) { // Project partial index PUT and DEL boolean columns. mb.projectPartialIndexPutAndDelCols(preCheckScope, mb.fetchScope) + mb.buildUniqueChecksForUpdate() + mb.buildFKChecksForUpdate() private := mb.makeMutationPrivate(returning != nil)