From 80dab3b2eeae044bdf1d7beb970385c6020aac0a Mon Sep 17 00:00:00 2001 From: Marcus Gartner Date: Tue, 16 Feb 2021 15:56:15 -0800 Subject: [PATCH] opt: support INSERT ON CONFLICT DO NOTHING with partial unique constraints To support INSERT ON CONFLICT DO NOTHING statements on tables with partial UNIQUE WITHOUT INDEX constraints, partial constraints are now selected as arbiters. These arbiters are used to filter out insert rows that would conflict with existing rows in the table. Informs #59195 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 | 38 ++ pkg/sql/opt/exec/execbuilder/testdata/unique | 287 ++++++++++++- pkg/sql/opt/optbuilder/insert.go | 51 ++- .../optbuilder/mutation_builder_arbiter.go | 129 ++++-- .../optbuilder/testdata/unique-checks-insert | 388 ++++++++++++++++++ 5 files changed, 834 insertions(+), 59 deletions(-) diff --git a/pkg/sql/logictest/testdata/logic_test/unique b/pkg/sql/logictest/testdata/logic_test/unique index acf32539420c..6abbf7863fa7 100644 --- a/pkg/sql/logictest/testdata/logic_test/unique +++ b/pkg/sql/logictest/testdata/logic_test/unique @@ -144,6 +144,12 @@ INSERT INTO uniq SELECT k, v, w, x, y FROM other statement ok INSERT INTO uniq VALUES (100, 10, 1), (200, 20, 2), (400, 40, 4) ON CONFLICT (w) DO NOTHING +# On conflict do nothing with constant input, conflict on UNIQUE WITHOUT INDEX +# column, conflicting insert rows. +# Only row (500, 50, 50) is inserted. +statement ok +INSERT INTO uniq VALUES (500, 50, 50), (600, 50, 50) ON CONFLICT (w) DO NOTHING + # On conflict do nothing with constant input, no conflict columns. # The only row that is successfully inserted here is (20, 20, 20, 20, 20). statement ok @@ -163,6 +169,7 @@ k v w x y 7 7 NULL 2 NULL 20 20 20 20 20 400 40 4 NULL 5 +500 50 50 NULL 5 # Insert into a table in which the primary key overlaps some of the unique @@ -281,6 +288,30 @@ INSERT INTO uniq_partial VALUES (NULL, 5), (5, 5), (NULL, 5) statement error pgcode 23505 pq: duplicate key value violates unique constraint "unique_a"\nDETAIL: Key \(a\)=\(1\) already exists\. INSERT INTO uniq_partial SELECT w, x FROM other +statement error there is no unique or exclusion constraint matching the ON CONFLICT specification +INSERT INTO uniq_partial VALUES (1, 6), (6, 6) ON CONFLICT (a) DO NOTHING + +# On conflict do nothing with constant input, conflict on UNIQUE WITHOUT INDEX +# column. Only the non-conflicting row (6, 6) is inserted. +statement ok +INSERT INTO uniq_partial VALUES (1, 6), (6, 6) ON CONFLICT (a) WHERE b > 0 DO NOTHING + +# On conflict do nothing with constant input, conflict on UNIQUE WITHOUT INDEX +# column, conflicting insert rows. +# Only rows (7, 7) and (7, -7) are inserted. +statement ok +INSERT INTO uniq_partial VALUES (7, 7), (7, 8), (7, -7) ON CONFLICT (a) WHERE b > 0 DO NOTHING + +# On conflict do nothing with constant input, no conflict columns. +# Only rows (9, 9) and (9, -9) are inserted. +statement ok +INSERT INTO uniq_partial VALUES (1, 9), (9, 9), (9, 10), (9, -9) ON CONFLICT DO NOTHING + +# On conflict do nothing with non-constant input. +# The (1, 10) row is not inserted because of a conflict with (1, 1). +statement ok +INSERT INTO uniq_partial SELECT w, k FROM other ON CONFLICT DO NOTHING + query II colnames,rowsort SELECT * FROM uniq_partial ---- @@ -290,6 +321,11 @@ a b 1 -3 2 2 5 5 +6 6 +7 7 +7 -7 +9 9 +9 -9 NULL 5 NULL 5 @@ -335,6 +371,7 @@ k v w x y 11 11 10 NULL 2 20 20 20 20 20 400 40 4 NULL 5 +500 50 50 NULL 5 # Update a table with multiple primary key columns. @@ -489,6 +526,7 @@ k v w x y 20 20 20 20 20 100 100 1 NULL 5 400 40 4 NULL 5 +500 50 50 NULL 5 # Upsert into a table in which the primary key overlaps some of the unique diff --git a/pkg/sql/opt/exec/execbuilder/testdata/unique b/pkg/sql/opt/exec/execbuilder/testdata/unique index 4fb2e7c4db0c..f9f2fbf1b712 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/unique +++ b/pkg/sql/opt/exec/execbuilder/testdata/unique @@ -847,7 +847,8 @@ vectorized: true columns: (column9, column1, column2, column10, check1) label: buffer 1 -# Test that we use the index when available for the ON CONFLICT checks. +# Test that we use the index when available for de-duplicating INSERT ON +# CONFLICT DO NOTHING rows before inserting. query T EXPLAIN (VERBOSE) INSERT INTO uniq_enum VALUES ('us-west', 'foo', 1, 1), ('us-east', 'bar', 2, 2) ON CONFLICT DO NOTHING @@ -1090,6 +1091,180 @@ vectorized: true └── • scan buffer label: buffer 1 +# Use all the unique indexes and constraints as arbiters for DO NOTHING with no +# conflict columns. +# TODO(mgartner): we should be able to remove the unique checks in this case +# (see #59119). +query T +EXPLAIN (VERBOSE) INSERT INTO uniq_partial VALUES (1, 2, 3) ON CONFLICT DO NOTHING +---- +distribution: local +vectorized: true +· +• root +│ columns: () +│ +├── • insert +│ │ columns: () +│ │ estimated row count: 0 (missing stats) +│ │ into: uniq_partial(k, a, b) +│ │ arbiter indexes: primary +│ │ arbiter constraints: unique_a, unique_b +│ │ +│ └── • buffer +│ │ columns: (column1, column2, column3) +│ │ label: buffer 1 +│ │ +│ └── • project +│ │ columns: (column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ +│ └── • distinct +│ │ columns: (upsert_partial_constraint_distinct1, column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ distinct on: upsert_partial_constraint_distinct1 +│ │ nulls are distinct +│ │ +│ └── • render +│ │ columns: (upsert_partial_constraint_distinct1, column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ render upsert_partial_constraint_distinct1: (column3 > 0) OR CAST(NULL AS BOOL) +│ │ render column1: column1 +│ │ render column2: column2 +│ │ render column3: column3 +│ │ +│ └── • distinct +│ │ columns: (upsert_partial_constraint_distinct0, column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ distinct on: upsert_partial_constraint_distinct0 +│ │ nulls are distinct +│ │ +│ └── • render +│ │ columns: (upsert_partial_constraint_distinct0, column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ render upsert_partial_constraint_distinct0: (column3 > 0) OR CAST(NULL AS BOOL) +│ │ render column1: column1 +│ │ render column2: column2 +│ │ render column3: column3 +│ │ +│ └── • hash join (right anti) +│ │ columns: (column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ equality: (b) = (column3) +│ │ right cols are key +│ │ +│ ├── • filter +│ │ │ columns: (b) +│ │ │ estimated row count: 333 (missing stats) +│ │ │ filter: b > 0 +│ │ │ +│ │ └── • scan +│ │ columns: (b) +│ │ estimated row count: 1,000 (missing stats) +│ │ table: uniq_partial@primary +│ │ spans: FULL SCAN +│ │ +│ └── • hash join (right anti) +│ │ columns: (column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ equality: (a) = (column2) +│ │ right cols are key +│ │ pred: column3 > 0 +│ │ +│ ├── • filter +│ │ │ columns: (a, b) +│ │ │ estimated row count: 333 (missing stats) +│ │ │ filter: b > 0 +│ │ │ +│ │ └── • scan +│ │ columns: (a, b) +│ │ estimated row count: 1,000 (missing stats) +│ │ table: uniq_partial@primary +│ │ spans: FULL SCAN +│ │ +│ └── • cross join (anti) +│ │ columns: (column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ +│ ├── • values +│ │ columns: (column1, column2, column3) +│ │ size: 3 columns, 1 row +│ │ row 0, expr 0: 1 +│ │ row 0, expr 1: 2 +│ │ row 0, expr 2: 3 +│ │ +│ └── • scan +│ columns: (k) +│ estimated row count: 1 (missing stats) +│ table: uniq_partial@primary +│ spans: /1/0-/1/1 +│ +├── • constraint-check +│ │ +│ └── • error if rows +│ │ columns: () +│ │ +│ └── • hash join (right semi) +│ │ columns: (column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ equality: (a) = (column2) +│ │ right cols are key +│ │ pred: column1 != k +│ │ +│ ├── • filter +│ │ │ columns: (k, a, b) +│ │ │ estimated row count: 333 (missing stats) +│ │ │ filter: b > 0 +│ │ │ +│ │ └── • scan +│ │ columns: (k, a, b) +│ │ estimated row count: 1,000 (missing stats) +│ │ table: uniq_partial@primary +│ │ spans: FULL SCAN +│ │ +│ └── • filter +│ │ columns: (column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ filter: column3 > 0 +│ │ +│ └── • scan buffer +│ columns: (column1, column2, column3) +│ estimated row count: 0 (missing stats) +│ label: buffer 1 +│ +└── • constraint-check + │ + └── • error if rows + │ columns: () + │ + └── • hash join (right semi) + │ columns: (column1, column2, column3) + │ estimated row count: 0 (missing stats) + │ equality: (b) = (column3) + │ right cols are key + │ pred: column1 != k + │ + ├── • filter + │ │ columns: (k, b) + │ │ estimated row count: 333 (missing stats) + │ │ filter: b > 0 + │ │ + │ └── • scan + │ columns: (k, b) + │ estimated row count: 1,000 (missing stats) + │ table: uniq_partial@primary + │ spans: FULL SCAN + │ + └── • filter + │ columns: (column1, column2, column3) + │ estimated row count: 0 (missing stats) + │ filter: column3 > 0 + │ + └── • scan buffer + columns: (column1, column2, column3) + estimated row count: 0 (missing stats) + label: buffer 1 + # Insert with non-constant input. query T EXPLAIN INSERT INTO uniq_partial SELECT k, v, w FROM other @@ -1290,6 +1465,116 @@ vectorized: true columns: (column1, column2, column3, 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') +ON CONFLICT DO NOTHING +---- +distribution: local +vectorized: true +· +• root +│ columns: () +│ +├── • insert +│ │ columns: () +│ │ estimated row count: 0 (missing stats) +│ │ into: uniq_partial_enum(r, i, s) +│ │ arbiter indexes: primary +│ │ arbiter constraints: unique_i +│ │ +│ └── • buffer +│ │ columns: (column1, column2, column3, 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') +│ │ render check1: column1 IN ('us-east', 'us-west', 'eu-west') +│ │ render column1: column1 +│ │ render column2: column2 +│ │ render column3: column3 +│ │ +│ └── • distinct +│ │ columns: (upsert_partial_constraint_distinct0, column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ distinct on: upsert_partial_constraint_distinct0, column2 +│ │ nulls are distinct +│ │ +│ └── • render +│ │ columns: (upsert_partial_constraint_distinct0, column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ render upsert_partial_constraint_distinct0: (column3 IN ('bar', 'baz', 'foo')) OR CAST(NULL AS BOOL) +│ │ render column1: column1 +│ │ render column2: column2 +│ │ render column3: column3 +│ │ +│ └── • 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') +│ │ +│ └── • lookup join (anti) +│ │ columns: (column1, column2, column3) +│ │ estimated row count: 0 (missing stats) +│ │ table: uniq_partial_enum@primary +│ │ equality: (column1, column2) = (r,i) +│ │ equality cols are key +│ │ +│ └── • values +│ columns: (column1, column2, column3) +│ size: 3 columns, 2 rows +│ row 0, expr 0: 'us-west' +│ row 0, expr 1: 1 +│ row 0, expr 2: 'foo' +│ row 1, expr 0: 'us-east' +│ row 1, expr 1: 2 +│ row 1, expr 2: 'bar' +│ +└── • constraint-check + │ + └── • error if rows + │ columns: () + │ + └── • project + │ columns: (column1, column2, column3) + │ estimated row count: 0 (missing stats) + │ + └── • 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 + │ + └── • cross join (inner) + │ columns: ("lookup_join_const_col_@22", column1, column2, column3) + │ estimated row count: 0 (missing stats) + │ + ├── • 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' + │ + └── • filter + │ columns: (column1, column2, column3) + │ estimated row count: 0 (missing stats) + │ filter: column3 IN ('bar', 'baz', 'foo') + │ + └── • project + │ columns: (column1, column2, column3) + │ estimated row count: 0 (missing stats) + │ + └── • scan buffer + columns: (column1, column2, column3, check1, partial_index_put1) + label: buffer 1 + # -- Tests with UPDATE -- subtest Update diff --git a/pkg/sql/opt/optbuilder/insert.go b/pkg/sql/opt/optbuilder/insert.go index 0ed852c8d8a5..e7886136187a 100644 --- a/pkg/sql/opt/optbuilder/insert.go +++ b/pkg/sql/opt/optbuilder/insert.go @@ -690,24 +690,27 @@ func (mb *mutationBuilder) buildInputForDoNothing( // each one. arbiterIndexes.ForEach(func(idx int) { index := mb.tab.Index(idx) - _, isPartial := index.Predicate() - var predExpr tree.Expr - if isPartial { - predExpr = mb.parsePartialIndexPredicateExpr(idx) + var pred tree.Expr + if _, isPartial := index.Predicate(); isPartial { + pred = mb.parsePartialIndexPredicateExpr(idx) } mb.buildAntiJoinForDoNothingArbiter( inScope, getIndexLaxKeyOrdinals(index), - predExpr, + pred, ) }) arbiterConstraints.ForEach(func(uc int) { uniqueConstraint := mb.tab.Unique(uc) + var pred tree.Expr + if _, isPartial := uniqueConstraint.Predicate(); isPartial { + pred = mb.parseUniqueConstraintPredicateExpr(uc) + } mb.buildAntiJoinForDoNothingArbiter( inScope, getUniqueConstraintOrdinals(mb.tab, uniqueConstraint), - nil, /* predExpr */ + pred, ) }) @@ -716,15 +719,16 @@ func (mb *mutationBuilder) buildInputForDoNothing( // the anti-joins created above, to avoid removing valid rows (see #59125). arbiterIndexes.ForEach(func(idx int) { index := mb.tab.Index(idx) - _, isPartial := index.Predicate() // If the index is a partial index, project a new column that allows the // UpsertDistinctOn to only de-duplicate insert rows that satisfy the - // partial index predicate. See projectPartialIndexDistinctColumn for more + // partial index predicate. See projectPartialArbiterDistinctColumn for more // details. var partialIndexDistinctCol *scopeColumn - if isPartial { - partialIndexDistinctCol = mb.projectPartialIndexDistinctColumn(insertColScope, idx) + if _, isPartial := index.Predicate(); isPartial { + alias := fmt.Sprintf("upsert_partial_index_distinct%d", idx) + pred := mb.parsePartialIndexPredicateExpr(idx) + partialIndexDistinctCol = mb.projectPartialArbiterDistinctColumn(insertColScope, pred, alias) } mb.buildDistinctOnForDoNothingArbiter( @@ -735,11 +739,23 @@ func (mb *mutationBuilder) buildInputForDoNothing( }) arbiterConstraints.ForEach(func(uc int) { uniqueConstraint := mb.tab.Unique(uc) + + // If the constraint is partial, project a new column that allows the + // UpsertDistinctOn to only de-duplicate insert rows that satisfy the + // partial predicate. See projectPartialArbiterDistinctColumn for more + // details. + var partialIndexDistinctCol *scopeColumn + if _, isPartial := uniqueConstraint.Predicate(); isPartial { + alias := fmt.Sprintf("upsert_partial_constraint_distinct%d", uc) + pred := mb.parseUniqueConstraintPredicateExpr(uc) + partialIndexDistinctCol = mb.projectPartialArbiterDistinctColumn(insertColScope, pred, alias) + } mb.buildDistinctOnForDoNothingArbiter( insertColScope, getUniqueConstraintOrdinals(mb.tab, uniqueConstraint), - nil, /* partialIndexDistinctCol */ + partialIndexDistinctCol, ) + }) mb.targetColList = make(opt.ColList, 0, mb.tab.ColumnCount()) @@ -925,23 +941,24 @@ func (mb *mutationBuilder) buildInputForUpsert( index := mb.tab.Index(idx) _, isPartial := index.Predicate() - var predExpr tree.Expr + var pred tree.Expr if isPartial { - predExpr = mb.parsePartialIndexPredicateExpr(idx) + pred = mb.parsePartialIndexPredicateExpr(idx) } // If the index is a partial index, project a new column that allows the // UpsertDistinctOn to only de-duplicate insert rows that satisfy the - // partial index predicate. See projectPartialIndexDistinctColumn for more - // details. + // partial index predicate. See projectPartialArbiterDistinctColumn for + // more details. var partialIndexDistinctCol *scopeColumn if isPartial { - partialIndexDistinctCol = mb.projectPartialIndexDistinctColumn(insertColScope, idx) + alias := fmt.Sprintf("upsert_partial_index_distinct%d", idx) + partialIndexDistinctCol = mb.projectPartialArbiterDistinctColumn(insertColScope, pred, alias) } buildInputForArbiter(func() *scopeColumn { return &mb.fetchScope.cols[findNotNullIndexCol(index)] - }, predExpr, partialIndexDistinctCol) + }, pred, partialIndexDistinctCol) } else if arbiterConstraints.Len() > 0 { buildInputForArbiter(func() *scopeColumn { // Use the primary index, since we don't know at this point whether diff --git a/pkg/sql/opt/optbuilder/mutation_builder_arbiter.go b/pkg/sql/opt/optbuilder/mutation_builder_arbiter.go index 99c0e22779fd..f1e37960a0ac 100644 --- a/pkg/sql/opt/optbuilder/mutation_builder_arbiter.go +++ b/pkg/sql/opt/optbuilder/mutation_builder_arbiter.go @@ -1,4 +1,4 @@ -// Copyright 2021 The Cockroach Authors. +// Copyright 2025 The Cockroach Authors. // // Use of this software is governed by the Business Source License // included in the file licenses/BSL.txt. @@ -11,8 +11,6 @@ package optbuilder import ( - "fmt" - "github.com/cockroachdb/cockroach/pkg/sql/opt" "github.com/cockroachdb/cockroach/pkg/sql/opt/cat" "github.com/cockroachdb/cockroach/pkg/sql/opt/memo" @@ -42,26 +40,38 @@ import ( // 2. If it is a partial index, its predicate must be implied by the // arbiterPredicate supplied by the user. // -// An arbiter constraint must have columns that match the columns in -// conflictOrds. Unique constraints without an index cannot be "partial", -// so they have no predicate to check for implication with arbiterPredicate. -// TODO(rytaft): revisit this for the case of implicitly partitioned partial -// unique indexes (see #59195). +// An arbiter constraint: +// +// 1. Must have columns that match the columns in conflictOrds. +// 2. If it is a partial constraint, its predicate must be implied by the +// arbiterPredicate supplied by the user. // // If conflictOrds is empty then all unique indexes and unique without index // constraints are returned as arbiters. This is required to support a // DO NOTHING with no ON CONFLICT columns. In this case, all unique indexes // and constraints are used to check for conflicts. // -// If a non-partial or pseudo-partial arbiter index is found, the return set -// contains only that index. No other arbiter is necessary because a non-partial -// or pseudo-partial index guarantees uniqueness of its columns across all -// rows. +// If conflictOrds is non-empty, there is an intentional preference for certain +// types of arbiters. Indexes are preferred over constraints because they will +// likely lead to a more efficient query plan. Non-partial or pseudo-partial +// indexes and constraints are preferred over partial indexes and constraints +// because a non-partial or pseudo-partial index or constraint guarantees +// uniqueness of its columns across all rows; there is no need for an additional +// arbiter for a subset of rows. If there are no non-partial or pseudo-partial +// indexes or constraints found, all valid partial indexes and partial +// constraints are returned so that uniqueness is guaranteed on the respective +// subsets of rows. In summary, if conflictOrds is non-empty, this function: +// +// 1. Returns a single non-partial or pseudo-partial arbiter index, if found. +// 2. Return a single non-partial or pseudo-partial arbiter constraint, if +// found. +// 3. Otherwise, returns all partial arbiter indexes and constraints. +// func (mb *mutationBuilder) arbiterIndexesAndConstraints( conflictOrds util.FastIntSet, arbiterPredicate tree.Expr, ) (indexes util.FastIntSet, uniqueConstraints util.FastIntSet) { - // If conflictOrds is empty, then all unique indexes and unique without index - // constraints are arbiters. + // If conflictOrds is empty, then all unique indexes and unique without + // index constraints are arbiters. if conflictOrds.Empty() { for idx, idxCount := 0, mb.tab.IndexCount(); idx < idxCount; idx++ { if mb.tab.Index(idx).IsUnique() { @@ -96,12 +106,10 @@ func (mb *mutationBuilder) arbiterIndexesAndConstraints( continue } - _, isPartial := index.Predicate() - // If the index is not a partial index, it can always be an arbiter. // Furthermore, it is the only arbiter needed because it guarantees // uniqueness of its columns across all rows. - if !isPartial { + if _, isPartial := index.Predicate(); !isPartial { return util.MakeFastIntSet(idx), util.FastIntSet{} } @@ -133,22 +141,47 @@ func (mb *mutationBuilder) arbiterIndexesAndConstraints( continue } - // Determine whether the conflict columns match the columns in the unique - // constraint. If not, the constraint cannot be an arbiter. + // Determine whether the conflict columns match the columns in the + // unique constraint. If not, the constraint cannot be an arbiter. We + // check the number of columns first to avoid unnecessarily collecting + // the unique constraint ordinals and determining set equality. + if uniqueConstraint.ColumnCount() != conflictOrds.Len() { + continue + } ucOrds := getUniqueConstraintOrdinals(mb.tab, uniqueConstraint) - if ucOrds.Equals(conflictOrds) { + if !ucOrds.Equals(conflictOrds) { + continue + } + + // If the unique constraint is not partial, it should be returned + // without any partial index arbiters. + if _, isPartial := uniqueConstraint.Predicate(); !isPartial { return util.FastIntSet{}, util.MakeFastIntSet(uc) } + + // If the constraint is a pseudo-partial unique constraint, it can + // always be an arbiter. It should be returned without any partial index + // arbiters. + pred := h.partialUniqueConstraintPredicate(uc) + if pred.IsTrue() { + return util.FastIntSet{}, util.MakeFastIntSet(uc) + } + + // If the unique constraint is partial, then it can only be an arbiter + // if the arbiterPredicate implies it. + if h.predicateIsImpliedByArbiterPredicate(pred) { + uniqueConstraints.Add(uc) + } } - // There are no full indexes or constraints, so return any partial indexes - // that were found. - if !indexes.Empty() { - return indexes, util.FastIntSet{} + // Err if we did not previously return and did not find partial indexes or + // partial unique constraints. + if indexes.Empty() && uniqueConstraints.Empty() { + panic(pgerror.Newf(pgcode.InvalidColumnReference, + "there is no unique or exclusion constraint matching the ON CONFLICT specification")) } - panic(pgerror.Newf(pgcode.InvalidColumnReference, - "there is no unique or exclusion constraint matching the ON CONFLICT specification")) + return indexes, uniqueConstraints } // buildAntiJoinForDoNothingArbiter builds an anti-join for a single arbiter index @@ -158,11 +191,11 @@ func (mb *mutationBuilder) arbiterIndexesAndConstraints( // // - columnOrds is the set of table column ordinals that the arbiter // guarantees uniqueness of. -// - predExpr is the partial index predicate. If the arbiter is not a partial -// index, predExpr is nil. +// - pred is the partial index predicate. If the arbiter is not a partial +// index, pred is nil. // func (mb *mutationBuilder) buildAntiJoinForDoNothingArbiter( - inScope *scope, columnOrds util.FastIntSet, predExpr tree.Expr, + inScope *scope, columnOrds util.FastIntSet, pred tree.Expr, ) { // Build the right side of the anti-join. Use a new metadata instance // of the mutation table so that a different set of column IDs are used for @@ -184,8 +217,8 @@ func (mb *mutationBuilder) buildAntiJoinForDoNothingArbiter( // partial index cannot conflict with insert rows. Therefore, a Select // wraps the scan on the right side of the anti-join with the partial // index predicate expression as the filter. - if predExpr != nil { - texpr := fetchScope.resolveAndRequireType(predExpr, types.Bool) + if pred != nil { + texpr := fetchScope.resolveAndRequireType(pred, types.Bool) predScalar := mb.b.buildScalar(texpr, fetchScope, nil, nil, nil) fetchScope.expr = mb.b.factory.ConstructSelect( fetchScope.expr, @@ -216,8 +249,8 @@ func (mb *mutationBuilder) buildAntiJoinForDoNothingArbiter( // satisfy the partial index predicate cannot conflict with existing // rows in the unique partial index. Therefore, the partial index // predicate expression is added to the ON filters. - if predExpr != nil { - texpr := mb.outScope.resolveAndRequireType(predExpr, types.Bool) + if pred != nil { + texpr := mb.outScope.resolveAndRequireType(pred, types.Bool) predScalar := mb.b.buildScalar(texpr, mb.outScope, nil, nil, nil) on = append(on, mb.b.factory.ConstructFiltersItem(predScalar)) } @@ -270,10 +303,11 @@ func (mb *mutationBuilder) buildDistinctOnForDoNothingArbiter( } } -// projectPartialIndexDistinctColumn projects a column to facilitate +// projectPartialArbiterDistinctColumn projects a column to facilitate // de-duplicating insert rows for UPSERT/INSERT ON CONFLICT when the arbiter -// index is a partial index. Only those insert rows that satisfy the partial -// index predicate should be de-duplicated. For example: +// index or constraint is partial. Only those insert rows that satisfy the +// partial index or unique constraint predicate should be de-duplicated. For +// example: // // CREATE TABLE t (a INT, b INT, UNIQUE INDEX (a) WHERE b > 0) // INSERT INTO t VALUES (1, 1), (1, 2), (1, -1), (1, -10) ON CONFLICT DO NOTHING @@ -300,20 +334,18 @@ func (mb *mutationBuilder) buildDistinctOnForDoNothingArbiter( // NULL). // // The newly project scopeColumn is returned. -func (mb *mutationBuilder) projectPartialIndexDistinctColumn( - insertScope *scope, idx cat.IndexOrdinal, +func (mb *mutationBuilder) projectPartialArbiterDistinctColumn( + insertScope *scope, pred tree.Expr, alias string, ) *scopeColumn { projectionScope := mb.outScope.replace() projectionScope.appendColumnsFromScope(insertScope) - predExpr := mb.parsePartialIndexPredicateExpr(idx) expr := &tree.OrExpr{ - Left: predExpr, + Left: pred, Right: tree.DNull, } texpr := insertScope.resolveAndRequireType(expr, types.Bool) - alias := fmt.Sprintf("upsert_partial_index_distinct%d", idx) scopeCol := projectionScope.addColumn(alias, texpr) mb.b.buildScalar(texpr, mb.outScope, projectionScope, scopeCol, nil) @@ -388,6 +420,21 @@ func (h *arbiterPredicateHelper) partialIndexPredicate(idx cat.IndexOrdinal) mem return *pred.(*memo.FiltersExpr) } +// partialUniqueConstraintPredicate returns the predicate of the given unique +// constraint. +func (h *arbiterPredicateHelper) partialUniqueConstraintPredicate( + idx cat.UniqueOrdinal, +) memo.FiltersExpr { + // Build and normalize the unique constraint predicate expression. + pred, err := h.mb.b.buildPartialIndexPredicate( + h.tabMeta, h.tableScope(), h.mb.parseUniqueConstraintPredicateExpr(idx), "unique constraint predicate", + ) + if err != nil { + panic(err) + } + return pred +} + // arbiterFilters returns a scalar expression representing the arbiter // predicate. If the arbiter predicate contains non-immutable operators, // ok=true is returned. diff --git a/pkg/sql/opt/optbuilder/testdata/unique-checks-insert b/pkg/sql/opt/optbuilder/testdata/unique-checks-insert index 84327424d230..c2df816c63d0 100644 --- a/pkg/sql/opt/optbuilder/testdata/unique-checks-insert +++ b/pkg/sql/opt/optbuilder/testdata/unique-checks-insert @@ -893,6 +893,150 @@ insert uniq_partial ├── (1, NULL::INT8, 1) └── (2, NULL::INT8, 2) +# Use all the unique constraint as an arbiter for DO NOTHING with no conflict +# columns. +# TODO(rytaft/mgartner): we should be able to remove the unique checks in this +# case (see #59119). +build +INSERT INTO uniq_partial VALUES (1, 2, 3), (2, 2, 3) ON CONFLICT DO NOTHING +---- +insert uniq_partial + ├── columns: + ├── arbiter indexes: primary + ├── arbiter constraints: unique_a + ├── insert-mapping: + │ ├── column1:5 => uniq_partial.k:1 + │ ├── column2:6 => uniq_partial.a:2 + │ └── column3:7 => uniq_partial.b:3 + ├── input binding: &1 + ├── project + │ ├── columns: column1:5!null column2:6!null column3:7!null + │ └── upsert-distinct-on + │ ├── columns: column1:5!null column2:6!null column3:7!null upsert_partial_constraint_distinct0:16 + │ ├── grouping columns: column2:6!null upsert_partial_constraint_distinct0:16 + │ ├── project + │ │ ├── columns: upsert_partial_constraint_distinct0:16 column1:5!null column2:6!null column3:7!null + │ │ ├── upsert-distinct-on + │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ ├── grouping columns: column1:5!null + │ │ │ ├── anti-join (hash) + │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ │ ├── anti-join (hash) + │ │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ │ │ ├── values + │ │ │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ │ │ │ ├── (1, 2, 3) + │ │ │ │ │ │ └── (2, 2, 3) + │ │ │ │ │ ├── scan uniq_partial + │ │ │ │ │ │ └── columns: uniq_partial.k:8!null uniq_partial.a:9 uniq_partial.b:10 + │ │ │ │ │ └── filters + │ │ │ │ │ └── column1:5 = uniq_partial.k:8 + │ │ │ │ ├── select + │ │ │ │ │ ├── columns: uniq_partial.k:12!null uniq_partial.a:13 uniq_partial.b:14!null + │ │ │ │ │ ├── scan uniq_partial + │ │ │ │ │ │ └── columns: uniq_partial.k:12!null uniq_partial.a:13 uniq_partial.b:14 + │ │ │ │ │ └── filters + │ │ │ │ │ └── uniq_partial.b:14 > 0 + │ │ │ │ └── filters + │ │ │ │ ├── column2:6 = uniq_partial.a:13 + │ │ │ │ └── column3:7 > 0 + │ │ │ └── aggregations + │ │ │ ├── first-agg [as=column2:6] + │ │ │ │ └── column2:6 + │ │ │ └── first-agg [as=column3:7] + │ │ │ └── column3:7 + │ │ └── projections + │ │ └── (column3:7 > 0) OR NULL::BOOL [as=upsert_partial_constraint_distinct0:16] + │ └── aggregations + │ ├── first-agg [as=column1:5] + │ │ └── column1:5 + │ └── first-agg [as=column3:7] + │ └── column3:7 + └── unique-checks + └── unique-checks-item: uniq_partial(a) + └── semi-join (hash) + ├── columns: k:21!null a:22!null b:23!null + ├── with-scan &1 + │ ├── columns: k:21!null a:22!null b:23!null + │ └── mapping: + │ ├── column1:5 => k:21 + │ ├── column2:6 => a:22 + │ └── column3:7 => b:23 + ├── scan uniq_partial + │ └── columns: uniq_partial.k:17!null uniq_partial.a:18 uniq_partial.b:19 + └── filters + ├── a:22 = uniq_partial.a:18 + ├── b:23 > 0 + ├── uniq_partial.b:19 > 0 + └── k:21 != uniq_partial.k:17 + +# Error when there is no arbiter predicate to match the partial unique +# constraint predicate. +build +INSERT INTO uniq_partial VALUES (1, 2, 3) ON CONFLICT (a) DO NOTHING +---- +error (42P10): there is no unique or exclusion constraint matching the ON CONFLICT specification + +# On conflict clause references unique without index constraint. +# TODO(rytaft/mgartner): we should be able to remove the unique check for a in +# this case (see #59119). +build +INSERT INTO uniq_partial VALUES (1, 2, 3) ON CONFLICT (a) WHERE b > 0 DO NOTHING +---- +insert uniq_partial + ├── columns: + ├── arbiter constraints: unique_a + ├── insert-mapping: + │ ├── column1:5 => uniq_partial.k:1 + │ ├── column2:6 => uniq_partial.a:2 + │ └── column3:7 => uniq_partial.b:3 + ├── input binding: &1 + ├── project + │ ├── columns: column1:5!null column2:6!null column3:7!null + │ └── upsert-distinct-on + │ ├── columns: column1:5!null column2:6!null column3:7!null upsert_partial_constraint_distinct0:12 + │ ├── grouping columns: column2:6!null upsert_partial_constraint_distinct0:12 + │ ├── project + │ │ ├── columns: upsert_partial_constraint_distinct0:12 column1:5!null column2:6!null column3:7!null + │ │ ├── anti-join (hash) + │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ ├── values + │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ │ └── (1, 2, 3) + │ │ │ ├── select + │ │ │ │ ├── columns: uniq_partial.k:8!null uniq_partial.a:9 uniq_partial.b:10!null + │ │ │ │ ├── scan uniq_partial + │ │ │ │ │ └── columns: uniq_partial.k:8!null uniq_partial.a:9 uniq_partial.b:10 + │ │ │ │ └── filters + │ │ │ │ └── uniq_partial.b:10 > 0 + │ │ │ └── filters + │ │ │ ├── column2:6 = uniq_partial.a:9 + │ │ │ └── column3:7 > 0 + │ │ └── projections + │ │ └── (column3:7 > 0) OR NULL::BOOL [as=upsert_partial_constraint_distinct0:12] + │ └── aggregations + │ ├── first-agg [as=column1:5] + │ │ └── column1:5 + │ └── first-agg [as=column3:7] + │ └── column3:7 + └── unique-checks + └── unique-checks-item: uniq_partial(a) + └── semi-join (hash) + ├── columns: k:17!null a:18!null b:19!null + ├── with-scan &1 + │ ├── columns: k:17!null a:18!null b:19!null + │ └── mapping: + │ ├── column1:5 => k:17 + │ ├── column2:6 => a:18 + │ └── column3:7 => b:19 + ├── scan uniq_partial + │ └── columns: uniq_partial.k:13!null uniq_partial.a:14 uniq_partial.b:15 + └── filters + ├── a:18 = uniq_partial.a:14 + ├── b:19 > 0 + ├── uniq_partial.b:15 > 0 + └── k:17 != uniq_partial.k:13 + # Insert with non-constant input. build INSERT INTO uniq_partial SELECT k, v, w FROM other @@ -1174,3 +1318,247 @@ insert uniq_partial_hidden_pk ├── c:22 > 0 ├── uniq_partial_hidden_pk.c:17 > 0 └── rowid:23 != uniq_partial_hidden_pk.rowid:18 + +exec-ddl +CREATE TABLE uniq_partial_constraint_and_index ( + k INT PRIMARY KEY, + a INT, + b INT, + UNIQUE INDEX (a) WHERE true, + UNIQUE WITHOUT INDEX (a) WHERE b > 10 +) +---- + +# Use a pseudo-partial index as the only arbiter. Note that we use the "norm" +# directive instead of "build" to ensure that partial index predicates are fully +# normalized when choosing arbiter indexes. +norm +INSERT INTO uniq_partial_constraint_and_index VALUES (1, 1, 1) +ON CONFLICT (a) WHERE b > 10 DO NOTHING +---- +insert uniq_partial_constraint_and_index + ├── columns: + ├── arbiter indexes: secondary + ├── insert-mapping: + │ ├── column1:5 => uniq_partial_constraint_and_index.k:1 + │ ├── column2:6 => uniq_partial_constraint_and_index.a:2 + │ └── column3:7 => uniq_partial_constraint_and_index.b:3 + ├── partial index put columns: partial_index_put1:13 + ├── input binding: &1 + ├── project + │ ├── columns: partial_index_put1:13!null column1:5!null column2:6!null column3:7!null + │ ├── anti-join (cross) + │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ ├── values + │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ └── (1, 1, 1) + │ │ ├── select + │ │ │ ├── columns: uniq_partial_constraint_and_index.a:9!null + │ │ │ ├── scan uniq_partial_constraint_and_index + │ │ │ │ ├── columns: uniq_partial_constraint_and_index.a:9 + │ │ │ │ └── partial index predicates + │ │ │ │ └── secondary: filters (true) + │ │ │ └── filters + │ │ │ └── uniq_partial_constraint_and_index.a:9 = 1 + │ │ └── filters (true) + │ └── projections + │ └── true [as=partial_index_put1:13] + └── unique-checks + └── unique-checks-item: uniq_partial_constraint_and_index(a) + └── semi-join (hash) + ├── columns: k:18!null a:19!null b:20!null + ├── select + │ ├── columns: k:18!null a:19!null b:20!null + │ ├── with-scan &1 + │ │ ├── columns: k:18!null a:19!null b:20!null + │ │ └── mapping: + │ │ ├── column1:5 => k:18 + │ │ ├── column2:6 => a:19 + │ │ └── column3:7 => b:20 + │ └── filters + │ └── b:20 > 10 + ├── select + │ ├── columns: uniq_partial_constraint_and_index.k:14!null uniq_partial_constraint_and_index.a:15 uniq_partial_constraint_and_index.b:16!null + │ ├── scan uniq_partial_constraint_and_index + │ │ ├── columns: uniq_partial_constraint_and_index.k:14!null uniq_partial_constraint_and_index.a:15 uniq_partial_constraint_and_index.b:16 + │ │ └── partial index predicates + │ │ └── secondary: filters (true) + │ └── filters + │ └── uniq_partial_constraint_and_index.b:16 > 10 + └── filters + ├── a:19 = uniq_partial_constraint_and_index.a:15 + └── k:18 != uniq_partial_constraint_and_index.k:14 + +exec-ddl +CREATE TABLE uniq_constraint_and_partial_index ( + k INT PRIMARY KEY, + a INT, + b INT, + UNIQUE INDEX (a) WHERE b > 0, + UNIQUE WITHOUT INDEX (a) WHERE true +) +---- + +# Use a pseudo-partial constraint as the only arbiter. Note that we use the +# "norm" directive instead of "build" to ensure that partial index predicates +# are fully normalized when choosing arbiter indexes. +norm +INSERT INTO uniq_constraint_and_partial_index VALUES (1, 1, 1) +ON CONFLICT (a) WHERE b > 0 DO NOTHING +---- +insert uniq_constraint_and_partial_index + ├── columns: + ├── arbiter constraints: unique_a + ├── insert-mapping: + │ ├── column1:5 => uniq_constraint_and_partial_index.k:1 + │ ├── column2:6 => uniq_constraint_and_partial_index.a:2 + │ └── column3:7 => uniq_constraint_and_partial_index.b:3 + ├── partial index put columns: partial_index_put1:13 + ├── input binding: &1 + ├── project + │ ├── columns: partial_index_put1:13!null column1:5!null column2:6!null column3:7!null + │ ├── anti-join (cross) + │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ ├── values + │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ └── (1, 1, 1) + │ │ ├── select + │ │ │ ├── columns: uniq_constraint_and_partial_index.a:9!null + │ │ │ ├── scan uniq_constraint_and_partial_index + │ │ │ │ ├── columns: uniq_constraint_and_partial_index.a:9 + │ │ │ │ └── partial index predicates + │ │ │ │ └── secondary: filters + │ │ │ │ └── uniq_constraint_and_partial_index.b:10 > 0 + │ │ │ └── filters + │ │ │ └── uniq_constraint_and_partial_index.a:9 = 1 + │ │ └── filters (true) + │ └── projections + │ └── column3:7 > 0 [as=partial_index_put1:13] + └── unique-checks + └── unique-checks-item: uniq_constraint_and_partial_index(a) + └── semi-join (hash) + ├── columns: k:18!null a:19!null b:20!null + ├── with-scan &1 + │ ├── columns: k:18!null a:19!null b:20!null + │ └── mapping: + │ ├── column1:5 => k:18 + │ ├── column2:6 => a:19 + │ └── column3:7 => b:20 + ├── scan uniq_constraint_and_partial_index + │ ├── columns: uniq_constraint_and_partial_index.k:14!null uniq_constraint_and_partial_index.a:15 + │ └── partial index predicates + │ └── secondary: filters + │ └── uniq_constraint_and_partial_index.b:16 > 0 + └── filters + ├── a:19 = uniq_constraint_and_partial_index.a:15 + └── k:18 != uniq_constraint_and_partial_index.k:14 + +exec-ddl +CREATE TABLE uniq_partial_constraint_and_partial_index ( + k INT PRIMARY KEY, + a INT, + b INT, + UNIQUE INDEX (a) WHERE b > 0, + UNIQUE WITHOUT INDEX (a) WHERE b > 10 +) +---- + +# Use both a partial index and partial constraint as arbiters when both +# predicates are implied by the arbiter predicate. +build +INSERT INTO uniq_partial_constraint_and_partial_index VALUES (1, 1, 1) +ON CONFLICT (a) WHERE b > 10 DO NOTHING +---- +insert uniq_partial_constraint_and_partial_index + ├── columns: + ├── arbiter indexes: secondary + ├── arbiter constraints: unique_a + ├── insert-mapping: + │ ├── column1:5 => uniq_partial_constraint_and_partial_index.k:1 + │ ├── column2:6 => uniq_partial_constraint_and_partial_index.a:2 + │ └── column3:7 => uniq_partial_constraint_and_partial_index.b:3 + ├── partial index put columns: partial_index_put1:18 + ├── input binding: &1 + ├── project + │ ├── columns: partial_index_put1:18!null column1:5!null column2:6!null column3:7!null + │ ├── project + │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ └── upsert-distinct-on + │ │ ├── columns: column1:5!null column2:6!null column3:7!null upsert_partial_constraint_distinct0:17 + │ │ ├── grouping columns: column2:6!null upsert_partial_constraint_distinct0:17 + │ │ ├── project + │ │ │ ├── columns: upsert_partial_constraint_distinct0:17 column1:5!null column2:6!null column3:7!null + │ │ │ ├── project + │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ │ └── upsert-distinct-on + │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null upsert_partial_index_distinct1:16 + │ │ │ │ ├── grouping columns: column2:6!null upsert_partial_index_distinct1:16 + │ │ │ │ ├── project + │ │ │ │ │ ├── columns: upsert_partial_index_distinct1:16 column1:5!null column2:6!null column3:7!null + │ │ │ │ │ ├── anti-join (hash) + │ │ │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ │ │ │ ├── anti-join (hash) + │ │ │ │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ │ │ │ │ ├── values + │ │ │ │ │ │ │ │ ├── columns: column1:5!null column2:6!null column3:7!null + │ │ │ │ │ │ │ │ └── (1, 1, 1) + │ │ │ │ │ │ │ ├── select + │ │ │ │ │ │ │ │ ├── columns: uniq_partial_constraint_and_partial_index.k:8!null uniq_partial_constraint_and_partial_index.a:9 uniq_partial_constraint_and_partial_index.b:10!null + │ │ │ │ │ │ │ │ ├── scan uniq_partial_constraint_and_partial_index + │ │ │ │ │ │ │ │ │ ├── columns: uniq_partial_constraint_and_partial_index.k:8!null uniq_partial_constraint_and_partial_index.a:9 uniq_partial_constraint_and_partial_index.b:10 + │ │ │ │ │ │ │ │ │ └── partial index predicates + │ │ │ │ │ │ │ │ │ └── secondary: filters + │ │ │ │ │ │ │ │ │ └── uniq_partial_constraint_and_partial_index.b:10 > 0 + │ │ │ │ │ │ │ │ └── filters + │ │ │ │ │ │ │ │ └── uniq_partial_constraint_and_partial_index.b:10 > 0 + │ │ │ │ │ │ │ └── filters + │ │ │ │ │ │ │ ├── column2:6 = uniq_partial_constraint_and_partial_index.a:9 + │ │ │ │ │ │ │ └── column3:7 > 0 + │ │ │ │ │ │ ├── select + │ │ │ │ │ │ │ ├── columns: uniq_partial_constraint_and_partial_index.k:12!null uniq_partial_constraint_and_partial_index.a:13 uniq_partial_constraint_and_partial_index.b:14!null + │ │ │ │ │ │ │ ├── scan uniq_partial_constraint_and_partial_index + │ │ │ │ │ │ │ │ ├── columns: uniq_partial_constraint_and_partial_index.k:12!null uniq_partial_constraint_and_partial_index.a:13 uniq_partial_constraint_and_partial_index.b:14 + │ │ │ │ │ │ │ │ └── partial index predicates + │ │ │ │ │ │ │ │ └── secondary: filters + │ │ │ │ │ │ │ │ └── uniq_partial_constraint_and_partial_index.b:14 > 0 + │ │ │ │ │ │ │ └── filters + │ │ │ │ │ │ │ └── uniq_partial_constraint_and_partial_index.b:14 > 10 + │ │ │ │ │ │ └── filters + │ │ │ │ │ │ ├── column2:6 = uniq_partial_constraint_and_partial_index.a:13 + │ │ │ │ │ │ └── column3:7 > 10 + │ │ │ │ │ └── projections + │ │ │ │ │ └── (column3:7 > 0) OR NULL::BOOL [as=upsert_partial_index_distinct1:16] + │ │ │ │ └── aggregations + │ │ │ │ ├── first-agg [as=column1:5] + │ │ │ │ │ └── column1:5 + │ │ │ │ └── first-agg [as=column3:7] + │ │ │ │ └── column3:7 + │ │ │ └── projections + │ │ │ └── (column3:7 > 10) OR NULL::BOOL [as=upsert_partial_constraint_distinct0:17] + │ │ └── aggregations + │ │ ├── first-agg [as=column1:5] + │ │ │ └── column1:5 + │ │ └── first-agg [as=column3:7] + │ │ └── column3:7 + │ └── projections + │ └── column3:7 > 0 [as=partial_index_put1:18] + └── unique-checks + └── unique-checks-item: uniq_partial_constraint_and_partial_index(a) + └── semi-join (hash) + ├── columns: k:23!null a:24!null b:25!null + ├── with-scan &1 + │ ├── columns: k:23!null a:24!null b:25!null + │ └── mapping: + │ ├── column1:5 => k:23 + │ ├── column2:6 => a:24 + │ └── column3:7 => b:25 + ├── scan uniq_partial_constraint_and_partial_index + │ ├── columns: uniq_partial_constraint_and_partial_index.k:19!null uniq_partial_constraint_and_partial_index.a:20 uniq_partial_constraint_and_partial_index.b:21 + │ └── partial index predicates + │ └── secondary: filters + │ └── uniq_partial_constraint_and_partial_index.b:21 > 0 + └── filters + ├── a:24 = uniq_partial_constraint_and_partial_index.a:20 + ├── b:25 > 10 + ├── uniq_partial_constraint_and_partial_index.b:21 > 10 + └── k:23 != uniq_partial_constraint_and_partial_index.k:19