Skip to content

Commit 272a77a

Browse files
committed
lib/logstorage: optimize OR filters where one of these filters is *
Such filters can be optimized to `*`. This avoid executing other OR filters. For example, `foo or * or bar` is optimized to `*`, while `foo` and `bar` filters aren't executed. Such filters are frequently generated by Grafana, so this should improve query performance there.
1 parent e72a3fd commit 272a77a

File tree

6 files changed

+76
-19
lines changed

6 files changed

+76
-19
lines changed

docs/victorialogs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ according to [these docs](https://docs.victoriametrics.com/victorialogs/quicksta
4545
* FEATURE: [Journald data ingestion](https://docs.victoriametrics.com/victorialogs/data-ingestion/journald/): automatically add `level` log field according to [the `PRIORITY` field value](https://wiki.archlinux.org/title/Systemd/Journal#Priority_level). This enables [log level highlighting in Grafana](https://grafana.com/docs/grafana/latest/explore/logs-integration/#log-level). See [#8535](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8535).
4646
* FEATURE: [Syslog data ingestion](https://docs.victoriametrics.com/victorialogs/data-ingestion/syslog/): automatically add `level` log field according to [the `severity` field value](https://en.wikipedia.org/wiki/Syslog#Severity_level). This enables [log level highlighting in Grafana](https://grafana.com/docs/grafana/latest/explore/logs-integration/#log-level).
4747
* FEATURE: [Journald data ingestion](https://docs.victoriametrics.com/victorialogs/data-ingestion/journald/): use `(_MACHINE_ID, _HOSTNAME, _SYSTEMD_UNIT)` fields as [log stream fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#stream-fields) by default. See [#9143](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9143).
48+
* FEATURE: [LogsQL](https://docs.victoriametrics.com/victorialogs/logsql/): optimize `(any_filter or *)` filters to `*`. This avoids executing the `any_filter`. Such filters are frequently generated by Grafana.
4849

4950
* BUGFIX: [query API](https://docs.victoriametrics.com/victorialogs/querying/#querying-logs): properly set storage node authorization in cluster mode when [Basic Auth](https://docs.victoriametrics.com/victorialogs/cluster/#security) is enabled. See [#9080](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9080).
5051
* BUGFIX: [Journald data ingestion](https://docs.victoriametrics.com/victorialogs/data-ingestion/journald/): properly read log timestamp from `__REALTIME_TIMESTAMP` field according to [the docs](https://docs.victoriametrics.com/victorialogs/data-ingestion/journald/#time-field). See [#9144](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9144). The bug has been introduced in [v1.22.0-victorialogs](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.22.0-victorialogs).

lib/logstorage/filter.go

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,30 +19,34 @@ type filter interface {
1919
applyToBlockResult(br *blockResult, bm *bitmap)
2020
}
2121

22-
// visitFilter sequentially calls visitFunc for filters inside f.
22+
// visitFilterRecursive recursively calls visitFunc for filters inside f.
2323
//
2424
// It stops calling visitFunc on the remaining filters as soon as visitFunc returns true.
2525
// It returns the result of the last visitFunc call.
26-
func visitFilter(f filter, visitFunc func(f filter) bool) bool {
26+
func visitFilterRecursive(f filter, visitFunc func(f filter) bool) bool {
27+
return visitFilterInternal(f, visitFunc) || visitFunc(f)
28+
}
29+
30+
func visitFilterInternal(f filter, visitFunc func(f filter) bool) bool {
2731
switch t := f.(type) {
2832
case *filterAnd:
29-
return visitFilters(t.filters, visitFunc)
33+
return visitFiltersRecursive(t.filters, visitFunc)
3034
case *filterOr:
31-
return visitFilters(t.filters, visitFunc)
35+
return visitFiltersRecursive(t.filters, visitFunc)
3236
case *filterNot:
33-
return visitFilter(t.f, visitFunc)
37+
return visitFilterRecursive(t.f, visitFunc)
3438
default:
35-
return visitFunc(f)
39+
return false
3640
}
3741
}
3842

39-
// visitFilters calls visitFunc per each filter in filters.
43+
// visitFiltersRecursive recursively calls visitFunc per each filter in filters.
4044
//
4145
// It stops calling visitFunc on the remaining filters as soon as visitFunc returns true.
4246
// It returns the result of the last visitFunc call.
43-
func visitFilters(filters []filter, visitFunc func(f filter) bool) bool {
47+
func visitFiltersRecursive(filters []filter, visitFunc func(f filter) bool) bool {
4448
for _, f := range filters {
45-
if visitFilter(f, visitFunc) {
49+
if visitFilterRecursive(f, visitFunc) {
4650
return true
4751
}
4852
}
@@ -53,6 +57,11 @@ func visitFilters(filters []filter, visitFunc func(f filter) bool) bool {
5357
//
5458
// It doesn't copy other filters by returning them as is.
5559
func copyFilter(f filter, visitFunc func(f filter) bool, copyFunc func(f filter) (filter, error)) (filter, error) {
60+
if !visitFilterRecursive(f, visitFunc) {
61+
// Nothing to copy
62+
return f, nil
63+
}
64+
5665
f, err := copyFilterInternal(f, visitFunc, copyFunc)
5766
if err != nil {
5867
return nil, err
@@ -101,12 +110,6 @@ func copyFilterInternal(f filter, visitFunc func(f filter) bool, copyFunc func(f
101110
//
102111
// It doesn't copy other filters by returning them as is.
103112
func copyFilters(filters []filter, visitFunc func(f filter) bool, copyFunc func(f filter) (filter, error)) ([]filter, error) {
104-
if !visitFilters(filters, visitFunc) {
105-
// Nothing to copy
106-
return filters, nil
107-
}
108-
109-
// Copy filters.
110113
filtersNew := make([]filter, len(filters))
111114
for i, f := range filters {
112115
fNew, err := copyFilter(f, visitFunc, copyFunc)

lib/logstorage/filter_or_test.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@ func TestGetCommonTokensForOrFilters(t *testing.T) {
8080
if err != nil {
8181
t.Fatalf("unexpected error in ParseQuery: %s", err)
8282
}
83+
84+
if _, ok := q.f.(*filterNoop); ok {
85+
// The query is noop, so there are no common tokens
86+
if len(tokensExpected) > 0 {
87+
t.Fatalf("got no tokens, expecting tokens %q", tokensExpected)
88+
}
89+
return
90+
}
91+
8392
fo, ok := q.f.(*filterOr)
8493
if !ok {
8594
t.Fatalf("unexpected filter type: %T; want *filterOr", q.f)
@@ -103,7 +112,7 @@ func TestGetCommonTokensForOrFilters(t *testing.T) {
103112
// no common tokens
104113
f(`foo OR bar`, nil)
105114

106-
// star filter matches non-empty value; it is skipped, since one of OR filters may contain an empty filter
115+
// OR filters with star are reduced to filterNoop
107116
f(`* OR foo`, nil)
108117
f(`foo or *`, nil)
109118
f(`* or *`, nil)

lib/logstorage/parser.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -666,7 +666,7 @@ func visitSubqueriesInFilter(f filter, visitFunc func(q *Query)) {
666666
}
667667
return false
668668
}
669-
_ = visitFilter(f, callback)
669+
_ = visitFilterRecursive(f, callback)
670670
}
671671

672672
func mergeFiltersStream(f filter) filter {
@@ -986,6 +986,28 @@ func removeStarFilters(f filter) filter {
986986
logger.Panicf("BUG: unexpected error: %s", err)
987987
}
988988

989+
// Replace filterOr with filterNoop if one of its sub-filters are filterNoop
990+
visitFunc = func(f filter) bool {
991+
fo, ok := f.(*filterOr)
992+
if !ok {
993+
return false
994+
}
995+
for _, f := range fo.filters {
996+
if _, ok := f.(*filterNoop); ok {
997+
return true
998+
}
999+
}
1000+
return false
1001+
}
1002+
copyFunc = func(_ filter) (filter, error) {
1003+
fn := &filterNoop{}
1004+
return fn, nil
1005+
}
1006+
f, err = copyFilter(f, visitFunc, copyFunc)
1007+
if err != nil {
1008+
logger.Panicf("BUG: unexpected error: %s", err)
1009+
}
1010+
9891011
// Drop filterNoop inside filterAnd
9901012
visitFunc = func(f filter) bool {
9911013
fa, ok := f.(*filterAnd)

lib/logstorage/parser_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,31 @@ func TestQuery_AddTimeFilter(t *testing.T) {
142142
f(`* | format if (x:contains_all(options(ignore_global_time_filter=true) y | keep x)) "foo"`, `_time:[2024-12-25T14:56:43Z,2025-01-13T12:45:34Z] * | format if (x:contains_all(options(ignore_global_time_filter=true) y | fields x)) foo`)
143143
}
144144

145+
func TestParseQuery_OptimizeStarFilters(t *testing.T) {
146+
f := func(s, resultExpected string) {
147+
t.Helper()
148+
149+
q, err := ParseQuery(s)
150+
if err != nil {
151+
t.Fatalf("unexpected error: %s", err)
152+
}
153+
result := q.String()
154+
if result != resultExpected {
155+
t.Fatalf("unexpected result; got\n%s\nwant\n%s", result, resultExpected)
156+
}
157+
}
158+
159+
f(`*`, `*`)
160+
f(`foo * bar`, `foo bar`)
161+
f(`foo or * or bar`, `*`)
162+
f(`foo and (bar or *)`, `foo`)
163+
f(`foo or (* or (baz and (x and *))) x`, `foo or x`)
164+
}
165+
145166
func TestParseQuery_OptimizeStreamFilters(t *testing.T) {
146167
f := func(s, resultExpected string) {
147168
t.Helper()
169+
148170
q, err := ParseQuery(s)
149171
if err != nil {
150172
t.Fatalf("unexpected error: %s", err)

lib/logstorage/storage_search.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -698,7 +698,7 @@ func hasFilterInWithQueryForFilter(f filter) bool {
698698
return false
699699
}
700700
}
701-
return visitFilter(f, visitFunc)
701+
return visitFilterRecursive(f, visitFunc)
702702
}
703703

704704
func hasFilterInWithQueryForPipes(pipes []pipe) bool {
@@ -1192,7 +1192,7 @@ func hasStreamFilters(f filter) bool {
11921192
_, ok := f.(*filterStream)
11931193
return ok
11941194
}
1195-
return visitFilter(f, visitFunc)
1195+
return visitFilterRecursive(f, visitFunc)
11961196
}
11971197

11981198
func initStreamFilters(tenantIDs []TenantID, idb *indexdb, f filter) filter {

0 commit comments

Comments
 (0)