Skip to content

Commit 83883b2

Browse files
committed
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.
1 parent 5f22be8 commit 83883b2

File tree

5 files changed

+152
-0
lines changed

5 files changed

+152
-0
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1927,3 +1927,13 @@ vectorized: true
19271927
table: t5@t5_a_idx
19281928
spans: [/'' - /''] [/'A' - /'A'] [/'A.B' - /'A.B'] [/'A.B.C' - /'A.B.C']
19291929

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/index_constraints.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,23 @@ func (c *indexConstraintCtx) makeSpansForSingleColumnDatum(
372372
out.Init(keyCtx, &spans)
373373
return true
374374
}
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+
}
375392
}
376393
c.unconstrained(offset, out)
377394
return false

pkg/sql/opt/idxconstraint/testdata/ltree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,59 @@ index-constraints vars=(a ltree) index=a
2121
a @> NULL
2222
----
2323

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
@@ -275,3 +275,46 @@ func LCA(ltrees []T) (_ T, isNull bool) { // lint: uppercase function OK
275275

276276
return T{path: ltrees[0].path[:i:i]}, false
277277
}
278+
279+
// NextSibling returns a LTree with a lexicographically incremented last label.
280+
// This is different from the next lexicographic LTree. This is mainly used for
281+
// defining a key-encoded span for ancestry operators.
282+
// Note that this could produce a LTREE with more labels than the maximum
283+
// allowed.
284+
// Example: 'A.B' -> 'A.C'
285+
func (lt T) NextSibling() T {
286+
if lt.Len() == 0 {
287+
return Empty
288+
}
289+
lastLabel := lt.path[len(lt.path)-1]
290+
nextLabel := incrementLabel(lastLabel)
291+
292+
newLTree := lt.Copy()
293+
newLTree.path[newLTree.Len()-1] = nextLabel
294+
return newLTree
295+
}
296+
297+
var nextCharMap = map[byte]byte{
298+
'-': '0',
299+
'9': 'A',
300+
'Z': '_',
301+
'_': 'a',
302+
'z': 0,
303+
}
304+
305+
func incrementLabel(label string) string {
306+
nextLabel := []byte(label)
307+
nextChar, ok := nextCharMap[nextLabel[len(nextLabel)-1]]
308+
309+
if ok && nextChar == 0 {
310+
// Technically, this could mean exceeding the length of the label.
311+
return string(append(nextLabel, '-'))
312+
}
313+
314+
if !ok {
315+
nextChar = nextLabel[len(nextLabel)-1] + 1
316+
}
317+
318+
nextLabel[len(nextLabel)-1] = nextChar
319+
return string(nextLabel)
320+
}

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)