Skip to content

Commit a6e4ba9

Browse files
craig[bot]paulniziolekyuzefovich
committed
152353: opt: index accelerate LTREE with ancestry operators, @> and <@ r=paulniziolek a=paulniziolek #### opt: index accelerate `@>` operator for ltree The `@>` operator is accelerated by restricting the spans to be the union of equality spans for all sub-ltrees rooted at ''. For example, a query with predicate `WHERE a `@>` 'A.B'` would create spans [/'' - /''], [/'A' - /'A'], [/'A.B' - /'A.B']. Informs: #44657 Epic: CRDB-148 Release note (performance improvement): LTREE is now index accelerated with the ``@>`` operator. #### opt: index accelerate <@ operator for ltree The <@ is index accelerated by restricting the span of key-encoded ltrees to be between a given ltree and the ltree with an incremented last label. For example, a query with predicate `WHERE a <@ 'A.B'` would create the span [/'A.B' - /'A.C']. Informs: #44657 Epic: CRDB-148 Release note (performance improvement): LTREE is now index accelerated with the `<`@`` operator. 152523: sql: harden recent fix to deleteRange r=yuzefovich a=yuzefovich In recently merged 59a28ec we fixed how we count "rows affected" by the deleteRangeNode, which was done by maintaining "cur row prefix" across BatchRequests. AI-generated code review pointed out that we can alias the memory of now-old BatchRequest while processing the response to the new one. Although I don't think it can lead to problems (since we shouldn't be modifying the BatchRequest's or BatchResponse's keys - which is verified via `GRPCTransportFactory` "race" variant), it seems prudent that we make a copy of the row prefix (among other benefits, this might allow for the old keys to be GCed sooner). Epic: None Release note: None Co-authored-by: Paul Niziolek <[email protected]> Co-authored-by: Yahor Yuzefovich <[email protected]>
3 parents 5cb14e2 + 83883b2 + 92577c2 commit a6e4ba9

File tree

7 files changed

+246
-0
lines changed

7 files changed

+246
-0
lines changed

