From 6bcf1218eb8f20c9d7906c315915bc1eac8c4dee Mon Sep 17 00:00:00 2001 From: Rebecca Taft Date: Fri, 2 Apr 2021 16:10:02 -0500 Subject: [PATCH 1/3] opt: fix a few omissions for locality optimized search This commit fixes a few omissions where locality optimized search was not included when it should have been. These omissions only have a small impact on the cost so they were not noticeable. Release note: None --- pkg/sql/opt/ops/relational.opt | 7 ++++--- pkg/sql/opt/xform/coster.go | 8 ++++---- pkg/sql/opt/xform/physical_props.go | 2 +- pkg/sql/opt/xform/testdata/coster/zone | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/pkg/sql/opt/ops/relational.opt b/pkg/sql/opt/ops/relational.opt index 498feb8c582a..4896068e1f8e 100644 --- a/pkg/sql/opt/ops/relational.opt +++ b/pkg/sql/opt/ops/relational.opt @@ -765,9 +765,10 @@ define Union { } # SetPrivate contains fields used by the relational set operators: Union, -# Intersect, Except, UnionAll, IntersectAll and ExceptAll. It matches columns -# from the left and right inputs of the operator with the output columns, since -# OutputCols are not ordered and may not correspond to each other. +# Intersect, Except, UnionAll, IntersectAll, ExceptAll, and +# LocalityOptimizedSearch. It matches columns from the left and right inputs of +# the operator with the output columns, since OutputCols are not ordered and may +# not correspond to each other. # # For example, consider the following query: # SELECT y, x FROM xy UNION SELECT b, a FROM ab diff --git a/pkg/sql/opt/xform/coster.go b/pkg/sql/opt/xform/coster.go index 7637f8ef87d9..9253ed0661cc 100644 --- a/pkg/sql/opt/xform/coster.go +++ b/pkg/sql/opt/xform/coster.go @@ -1010,10 +1010,10 @@ func (c *coster) computeSetCost(set memo.RelExpr) memo.Cost { // Add the CPU cost of emitting the rows. cost := memo.Cost(set.Relational().Stats.RowCount) * cpuCostFactor - // A set operation must process every row from both tables once. - // UnionAll can avoid any extra computation, but all other set operations - // must perform a hash table lookup or update for each input row. - if set.Op() != opt.UnionAllOp { + // A set operation must process every row from both tables once. UnionAll and + // LocalityOptimizedSearch can avoid any extra computation, but all other set + // operations must perform a hash table lookup or update for each input row. + if set.Op() != opt.UnionAllOp && set.Op() != opt.LocalityOptimizedSearchOp { leftRowCount := set.Child(0).(memo.RelExpr).Relational().Stats.RowCount rightRowCount := set.Child(1).(memo.RelExpr).Relational().Stats.RowCount cost += memo.Cost(leftRowCount+rightRowCount) * cpuCostFactor diff --git a/pkg/sql/opt/xform/physical_props.go b/pkg/sql/opt/xform/physical_props.go index 310499e3d407..0379bd3508dd 100644 --- a/pkg/sql/opt/xform/physical_props.go +++ b/pkg/sql/opt/xform/physical_props.go @@ -103,7 +103,7 @@ func BuildChildPhysicalProps( childProps.LimitHint = parentProps.LimitHint case opt.ExceptOp, opt.ExceptAllOp, opt.IntersectOp, opt.IntersectAllOp, - opt.UnionOp, opt.UnionAllOp: + opt.UnionOp, opt.UnionAllOp, opt.LocalityOptimizedSearchOp: // TODO(celine): Set operation limits need further thought; for example, // the right child of an ExceptOp should not be limited. childProps.LimitHint = parentProps.LimitHint diff --git a/pkg/sql/opt/xform/testdata/coster/zone b/pkg/sql/opt/xform/testdata/coster/zone index b34fb6ad5d9f..b17597509361 100644 --- a/pkg/sql/opt/xform/testdata/coster/zone +++ b/pkg/sql/opt/xform/testdata/coster/zone @@ -715,7 +715,7 @@ locality-optimized-search ├── right columns: t.public.abc_part.r:11(string) t.public.abc_part.a:12(int) t.public.abc_part.b:13(int) t.public.abc_part.c:14(string) ├── cardinality: [0 - 1] ├── stats: [rows=0.910000001, distinct(3)=0.910000001, null(3)=0, distinct(4)=0.910000001, null(4)=0, distinct(3,4)=0.910000001, null(3,4)=0] - ├── cost: 5.101218 + ├── cost: 5.083216 ├── key: () ├── fd: ()-->(1-4) ├── prune: (1,2) From f27a41b8fb19f42c144cec1cc550186c17a02c45 Mon Sep 17 00:00:00 2001 From: Rebecca Taft Date: Fri, 2 Apr 2021 16:15:41 -0500 Subject: [PATCH 2/3] opt: refactor and move some functions for locality optimized search This commit refactors some functions previously in xform/scan_funcs.go for GenerateLocalityOptimizedScan and moves them to xform/general_funcs.go so they can be used to generate locality optimized anti joins in a future commit. Release note: None --- pkg/sql/opt/xform/BUILD.bazel | 2 +- pkg/sql/opt/xform/general_funcs.go | 151 ++++++++++++++++++ ...an_funcs_test.go => general_funcs_test.go} | 0 pkg/sql/opt/xform/scan_funcs.go | 144 +---------------- 4 files changed, 153 insertions(+), 144 deletions(-) rename pkg/sql/opt/xform/{scan_funcs_test.go => general_funcs_test.go} (100%) diff --git a/pkg/sql/opt/xform/BUILD.bazel b/pkg/sql/opt/xform/BUILD.bazel index e99ae87f1a0b..a0e85ce626d1 100644 --- a/pkg/sql/opt/xform/BUILD.bazel +++ b/pkg/sql/opt/xform/BUILD.bazel @@ -54,11 +54,11 @@ go_test( size = "small", srcs = [ "coster_test.go", + "general_funcs_test.go", "join_order_builder_test.go", "main_test.go", "optimizer_test.go", "physical_props_test.go", - "scan_funcs_test.go", ], data = glob(["testdata/**"]) + [ "@cockroach//c-deps:libgeos", diff --git a/pkg/sql/opt/xform/general_funcs.go b/pkg/sql/opt/xform/general_funcs.go index 52ea25fff4f4..8edbd7e15d27 100644 --- a/pkg/sql/opt/xform/general_funcs.go +++ b/pkg/sql/opt/xform/general_funcs.go @@ -11,13 +11,17 @@ package xform import ( + "sort" + "github.com/cockroachdb/cockroach/pkg/sql/catalog/colinfo" "github.com/cockroachdb/cockroach/pkg/sql/opt" + "github.com/cockroachdb/cockroach/pkg/sql/opt/cat" "github.com/cockroachdb/cockroach/pkg/sql/opt/idxconstraint" "github.com/cockroachdb/cockroach/pkg/sql/opt/memo" "github.com/cockroachdb/cockroach/pkg/sql/opt/norm" "github.com/cockroachdb/cockroach/pkg/sql/opt/partialidx" "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" + "github.com/cockroachdb/cockroach/pkg/util" "github.com/cockroachdb/errors" ) @@ -323,3 +327,150 @@ func (c *CustomFuncs) findConstantFilterCols( } } } + +// isZoneLocal returns true if the given zone config indicates that the replicas +// it constrains will be primarily located in the localRegion. +func isZoneLocal(zone cat.Zone, localRegion string) bool { + // First count the number of local and remote replica constraints. If all + // are local or all are remote, we can return early. + local, remote := 0, 0 + for i, n := 0, zone.ReplicaConstraintsCount(); i < n; i++ { + replicaConstraint := zone.ReplicaConstraints(i) + for j, m := 0, replicaConstraint.ConstraintCount(); j < m; j++ { + constraint := replicaConstraint.Constraint(j) + if isLocal, ok := isConstraintLocal(constraint, localRegion); ok { + if isLocal { + local++ + } else { + remote++ + } + } + } + } + if local > 0 && remote == 0 { + return true + } + if remote > 0 && local == 0 { + return false + } + + // Next check the voter replica constraints. Once again, if all are local or + // all are remote, we can return early. + local, remote = 0, 0 + for i, n := 0, zone.VoterConstraintsCount(); i < n; i++ { + replicaConstraint := zone.VoterConstraint(i) + for j, m := 0, replicaConstraint.ConstraintCount(); j < m; j++ { + constraint := replicaConstraint.Constraint(j) + if isLocal, ok := isConstraintLocal(constraint, localRegion); ok { + if isLocal { + local++ + } else { + remote++ + } + } + } + } + if local > 0 && remote == 0 { + return true + } + if remote > 0 && local == 0 { + return false + } + + // Use the lease preferences as a tie breaker. We only really care about the + // first one, since subsequent lease preferences only apply in edge cases. + if zone.LeasePreferenceCount() > 0 { + leasePref := zone.LeasePreference(0) + for i, n := 0, leasePref.ConstraintCount(); i < n; i++ { + constraint := leasePref.Constraint(i) + if isLocal, ok := isConstraintLocal(constraint, localRegion); ok { + return isLocal + } + } + } + + return false +} + +// isConstraintLocal returns isLocal=true and ok=true if the given constraint is +// a required constraint matching the given localRegion. Returns isLocal=false +// and ok=true if the given constraint is a prohibited constraint matching the +// given local region or if it is a required constraint matching a different +// region. Any other scenario returns ok=false, since this constraint gives no +// information about whether the constrained replicas are local or remote. +func isConstraintLocal(constraint cat.Constraint, localRegion string) (isLocal bool, ok bool) { + if constraint.GetKey() != regionKey { + // We only care about constraints on the region. + return false /* isLocal */, false /* ok */ + } + if constraint.GetValue() == localRegion { + if constraint.IsRequired() { + // The local region is required. + return true /* isLocal */, true /* ok */ + } + // The local region is prohibited. + return false /* isLocal */, true /* ok */ + } + if constraint.IsRequired() { + // A remote region is required. + return false /* isLocal */, true /* ok */ + } + // A remote region is prohibited, so this constraint gives no information + // about whether the constrained replicas are local or remote. + return false /* isLocal */, false /* ok */ +} + +// prefixIsLocal contains a PARTITION BY LIST prefix, and a boolean indicating +// whether the prefix is from a local partition. +type prefixIsLocal struct { + prefix tree.Datums + isLocal bool +} + +// prefixSorter sorts prefixes (which are wrapped in prefixIsLocal structs) so +// that longer prefixes are ordered first. +type prefixSorter []prefixIsLocal + +var _ sort.Interface = &prefixSorter{} + +// Len is part of sort.Interface. +func (ps prefixSorter) Len() int { + return len(ps) +} + +// Less is part of sort.Interface. +func (ps prefixSorter) Less(i, j int) bool { + return len(ps[i].prefix) > len(ps[j].prefix) +} + +// Swap is part of sort.Interface. +func (ps prefixSorter) Swap(i, j int) { + ps[i], ps[j] = ps[j], ps[i] +} + +// getSortedPrefixes collects all the prefixes from all the different partitions +// in the index (remembering which ones came from local partitions), and sorts +// them so that longer prefixes come before shorter prefixes. +func getSortedPrefixes(index cat.Index, localPartitions util.FastIntSet) []prefixIsLocal { + allPrefixes := make(prefixSorter, 0, index.PartitionCount()) + for i, n := 0, index.PartitionCount(); i < n; i++ { + part := index.Partition(i) + isLocal := localPartitions.Contains(i) + partitionPrefixes := part.PartitionByListPrefixes() + if len(partitionPrefixes) == 0 { + // This can happen when the partition value is DEFAULT. + allPrefixes = append(allPrefixes, prefixIsLocal{ + prefix: nil, + isLocal: isLocal, + }) + } + for j := range partitionPrefixes { + allPrefixes = append(allPrefixes, prefixIsLocal{ + prefix: partitionPrefixes[j], + isLocal: isLocal, + }) + } + } + sort.Sort(allPrefixes) + return allPrefixes +} diff --git a/pkg/sql/opt/xform/scan_funcs_test.go b/pkg/sql/opt/xform/general_funcs_test.go similarity index 100% rename from pkg/sql/opt/xform/scan_funcs_test.go rename to pkg/sql/opt/xform/general_funcs_test.go diff --git a/pkg/sql/opt/xform/scan_funcs.go b/pkg/sql/opt/xform/scan_funcs.go index 175a4a97ec31..0be527fdc017 100644 --- a/pkg/sql/opt/xform/scan_funcs.go +++ b/pkg/sql/opt/xform/scan_funcs.go @@ -11,13 +11,10 @@ package xform import ( - "sort" - "github.com/cockroachdb/cockroach/pkg/sql/opt" "github.com/cockroachdb/cockroach/pkg/sql/opt/cat" "github.com/cockroachdb/cockroach/pkg/sql/opt/constraint" "github.com/cockroachdb/cockroach/pkg/sql/opt/memo" - "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" "github.com/cockroachdb/cockroach/pkg/util" "github.com/cockroachdb/errors" ) @@ -214,126 +211,6 @@ func (c *CustomFuncs) GenerateLocalityOptimizedScan( c.e.mem.AddLocalityOptimizedSearchToGroup(&locOptSearch, grp) } -// isZoneLocal returns true if the given zone config indicates that the replicas -// it constrains will be primarily located in the localRegion. -func isZoneLocal(zone cat.Zone, localRegion string) bool { - // First count the number of local and remote replica constraints. If all - // are local or all are remote, we can return early. - local, remote := 0, 0 - for i, n := 0, zone.ReplicaConstraintsCount(); i < n; i++ { - replicaConstraint := zone.ReplicaConstraints(i) - for j, m := 0, replicaConstraint.ConstraintCount(); j < m; j++ { - constraint := replicaConstraint.Constraint(j) - if isLocal, ok := isConstraintLocal(constraint, localRegion); ok { - if isLocal { - local++ - } else { - remote++ - } - } - } - } - if local > 0 && remote == 0 { - return true - } - if remote > 0 && local == 0 { - return false - } - - // Next check the voter replica constraints. Once again, if all are local or - // all are remote, we can return early. - local, remote = 0, 0 - for i, n := 0, zone.VoterConstraintsCount(); i < n; i++ { - replicaConstraint := zone.VoterConstraint(i) - for j, m := 0, replicaConstraint.ConstraintCount(); j < m; j++ { - constraint := replicaConstraint.Constraint(j) - if isLocal, ok := isConstraintLocal(constraint, localRegion); ok { - if isLocal { - local++ - } else { - remote++ - } - } - } - } - if local > 0 && remote == 0 { - return true - } - if remote > 0 && local == 0 { - return false - } - - // Use the lease preferences as a tie breaker. We only really care about the - // first one, since subsequent lease preferences only apply in edge cases. - if zone.LeasePreferenceCount() > 0 { - leasePref := zone.LeasePreference(0) - for i, n := 0, leasePref.ConstraintCount(); i < n; i++ { - constraint := leasePref.Constraint(i) - if isLocal, ok := isConstraintLocal(constraint, localRegion); ok { - return isLocal - } - } - } - - return false -} - -// isConstraintLocal returns isLocal=true and ok=true if the given constraint is -// a required constraint matching the given localRegion. Returns isLocal=false -// and ok=true if the given constraint is a prohibited constraint matching the -// given local region or if it is a required constraint matching a different -// region. Any other scenario returns ok=false, since this constraint gives no -// information about whether the constrained replicas are local or remote. -func isConstraintLocal(constraint cat.Constraint, localRegion string) (isLocal bool, ok bool) { - if constraint.GetKey() != regionKey { - // We only care about constraints on the region. - return false /* isLocal */, false /* ok */ - } - if constraint.GetValue() == localRegion { - if constraint.IsRequired() { - // The local region is required. - return true /* isLocal */, true /* ok */ - } - // The local region is prohibited. - return false /* isLocal */, true /* ok */ - } - if constraint.IsRequired() { - // A remote region is required. - return false /* isLocal */, true /* ok */ - } - // A remote region is prohibited, so this constraint gives no information - // about whether the constrained replicas are local or remote. - return false /* isLocal */, false /* ok */ -} - -// prefixIsLocal contains a PARTITION BY LIST prefix, and a boolean indicating -// whether the prefix is from a local partition. -type prefixIsLocal struct { - prefix tree.Datums - isLocal bool -} - -// prefixSorter sorts prefixes (which are wrapped in prefixIsLocal structs) so -// that longer prefixes are ordered first. -type prefixSorter []prefixIsLocal - -var _ sort.Interface = &prefixSorter{} - -// Len is part of sort.Interface. -func (ps prefixSorter) Len() int { - return len(ps) -} - -// Less is part of sort.Interface. -func (ps prefixSorter) Less(i, j int) bool { - return len(ps[i].prefix) > len(ps[j].prefix) -} - -// Swap is part of sort.Interface. -func (ps prefixSorter) Swap(i, j int) { - ps[i], ps[j] = ps[j], ps[i] -} - // getLocalSpans returns the indexes of the spans from the given constraint that // target local partitions. func (c *CustomFuncs) getLocalSpans( @@ -345,26 +222,7 @@ func (c *CustomFuncs) getLocalSpans( // will iterate through the list of prefixes until we find a match, so // ordering them with longer prefixes first ensures that the correct match is // found. - allPrefixes := make(prefixSorter, 0, index.PartitionCount()) - for i, n := 0, index.PartitionCount(); i < n; i++ { - part := index.Partition(i) - isLocal := localPartitions.Contains(i) - partitionPrefixes := part.PartitionByListPrefixes() - if len(partitionPrefixes) == 0 { - // This can happen when the partition value is DEFAULT. - allPrefixes = append(allPrefixes, prefixIsLocal{ - prefix: nil, - isLocal: isLocal, - }) - } - for j := range partitionPrefixes { - allPrefixes = append(allPrefixes, prefixIsLocal{ - prefix: partitionPrefixes[j], - isLocal: isLocal, - }) - } - } - sort.Sort(allPrefixes) + allPrefixes := getSortedPrefixes(index, localPartitions) // Now iterate through the spans and determine whether each one matches // with a prefix from a local partition. From 01ea5a2fd5f1c620cdddbbfb64e8253e68b23047 Mon Sep 17 00:00:00 2001 From: Rebecca Taft Date: Fri, 2 Apr 2021 16:52:06 -0500 Subject: [PATCH 3/3] opt: support locality optimized anti join MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds a new transformation rule, GenerateLocalityOptimizedAntiJoin. GenerateLocalityOptimizedAntiJoin converts an anti join into a locality optimized anti join if possible. A locality optimized anti join is implemented as a nested pair of anti lookup joins and is designed to avoid communicating with remote nodes (relative to the gateway region) if at all possible. A locality optimized anti join can be planned under the following conditions: - The anti join can be planned as a lookup join. - The lookup join scans multiple spans in the lookup index for each input row, with some spans targeting partitions on local nodes (relative to the gateway region), and some targeting partitions on remote nodes. It is not known which span(s) will contain the matching row(s). The result of GenerateLocalityOptimizedAntiJoin will be a nested pair of anti lookup joins in which the first lookup join is an anti join targeting the local values from the original join, and the second lookup join is an anti join targeting the remote values. Because of the way anti join is defined, a row will only be returned by the first anti join if a match is *not* found locally. If a match is found, no row will be returned and therefore the second lookup join will not need to search the remote nodes. This nested pair of anti joins is logically equivalent to the original, single anti join. This is a useful optimization if there is locality of access in the workload, such that rows tend to be accessed from the region where they are located. If there is no locality of access, using a locality optimized anti join could be a slight pessimization, since rows residing in remote regions will be found slightly more slowly than they would be otherwise. For example, suppose we have a multi-region database with regions 'us-east1', 'us-west1' and 'europe-west1', and we have the following tables and query, issued from 'us-east1': CREATE TABLE parent ( p_id INT PRIMARY KEY ) LOCALITY REGIONAL BY ROW; CREATE TABLE child ( c_id INT PRIMARY KEY, c_p_id INT REFERENCES parent (p_id) ) LOCALITY REGIONAL BY ROW; SELECT * FROM child WHERE NOT EXISTS ( SELECT * FROM parent WHERE p_id = c_p_id ) AND c_id = 10; Normally, this would produce the following plan: anti-join (lookup parent) ├── lookup columns are key ├── lookup expr: (p_id = c_p_id) AND (crdb_region IN ('europe-west1', 'us-east1', 'us-west1')) ├── scan child │ └── constraint: /7/5 │ ├── [/'europe-west1'/10 - /'europe-west1'/10] │ ├── [/'us-east1'/10 - /'us-east1'/10] │ └── [/'us-west1'/10 - /'us-west1'/10] └── filters (true) but if the session setting locality_optimized_partitioned_index_scan is enabled, the optimizer will produce this plan, using locality optimized search, both for the scan of child and for the lookup join with parent. See the rule GenerateLocalityOptimizedScan for details about how the optimization is applied for scans. anti-join (lookup parent) ├── lookup columns are key ├── lookup expr: (p_id = c_p_id) AND (crdb_region IN ('europe-west1', 'us-west1')) ├── anti-join (lookup parent) │ ├── lookup columns are key │ ├── lookup expr: (p_id = c_p_id) AND (crdb_region = 'us-east1') │ ├── locality-optimized-search │ │ ├── scan child │ │ │ └── constraint: /13/11: [/'us-east1'/10 - /'us-east1'/10] │ │ └── scan child │ │ └── constraint: /18/16 │ │ ├── [/'europe-west1'/10 - /'europe-west1'/10] │ │ └── [/'us-west1'/10 - /'us-west1'/10] │ └── filters (true) └── filters (true) As long as child.c_id = 10 and the matching row in parent are both located in 'us-east1', the second plan will be much faster. But if they are located in one of the other regions, the first plan would be slightly faster. Informs #55185 Release note (performance improvement): The optimizer will now try to plan anti lookup joins using "locality optimized search". This optimization applies for anti lookup joins into REGIONAL BY ROW tables (i.e., the right side of the join is a REGIONAL BY ROW table), and if enabled, it means that the execution engine will first search locally for matching rows before searching remote nodes. If a matching row is found in a local node, remote nodes will not be searched. This optimization may improve the performance of foreign key checks when rows are inserted or updated in a table that references a foreign key in a REGIONAL BY ROW table. --- .../testdata/logic_test/regional_by_row | 275 ++++++++++ pkg/sql/opt/ops/relational.opt | 10 + pkg/sql/opt/xform/coster.go | 14 +- pkg/sql/opt/xform/join_funcs.go | 220 ++++++++ pkg/sql/opt/xform/rules/join.opt | 110 ++++ pkg/sql/opt/xform/rules/scan.opt | 3 + pkg/sql/opt/xform/testdata/coster/zone | 56 ++ pkg/sql/opt/xform/testdata/rules/join | 507 ++++++++++++++++++ 8 files changed, 1194 insertions(+), 1 deletion(-) diff --git a/pkg/ccl/logictestccl/testdata/logic_test/regional_by_row b/pkg/ccl/logictestccl/testdata/logic_test/regional_by_row index e81a2eb32fad..e6667d7a2c33 100644 --- a/pkg/ccl/logictestccl/testdata/logic_test/regional_by_row +++ b/pkg/ccl/logictestccl/testdata/logic_test/regional_by_row @@ -727,6 +727,281 @@ vectorized: true table: regional_by_row_table@primary spans: [/'ap-southeast-2'/1 - /'ap-southeast-2'/1] [/'us-east-1'/1 - /'us-east-1'/1] +# Tests using locality optimized search for lookup anti joins (including foreign +# key checks). +statement ok +CREATE TABLE parent ( + p_id INT PRIMARY KEY, + FAMILY (p_id) +) LOCALITY REGIONAL BY ROW; + +statement ok +CREATE TABLE child ( + c_id INT PRIMARY KEY, + c_p_id INT REFERENCES parent (p_id), + INDEX (c_p_id), + FAMILY (c_id, c_p_id) +) LOCALITY REGIONAL BY ROW; + +statement ok +INSERT INTO parent (crdb_region, p_id) +VALUES ('ap-southeast-2', 10), ('ca-central-1', 20), ('us-east-1', 30) + +statement ok +INSERT INTO child (crdb_region, c_id, c_p_id) +VALUES ('ap-southeast-2', 10, 10), ('ca-central-1', 20, 20), ('us-east-1', 30, 30) + +statement ok +SET locality_optimized_partitioned_index_scan = false + +# Query with locality optimized search disabled. +query T +EXPLAIN SELECT * FROM child WHERE NOT EXISTS (SELECT * FROM parent WHERE p_id = c_p_id) AND c_id = 10 +---- +distribution: full +vectorized: true +· +• lookup join (anti) +│ table: parent@primary +│ equality cols are key +│ lookup condition: (p_id = c_p_id) AND (crdb_region IN ('ap-southeast-2', 'ca-central-1', 'us-east-1')) +│ +└── • scan + missing stats + table: child@primary + spans: [/'ap-southeast-2'/10 - /'ap-southeast-2'/10] [/'ca-central-1'/10 - /'ca-central-1'/10] [/'us-east-1'/10 - /'us-east-1'/10] + +statement ok +SET tracing = on,kv,results; SELECT * FROM child WHERE NOT EXISTS (SELECT * FROM parent WHERE p_id = c_p_id) AND c_id = 10; SET tracing = off + +# All regions are scanned without the optimization. +query T +SELECT message FROM [SHOW KV TRACE FOR SESSION] WITH ORDINALITY + WHERE message LIKE 'fetched:%' OR message LIKE 'output row%' + OR message LIKE 'Scan%' + ORDER BY ordinality ASC +---- +Scan /Table/75/1/"@"/10{-/#}, /Table/75/1/"\x80"/10{-/#}, /Table/75/1/"\xc0"/10{-/#} +fetched: /child/primary/'ap-southeast-2'/10/c_p_id -> /10 +Scan /Table/74/1/"@"/10{-/#}, /Table/74/1/"\x80"/10{-/#}, /Table/74/1/"\xc0"/10{-/#} +fetched: /parent/primary/'ap-southeast-2'/10 -> NULL + +statement ok +SET locality_optimized_partitioned_index_scan = true + +# Same query with locality optimized search enabled. +query T +EXPLAIN (DISTSQL) SELECT * FROM child WHERE NOT EXISTS (SELECT * FROM parent WHERE p_id = c_p_id) AND c_id = 10 +---- +distribution: local +vectorized: true +· +• lookup join (anti) +│ table: parent@primary +│ equality cols are key +│ lookup condition: (p_id = c_p_id) AND (crdb_region IN ('ca-central-1', 'us-east-1')) +│ +└── • lookup join (anti) + │ table: parent@primary + │ equality cols are key + │ lookup condition: (p_id = c_p_id) AND (crdb_region = 'ap-southeast-2') + │ + └── • union all + │ limit: 1 + │ + ├── • scan + │ missing stats + │ table: child@primary + │ spans: [/'ap-southeast-2'/10 - /'ap-southeast-2'/10] + │ + └── • scan + missing stats + table: child@primary + spans: [/'ca-central-1'/10 - /'ca-central-1'/10] [/'us-east-1'/10 - /'us-east-1'/10] +· +Diagram: https://cockroachdb.github.io/distsqlplan/decode.html#eJy0k1Fr2zAQx9_3KY7bQ5KhEtlxoQgKLqvLXDynSwwrLKZ41tF6SyRPliEl5LuP2GGNwxLSjr35Tve3fv_T3QqrX3MUGNzfRVdhDP3rcJpMv0QDmAZR8DGBD3AzGX-G_KmYS_j6KZgE0I_HCQT3m0Lod8vKzJCy27ryoZBwCfnD5mMwgKv4Gvp5m3T4ABkqLSnOFlSh-IYOpgxLo3OqKm02qVVTEMolCs6wUGVtN-mUYa4NoVihLeycUGCSfZ_ThDJJZsiRoSSbFfPmtw25X5pikZlnZDgtM1UJGM5wNlte8BkOHX7WiYbvIVMSHND2iQwyHNdWgO8w38V0zVDX9oWkstkjoXDW7G20zqm0_h9Sf0t5Gpl7kOwFqFbaSDIkOzDp-i_ssT7T5dDtUkfForDgHGTgr-nOrS7Utjmj7jXJc0kCouAmgas4CeF2HMbIsB26naZFWv-sS_ihCwVaCej7I7gE392OoO_BJSx7Hu8JIXyHc37uDU5r5uiNRrz_ZWSzssvexa4VBste3vF2ojnvNeYmVJVaVbQ3MYfeP2VI8pHasat0bXK6MzpvrmnDcaNrEpIq2566bRCq5qhZsl2x8y9i96h41BHzffHoqNg7LvaOis_3xOn63e8AAAD__2Pcwcc= + +statement ok +SET tracing = on,kv,results; SELECT * FROM child WHERE NOT EXISTS (SELECT * FROM parent WHERE p_id = c_p_id) AND c_id = 10; SET tracing = off + +# If the row is found in the local region, the other regions are not searched. +query T +SELECT message FROM [SHOW KV TRACE FOR SESSION] WITH ORDINALITY + WHERE message LIKE 'fetched:%' OR message LIKE 'output row%' + OR message LIKE 'Scan%' + ORDER BY ordinality ASC +---- +Scan /Table/75/1/"@"/10{-/#} +fetched: /child/primary/'ap-southeast-2'/10/c_p_id -> /10 +Scan /Table/74/1/"@"/10{-/#} +fetched: /parent/primary/'ap-southeast-2'/10 -> NULL + +statement ok +SET tracing = on,kv,results; SELECT * FROM child WHERE NOT EXISTS (SELECT * FROM parent WHERE p_id = c_p_id) AND c_id = 20; SET tracing = off + +# If the row is not found in the local region, the other regions are searched in +# parallel. +query T +SELECT message FROM [SHOW KV TRACE FOR SESSION] WITH ORDINALITY + WHERE message LIKE 'fetched:%' OR message LIKE 'output row%' + OR message LIKE 'Scan%' + ORDER BY ordinality ASC +---- +Scan /Table/75/1/"@"/20{-/#} +Scan /Table/75/1/"\x80"/20{-/#}, /Table/75/1/"\xc0"/20{-/#} +fetched: /child/primary/'ca-central-1'/20/c_p_id -> /20 +Scan /Table/74/1/"@"/20{-/#} +Scan /Table/74/1/"\x80"/20{-/#}, /Table/74/1/"\xc0"/20{-/#} +fetched: /parent/primary/'ca-central-1'/20 -> NULL + +query T +EXPLAIN INSERT INTO child VALUES (1, 1) +---- +distribution: local +vectorized: true +· +• root +│ +├── • insert +│ │ into: child(c_id, c_p_id, crdb_region) +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • values +│ size: 4 columns, 1 row +│ +├── • constraint-check +│ │ +│ └── • error if rows +│ │ +│ └── • lookup join (semi) +│ │ table: child@primary +│ │ equality: (lookup_join_const_col_@12, column1) = (crdb_region,c_id) +│ │ equality cols are key +│ │ pred: column8 != crdb_region +│ │ +│ └── • cross join +│ │ estimated row count: 3 +│ │ +│ ├── • values +│ │ size: 1 column, 3 rows +│ │ +│ └── • scan buffer +│ label: buffer 1 +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • lookup join (anti) + │ table: parent@primary + │ equality cols are key + │ lookup condition: (column2 = p_id) AND (crdb_region IN ('ca-central-1', 'us-east-1')) + │ + └── • lookup join (anti) + │ table: parent@primary + │ equality cols are key + │ lookup condition: (column2 = p_id) AND (crdb_region = 'ap-southeast-2') + │ + └── • scan buffer + label: buffer 1 + +query T +EXPLAIN UPSERT INTO child VALUES (1, 1) +---- +distribution: local +vectorized: true +· +• root +│ +├── • upsert +│ │ into: child(c_id, c_p_id, crdb_region) +│ │ arbiter constraints: primary +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • render +│ │ +│ └── • cross join (left outer) +│ │ +│ ├── • values +│ │ size: 3 columns, 1 row +│ │ +│ └── • union all +│ │ limit: 1 +│ │ +│ ├── • scan +│ │ missing stats +│ │ table: child@primary +│ │ spans: [/'ap-southeast-2'/1 - /'ap-southeast-2'/1] +│ │ +│ └── • scan +│ missing stats +│ table: child@primary +│ spans: [/'ca-central-1'/1 - /'ca-central-1'/1] [/'us-east-1'/1 - /'us-east-1'/1] +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • lookup join (anti) + │ table: parent@primary + │ equality cols are key + │ lookup condition: (column2 = p_id) AND (crdb_region IN ('ca-central-1', 'us-east-1')) + │ + └── • lookup join (anti) + │ table: parent@primary + │ equality cols are key + │ lookup condition: (column2 = p_id) AND (crdb_region = 'ap-southeast-2') + │ + └── • scan buffer + label: buffer 1 + +# We don't yet support locality optimized search for semi join. +query T +EXPLAIN DELETE FROM parent WHERE p_id = 1 +---- +distribution: local +vectorized: true +· +• root +│ +├── • delete +│ │ from: parent +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • union all +│ │ limit: 1 +│ │ +│ ├── • scan +│ │ missing stats +│ │ table: parent@primary +│ │ spans: [/'ap-southeast-2'/1 - /'ap-southeast-2'/1] +│ │ +│ └── • scan +│ missing stats +│ table: parent@primary +│ spans: [/'ca-central-1'/1 - /'ca-central-1'/1] [/'us-east-1'/1 - /'us-east-1'/1] +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • lookup join (semi) + │ table: child@child_c_p_id_idx + │ equality: (lookup_join_const_col_@12, p_id) = (crdb_region,c_p_id) + │ + └── • cross join + │ + ├── • values + │ size: 1 column, 3 rows + │ + └── • scan buffer + label: buffer 1 # Tests creating a index and a unique constraint on a REGIONAL BY ROW table. statement ok diff --git a/pkg/sql/opt/ops/relational.opt b/pkg/sql/opt/ops/relational.opt index 4896068e1f8e..a5b298bb952e 100644 --- a/pkg/sql/opt/ops/relational.opt +++ b/pkg/sql/opt/ops/relational.opt @@ -372,6 +372,16 @@ define LookupJoinPrivate { # paired-joiner used for left joins. IsSecondJoinInPairedJoiner bool + # LocalityOptimized is true if this lookup join is part of a locality + # optimized search strategy, indicating that it either requires all local + # (relative to the gateway region) or all remote lookups. Currently, only + # anti joins can be locality optimized, and they are implemented with two + # nested anti lookup joins in which the first join targets local partitions + # and the second join targets remote partitions. Therefore, + # LocalityOptimized is used as a hint to the coster to reduce the cost of + # this lookup join. + LocalityOptimized bool + # ConstFilters contains the constant filters that are represented as equality # conditions on the KeyCols. These filters are needed by the statistics code to # correctly estimate selectivity. diff --git a/pkg/sql/opt/xform/coster.go b/pkg/sql/opt/xform/coster.go index 9253ed0661cc..8752581a54db 100644 --- a/pkg/sql/opt/xform/coster.go +++ b/pkg/sql/opt/xform/coster.go @@ -628,7 +628,7 @@ func (c *coster) computeScanCost(scan *memo.ScanExpr, required *physical.Require cost := baseCost + memo.Cost(rowCount)*(seqIOCostFactor+perRowCost) // If this scan is locality optimized, divide the cost in two in order to make - // the total cost of the two scans in the locality optimized plan less then + // the total cost of the two scans in the locality optimized plan less than // the cost of the single scan in the non-locality optimized plan. // TODO(rytaft): This is hacky. We should really be making this determination // based on the latency between regions. @@ -773,6 +773,7 @@ func (c *coster) computeIndexJoinCost( join.Table, cat.PrimaryIndex, memo.JoinFlags(0), + false, /* localityOptimized */ ) } @@ -791,6 +792,7 @@ func (c *coster) computeLookupJoinCost( join.Table, join.Index, join.Flags, + join.LocalityOptimized, ) } @@ -803,6 +805,7 @@ func (c *coster) computeIndexLookupJoinCost( table opt.TableID, index cat.IndexOrdinal, flags memo.JoinFlags, + localityOptimized bool, ) memo.Cost { input := join.Child(0).(memo.RelExpr) lookupCount := input.Relational().Stats.RowCount @@ -872,6 +875,15 @@ func (c *coster) computeIndexLookupJoinCost( // If we prefer a lookup join, make the cost much smaller. cost *= preferLookupJoinFactor } + + // If this lookup join is locality optimized, divide the cost by two in order to make + // the total cost of the two lookup joins in the locality optimized plan less than + // the cost of the single lookup join in the non-locality optimized plan. + // TODO(rytaft): This is hacky. We should really be making this determination + // based on the latency between regions. + if localityOptimized { + cost /= 2 + } return cost } diff --git a/pkg/sql/opt/xform/join_funcs.go b/pkg/sql/opt/xform/join_funcs.go index 8bf1dc405d6e..cba1be97fa62 100644 --- a/pkg/sql/opt/xform/join_funcs.go +++ b/pkg/sql/opt/xform/join_funcs.go @@ -1125,3 +1125,223 @@ func (c *CustomFuncs) MakeProjectionsForOuterJoin( } return result } + +// LocalAndRemoteLookupExprs is used by the GenerateLocalityOptimizedAntiJoin +// rule to hold two sets of filters: one targeting local partitions and one +// targeting remote partitions. +type LocalAndRemoteLookupExprs struct { + Local memo.FiltersExpr + Remote memo.FiltersExpr +} + +// LocalAndRemoteLookupExprsSucceeded returns true if the +// LocalAndRemoteLookupExprs is not empty. +func (c *CustomFuncs) LocalAndRemoteLookupExprsSucceeded(le LocalAndRemoteLookupExprs) bool { + return len(le.Local) != 0 && len(le.Remote) != 0 +} + +// CreateLocalityOptimizedAntiLookupJoinPrivate creates a new lookup join +// private from the given private and replaces the LookupExpr with the given +// filters. It also marks the private as locality optimized. +func (c *CustomFuncs) CreateLocalityOptimizedAntiLookupJoinPrivate( + lookupExpr memo.FiltersExpr, private *memo.LookupJoinPrivate, +) *memo.LookupJoinPrivate { + newPrivate := *private + newPrivate.LookupExpr = lookupExpr + newPrivate.LocalityOptimized = true + return &newPrivate +} + +// LocalLookupExpr extracts the Local filters expr from the given +// LocalAndRemoteLookupExprs. +func (c *CustomFuncs) LocalLookupExpr(le LocalAndRemoteLookupExprs) memo.FiltersExpr { + return le.Local +} + +// RemoteLookupExpr extracts the Remote filters expr from the given +// LocalAndRemoteLookupExprs. +func (c *CustomFuncs) RemoteLookupExpr(le LocalAndRemoteLookupExprs) memo.FiltersExpr { + return le.Remote +} + +// GetLocalityOptimizedAntiJoinLookupExprs gets the lookup expressions needed to +// build a locality optimized anti join if possible from the given lookup join +// private. See the comment above the GenerateLocalityOptimizedAntiJoin rule for +// more details. +func (c *CustomFuncs) GetLocalityOptimizedAntiJoinLookupExprs( + input memo.RelExpr, private *memo.LookupJoinPrivate, +) LocalAndRemoteLookupExprs { + // Respect the session setting LocalityOptimizedSearch. + if !c.e.evalCtx.SessionData.LocalityOptimizedSearch { + return LocalAndRemoteLookupExprs{} + } + + // Check whether this lookup join has already been locality optimized. + if private.LocalityOptimized { + return LocalAndRemoteLookupExprs{} + } + + // We can only apply this optimization to anti-joins. + if private.JoinType != opt.AntiJoinOp { + return LocalAndRemoteLookupExprs{} + } + + // This lookup join cannot not be part of a paired join. + if private.IsSecondJoinInPairedJoiner { + return LocalAndRemoteLookupExprs{} + } + + // This lookup join should have the LookupExpr filled in, indicating that one + // or more of the join filters constrain an index column to multiple constant + // values. + if private.LookupExpr == nil { + return LocalAndRemoteLookupExprs{} + } + + // The local region must be set, or we won't be able to determine which + // partitions are local. + localRegion, found := c.e.evalCtx.Locality.Find(regionKey) + if !found { + return LocalAndRemoteLookupExprs{} + } + + // There should be at least two partitions, or we won't be able to + // differentiate between local and remote partitions. + tabMeta := c.e.mem.Metadata().TableMeta(private.Table) + index := tabMeta.Table.Index(private.Index) + if index.PartitionCount() < 2 { + return LocalAndRemoteLookupExprs{} + } + + // Determine whether the index has both local and remote partitions. + var localPartitions util.FastIntSet + for i, n := 0, index.PartitionCount(); i < n; i++ { + part := index.Partition(i) + if isZoneLocal(part.Zone(), localRegion) { + localPartitions.Add(i) + } + } + if localPartitions.Len() == 0 || localPartitions.Len() == index.PartitionCount() { + // The partitions are either all local or all remote. + return LocalAndRemoteLookupExprs{} + } + + // Find a filter that constrains the first column of the index. + filterIdx, ok := c.getConstPrefixFilter(index, private.Table, private.LookupExpr) + if !ok { + return LocalAndRemoteLookupExprs{} + } + filter := private.LookupExpr[filterIdx] + + // Check whether the filter constrains the first column of the index + // to at least two constant values. We need at least two values so that one + // can target a local partition and one can target a remote partition. + col, vals, ok := filter.ScalarProps().Constraints.HasSingleColumnConstValues(c.e.evalCtx) + if !ok || len(vals) < 2 { + return LocalAndRemoteLookupExprs{} + } + + // Determine whether the values target both local and remote partitions. + localValOrds := c.getLocalValues(index, localPartitions, vals) + if localValOrds.Len() == 0 || localValOrds.Len() == len(vals) { + // The values target all local or all remote partitions. + return LocalAndRemoteLookupExprs{} + } + + // Split the values into local and remote sets. + localValues, remoteValues := c.splitValues(vals, localValOrds) + + // Copy all of the filters from the LookupExpr, and replace the filter that + // constrains the first index column with a filter targeting only local + // partitions or only remote partitions. + localExpr := make(memo.FiltersExpr, len(private.LookupExpr)) + copy(localExpr, private.LookupExpr) + localExpr[filterIdx] = c.makeConstFilter(col, localValues) + + remoteExpr := make(memo.FiltersExpr, len(private.LookupExpr)) + copy(remoteExpr, private.LookupExpr) + remoteExpr[filterIdx] = c.makeConstFilter(col, remoteValues) + + // Return the two sets of lookup expressions. They will be used to construct + // two nested anti joins. + return LocalAndRemoteLookupExprs{ + Local: localExpr, + Remote: remoteExpr, + } +} + +// getConstPrefixFilter finds the position of the filter in the given slice of +// filters that constrains the first index column to one or more constant +// values. If such a filter is found, getConstPrefixFilter returns the position +// of the filter and ok=true. Otherwise, returns ok=false. +func (c CustomFuncs) getConstPrefixFilter( + index cat.Index, table opt.TableID, filters memo.FiltersExpr, +) (pos int, ok bool) { + idxCol := table.IndexColumnID(index, 0) + for i := range filters { + props := filters[i].ScalarProps() + if !props.TightConstraints { + continue + } + if props.OuterCols.Len() != 1 { + continue + } + col := props.OuterCols.SingleColumn() + if col == idxCol { + return i, true + } + } + return 0, false +} + +// getLocalValues returns the indexes of the values in the given Datums slice +// that target local partitions. +func (c *CustomFuncs) getLocalValues( + index cat.Index, localPartitions util.FastIntSet, values tree.Datums, +) util.FastIntSet { + // Collect all the prefixes from all the different partitions (remembering + // which ones came from local partitions), and sort them so that longer + // prefixes come before shorter prefixes. For each value in the given Datums, + // we will iterate through the list of prefixes until we find a match, so + // ordering them with longer prefixes first ensures that the correct match is + // found. + allPrefixes := getSortedPrefixes(index, localPartitions) + + // TODO(rytaft): Sort the prefixes by key in addition to length, and use + // binary search here. + var localVals util.FastIntSet + for i, val := range values { + for j := range allPrefixes { + prefix := allPrefixes[j].prefix + isLocal := allPrefixes[j].isLocal + if len(prefix) > 1 { + continue + } + if val.Compare(c.e.evalCtx, prefix[0]) == 0 { + if isLocal { + localVals.Add(i) + } + break + } + } + } + return localVals +} + +// splitValues splits the given slice of Datums into local and remote slices +// by putting the Datums at positions identified by localValOrds into the local +// slice, and the remaining Datums into the remote slice. +func (c *CustomFuncs) splitValues( + values tree.Datums, localValOrds util.FastIntSet, +) (localVals, remoteVals tree.Datums) { + localVals = make(tree.Datums, 0, localValOrds.Len()) + remoteVals = make(tree.Datums, 0, len(values)-len(localVals)) + for i, val := range values { + if localValOrds.Contains(i) { + localVals = append(localVals, val) + } else { + remoteVals = append(remoteVals, val) + } + } + return localVals, remoteVals +} diff --git a/pkg/sql/opt/xform/rules/join.opt b/pkg/sql/opt/xform/rules/join.opt index 36a1c19187d0..c239c385e5c7 100644 --- a/pkg/sql/opt/xform/rules/join.opt +++ b/pkg/sql/opt/xform/rules/join.opt @@ -317,3 +317,113 @@ (MakeProjectionsForOuterJoin $canaryCol $projections) (UnionCols $passthrough (OutputCols $left)) ) + +# GenerateLocalityOptimizedAntiJoin converts an anti join into a locality +# optimized anti join if possible. A locality optimized anti join is implemented +# as a nested pair of anti lookup joins and is designed to avoid communicating +# with remote nodes (relative to the gateway region) if at all possible. +# +# A locality optimized anti join can be planned under the following conditions: +# - The anti join can be planned as a lookup join. +# - The lookup join scans multiple spans in the lookup index for each input +# row, with some spans targeting partitions on local nodes (relative to the +# gateway region), and some targeting partitions on remote nodes. It is not +# known which span(s) will contain the matching row(s). +# +# The result of GenerateLocalityOptimizedAntiJoin will be a nested pair of anti +# lookup joins in which the first lookup join is an anti join targeting the +# local values from the original join, and the second lookup join is an anti +# join targeting the remote values. Because of the way anti join is defined, a +# row will only be returned by the first anti join if a match is *not* found +# locally. If a match is found, no row will be returned and therefore the second +# lookup join will not need to search the remote nodes. This nested pair of anti +# joins is logically equivalent to the original, single anti join. +# +# This is a useful optimization if there is locality of access in the workload, +# such that rows tend to be accessed from the region where they are located. If +# there is no locality of access, using a locality optimized anti join could be +# a slight pessimization, since rows residing in remote regions will be found +# slightly more slowly than they would be otherwise. +# +# For example, suppose we have a multi-region database with regions 'us-east1', +# 'us-west1' and 'europe-west1', and we have the following tables and query, +# issued from 'us-east1': +# +# CREATE TABLE parent ( +# p_id INT PRIMARY KEY +# ) LOCALITY REGIONAL BY ROW; +# +# CREATE TABLE child ( +# c_id INT PRIMARY KEY, +# c_p_id INT REFERENCES parent (p_id) +# ) LOCALITY REGIONAL BY ROW; +# +# SELECT * FROM child WHERE NOT EXISTS ( +# SELECT * FROM parent WHERE p_id = c_p_id +# ) AND c_id = 10; +# +# Normally, this would produce the following plan: +# +# anti-join (lookup parent) +# ├── lookup columns are key +# ├── lookup expr: (p_id = c_p_id) AND (crdb_region IN ('europe-west1', 'us-east1', 'us-west1')) +# ├── scan child +# │ └── constraint: /7/5 +# │ ├── [/'europe-west1'/10 - /'europe-west1'/10] +# │ ├── [/'us-east1'/10 - /'us-east1'/10] +# │ └── [/'us-west1'/10 - /'us-west1'/10] +# └── filters (true) +# +# but if the session setting locality_optimized_partitioned_index_scan is enabled, +# the optimizer will produce this plan, using locality optimized search, both for +# the scan of child and for the lookup join with parent. See the rule +# GenerateLocalityOptimizedScan for details about how the optimization is applied +# for scans. +# +# anti-join (lookup parent) +# ├── lookup columns are key +# ├── lookup expr: (p_id = c_p_id) AND (crdb_region IN ('europe-west1', 'us-west1')) +# ├── anti-join (lookup parent) +# │ ├── lookup columns are key +# │ ├── lookup expr: (p_id = c_p_id) AND (crdb_region = 'us-east1') +# │ ├── locality-optimized-search +# │ │ ├── scan child +# │ │ │ └── constraint: /13/11: [/'us-east1'/10 - /'us-east1'/10] +# │ │ └── scan child +# │ │ └── constraint: /18/16 +# │ │ ├── [/'europe-west1'/10 - /'europe-west1'/10] +# │ │ └── [/'us-west1'/10 - /'us-west1'/10] +# │ └── filters (true) +# └── filters (true) +# +# As long as child.c_id = 10 and the matching row in parent are both located in +# 'us-east1', the second plan will be much faster. But if they are located in +# one of the other regions, the first plan would be slightly faster. +[GenerateLocalityOptimizedAntiJoin, Explore] +(LookupJoin + $input:* + $on:* + $private:* & + (LocalAndRemoteLookupExprsSucceeded + $localAndRemoteLookupExprs:(GetLocalityOptimizedAntiJoinLookupExprs + $input + $private + ) + ) +) +=> +(LookupJoin + (LookupJoin + $input + $on + (CreateLocalityOptimizedAntiLookupJoinPrivate + (LocalLookupExpr $localAndRemoteLookupExprs) + $private + ) + ) + $on + (CreateLocalityOptimizedAntiLookupJoinPrivate + (RemoteLookupExpr $localAndRemoteLookupExprs) + $private + ) +) diff --git a/pkg/sql/opt/xform/rules/scan.opt b/pkg/sql/opt/xform/rules/scan.opt index 5742b9ff3651..d945fcd01421 100644 --- a/pkg/sql/opt/xform/rules/scan.opt +++ b/pkg/sql/opt/xform/rules/scan.opt @@ -66,6 +66,9 @@ # As long as k = 10 is located in 'us-east1', the second plan will be much faster. # But if k = 10 is located in one of the other regions, the first plan would be # slightly faster. +# +# Note: we also apply a similar optimization for anti lookup joins; see +# GenerateLocalityOptimizedAntiJoin. [GenerateLocalityOptimizedScan, Explore] (Scan $scanPrivate:* & diff --git a/pkg/sql/opt/xform/testdata/coster/zone b/pkg/sql/opt/xform/testdata/coster/zone index b17597509361..dccdccd216f4 100644 --- a/pkg/sql/opt/xform/testdata/coster/zone +++ b/pkg/sql/opt/xform/testdata/coster/zone @@ -740,3 +740,59 @@ locality-optimized-search ├── fd: ()-->(11-14) ├── prune: (11-14) └── interesting orderings: (+12) (+11,+13,+14,+12) (+11,+13,+12) + +# We should prefer locality optimized anti join (a pair of nested anti joins). +opt locality=(region=east,dc=a) +SELECT * FROM abc_part AS a1 WHERE NOT EXISTS ( + SELECT * FROM abc_part AS a2 WHERE a1.a = a2.b +) AND b = 1 AND c = 'foo' +---- +anti-join (lookup abc_part@bc_idx [as=a2]) + ├── columns: r:1!null a:2!null b:3!null c:4!null + ├── lookup expression + │ └── filters + │ ├── a1.a:2 = a2.b:8 [outer=(2,8), constraints=(/2: (/NULL - ]; /8: (/NULL - ]), fd=(2)==(8), (8)==(2)] + │ └── a2.r:6 = 'west' [outer=(6), constraints=(/6: [/'west' - /'west']; tight), fd=()-->(6)] + ├── cardinality: [0 - 1] + ├── stats: [rows=1e-10] + ├── cost: 23.4031483 + ├── key: () + ├── fd: ()-->(1-4) + ├── anti-join (lookup abc_part@bc_idx [as=a2]) + │ ├── columns: a1.r:1!null a1.a:2!null a1.b:3!null a1.c:4!null + │ ├── lookup expression + │ │ └── filters + │ │ ├── a1.a:2 = a2.b:8 [outer=(2,8), constraints=(/2: (/NULL - ]; /8: (/NULL - ]), fd=(2)==(8), (8)==(2)] + │ │ └── a2.r:6 = 'east' [outer=(6), constraints=(/6: [/'east' - /'east']; tight), fd=()-->(6)] + │ ├── cardinality: [0 - 1] + │ ├── stats: [rows=0.900900001, distinct(1)=0.89738934, null(1)=0, distinct(2)=0.900900001, null(2)=0, distinct(3)=0.900900001, null(3)=0, distinct(4)=0.900900001, null(4)=0] + │ ├── cost: 14.2891619 + │ ├── key: () + │ ├── fd: ()-->(1-4) + │ ├── locality-optimized-search + │ │ ├── columns: a1.r:1!null a1.a:2!null a1.b:3!null a1.c:4!null + │ │ ├── left columns: a1.r:11 a1.a:12 a1.b:13 a1.c:14 + │ │ ├── right columns: a1.r:16 a1.a:17 a1.b:18 a1.c:19 + │ │ ├── cardinality: [0 - 1] + │ │ ├── stats: [rows=0.910000001, distinct(1)=0.906282579, null(1)=0, distinct(2)=0.910000001, null(2)=0, distinct(3)=0.910000001, null(3)=0, distinct(4)=0.910000001, null(4)=0, distinct(3,4)=0.910000001, null(3,4)=0] + │ │ ├── cost: 5.083216 + │ │ ├── key: () + │ │ ├── fd: ()-->(1-4) + │ │ ├── scan abc_part@bc_idx [as=a1] + │ │ │ ├── columns: a1.r:11!null a1.a:12!null a1.b:13!null a1.c:14!null + │ │ │ ├── constraint: /11/13/14: [/'east'/1/'foo' - /'east'/1/'foo'] + │ │ │ ├── cardinality: [0 - 1] + │ │ │ ├── stats: [rows=0.9001, distinct(11)=0.9001, null(11)=0, distinct(13)=0.9001, null(13)=0, distinct(14)=0.9001, null(14)=0, distinct(11,13,14)=0.9001, null(11,13,14)=0] + │ │ │ ├── cost: 2.532058 + │ │ │ ├── key: () + │ │ │ └── fd: ()-->(11-14) + │ │ └── scan abc_part@bc_idx [as=a1] + │ │ ├── columns: a1.r:16!null a1.a:17!null a1.b:18!null a1.c:19!null + │ │ ├── constraint: /16/18/19: [/'west'/1/'foo' - /'west'/1/'foo'] + │ │ ├── cardinality: [0 - 1] + │ │ ├── stats: [rows=0.9001, distinct(16)=0.9001, null(16)=0, distinct(18)=0.9001, null(18)=0, distinct(19)=0.9001, null(19)=0, distinct(16,18,19)=0.9001, null(16,18,19)=0] + │ │ ├── cost: 2.532058 + │ │ ├── key: () + │ │ └── fd: ()-->(16-19) + │ └── filters (true) + └── filters (true) diff --git a/pkg/sql/opt/xform/testdata/rules/join b/pkg/sql/opt/xform/testdata/rules/join index 7bd71a100c47..e13cdd634bb4 100644 --- a/pkg/sql/opt/xform/testdata/rules/join +++ b/pkg/sql/opt/xform/testdata/rules/join @@ -7842,3 +7842,510 @@ right-join (hash) │ └── columns: m:1 n:2 └── filters └── p:5 = m:1 [outer=(1,5), constraints=(/1: (/NULL - ]; /5: (/NULL - ]), fd=(1)==(5), (5)==(1)] + +# -------------------------------------------------- +# GenerateLocalityOptimizedAntiJoin +# -------------------------------------------------- + +# These tables mimic REGIONAL BY ROW tables. +exec-ddl +CREATE TABLE abc_part ( + r STRING NOT NULL CHECK (r IN ('east', 'west', 'central')), + a INT NOT NULL, + b INT, + c INT, + PRIMARY KEY (r, a), + UNIQUE WITHOUT INDEX (a), + UNIQUE WITHOUT INDEX (b), + UNIQUE INDEX b_idx (r, b) PARTITION BY LIST (r) ( + PARTITION east VALUES IN (('east')), + PARTITION west VALUES IN (('west')), + PARTITION central VALUES IN (('central')) + ), + INDEX c_idx (r, c) PARTITION BY LIST (r) ( + PARTITION east VALUES IN (('east')), + PARTITION west VALUES IN (('west')), + PARTITION central VALUES IN (('central')) + ) +) PARTITION BY LIST (r) ( + PARTITION east VALUES IN (('east')), + PARTITION west VALUES IN (('west')), + PARTITION central VALUES IN (('central')) +) +---- + +exec-ddl +ALTER PARTITION "east" OF INDEX abc_part@primary CONFIGURE ZONE USING + num_voters = 5, + voter_constraints = '{+region=east: 2}', + lease_preferences = '[[+region=east]]' +---- + +exec-ddl +ALTER PARTITION "west" OF INDEX abc_part@primary CONFIGURE ZONE USING + num_voters = 5, + voter_constraints = '{+region=west: 2}', + lease_preferences = '[[+region=west]]'; +---- + +exec-ddl +ALTER PARTITION "central" OF INDEX abc_part@primary CONFIGURE ZONE USING + num_voters = 5, + voter_constraints = '{+region=central: 2}', + lease_preferences = '[[+region=central]]'; +---- + +exec-ddl +ALTER PARTITION "east" OF INDEX abc_part@b_idx CONFIGURE ZONE USING + num_voters = 5, + voter_constraints = '{+region=east: 2}', + lease_preferences = '[[+region=east]]' +---- + +exec-ddl +ALTER PARTITION "west" OF INDEX abc_part@b_idx CONFIGURE ZONE USING + num_voters = 5, + voter_constraints = '{+region=west: 2}', + lease_preferences = '[[+region=west]]'; +---- + +exec-ddl +ALTER PARTITION "central" OF INDEX abc_part@b_idx CONFIGURE ZONE USING + num_voters = 5, + voter_constraints = '{+region=central: 2}', + lease_preferences = '[[+region=central]]'; +---- + +exec-ddl +ALTER PARTITION "east" OF INDEX abc_part@c_idx CONFIGURE ZONE USING + num_voters = 5, + voter_constraints = '{+region=east: 2}', + lease_preferences = '[[+region=east]]' +---- + +exec-ddl +ALTER PARTITION "west" OF INDEX abc_part@c_idx CONFIGURE ZONE USING + num_voters = 5, + voter_constraints = '{+region=west: 2}', + lease_preferences = '[[+region=west]]'; +---- + +exec-ddl +ALTER PARTITION "central" OF INDEX abc_part@c_idx CONFIGURE ZONE USING + num_voters = 5, + voter_constraints = '{+region=central: 2}', + lease_preferences = '[[+region=central]]'; +---- + +exec-ddl +CREATE TABLE def_part ( + r STRING NOT NULL CHECK (r IN ('east', 'west', 'central')), + d INT NOT NULL, + e INT REFERENCES abc_part (a), + f INT REFERENCES abc_part (b), + PRIMARY KEY (r, d), + UNIQUE WITHOUT INDEX (d), + UNIQUE WITHOUT INDEX (e), + UNIQUE INDEX e_idx (r, e) PARTITION BY LIST (r) ( + PARTITION east VALUES IN (('east')), + PARTITION west VALUES IN (('west')), + PARTITION central VALUES IN (('central')) + ), + INDEX f_idx (r, f) PARTITION BY LIST (r) ( + PARTITION east VALUES IN (('east')), + PARTITION west VALUES IN (('west')), + PARTITION central VALUES IN (('central')) + ) +) PARTITION BY LIST (r) ( + PARTITION east VALUES IN (('east')), + PARTITION west VALUES IN (('west')), + PARTITION central VALUES IN (('central')) +) +---- + +exec-ddl +ALTER PARTITION "east" OF INDEX def_part@primary CONFIGURE ZONE USING + num_voters = 5, + voter_constraints = '{+region=east: 2}', + lease_preferences = '[[+region=east]]' +---- + +exec-ddl +ALTER PARTITION "west" OF INDEX def_part@primary CONFIGURE ZONE USING + num_voters = 5, + voter_constraints = '{+region=west: 2}', + lease_preferences = '[[+region=west]]'; +---- + +exec-ddl +ALTER PARTITION "central" OF INDEX def_part@primary CONFIGURE ZONE USING + num_voters = 5, + voter_constraints = '{+region=central: 2}', + lease_preferences = '[[+region=central]]'; +---- + +exec-ddl +ALTER PARTITION "east" OF INDEX def_part@e_idx CONFIGURE ZONE USING + num_voters = 5, + voter_constraints = '{+region=east: 2}', + lease_preferences = '[[+region=east]]' +---- + +exec-ddl +ALTER PARTITION "west" OF INDEX def_part@e_idx CONFIGURE ZONE USING + num_voters = 5, + voter_constraints = '{+region=west: 2}', + lease_preferences = '[[+region=west]]'; +---- + +exec-ddl +ALTER PARTITION "central" OF INDEX def_part@e_idx CONFIGURE ZONE USING + num_voters = 5, + voter_constraints = '{+region=central: 2}', + lease_preferences = '[[+region=central]]'; +---- + +exec-ddl +ALTER PARTITION "east" OF INDEX def_part@f_idx CONFIGURE ZONE USING + num_voters = 5, + voter_constraints = '{+region=east: 2}', + lease_preferences = '[[+region=east]]' +---- + +exec-ddl +ALTER PARTITION "west" OF INDEX def_part@f_idx CONFIGURE ZONE USING + num_voters = 5, + voter_constraints = '{+region=west: 2}', + lease_preferences = '[[+region=west]]'; +---- + +exec-ddl +ALTER PARTITION "central" OF INDEX def_part@f_idx CONFIGURE ZONE USING + num_voters = 5, + voter_constraints = '{+region=central: 2}', + lease_preferences = '[[+region=central]]'; +---- + +# Locality optimized anti join. +opt locality=(region=east) expect=GenerateLocalityOptimizedAntiJoin +SELECT * FROM def_part WHERE NOT EXISTS (SELECT * FROM abc_part WHERE e = a) AND d = 1 +---- +anti-join (lookup abc_part) + ├── columns: r:1!null d:2!null e:3 f:4 + ├── lookup expression + │ └── filters + │ ├── e:3 = a:7 [outer=(3,7), constraints=(/3: (/NULL - ]; /7: (/NULL - ]), fd=(3)==(7), (7)==(3)] + │ └── abc_part.r:6 IN ('central', 'west') [outer=(6), constraints=(/6: [/'central' - /'central'] [/'west' - /'west']; tight)] + ├── lookup columns are key + ├── cardinality: [0 - 1] + ├── key: () + ├── fd: ()-->(1-4) + ├── anti-join (lookup abc_part) + │ ├── columns: def_part.r:1!null d:2!null e:3 f:4 + │ ├── lookup expression + │ │ └── filters + │ │ ├── e:3 = a:7 [outer=(3,7), constraints=(/3: (/NULL - ]; /7: (/NULL - ]), fd=(3)==(7), (7)==(3)] + │ │ └── abc_part.r:6 = 'east' [outer=(6), constraints=(/6: [/'east' - /'east']; tight), fd=()-->(6)] + │ ├── lookup columns are key + │ ├── cardinality: [0 - 1] + │ ├── key: () + │ ├── fd: ()-->(1-4) + │ ├── locality-optimized-search + │ │ ├── columns: def_part.r:1!null d:2!null e:3 f:4 + │ │ ├── left columns: def_part.r:11 d:12 e:13 f:14 + │ │ ├── right columns: def_part.r:16 d:17 e:18 f:19 + │ │ ├── cardinality: [0 - 1] + │ │ ├── key: () + │ │ ├── fd: ()-->(1-4) + │ │ ├── scan def_part + │ │ │ ├── columns: def_part.r:11!null d:12!null e:13 f:14 + │ │ │ ├── constraint: /11/12: [/'east'/1 - /'east'/1] + │ │ │ ├── cardinality: [0 - 1] + │ │ │ ├── key: () + │ │ │ └── fd: ()-->(11-14) + │ │ └── scan def_part + │ │ ├── columns: def_part.r:16!null d:17!null e:18 f:19 + │ │ ├── constraint: /16/17 + │ │ │ ├── [/'central'/1 - /'central'/1] + │ │ │ └── [/'west'/1 - /'west'/1] + │ │ ├── cardinality: [0 - 1] + │ │ ├── key: () + │ │ └── fd: ()-->(16-19) + │ └── filters (true) + └── filters (true) + +# Locality optimized anti join in different region. +opt locality=(region=west) expect=GenerateLocalityOptimizedAntiJoin +SELECT * FROM def_part WHERE NOT EXISTS (SELECT * FROM abc_part WHERE e = a) AND d = 1 +---- +anti-join (lookup abc_part) + ├── columns: r:1!null d:2!null e:3 f:4 + ├── lookup expression + │ └── filters + │ ├── e:3 = a:7 [outer=(3,7), constraints=(/3: (/NULL - ]; /7: (/NULL - ]), fd=(3)==(7), (7)==(3)] + │ └── abc_part.r:6 IN ('central', 'east') [outer=(6), constraints=(/6: [/'central' - /'central'] [/'east' - /'east']; tight)] + ├── lookup columns are key + ├── cardinality: [0 - 1] + ├── key: () + ├── fd: ()-->(1-4) + ├── anti-join (lookup abc_part) + │ ├── columns: def_part.r:1!null d:2!null e:3 f:4 + │ ├── lookup expression + │ │ └── filters + │ │ ├── e:3 = a:7 [outer=(3,7), constraints=(/3: (/NULL - ]; /7: (/NULL - ]), fd=(3)==(7), (7)==(3)] + │ │ └── abc_part.r:6 = 'west' [outer=(6), constraints=(/6: [/'west' - /'west']; tight), fd=()-->(6)] + │ ├── lookup columns are key + │ ├── cardinality: [0 - 1] + │ ├── key: () + │ ├── fd: ()-->(1-4) + │ ├── locality-optimized-search + │ │ ├── columns: def_part.r:1!null d:2!null e:3 f:4 + │ │ ├── left columns: def_part.r:11 d:12 e:13 f:14 + │ │ ├── right columns: def_part.r:16 d:17 e:18 f:19 + │ │ ├── cardinality: [0 - 1] + │ │ ├── key: () + │ │ ├── fd: ()-->(1-4) + │ │ ├── scan def_part + │ │ │ ├── columns: def_part.r:11!null d:12!null e:13 f:14 + │ │ │ ├── constraint: /11/12: [/'west'/1 - /'west'/1] + │ │ │ ├── cardinality: [0 - 1] + │ │ │ ├── key: () + │ │ │ └── fd: ()-->(11-14) + │ │ └── scan def_part + │ │ ├── columns: def_part.r:16!null d:17!null e:18 f:19 + │ │ ├── constraint: /16/17 + │ │ │ ├── [/'central'/1 - /'central'/1] + │ │ │ └── [/'east'/1 - /'east'/1] + │ │ ├── cardinality: [0 - 1] + │ │ ├── key: () + │ │ └── fd: ()-->(16-19) + │ └── filters (true) + └── filters (true) + +# Different join condition. +opt locality=(region=east) expect=GenerateLocalityOptimizedAntiJoin +SELECT * FROM def_part WHERE NOT EXISTS (SELECT * FROM abc_part WHERE f = b) AND d = 10 +---- +anti-join (lookup abc_part@b_idx) + ├── columns: r:1!null d:2!null e:3 f:4 + ├── lookup expression + │ └── filters + │ ├── f:4 = b:8 [outer=(4,8), constraints=(/4: (/NULL - ]; /8: (/NULL - ]), fd=(4)==(8), (8)==(4)] + │ └── abc_part.r:6 IN ('central', 'west') [outer=(6), constraints=(/6: [/'central' - /'central'] [/'west' - /'west']; tight)] + ├── lookup columns are key + ├── cardinality: [0 - 1] + ├── key: () + ├── fd: ()-->(1-4) + ├── anti-join (lookup abc_part@b_idx) + │ ├── columns: def_part.r:1!null d:2!null e:3 f:4 + │ ├── lookup expression + │ │ └── filters + │ │ ├── f:4 = b:8 [outer=(4,8), constraints=(/4: (/NULL - ]; /8: (/NULL - ]), fd=(4)==(8), (8)==(4)] + │ │ └── abc_part.r:6 = 'east' [outer=(6), constraints=(/6: [/'east' - /'east']; tight), fd=()-->(6)] + │ ├── lookup columns are key + │ ├── cardinality: [0 - 1] + │ ├── key: () + │ ├── fd: ()-->(1-4) + │ ├── locality-optimized-search + │ │ ├── columns: def_part.r:1!null d:2!null e:3 f:4 + │ │ ├── left columns: def_part.r:11 d:12 e:13 f:14 + │ │ ├── right columns: def_part.r:16 d:17 e:18 f:19 + │ │ ├── cardinality: [0 - 1] + │ │ ├── key: () + │ │ ├── fd: ()-->(1-4) + │ │ ├── scan def_part + │ │ │ ├── columns: def_part.r:11!null d:12!null e:13 f:14 + │ │ │ ├── constraint: /11/12: [/'east'/10 - /'east'/10] + │ │ │ ├── cardinality: [0 - 1] + │ │ │ ├── key: () + │ │ │ └── fd: ()-->(11-14) + │ │ └── scan def_part + │ │ ├── columns: def_part.r:16!null d:17!null e:18 f:19 + │ │ ├── constraint: /16/17 + │ │ │ ├── [/'central'/10 - /'central'/10] + │ │ │ └── [/'west'/10 - /'west'/10] + │ │ ├── cardinality: [0 - 1] + │ │ ├── key: () + │ │ └── fd: ()-->(16-19) + │ └── filters (true) + └── filters (true) + +# With an extra ON filter. +opt locality=(region=east) expect=GenerateLocalityOptimizedAntiJoin +SELECT * FROM def_part WHERE NOT EXISTS (SELECT * FROM abc_part WHERE e = a AND f > b) AND d = 1 +---- +anti-join (lookup abc_part) + ├── columns: r:1!null d:2!null e:3 f:4 + ├── lookup expression + │ └── filters + │ ├── e:3 = a:7 [outer=(3,7), constraints=(/3: (/NULL - ]; /7: (/NULL - ]), fd=(3)==(7), (7)==(3)] + │ └── abc_part.r:6 IN ('central', 'west') [outer=(6), constraints=(/6: [/'central' - /'central'] [/'west' - /'west']; tight)] + ├── lookup columns are key + ├── cardinality: [0 - 1] + ├── key: () + ├── fd: ()-->(1-4) + ├── anti-join (lookup abc_part) + │ ├── columns: def_part.r:1!null d:2!null e:3 f:4 + │ ├── lookup expression + │ │ └── filters + │ │ ├── e:3 = a:7 [outer=(3,7), constraints=(/3: (/NULL - ]; /7: (/NULL - ]), fd=(3)==(7), (7)==(3)] + │ │ └── abc_part.r:6 = 'east' [outer=(6), constraints=(/6: [/'east' - /'east']; tight), fd=()-->(6)] + │ ├── lookup columns are key + │ ├── cardinality: [0 - 1] + │ ├── key: () + │ ├── fd: ()-->(1-4) + │ ├── locality-optimized-search + │ │ ├── columns: def_part.r:1!null d:2!null e:3 f:4 + │ │ ├── left columns: def_part.r:11 d:12 e:13 f:14 + │ │ ├── right columns: def_part.r:16 d:17 e:18 f:19 + │ │ ├── cardinality: [0 - 1] + │ │ ├── key: () + │ │ ├── fd: ()-->(1-4) + │ │ ├── scan def_part + │ │ │ ├── columns: def_part.r:11!null d:12!null e:13 f:14 + │ │ │ ├── constraint: /11/12: [/'east'/1 - /'east'/1] + │ │ │ ├── cardinality: [0 - 1] + │ │ │ ├── key: () + │ │ │ └── fd: ()-->(11-14) + │ │ └── scan def_part + │ │ ├── columns: def_part.r:16!null d:17!null e:18 f:19 + │ │ ├── constraint: /16/17 + │ │ │ ├── [/'central'/1 - /'central'/1] + │ │ │ └── [/'west'/1 - /'west'/1] + │ │ ├── cardinality: [0 - 1] + │ │ ├── key: () + │ │ └── fd: ()-->(16-19) + │ └── filters + │ └── f:4 > b:8 [outer=(4,8), constraints=(/4: (/NULL - ]; /8: (/NULL - ])] + └── filters + └── f:4 > b:8 [outer=(4,8), constraints=(/4: (/NULL - ]; /8: (/NULL - ])] + +# Optimization applies even though the scan may produce more than one row. +opt locality=(region=east) expect=GenerateLocalityOptimizedAntiJoin +SELECT * FROM def_part WHERE NOT EXISTS (SELECT * FROM abc_part WHERE e = a) AND f = 10 +---- +anti-join (lookup abc_part) + ├── columns: r:1!null d:2!null e:3 f:4!null + ├── lookup expression + │ └── filters + │ ├── e:3 = a:7 [outer=(3,7), constraints=(/3: (/NULL - ]; /7: (/NULL - ]), fd=(3)==(7), (7)==(3)] + │ └── abc_part.r:6 IN ('central', 'west') [outer=(6), constraints=(/6: [/'central' - /'central'] [/'west' - /'west']; tight)] + ├── lookup columns are key + ├── key: (2) + ├── fd: ()-->(4), (2)-->(1,3), (3)~~>(1,2) + ├── anti-join (lookup abc_part) + │ ├── columns: def_part.r:1!null d:2!null e:3 f:4!null + │ ├── lookup expression + │ │ └── filters + │ │ ├── e:3 = a:7 [outer=(3,7), constraints=(/3: (/NULL - ]; /7: (/NULL - ]), fd=(3)==(7), (7)==(3)] + │ │ └── abc_part.r:6 = 'east' [outer=(6), constraints=(/6: [/'east' - /'east']; tight), fd=()-->(6)] + │ ├── lookup columns are key + │ ├── key: (2) + │ ├── fd: ()-->(4), (2)-->(1,3), (3)~~>(1,2) + │ ├── index-join def_part + │ │ ├── columns: def_part.r:1!null d:2!null e:3 f:4!null + │ │ ├── key: (2) + │ │ ├── fd: ()-->(4), (2)-->(1,3), (3)~~>(1,2) + │ │ └── scan def_part@f_idx + │ │ ├── columns: def_part.r:1!null d:2!null f:4!null + │ │ ├── constraint: /1/4/2 + │ │ │ ├── [/'central'/10 - /'central'/10] + │ │ │ ├── [/'east'/10 - /'east'/10] + │ │ │ └── [/'west'/10 - /'west'/10] + │ │ ├── key: (2) + │ │ └── fd: ()-->(4), (2)-->(1) + │ └── filters (true) + └── filters (true) + +# Optimization applies even though the lookup join may have more than one +# matching row. +opt locality=(region=east) expect=GenerateLocalityOptimizedAntiJoin +SELECT * FROM def_part WHERE NOT EXISTS (SELECT * FROM abc_part WHERE f = c) AND d = 10 +---- +anti-join (lookup abc_part@c_idx) + ├── columns: r:1!null d:2!null e:3 f:4 + ├── lookup expression + │ └── filters + │ ├── f:4 = c:9 [outer=(4,9), constraints=(/4: (/NULL - ]; /9: (/NULL - ]), fd=(4)==(9), (9)==(4)] + │ └── abc_part.r:6 IN ('central', 'west') [outer=(6), constraints=(/6: [/'central' - /'central'] [/'west' - /'west']; tight)] + ├── cardinality: [0 - 1] + ├── key: () + ├── fd: ()-->(1-4) + ├── anti-join (lookup abc_part@c_idx) + │ ├── columns: def_part.r:1!null d:2!null e:3 f:4 + │ ├── lookup expression + │ │ └── filters + │ │ ├── f:4 = c:9 [outer=(4,9), constraints=(/4: (/NULL - ]; /9: (/NULL - ]), fd=(4)==(9), (9)==(4)] + │ │ └── abc_part.r:6 = 'east' [outer=(6), constraints=(/6: [/'east' - /'east']; tight), fd=()-->(6)] + │ ├── cardinality: [0 - 1] + │ ├── key: () + │ ├── fd: ()-->(1-4) + │ ├── locality-optimized-search + │ │ ├── columns: def_part.r:1!null d:2!null e:3 f:4 + │ │ ├── left columns: def_part.r:11 d:12 e:13 f:14 + │ │ ├── right columns: def_part.r:16 d:17 e:18 f:19 + │ │ ├── cardinality: [0 - 1] + │ │ ├── key: () + │ │ ├── fd: ()-->(1-4) + │ │ ├── scan def_part + │ │ │ ├── columns: def_part.r:11!null d:12!null e:13 f:14 + │ │ │ ├── constraint: /11/12: [/'east'/10 - /'east'/10] + │ │ │ ├── cardinality: [0 - 1] + │ │ │ ├── key: () + │ │ │ └── fd: ()-->(11-14) + │ │ └── scan def_part + │ │ ├── columns: def_part.r:16!null d:17!null e:18 f:19 + │ │ ├── constraint: /16/17 + │ │ │ ├── [/'central'/10 - /'central'/10] + │ │ │ └── [/'west'/10 - /'west'/10] + │ │ ├── cardinality: [0 - 1] + │ │ ├── key: () + │ │ └── fd: ()-->(16-19) + │ └── filters (true) + └── filters (true) + +# Optimization does not apply for semi join. +opt locality=(region=central) expect-not=GenerateLocalityOptimizedAntiJoin +SELECT * FROM def_part WHERE EXISTS (SELECT * FROM abc_part WHERE e = a) AND d = 1 +---- +semi-join (lookup abc_part) + ├── columns: r:1!null d:2!null e:3 f:4 + ├── key columns: [21 3] = [6 7] + ├── lookup columns are key + ├── cardinality: [0 - 1] + ├── key: () + ├── fd: ()-->(1-4) + ├── inner-join (cross) + │ ├── columns: def_part.r:1!null d:2!null e:3 f:4 "lookup_join_const_col_@6":21!null + │ ├── cardinality: [0 - 3] + │ ├── multiplicity: left-rows(zero-or-one), right-rows(one-or-more) + │ ├── fd: ()-->(1-4) + │ ├── values + │ │ ├── columns: "lookup_join_const_col_@6":21!null + │ │ ├── cardinality: [3 - 3] + │ │ ├── ('central',) + │ │ ├── ('east',) + │ │ └── ('west',) + │ ├── locality-optimized-search + │ │ ├── columns: def_part.r:1!null d:2!null e:3 f:4 + │ │ ├── left columns: def_part.r:11 d:12 e:13 f:14 + │ │ ├── right columns: def_part.r:16 d:17 e:18 f:19 + │ │ ├── cardinality: [0 - 1] + │ │ ├── key: () + │ │ ├── fd: ()-->(1-4) + │ │ ├── scan def_part + │ │ │ ├── columns: def_part.r:11!null d:12!null e:13 f:14 + │ │ │ ├── constraint: /11/12: [/'central'/1 - /'central'/1] + │ │ │ ├── cardinality: [0 - 1] + │ │ │ ├── key: () + │ │ │ └── fd: ()-->(11-14) + │ │ └── scan def_part + │ │ ├── columns: def_part.r:16!null d:17!null e:18 f:19 + │ │ ├── constraint: /16/17 + │ │ │ ├── [/'east'/1 - /'east'/1] + │ │ │ └── [/'west'/1 - /'west'/1] + │ │ ├── cardinality: [0 - 1] + │ │ ├── key: () + │ │ └── fd: ()-->(16-19) + │ └── filters (true) + └── filters (true)