Skip to content

Commit e33cbda

Browse files
committed
sql/jsonpath: support index acceleration with AnyKey (*) at the chain end
We now support index accelerating `jsonb_path_exists` filters with json path expression that ends with an AnyKey (`*`). Note that the AnyKey is allowed only at the end of the expression. I.e. the following are not allowed: ``` $.a.*.b $.a.b.*.* ``` Release note (sql change): We now support index accelerating `jsonb_path_exists` filters with json path expression that ends with an AnyKey (`*`).
1 parent 21b75ac commit e33cbda

File tree

5 files changed

+125
-12
lines changed

5 files changed

+125
-12
lines changed

pkg/sql/inverted/expression.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -682,8 +682,8 @@ func intersectSpanExpressions(left, right *SpanExpression) *SpanExpression {
682682
Right: right,
683683
}
684684
if expr.FactoredUnionSpans != nil {
685-
left.FactoredUnionSpans = subtractSpans(left.FactoredUnionSpans, expr.FactoredUnionSpans)
686-
right.FactoredUnionSpans = subtractSpans(right.FactoredUnionSpans, expr.FactoredUnionSpans)
685+
left.FactoredUnionSpans = SubtractSpans(left.FactoredUnionSpans, expr.FactoredUnionSpans)
686+
right.FactoredUnionSpans = SubtractSpans(right.FactoredUnionSpans, expr.FactoredUnionSpans)
687687
}
688688
tryPruneChildren(expr)
689689
return expr
@@ -910,9 +910,9 @@ func intersectSpans(left []Span, right []Span) []Span {
910910
return spans
911911
}
912912