pkg/sql/delete_range.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ func (d *deleteRangeNode) startExec(params runParams) error {
111111

112112
ctx := params.ctx
113113
log.VEvent(ctx, 2, "fast delete: skipping scan")
114+
// TODO(yuzefovich): why are we making a copy of spans?
114115
spans := make([]roachpb.Span, len(d.spans))
115116
copy(spans, d.spans)
116117
if !d.autoCommitEnabled {
@@ -198,6 +199,18 @@ func (d *deleteRangeNode) deleteSpans(params runParams, b *kv.Batch, spans roach
198199
func (d *deleteRangeNode) processResults(
199200
results []kv.Result, resumeSpans []roachpb.Span,
200201
) (roachpb.Spans, error) {
202+
if !d.autoCommitEnabled {
203+
defer func() {
204+
// Make a copy of curRowPrefix to avoid referencing the memory from
205+
// the now-old BatchRequest.
206+
//
207+
// When auto-commit is enabled, we expect to see any resume spans,
208+
// so we won't need to access d.curRowPrefix later.
209+
curRowPrefix := make([]byte, len(d.curRowPrefix))
210+
copy(curRowPrefix, d.curRowPrefix)
211+
d.curRowPrefix = curRowPrefix
212+
}()
213+
}
201214
for _, r := range results {
202215
// TODO(yuzefovich): when the table has 1 column family, we don't need
203216
// to compare the key prefixes since each deleted key corresponds to a

pkg/sql/opt/exec/execbuilder/testdata/select_index

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1904,3 +1904,36 @@ vectorized: true
19041904
missing stats
19051905
table: t4@t4_pkey
19061906
spans: [/1/1 - /1/1] [/1/5 - /1/5] [/5/1 - /5/1] [/5/5 - /5/5]
1907+
1908+
# ------------------------------------------------------------------------------
1909+
# Tests for index acceleration on ancestry operators (@> and <@) using
1910+
# LTREE columns
1911+
# ------------------------------------------------------------------------------
1912+
1913+
statement ok
1914+
CREATE TABLE t5 (a LTREE)
1915+
1916+
statement ok
1917+
CREATE INDEX t5_a_idx ON t5 (a)
1918+
1919+
query T
1920+
EXPLAIN SELECT a FROM t5 WHERE a @> 'A.B.C'
1921+
----
1922+
distribution: local
1923+
vectorized: true
1924+
·
1925+
• scan
1926+
missing stats
1927+
table: t5@t5_a_idx
1928+
spans: [/'' - /''] [/'A' - /'A'] [/'A.B' - /'A.B'] [/'A.B.C' - /'A.B.C']
1929+
1930+
query T
1931+
EXPLAIN SELECT a FROM t5 WHERE a <@ 'A.B.C'
1932+
----
1933+
distribution: local
1934+
vectorized: true
1935+
·
1936+
• scan
1937+
missing stats
1938+
table: t5@t5_a_idx
1939+
spans: [/'A.B.C' - /'A.B.D')

pkg/sql/opt/idxconstraint/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ go_library(
1616
"//pkg/sql/sem/tree",
1717
"//pkg/sql/types",
1818
"//pkg/util",
19+
"//pkg/util/ltree",
1920
"@com_github_cockroachdb_errors//:errors",
2021
],
2122
)

pkg/sql/opt/idxconstraint/index_constraints.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
2121
"github.com/cockroachdb/cockroach/pkg/sql/types"
2222
"github.com/cockroachdb/cockroach/pkg/util"
23+
"github.com/cockroachdb/cockroach/pkg/util/ltree"
2324
"github.com/cockroachdb/errors"
2425
)
2526

@@ -338,6 +339,56 @@ func (c *indexConstraintCtx) makeSpansForSingleColumnDatum(
338339
return complete
339340
}
340341
}
342+
343+
case opt.ContainsOp:
344+
if l, ok := datum.(*tree.DLTree); ok {
345+
var spans constraint.Spans
346+
// We need to create an equality span for each subtree of the LTree that
347+
// is rooted from the root, including the empty ltree.
348+
spans.Alloc(l.LTree.Len() + 1)
349+
keyCtx := &c.keyCtx[offset]
350+
for i := 0; i <= l.LTree.Len(); i++ {
351+
var subLTree ltree.T
352+
if l.LTree.Compare(ltree.Empty) != 0 {
353+
var err error
354+
subLTree, err = l.LTree.SubPath(0 /* offset */, i /* length */)
355+
if err != nil {
356+
panic(err)
357+
}
358+
} else {
359+
// SubPath is not graceful with empty ltree, thus we handle it here.
360+
subLTree = ltree.Empty
361+
}
362+
key := constraint.MakeKey(tree.NewDLTree(subLTree))
363+
var sp constraint.Span
364+
sp.Init(key, includeBoundary, key, includeBoundary)
365+
spans.Append(&sp)
366+
}
367+
// Given how we've constructed the spans, they already are ordered and
368+
// unique, but we choose to call SortAndMerge for symmetry with other
369+
// expressions and as a sanity check (the function exits quickly if the
370+
// ordering is already correct).
371+
spans.SortAndMerge(keyCtx)
372+
out.Init(keyCtx, &spans)
373+
return true
374+
}
375+
376+
case opt.ContainedByOp:
377+
if l, ok := datum.(*tree.DLTree); ok {
378+
if l.LTree.Compare(ltree.Empty) == 0 {
379+
// Empty LTree shouldn't be constrained.
380+
// TODO: This could be constrained by excluding NULLs.
381+
break
382+
}
383+
startKey := constraint.MakeKey(l)
384+
endKey := constraint.MakeKey(tree.NewDLTree(l.LTree.NextSibling()))
385+
c.singleSpan(
386+
offset, startKey, includeBoundary, endKey, excludeBoundary,
387+
c.columns[offset].Descending(),
388+
out,
389+
)
390+
return true
391+
}
341392
}
342393
c.unconstrained(offset, out)
343394
return false
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
index-constraints vars=(a ltree) index=a
2+
a @> '-'
3+
----
4+
[/'' - /'']
5+
[/'-' - /'-']
6+
7+
index-constraints vars=(a ltree) index=a
8+
a @> 'A.B.C'
9+
----
10+
[/'' - /'']
11+
[/'A' - /'A']
12+
[/'A.B' - /'A.B']
13+
[/'A.B.C' - /'A.B.C']
14+
15+
index-constraints vars=(a ltree) index=a
16+
a @> ''
17+
----
18+
[/'' - /'']
19+
20+
index-constraints vars=(a ltree) index=a
21+
a @> NULL
22+
----
23+
24+
25+
index-constraints vars=(a ltree) index=a
26+
a <@ 'A.B.C'
27+
----
28+
[/'A.B.C' - /'A.B.D')
29+
30+
index-constraints vars=(a ltree) index=a
31+
a <@ 'z'
32+
----
33+
[/'z' - /'z-')
34+
35+
index-constraints vars=(a ltree) index=a
36+
a <@ ''
37+
----
38+
[ - ]
39+
Remaining filter: a <@ ''
40+
41+
index-constraints vars=(a ltree) index=a
42+
a <@ NULL
43+
----
44+
45+
index-constraints vars=(a ltree) index=a
46+
a @> '-' OR a @> 'foo.bar'
47+
----
48+
[/'' - /'']
49+
[/'-' - /'-']
50+
[/'foo' - /'foo']
51+
[/'foo.bar' - /'foo.bar']
52+
53+
index-constraints vars=(a ltree) index=a
54+
a <@ '-' OR a <@ 'foo.bar'
55+
----
56+
[/'-' - /'0')
57+
[/'foo.bar' - /'foo.bas')
58+
59+
index-constraints vars=(a ltree) index=a
60+
a <@ 'A.B.C' OR a @> 'A.B'
61+
----
62+
[/'' - /'']
63+
[/'A' - /'A']
64+
[/'A.B' - /'A.B']
65+
[/'A.B.C' - /'A.B.D')
66+
67+
index-constraints vars=(a ltree) index=a
68+
a @> 'A.B.C' OR a <@ 'A.B'
69+
----
70+
[/'' - /'']
71+
[/'A' - /'A']
72+
[/'A.B' - /'A.C')
73+
74+
index-constraints vars=(a ltree) index=a
75+
a <@ 'A.B.C' AND a @> 'A.B.C.D.E'
76+
----
77+
[/'A.B.C' - /'A.B.C']
78+
[/'A.B.C.D' - /'A.B.C.D']
79+
[/'A.B.C.D.E' - /'A.B.C.D.E']

pkg/util/ltree/ltree.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,3 +295,46 @@ func LCA(ltrees []T) (_ T, isNull bool) { // lint: uppercase function OK
295295

296296
return T{path: ltrees[0].path[:i:i]}, false
297297
}
298+
299+
// NextSibling returns a LTree with a lexicographically incremented last label.
300+
// This is different from the next lexicographic LTree. This is mainly used for
301+
// defining a key-encoded span for ancestry operators.
302+
// Note that this could produce a LTREE with more labels than the maximum
303+
// allowed.
304+
// Example: 'A.B' -> 'A.C'
305+
func (lt T) NextSibling() T {
306+
if lt.Len() == 0 {
307+
return Empty
308+
}
309+
lastLabel := lt.path[len(lt.path)-1]
310+
nextLabel := incrementLabel(lastLabel)
311+
312+
newLTree := lt.Copy()
313+
newLTree.path[newLTree.Len()-1] = nextLabel
314+
return newLTree
315+
}
316+
317+
var nextCharMap = map[byte]byte{
318+
'-': '0',
319+
'9': 'A',
320+
'Z': '_',
321+
'_': 'a',
322+
'z': 0,
323+
}
324+
325+
func incrementLabel(label string) string {
326+
nextLabel := []byte(label)
327+
nextChar, ok := nextCharMap[nextLabel[len(nextLabel)-1]]
328+
329+
if ok && nextChar == 0 {
330+
// Technically, this could mean exceeding the length of the label.
331+
return string(append(nextLabel, '-'))
332+
}
333+
334+
if !ok {
335+
nextChar = nextLabel[len(nextLabel)-1] + 1
336+
}
337+
338+
nextLabel[len(nextLabel)-1] = nextChar
339+
return string(nextLabel)
340+
}

pkg/util/ltree/ltree_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,29 @@ func TestCompare(t *testing.T) {
152152
}
153153
}
154154
}
155+
156+
func TestNext(t *testing.T) {
157+
tests := []struct {
158+
input string
159+
expected string
160+
}{
161+
{"a", "b"},
162+
{"a.b", "a.c"},
163+
{"a.z", "a.z-"},
164+
{"-", "0"},
165+
{"9", "A"},
166+
{"Z", "_"},
167+
{"_", "a"},
168+
}
169+
170+
for _, tc := range tests {
171+
a, err := ParseLTree(tc.input)
172+
if err != nil {
173+
t.Fatalf("unexpected error parsing %q: %v", tc.input, err)
174+
}
175+
next := a.NextSibling()
176+
if next.String() != tc.expected {
177+
t.Errorf("expected next of %q to be %q, got %q", tc.input, tc.expected, next.String())
178+
}
179+
}
180+
}

0 commit comments

Comments
 (0)