diff --git a/pkg/sql/logictest/testdata/logic_test/regional_by_row_foreign_key b/pkg/sql/logictest/testdata/logic_test/regional_by_row_foreign_key index 1bfa3e14e1e..a39a1e8cf1e 100644 --- a/pkg/sql/logictest/testdata/logic_test/regional_by_row_foreign_key +++ b/pkg/sql/logictest/testdata/logic_test/regional_by_row_foreign_key @@ -2440,3 +2440,54 @@ statement ok DROP TABLE parent_160013 subtest end + +subtest non_unique_parent + +# Regression test for #170768: a REGIONAL BY ROW child can set +# infer_rbr_region_col_using_constraint even when the parent's non-region +# foreign-key columns aren't covered by a unique constraint. The optimizer +# wraps the lookup join in a DistinctOn so each child input row maps to at +# most one parent row; the chosen parent row's region is arbitrary if more +# than one parent row matches. +statement ok +CREATE TABLE p_nonunique ( + region crdb_internal_region NOT NULL, + k INT NOT NULL, + PRIMARY KEY (region, k) +) + +statement ok +INSERT INTO p_nonunique VALUES ('ca-central-1', 1), ('ap-southeast-2', 2), ('us-east-1', 3) + +statement ok +CREATE TABLE c_nonunique ( + pk INT PRIMARY KEY, + data INT, + CONSTRAINT fk_region FOREIGN KEY (crdb_region, pk) REFERENCES p_nonunique (region, k) +) WITH (infer_rbr_region_col_using_constraint = 'fk_region') LOCALITY REGIONAL BY ROW + +# Before #170768's fix the INSERT panicked with an assertion failure +# ("lookup columns using constraint X must be a lax key"). It now succeeds: +# the single matching parent row's region is selected. +statement ok +INSERT INTO c_nonunique (pk, data) VALUES (1, 100) + +query IT +SELECT pk, crdb_region FROM c_nonunique +---- +1 ca-central-1 + +# With two parent rows sharing the join key, the inference still completes +# (picking one arbitrarily) rather than failing. +statement ok +INSERT INTO p_nonunique VALUES ('ca-central-1', 10), ('us-east-1', 10) + +statement ok +INSERT INTO c_nonunique (pk, data) VALUES (10, 100) + +query I +SELECT count(*) FROM c_nonunique WHERE pk = 10 +---- +1 + +subtest end diff --git a/pkg/sql/opt/norm/decorrelate_funcs.go b/pkg/sql/opt/norm/decorrelate_funcs.go index 2387ef7ec90..b532c8ed9af 100644 --- a/pkg/sql/opt/norm/decorrelate_funcs.go +++ b/pkg/sql/opt/norm/decorrelate_funcs.go @@ -518,81 +518,6 @@ func (c *CustomFuncs) ConstructApplyJoin( panic(errors.AssertionFailedf("unexpected join operator: %v", redact.Safe(joinOp))) } -// EnsureKey finds the shortest strong key for the input expression. If no -// strong key exists and the input expression is a Scan or a Scan wrapped in a -// Select, EnsureKey returns a new Scan (possibly wrapped in a Select) with the -// preexisting primary key for the table. If the input is not a Scan or -// Select(Scan), EnsureKey wraps the input in an Ordinality operator, which -// provides a key column by uniquely numbering the rows. EnsureKey returns the -// input expression (perhaps augmented with a key column(s) or wrapped by -// Ordinality). -func (c *CustomFuncs) EnsureKey(in memo.RelExpr) memo.RelExpr { - // Try to add the preexisting primary key if the input is a Scan or Scan - // wrapped in a Select. - if res, ok := c.tryFindExistingKey(in); ok { - return res - } - - // Otherwise, wrap the input in an Ordinality operator. - colID := c.f.Metadata().AddColumn("rownum", types.Int) - private := memo.OrdinalityPrivate{ColID: colID, ForDuplicateRemoval: true} - return c.f.ConstructOrdinality(in, &private) -} - -// tryFindExistingKey attempts to find an existing key for the input expression. -// It may modify the expression in order to project the key column. -func (c *CustomFuncs) tryFindExistingKey(in memo.RelExpr) (_ memo.RelExpr, ok bool) { - _, hasKey := c.CandidateKey(in) - if hasKey { - return in, true - } - switch t := in.(type) { - case *memo.ProjectExpr: - input, foundKey := c.tryFindExistingKey(t.Input) - if foundKey { - return c.f.ConstructProject(input, t.Projections, input.Relational().OutputCols), true - } - - case *memo.ScanExpr: - private := t.ScanPrivate - tableID := private.Table - table := c.f.Metadata().Table(tableID) - if !table.IsVirtualTable() { - keyCols := c.PrimaryKeyCols(tableID) - private.Cols = private.Cols.Union(keyCols) - return c.f.ConstructScan(&private), true - } - - case *memo.SelectExpr: - input, foundKey := c.tryFindExistingKey(t.Input) - if foundKey { - return c.f.ConstructSelect(input, t.Filters), true - } - } - - return nil, false -} - -// KeyCols returns a column set consisting of the columns that make up the -// candidate key for the input expression (a key must be present). -func (c *CustomFuncs) KeyCols(in memo.RelExpr) opt.ColSet { - keyCols, ok := c.CandidateKey(in) - if !ok { - panic(errors.AssertionFailedf("expected expression to have key")) - } - return keyCols -} - -// NonKeyCols returns a column set consisting of the output columns of the given -// input, minus the columns that make up its candidate key (which it must have). -func (c *CustomFuncs) NonKeyCols(in memo.RelExpr) opt.ColSet { - keyCols, ok := c.CandidateKey(in) - if !ok { - panic(errors.AssertionFailedf("expected expression to have key")) - } - return c.OutputCols(in).Difference(keyCols) -} - // MakeAggCols2 is similar to MakeAggCols, except that it allows two different // sets of aggregate functions to be added to the resulting Aggregations // operator, with one set appended to the other, like this: diff --git a/pkg/sql/opt/norm/general_funcs.go b/pkg/sql/opt/norm/general_funcs.go index a2f988801d1..8838fb14a83 100644 --- a/pkg/sql/opt/norm/general_funcs.go +++ b/pkg/sql/opt/norm/general_funcs.go @@ -611,6 +611,81 @@ func (c *CustomFuncs) PrimaryKeyCols(table opt.TableID) opt.ColSet { return tabMeta.IndexKeyColumns(cat.PrimaryIndex) } +// KeyCols returns a column set consisting of the columns that make up the +// candidate key for the input expression (a key must be present). +func (c *CustomFuncs) KeyCols(in memo.RelExpr) opt.ColSet { + keyCols, ok := c.CandidateKey(in) + if !ok { + panic(errors.AssertionFailedf("expected expression to have key")) + } + return keyCols +} + +// NonKeyCols returns a column set consisting of the output columns of the given +// input, minus the columns that make up its candidate key (which it must have). +func (c *CustomFuncs) NonKeyCols(in memo.RelExpr) opt.ColSet { + keyCols, ok := c.CandidateKey(in) + if !ok { + panic(errors.AssertionFailedf("expected expression to have key")) + } + return c.OutputCols(in).Difference(keyCols) +} + +// EnsureKey finds the shortest strong key for the input expression. If no +// strong key exists and the input expression is a Scan or a Scan wrapped in a +// Select, EnsureKey returns a new Scan (possibly wrapped in a Select) with the +// preexisting primary key for the table. If the input is not a Scan or +// Select(Scan), EnsureKey wraps the input in an Ordinality operator, which +// provides a key column by uniquely numbering the rows. EnsureKey returns the +// input expression (perhaps augmented with a key column(s) or wrapped by +// Ordinality). +func (c *CustomFuncs) EnsureKey(in memo.RelExpr) memo.RelExpr { + // Try to add the preexisting primary key if the input is a Scan or Scan + // wrapped in a Select. + if res, ok := c.tryFindExistingKey(in); ok { + return res + } + + // Otherwise, wrap the input in an Ordinality operator. + colID := c.f.Metadata().AddColumn("rownum", types.Int) + private := memo.OrdinalityPrivate{ColID: colID, ForDuplicateRemoval: true} + return c.f.ConstructOrdinality(in, &private) +} + +// tryFindExistingKey attempts to find an existing key for the input expression. +// It may modify the expression in order to project the key column. +func (c *CustomFuncs) tryFindExistingKey(in memo.RelExpr) (_ memo.RelExpr, ok bool) { + _, hasKey := c.CandidateKey(in) + if hasKey { + return in, true + } + switch t := in.(type) { + case *memo.ProjectExpr: + input, foundKey := c.tryFindExistingKey(t.Input) + if foundKey { + return c.f.ConstructProject(input, t.Projections, input.Relational().OutputCols), true + } + + case *memo.ScanExpr: + private := t.ScanPrivate + tableID := private.Table + table := c.f.Metadata().Table(tableID) + if !table.IsVirtualTable() { + keyCols := c.PrimaryKeyCols(tableID) + private.Cols = private.Cols.Union(keyCols) + return c.f.ConstructScan(&private), true + } + + case *memo.SelectExpr: + input, foundKey := c.tryFindExistingKey(t.Input) + if foundKey { + return c.f.ConstructSelect(input, t.Filters), true + } + } + + return nil, false +} + // ---------------------------------------------------------------------- // // Property functions diff --git a/pkg/sql/opt/optbuilder/mutation_builder.go b/pkg/sql/opt/optbuilder/mutation_builder.go index 008d515ee4c..ee4953af430 100644 --- a/pkg/sql/opt/optbuilder/mutation_builder.go +++ b/pkg/sql/opt/optbuilder/mutation_builder.go @@ -1048,20 +1048,40 @@ func (mb *mutationBuilder) maybeAddRegionColLookup(op opt.Operator) { // The scan is exempt from RLS to maintain data integrity. cat.PolicyScopeExempt, ) - if !refScope.expr.Relational().FuncDeps.ColsAreLaxKey(refLookupCols) { - // The lookup columns must be a lax key, otherwise the join may return - // multiple rows for a single row in the target table. This should already - // be enforced by the foreign-key constraint. - panic(errors.AssertionFailedf( - "lookup columns using constraint %q must be a lax key", lookupFK.Name())) - } var joinFlags memo.JoinFlags if mb.b.evalCtx.SessionData().PreferLookupJoinsForFKs { joinFlags = memo.PreferLookupJoinIntoRight } + parentNonKey := !refScope.expr.Relational().FuncDeps.ColsAreLaxKey(refLookupCols) + if parentNonKey { + // The non-region foreign-key columns aren't a lax key on the parent, so + // the join could return multiple rows per child input row. We will use an + // unordered DistinctOn to select an arbitrary matching parent row for each + // child row. This requires a key on the child rows, so ensure it here. + mb.outScope.expr = mb.b.factory.CustomFuncs().EnsureKey(mb.outScope.expr) + } + inputCols := mb.outScope.expr.Relational().OutputCols mb.outScope.expr = mb.b.factory.ConstructLeftJoin( mb.outScope.expr, refScope.expr, joinCond, &memo.JoinPrivate{Flags: joinFlags}, ) + if parentNonKey { + // Use an unordered DistinctOn to arbitrarily select matching parent rows. + // Group on all input columns from before the join (they form a superkey + // after EnsureKey); optimizer rules can simplify the grouping columns + // using the input key. The parent's region column is the only parent + // column needed downstream, and FirstAgg picks one arbitrary value from + // the matching parent rows. + aggs := memo.AggregationsExpr{ + f.ConstructAggregationsItem( + f.ConstructFirstAgg(f.ConstructVariable(lookupRegionColID)), + lookupRegionColID, + ), + } + groupingPrivate := memo.GroupingPrivate{GroupingCols: inputCols} + mb.outScope.expr = f.ConstructDistinctOn( + mb.outScope.expr, aggs, &groupingPrivate, + ) + } // Build a CASE expression to determine the final value of the region column. // Use the looked-up value if non-NULL, and otherwise use the default value // which was already projected in the input. diff --git a/pkg/sql/opt/optbuilder/testdata/region_lookup b/pkg/sql/opt/optbuilder/testdata/region_lookup index a34faa6ba63..cce6df959ca 100644 --- a/pkg/sql/opt/optbuilder/testdata/region_lookup +++ b/pkg/sql/opt/optbuilder/testdata/region_lookup @@ -1675,3 +1675,280 @@ upsert child │ ├── column1:8 => x:35 │ └── column2:9 => y:36 └── filters (true) + +# -------------------------------------------------- +# Parent without a unique constraint on the non-region inference columns. +# The optimizer wraps the right side of the lookup join in a DistinctOn so +# at most one parent row is paired with each child input row. The parent +# here is a non-RBR table whose PK is (region, a, b), so (a, b) alone is +# not a key. +# -------------------------------------------------- + +exec-ddl +CREATE TABLE parent_nonunique ( + region STRING NOT NULL, + a INT NOT NULL, + b INT NOT NULL, + PRIMARY KEY (region, a, b), + INDEX a_b_idx (a, b) +); +---- + +exec-ddl +CREATE TABLE child_nonunique ( + x INT, + y INT, + z INT, + UNIQUE (z), + CONSTRAINT foo FOREIGN KEY (crdb_region, x, y) REFERENCES parent_nonunique (region, a, b) +) WITH (infer_rbr_region_col_using_constraint = 'foo') LOCALITY REGIONAL BY ROW; +---- + +opt locality=(region=east) format=(hide-all,show-columns) +INSERT INTO child_nonunique VALUES (1, 2, 3); +---- +insert child_nonunique + ├── columns: + ├── insert-mapping: + │ ├── column1:8 => child_nonunique.x:1 + │ ├── column2:9 => child_nonunique.y:2 + │ ├── column3:10 => child_nonunique.z:3 + │ ├── fk_lookup_crdb_region:18 => child_nonunique.crdb_region:4 + │ └── rowid_default:12 => child_nonunique.rowid:5 + ├── check columns: check1:19 + ├── input binding: &1 + ├── project + │ ├── columns: check1:19 column1:8 column2:9 column3:10 crdb_region_default:11 rowid_default:12 fk_lookup_crdb_region:18 + │ ├── project + │ │ ├── columns: fk_lookup_crdb_region:18 column1:8 column2:9 column3:10 crdb_region_default:11 rowid_default:12 + │ │ ├── limit + │ │ │ ├── columns: column1:8 column2:9 column3:10 crdb_region_default:11 rowid_default:12 region:13 a:14 b:15 + │ │ │ ├── left-join (cross) + │ │ │ │ ├── columns: column1:8 column2:9 column3:10 crdb_region_default:11 rowid_default:12 region:13 a:14 b:15 + │ │ │ │ ├── values + │ │ │ │ │ ├── columns: column1:8 column2:9 column3:10 crdb_region_default:11 rowid_default:12 + │ │ │ │ │ └── (1, 2, 3, default_to_database_primary_region('east'), unique_rowid()) + │ │ │ │ ├── scan parent_nonunique@a_b_idx + │ │ │ │ │ ├── columns: region:13 a:14 b:15 + │ │ │ │ │ ├── constraint: /14/15/13: [/1/2 - /1/2] + │ │ │ │ │ └── flags: avoid-full-scan + │ │ │ │ └── filters (true) + │ │ │ └── 1 + │ │ └── projections + │ │ └── CASE WHEN region:13 IS NOT DISTINCT FROM CAST(NULL AS STRING) THEN crdb_region_default:11 ELSE region:13 END [as=fk_lookup_crdb_region:18] + │ └── projections + │ └── fk_lookup_crdb_region:18 IN ('central', 'east', 'west') [as=check1:19] + ├── unique-checks + │ ├── unique-checks-item: child_nonunique(rowid) + │ │ └── project + │ │ ├── columns: rowid:31 + │ │ └── semi-join (lookup child_nonunique) + │ │ ├── columns: crdb_region:30 rowid:31 + │ │ ├── lookup expression + │ │ │ └── filters + │ │ │ ├── child_nonunique.crdb_region:23 IN ('central', 'east', 'west') + │ │ │ └── rowid:31 = child_nonunique.rowid:24 + │ │ ├── with-scan &1 + │ │ │ ├── columns: crdb_region:30 rowid:31 + │ │ │ └── mapping: + │ │ │ ├── fk_lookup_crdb_region:18 => crdb_region:30 + │ │ │ └── rowid_default:12 => rowid:31 + │ │ └── filters + │ │ └── crdb_region:30 != child_nonunique.crdb_region:23 + │ └── unique-checks-item: child_nonunique(z) + │ └── project + │ ├── columns: z:41 + │ └── semi-join (lookup child_nonunique@child_nonunique_crdb_region_z_key) + │ ├── columns: z:41 crdb_region:42 rowid:43 + │ ├── lookup expression + │ │ └── filters + │ │ ├── child_nonunique.crdb_region:35 IN ('central', 'east', 'west') + │ │ └── z:41 = child_nonunique.z:34 + │ ├── with-scan &1 + │ │ ├── columns: z:41 crdb_region:42 rowid:43 + │ │ └── mapping: + │ │ ├── column3:10 => z:41 + │ │ ├── fk_lookup_crdb_region:18 => crdb_region:42 + │ │ └── rowid_default:12 => rowid:43 + │ └── filters + │ └── (crdb_region:42 != child_nonunique.crdb_region:35) OR (rowid:43 != child_nonunique.rowid:36) + └── f-k-checks + └── f-k-checks-item: child_nonunique(crdb_region,x,y) -> parent_nonunique(region,a,b) + └── anti-join (lookup parent_nonunique) + ├── columns: crdb_region:44 x:45 y:46 + ├── key columns: [44 45 46] = [47 48 49] + ├── lookup columns are key + ├── with-scan &1 + │ ├── columns: crdb_region:44 x:45 y:46 + │ └── mapping: + │ ├── fk_lookup_crdb_region:18 => crdb_region:44 + │ ├── column1:8 => x:45 + │ └── column2:9 => y:46 + └── filters (true) + +# A multi-row INSERT exercises the non-constant path, where the DistinctOn +# explicitly appears in the plan. +opt locality=(region=east) format=(hide-all,show-columns) +INSERT INTO child_nonunique VALUES (1, 2, 3), (4, 5, 6); +---- +insert child_nonunique + ├── columns: + ├── insert-mapping: + │ ├── column1:8 => child_nonunique.x:1 + │ ├── column2:9 => child_nonunique.y:2 + │ ├── column3:10 => child_nonunique.z:3 + │ ├── fk_lookup_crdb_region:19 => child_nonunique.crdb_region:4 + │ └── rowid_default:12 => child_nonunique.rowid:5 + ├── check columns: check1:20 + ├── input binding: &1 + ├── project + │ ├── columns: check1:20 column1:8 column2:9 column3:10 crdb_region_default:11 rowid_default:12 fk_lookup_crdb_region:19 + │ ├── project + │ │ ├── columns: fk_lookup_crdb_region:19 column1:8 column2:9 column3:10 crdb_region_default:11 rowid_default:12 + │ │ ├── distinct-on + │ │ │ ├── columns: column1:8 column2:9 column3:10 crdb_region_default:11 rowid_default:12 region:13 rownum:18 + │ │ │ ├── grouping columns: rownum:18 + │ │ │ ├── left-join (lookup parent_nonunique@a_b_idx) + │ │ │ │ ├── columns: column1:8 column2:9 column3:10 crdb_region_default:11 rowid_default:12 region:13 a:14 b:15 rownum:18 + │ │ │ │ ├── key columns: [8 9] = [14 15] + │ │ │ │ ├── ordinality + │ │ │ │ │ ├── columns: column1:8 column2:9 column3:10 crdb_region_default:11 rowid_default:12 rownum:18 + │ │ │ │ │ └── project + │ │ │ │ │ ├── columns: crdb_region_default:11 rowid_default:12 column1:8 column2:9 column3:10 + │ │ │ │ │ ├── values + │ │ │ │ │ │ ├── columns: column1:8 column2:9 column3:10 + │ │ │ │ │ │ ├── (1, 2, 3) + │ │ │ │ │ │ └── (4, 5, 6) + │ │ │ │ │ └── projections + │ │ │ │ │ ├── default_to_database_primary_region('east') [as=crdb_region_default:11] + │ │ │ │ │ └── unique_rowid() [as=rowid_default:12] + │ │ │ │ └── filters (true) + │ │ │ └── aggregations + │ │ │ ├── first-agg [as=region:13] + │ │ │ │ └── region:13 + │ │ │ ├── const-agg [as=column1:8] + │ │ │ │ └── column1:8 + │ │ │ ├── const-agg [as=column2:9] + │ │ │ │ └── column2:9 + │ │ │ ├── const-agg [as=column3:10] + │ │ │ │ └── column3:10 + │ │ │ ├── const-agg [as=crdb_region_default:11] + │ │ │ │ └── crdb_region_default:11 + │ │ │ └── const-agg [as=rowid_default:12] + │ │ │ └── rowid_default:12 + │ │ └── projections + │ │ └── CASE WHEN region:13 IS NOT DISTINCT FROM CAST(NULL AS STRING) THEN crdb_region_default:11 ELSE region:13 END [as=fk_lookup_crdb_region:19] + │ └── projections + │ └── fk_lookup_crdb_region:19 IN ('central', 'east', 'west') [as=check1:20] + ├── unique-checks + │ ├── unique-checks-item: child_nonunique(rowid) + │ │ └── project + │ │ ├── columns: rowid:32 + │ │ └── semi-join (lookup child_nonunique) + │ │ ├── columns: crdb_region:31 rowid:32 + │ │ ├── lookup expression + │ │ │ └── filters + │ │ │ ├── child_nonunique.crdb_region:24 IN ('central', 'east', 'west') + │ │ │ └── rowid:32 = child_nonunique.rowid:25 + │ │ ├── with-scan &1 + │ │ │ ├── columns: crdb_region:31 rowid:32 + │ │ │ └── mapping: + │ │ │ ├── fk_lookup_crdb_region:19 => crdb_region:31 + │ │ │ └── rowid_default:12 => rowid:32 + │ │ └── filters + │ │ └── crdb_region:31 != child_nonunique.crdb_region:24 + │ └── unique-checks-item: child_nonunique(z) + │ └── project + │ ├── columns: z:42 + │ └── semi-join (lookup child_nonunique@child_nonunique_crdb_region_z_key) + │ ├── columns: z:42 crdb_region:43 rowid:44 + │ ├── lookup expression + │ │ └── filters + │ │ ├── child_nonunique.crdb_region:36 IN ('central', 'east', 'west') + │ │ └── z:42 = child_nonunique.z:35 + │ ├── with-scan &1 + │ │ ├── columns: z:42 crdb_region:43 rowid:44 + │ │ └── mapping: + │ │ ├── column3:10 => z:42 + │ │ ├── fk_lookup_crdb_region:19 => crdb_region:43 + │ │ └── rowid_default:12 => rowid:44 + │ └── filters + │ └── (crdb_region:43 != child_nonunique.crdb_region:36) OR (rowid:44 != child_nonunique.rowid:37) + └── f-k-checks + └── f-k-checks-item: child_nonunique(crdb_region,x,y) -> parent_nonunique(region,a,b) + └── anti-join (lookup parent_nonunique) + ├── columns: crdb_region:45 x:46 y:47 + ├── key columns: [45 46 47] = [48 49 50] + ├── lookup columns are key + ├── with-scan &1 + │ ├── columns: crdb_region:45 x:46 y:47 + │ └── mapping: + │ ├── fk_lookup_crdb_region:19 => crdb_region:45 + │ ├── column1:8 => x:46 + │ └── column2:9 => y:47 + └── filters (true) + +opt locality=(region=east) format=(hide-all,show-columns) +UPDATE child_nonunique SET y = 100 WHERE z = 7; +---- +update child_nonunique + ├── columns: + ├── fetch columns: child_nonunique.x:8 child_nonunique.y:9 z:10 child_nonunique.crdb_region:11 rowid:12 + ├── update-mapping: + │ ├── y_new:15 => child_nonunique.y:2 + │ └── fk_lookup_crdb_region:21 => child_nonunique.crdb_region:4 + ├── check columns: check1:22 + ├── input binding: &1 + ├── project + │ ├── columns: check1:22 child_nonunique.x:8 child_nonunique.y:9 z:10 child_nonunique.crdb_region:11 rowid:12 y_new:15 fk_lookup_crdb_region:21 + │ ├── project + │ │ ├── columns: fk_lookup_crdb_region:21 y_new:15 child_nonunique.x:8 child_nonunique.y:9 z:10 child_nonunique.crdb_region:11 rowid:12 + │ │ ├── limit + │ │ │ ├── columns: child_nonunique.x:8 child_nonunique.y:9 z:10 child_nonunique.crdb_region:11 rowid:12 region:16 a:17 b:18 + │ │ │ ├── left-join (lookup parent_nonunique@a_b_idx) + │ │ │ │ ├── columns: child_nonunique.x:8 child_nonunique.y:9 z:10 child_nonunique.crdb_region:11 rowid:12 region:16 a:17 b:18 + │ │ │ │ ├── key columns: [8 45] = [17 18] + │ │ │ │ ├── project + │ │ │ │ │ ├── columns: "lookup_join_const_col_@18":45 child_nonunique.x:8 child_nonunique.y:9 z:10 child_nonunique.crdb_region:11 rowid:12 + │ │ │ │ │ ├── index-join child_nonunique + │ │ │ │ │ │ ├── columns: child_nonunique.x:8 child_nonunique.y:9 z:10 child_nonunique.crdb_region:11 rowid:12 + │ │ │ │ │ │ └── locality-optimized-search + │ │ │ │ │ │ ├── columns: z:10 child_nonunique.crdb_region:11 rowid:12 + │ │ │ │ │ │ ├── left columns: z:33 child_nonunique.crdb_region:34 rowid:35 + │ │ │ │ │ │ ├── right columns: z:40 child_nonunique.crdb_region:41 rowid:42 + │ │ │ │ │ │ ├── scan child_nonunique@child_nonunique_crdb_region_z_key + │ │ │ │ │ │ │ ├── columns: z:33 child_nonunique.crdb_region:34 rowid:35 + │ │ │ │ │ │ │ ├── constraint: /34/33: [/'east'/7 - /'east'/7] + │ │ │ │ │ │ │ └── flags: avoid-full-scan + │ │ │ │ │ │ └── scan child_nonunique@child_nonunique_crdb_region_z_key + │ │ │ │ │ │ ├── columns: z:40 child_nonunique.crdb_region:41 rowid:42 + │ │ │ │ │ │ ├── constraint: /41/40 + │ │ │ │ │ │ │ ├── [/'central'/7 - /'central'/7] + │ │ │ │ │ │ │ └── [/'west'/7 - /'west'/7] + │ │ │ │ │ │ └── flags: avoid-full-scan + │ │ │ │ │ └── projections + │ │ │ │ │ └── 100 [as="lookup_join_const_col_@18":45] + │ │ │ │ └── filters (true) + │ │ │ └── 1 + │ │ └── projections + │ │ ├── CASE WHEN region:16 IS NOT DISTINCT FROM CAST(NULL AS STRING) THEN child_nonunique.crdb_region:11 ELSE region:16 END [as=fk_lookup_crdb_region:21] + │ │ └── 100 [as=y_new:15] + │ └── projections + │ └── fk_lookup_crdb_region:21 IN ('central', 'east', 'west') [as=check1:22] + └── f-k-checks + └── f-k-checks-item: child_nonunique(crdb_region,x,y) -> parent_nonunique(region,a,b) + └── anti-join (lookup parent_nonunique) + ├── columns: crdb_region:23 x:24 y:25 + ├── key columns: [23 24 25] = [26 27 28] + ├── lookup columns are key + ├── select + │ ├── columns: crdb_region:23 x:24 y:25 + │ ├── with-scan &1 + │ │ ├── columns: crdb_region:23 x:24 y:25 + │ │ └── mapping: + │ │ ├── fk_lookup_crdb_region:21 => crdb_region:23 + │ │ ├── child_nonunique.x:8 => x:24 + │ │ └── y_new:15 => y:25 + │ └── filters + │ └── x:24 IS NOT NULL + └── filters (true)