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)