Skip to content

Commit c22a945

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 c22a945

File tree

5 files changed

+126
-13
lines changed

5 files changed

+126
-13
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: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,3 +556,50 @@ 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+
562+
statement ok
563+
DROP TABLE IF EXISTS anykey_json_tab;
564+
565+
statement ok
566+
CREATE TABLE anykey_json_tab (
567+
a INT PRIMARY KEY,
568+
b JSONB
569+
);
570+
571+
statement ok
572+
CREATE INVERTED INDEX anykey_inv ON anykey_json_tab(b)
573+
574+
statement ok
575+
INSERT INTO anykey_json_tab VALUES
576+
(1, '{"a": {"b": {"c": "d"}}}'),
577+
(2, '{"a": {"b": {"c": {"d": "e"}}}}'),
578+
(3, '{"a": {"b": [{"c": {"d": "e"}}]}}'),
579+
(4, '{"a": {"b": ["c", "d"]}}'),
580+
(5, '{"a": {"b": "d"}}'),
581+
(6, '{"a": {"b1": "d"}}'),
582+
(7, '{"a": {"b1": {"c": {"d": "e"}}}}');
583+
584+
query IT
585+
SELECT a, b FROM anykey_json_tab@primary WHERE jsonb_path_exists(b, '$.a.b.*') ORDER BY a;
586+
----
587+
1 {"a": {"b": {"c": "d"}}}
588+
2 {"a": {"b": {"c": {"d": "e"}}}}
589+
3 {"a": {"b": [{"c": {"d": "e"}}]}}
590+
591+
query IT
592+
SELECT a, b FROM anykey_json_tab@anykey_inv WHERE jsonb_path_exists(b, '$.a.b.*') ORDER BY a;
593+
----
594+
1 {"a": {"b": {"c": "d"}}}
595+
2 {"a": {"b": {"c": {"d": "e"}}}}
596+
3 {"a": {"b": [{"c": {"d": "e"}}]}}
597+
598+
# AnyKey should only be allowed at the end of the path chain.
599+
statement error index "anykey_inv" is inverted and cannot be used for this query
600+
SELECT a, b FROM anykey_json_tab@anykey_inv WHERE jsonb_path_exists(b, '$.a.*.b') ORDER BY a;
601+
602+
statement error index "anykey_inv" is inverted and cannot be used for this query
603+
SELECT a, b FROM anykey_json_tab@anykey_inv WHERE jsonb_path_exists(b, '$.a.b.*.*') ORDER BY a;
604+
605+
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 & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ func (a AnyKey) Validate(nestingLevel int, insideArraySubscript bool) error {
316316

317317
// isSupportedPathPattern returns true if the given paths matches one of
318318
// the following patterns, which can be supported by the inverted index:
319-
// - keychain mode: $.[key|wildcard].[key|wildcard]...
319+
// - keychain mode: $.[key|wildcard].[key|wildcard]...(*)
320320
// - end value mode: $.[key|wildcard]? (@.[key|wildcard].[key|wildcard]... == [string|number|null|boolean])
321321
// We might call this function recursively if a Path is a Filter, which contains
322322
// child Paths. If isSupportedPathPattern is called within a Filter, atRoot
@@ -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,43 @@ 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+
// If the last path component is an AnyKey, it means the
580+
// current key must not be an end key (since AnyKey matches
581+
// any key under the current object/array). For example, for
582+
// path $.a.b.*, the following won't match because "b" is the
583+
// end key:
584+
// - {"a": {"b": "d"}}
585+
// - {"a": {"b": ["c"]}}
586+
// But the following will match:
587+
// - {"a": {"b": {"c": "d"}}}
588+
// - {"a": {"b": [{"c": {"d": "e"}}]}}
589+
// In these 2 cases, after "b", there is still "c"
590+
// as the next key, so "b" is not an end key.
591+
if _, isAnyKey := pathComponents[len(pathComponents)-1].(AnyKey); isAnyKey {
592+
// asEndValKey means the baseKey is mapped to an end value
593+
// in the json object. (e.g. {"a": {"b": "d"}})
594+
asEndValKey := encoding.AddJSONPathTerminator(baseKey[:len(baseKey):len(baseKey)])
595+
asEndValKeySpan := inverted.Span{
596+
Start: asEndValKey,
597+
End: keysbase.PrefixEnd(asEndValKey),
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+
for _, sp := range resSpans {
610+
resultExpression = addSpanToResult(resultExpression, sp)
611+
}
577612
}
578613
}
579614
return resultExpression

0 commit comments

Comments
 (0)