913-
// subtractSpans subtracts right from left, under the assumption that right is a
913+
// SubtractSpans subtracts right from left, under the assumption that right is a
914914
// subset of left.
915-
func subtractSpans(left []Span, right []Span) []Span {
915+
func SubtractSpans(left []Span, right []Span) []Span {
916916
if len(right) == 0 {
917917
return left
918918
}

pkg/sql/inverted/expression_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -291,29 +291,29 @@ func TestSetIntersection(t *testing.T) {
291291
func TestSetSubtraction(t *testing.T) {
292292
checkEqual(t,
293293
nil,
294-
subtractSpans(
294+
SubtractSpans(
295295
[]Span{single("b")},
296296
[]Span{span("b", "c")},
297297
),
298298
)
299299
checkEqual(t,
300300
[]Span{span("b\x00", "d")},
301-
subtractSpans(
301+
SubtractSpans(
302302
[]Span{span("b", "d")},
303303
[]Span{span("b", "b\x00")},
304304
),
305305
)
306306
checkEqual(t,
307307
[]Span{span("b", "d"), span("e", "ea")},
308-
subtractSpans(
308+
SubtractSpans(
309309
[]Span{span("b", "d"), span("e", "f")},
310310
[]Span{span("ea", "f")},
311311
),
312312
)
313313
checkEqual(t,
314314
[]Span{span("d", "da"), span("db", "dc"),
315315
span("dd", "df"), span("fa", "g")},
316-
subtractSpans(
316+
SubtractSpans(
317317
[]Span{single("b"), span("d", "e"), span("f", "g")},
318318
[]Span{span("b", "c"), span("da", "db"),
319319
span("dc", "dd"), span("df", "e"), span("f", "fa")},

pkg/sql/logictest/testdata/logic_test/jsonb_path_exists_index_acceleration

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,3 +556,49 @@ SELECT a FROM json_tab@primary WHERE jsonb_path_exists(b, '$.a ? (@.b == $x)', '
556556

557557
statement error index "foo_inv" is inverted and cannot be used for this query
558558
SELECT a FROM json_tab@foo_inv WHERE jsonb_path_exists(b, '$.a ? (@.b == $x)', '{"x": "c"}') ORDER BY a;
559+
560+
subtest anykey
561+
statement ok
562+
DROP TABLE IF EXISTS anykey_json_tab;
563+
564+
statement ok
565+
CREATE TABLE anykey_json_tab (
566+
a INT PRIMARY KEY,
567+
b JSONB
568+
);
569+
570+
statement ok
571+
CREATE INVERTED INDEX anykey_inv ON anykey_json_tab(b)
572+
573+
statement ok
574+
INSERT INTO anykey_json_tab VALUES
575+
(1, '{"a": {"b": {"c": "d"}}}'),
576+
(2, '{"a": {"b": {"c": {"d": "e"}}}}'),
577+
(3, '{"a": {"b": [{"c": {"d": "e"}}]}}'),
578+
(4, '{"a": {"b": ["c", "d"]}}'),
579+
(5, '{"a": {"b": "d"}}'),
580+
(6, '{"a": {"b1": "d"}}'),
581+
(7, '{"a": {"b1": {"c": {"d": "e"}}}}');
582+
583+
query IT
584+
SELECT a, b FROM anykey_json_tab@primary WHERE jsonb_path_exists(b, '$.a.b.*') ORDER BY a;
585+
----
586+
1 {"a": {"b": {"c": "d"}}}
587+
2 {"a": {"b": {"c": {"d": "e"}}}}
588+
3 {"a": {"b": [{"c": {"d": "e"}}]}}
589+
590+
query IT
591+
SELECT a, b FROM anykey_json_tab@anykey_inv WHERE jsonb_path_exists(b, '$.a.b.*') ORDER BY a;
592+
----
593+
1 {"a": {"b": {"c": "d"}}}
594+
2 {"a": {"b": {"c": {"d": "e"}}}}
595+
3 {"a": {"b": [{"c": {"d": "e"}}]}}
596+
597+
# AnyKey should only be allowed at the end of the path chain.
598+
statement error index "anykey_inv" is inverted and cannot be used for this query
599+
SELECT a, b FROM anykey_json_tab@anykey_inv WHERE jsonb_path_exists(b, '$.a.*.b') ORDER BY a;
600+
601+
statement error index "anykey_inv" is inverted and cannot be used for this query
602+
SELECT a, b FROM anykey_json_tab@anykey_inv WHERE jsonb_path_exists(b, '$.a.b.*.*') ORDER BY a;
603+
604+
subtest end

pkg/sql/opt/xform/testdata/rules/select

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13883,3 +13883,34 @@ project
1388313883
├── ["a"/Arr/"x"/Arr/False, "a"/Arr/"x"/Arr/False]
1388413884
├── ["a"/"x"/False, "a"/"x"/False]
1388513885
└── ["a"/"x"/Arr/False, "a"/"x"/Arr/False]
13886+
13887+
13888+
opt expect=GenerateInvertedIndexScans
13889+
SELECT k FROM b WHERE jsonb_path_exists(j, '$.a.b.*')
13890+
----
13891+
project
13892+
├── columns: k:1!null
13893+
├── immutable
13894+
├── key: (1)
13895+
└── inverted-filter
13896+
├── columns: k:1!null
13897+
├── inverted expression: /9
13898+
│ ├── tight: true, unique: false
13899+
│ └── union spans
13900+
│ ├── [???, "a"/Arr/"b")
13901+
│ ├── ["a"/Arr/"b"/PrefixEnd, "a"/Arr/"b"/Arr)
13902+
│ ├── ["a"/Arr/"b"/Arr/PrefixEnd, "a"/Arr/Arr/)
13903+
│ ├── [???, "a"/"b")
13904+
│ ├── ["a"/"b"/PrefixEnd, "a"/"b"/Arr)
13905+
│ └── ["a"/"b"/Arr/PrefixEnd, "a"/Arr/)
13906+
├── key: (1)
13907+
└── scan b@j_inv_idx,inverted
13908+
├── columns: k:1!null j_inverted_key:9!null
13909+
└── inverted constraint: /9/1
13910+
└── spans
13911+
├── [???, "a"/Arr/"b")
13912+
├── ["a"/Arr/"b"/PrefixEnd, "a"/Arr/"b"/Arr)
13913+
├── ["a"/Arr/"b"/Arr/PrefixEnd, "a"/Arr/Arr/)
13914+
├── [???, "a"/"b")
13915+
├── ["a"/"b"/PrefixEnd, "a"/"b"/Arr)
13916+
└── ["a"/"b"/Arr/PrefixEnd, "a"/Arr/)

pkg/util/jsonpath/path.go

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,9 @@ func isSupportedPathPattern(ps []Path, atRoot bool) bool {
350350
for i := 1; i < len(ps); i++ {
351351
switch pt := ps[i].(type) {
352352
case Wildcard, Key:
353+
case AnyKey:
354+
// We only allow AnyKey at the end of the root path.
355+
return i == len(ps)-1 && atRoot
353356
case Filter:
354357
// We only allow filter at the end of the path.
355358
if i != len(ps)-1 {
@@ -569,11 +572,44 @@ func buildInvertedIndexSpans(
569572
}
570573
resultExpression = addSpanToResult(resultExpression, inverted.MakeSingleValSpan(arrayKeys[0]))
571574
} else {
572-
// Meaning this is of the keychain mode. (See isSupportedPathPattern).
573-
resultExpression = addSpanToResult(resultExpression, inverted.Span{
575+
resSpans := []inverted.Span{{
574576
Start: baseKey,
575-
End: keysbase.PrefixEnd(encoding.AddJSONPathSeparator(baseKey)),
576-
})
577+
End: keysbase.PrefixEnd(encoding.AddJSONPathSeparator(baseKey[:len(baseKey):len(baseKey)])),
578+
}}
579+
for j := currentIndex; j < len(pathComponents); j++ {
580+
// If any of the remaining path components is an AnyKey,
581+
// it means the current key must not be an end key (since
582+
// AnyKey matches any key under the current object/array).
583+
// For example, for path $.a.b.x, the following won't match
584+
// because "b" is the end key:
585+
// - {"a": {"b": "d"}}
586+
// - {"a": {"b": ["c"]}}
587+
// But the following will match:
588+
// - {"a": {"b": {"c": "d"}}}
589+
// - {"a": {"b": [{"c": {"d": "e"}}]}}
590+
// In these 2 cases, after "b", there is still "c"
591+
// as the next key, so "b" is not an end key.
592+
if _, isAnyKey := pathComponents[j].(AnyKey); isAnyKey {
593+
// asEndValKey means the baseKey is mapped to an end value
594+
// in the json object. (e.g. {"a": {"b": "d"}})
595+
asEndValKeySpan := inverted.Span{
596+
Start: encoding.AddJSONPathTerminator(baseKey[:len(baseKey):len(baseKey)]),
597+
End: keysbase.PrefixEnd(encoding.AddJSONPathTerminator(baseKey[:len(baseKey):len(baseKey)])),
598+
}
599+
// asEndArrayValKey means the baseKey is mapped to an end value
600+
// in the array object. (e.g. {"a": {"b": ["c"]}}})
601+
asEndArrayValKey := encoding.AddJSONPathTerminator(encoding.EncodeArrayAscending(encoding.AddJSONPathSeparator(baseKey[:len(baseKey):len(baseKey)])))
602+
asEndArrayValKeySpan := inverted.Span{
603+
Start: asEndArrayValKey,
604+
End: keysbase.PrefixEnd(asEndArrayValKey),
605+
}
606+
resSpans = inverted.SubtractSpans(resSpans, []inverted.Span{asEndValKeySpan, asEndArrayValKeySpan})
607+
}
608+
}
609+
610+
for _, sp := range resSpans {
611+
resultExpression = addSpanToResult(resultExpression, sp)
612+
}
577613
}
578614
}
579615
return resultExpression

0 commit comments

Comments
 (0)