Skip to content

Commit 7220402

Browse files
committed
Make distinct deduplication linear after sorting
1 parent 48e2d93 commit 7220402

3 files changed

Lines changed: 118 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@
66

77
### Fixed 🐛
88

9+
- **Large `distinct` results no longer perform quadratic BSON
10+
deduplication.** The handler previously called linear `Array.Contains` for
11+
every value even after SQLite had already collapsed most duplicates, making
12+
45,640 unique IDs consume about 15–17 seconds of CPU. Values are now gathered,
13+
sorted once as already required by the command, and compacted by comparing
14+
adjacent BSON values; the field path is also parsed once per command instead
15+
of once per document. The representative 45,640-value compaction benchmark
16+
completes in about 5–18 milliseconds. Regression tests cover stable ordering,
17+
cross-width numeric equality and duplicate strings, nulls, documents and
18+
nested arrays by @xet7. Thanks to xet7.
19+
920
- **Numeric `$type` checks combined with a negated range no longer decode whole
1021
SQLite collections.** The unchanged corruption-check selector
1122
`{sort: {$type: "number"}, $nor: [{sort: {$gte: low, $lte: high}}]}` now

internal/handler/common/distinct.go

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/FerretDB/FerretDB/internal/types"
2626
"github.com/FerretDB/FerretDB/internal/util/iterator"
2727
"github.com/FerretDB/FerretDB/internal/util/lazyerrors"
28+
"github.com/FerretDB/FerretDB/internal/util/must"
2829
)
2930

3031
// DistinctParams contains `distinct` command parameters supported by at least one handler.
@@ -93,7 +94,11 @@ func GetDistinctParams(document *types.Document, l *slog.Logger) (*DistinctParam
9394
// If the key is found in the document, and the value is an array, each element of the array is added to the result.
9495
// Otherwise, the value itself is added to the result.
9596
func FilterDistinctValues(iter types.DocumentsIterator, key string) (*types.Array, error) {
96-
distinct := types.MakeArray(0)
97+
values := types.MakeArray(0)
98+
path, err := types.NewPathFromString(key)
99+
if err != nil {
100+
return nil, lazyerrors.Error(err)
101+
}
97102

98103
defer iter.Close()
99104

@@ -107,11 +112,6 @@ func FilterDistinctValues(iter types.DocumentsIterator, key string) (*types.Arra
107112
return nil, lazyerrors.Error(err)
108113
}
109114

110-
path, err := types.NewPathFromString(key)
111-
if err != nil {
112-
return nil, lazyerrors.Error(err)
113-
}
114-
115115
// distinct using dot notation returns the value by valid array index
116116
// or values for the given key in array's document
117117
vals, err := commonpath.FindValues(doc, path, &commonpath.FindValuesOpts{
@@ -131,20 +131,36 @@ func FilterDistinctValues(iter types.DocumentsIterator, key string) (*types.Arra
131131
return nil, lazyerrors.Error(err)
132132
}
133133

134-
if !distinct.Contains(el) {
135-
distinct.Append(el)
136-
}
134+
values.Append(el)
137135
}
138136

139137
default:
140-
if !distinct.Contains(v) {
141-
distinct.Append(v)
142-
}
138+
values.Append(v)
143139
}
144140
}
145141
}
146142

147-
SortArray(distinct, types.Ascending)
143+
return sortAndDeduplicateDistinctValues(values), nil
144+
145+
}
146+
147+
// sortAndDeduplicateDistinctValues replaces the former incremental
148+
// Array.Contains pass. That pass compared every new value with all preceding
149+
// values and made a large already-distinct result quadratic. Sorting is already
150+
// required by the command; equal BSON values become adjacent and need one
151+
// comparison each during the compaction pass.
152+
func sortAndDeduplicateDistinctValues(values *types.Array) *types.Array {
153+
SortArray(values, types.Ascending)
154+
distinct := types.MakeArray(values.Len())
155+
156+
var previous any
157+
for i := 0; i < values.Len(); i++ {
158+
value := must.NotFail(values.Get(i))
159+
if i == 0 || types.Compare(previous, value) != types.Equal {
160+
distinct.Append(value)
161+
previous = value
162+
}
163+
}
148164

149-
return distinct, nil
165+
return distinct
150166
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright 2021 FerretDB Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package common
16+
17+
import (
18+
"fmt"
19+
"testing"
20+
21+
"github.com/stretchr/testify/assert"
22+
"github.com/stretchr/testify/require"
23+
24+
"github.com/FerretDB/FerretDB/internal/types"
25+
"github.com/FerretDB/FerretDB/internal/util/must"
26+
)
27+
28+
func TestSortAndDeduplicateDistinctValues(t *testing.T) {
29+
t.Parallel()
30+
31+
doc := must.NotFail(types.NewDocument("x", int32(1)))
32+
array := must.NotFail(types.NewArray("nested"))
33+
values := must.NotFail(types.NewArray(
34+
"b", int32(1), doc, "a", types.Null, array,
35+
"a", int64(1), doc.DeepCopy(), types.Null, array.DeepCopy(),
36+
))
37+
38+
actual := sortAndDeduplicateDistinctValues(values)
39+
require.Equal(t, 6, actual.Len())
40+
41+
expected := []any{types.Null, int32(1), "a", "b", doc, array}
42+
for _, value := range expected {
43+
matches := 0
44+
for i := 0; i < actual.Len(); i++ {
45+
if types.Compare(must.NotFail(actual.Get(i)), value) == types.Equal {
46+
matches++
47+
}
48+
}
49+
assert.Equal(t, 1, matches, "value %v must occur exactly once", value)
50+
}
51+
52+
for i := 1; i < actual.Len(); i++ {
53+
assert.NotEqual(t, types.Greater, types.CompareOrderForSort(
54+
must.NotFail(actual.Get(i-1)), must.NotFail(actual.Get(i)), types.Ascending,
55+
))
56+
}
57+
}
58+
59+
func BenchmarkSortAndDeduplicateDistinctValues(b *testing.B) {
60+
const values = 45640
61+
input := make([]string, values)
62+
for i := range input {
63+
input[i] = fmt.Sprintf("%024d", values-i)
64+
}
65+
66+
b.ReportAllocs()
67+
b.ResetTimer()
68+
for i := 0; i < b.N; i++ {
69+
array := types.MakeArray(values)
70+
for _, value := range input {
71+
array.Append(value)
72+
}
73+
if got := sortAndDeduplicateDistinctValues(array); got.Len() != values {
74+
b.Fatalf("got %d values", got.Len())
75+
}
76+
}
77+
}

0 commit comments

Comments
 (0)