Skip to content

Commit 6c1c19c

Browse files
authored
Optimized order by that lazily loads more data when needed (#410)
1 parent be66629 commit 6c1c19c

19 files changed

+501
-75
lines changed

.changeset/common-clubs-add.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@tanstack/db-ivm": patch
3+
"@tanstack/db": patch
4+
---
5+
6+
Optimize order by to lazily load ordered data if a range index is available on the field that is being ordered on.

packages/db-ivm/src/operators/orderBy.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ export interface OrderByOptions<Ve> {
1212
offset?: number
1313
}
1414

15+
type OrderByWithFractionalIndexOptions<Ve> = OrderByOptions<Ve> & {
16+
setSizeCallback?: (getSize: () => number) => void
17+
}
18+
1519
/**
1620
* Orders the elements and limits the number of results, with optional offset
1721
* This requires a keyed stream, and uses the `topK` operator to order all the elements.
@@ -136,13 +140,14 @@ export function orderByWithFractionalIndexBase<
136140
valueExtractor: (
137141
value: T extends KeyValue<unknown, infer V> ? V : never
138142
) => Ve,
139-
options?: OrderByOptions<Ve>
143+
options?: OrderByWithFractionalIndexOptions<Ve>
140144
) {
141145
type KeyType = T extends KeyValue<infer K, unknown> ? K : never
142146
type ValueType = T extends KeyValue<unknown, infer V> ? V : never
143147

144148
const limit = options?.limit ?? Infinity
145149
const offset = options?.offset ?? 0
150+
const setSizeCallback = options?.setSizeCallback
146151
const comparator =
147152
options?.comparator ??
148153
((a, b) => {
@@ -162,6 +167,7 @@ export function orderByWithFractionalIndexBase<
162167
{
163168
limit,
164169
offset,
170+
setSizeCallback,
165171
}
166172
),
167173
consolidate()
@@ -185,7 +191,7 @@ export function orderByWithFractionalIndex<
185191
valueExtractor: (
186192
value: T extends KeyValue<unknown, infer V> ? V : never
187193
) => Ve,
188-
options?: OrderByOptions<Ve>
194+
options?: OrderByWithFractionalIndexOptions<Ve>
189195
) {
190196
return orderByWithFractionalIndexBase(
191197
topKWithFractionalIndex,

packages/db-ivm/src/operators/topKWithFractionalIndex.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { IStreamBuilder, PipedOperator } from "../types.js"
99
export interface TopKWithFractionalIndexOptions {
1010
limit?: number
1111
offset?: number
12+
setSizeCallback?: (getSize: () => number) => void
1213
}
1314

1415
export type TopKChanges<V> = {
@@ -23,6 +24,7 @@ export type TopKChanges<V> = {
2324
* and returns changes to the topK.
2425
*/
2526
export interface TopK<V> {
27+
size: number
2628
insert: (value: V) => TopKChanges<V>
2729
delete: (value: V) => TopKChanges<V>
2830
}
@@ -49,6 +51,13 @@ class TopKArray<V> implements TopK<V> {
4951
this.#comparator = comparator
5052
}
5153

54+
get size(): number {
55+
const offset = this.#topKStart
56+
const limit = this.#topKEnd - this.#topKStart
57+
const available = this.#sortedValues.length - offset
58+
return Math.max(0, Math.min(limit, available))
59+
}
60+
5261
insert(value: V): TopKChanges<V> {
5362
const result: TopKChanges<V> = { moveIn: null, moveOut: null }
5463

@@ -169,6 +178,8 @@ export class TopKWithFractionalIndexOperator<K, T> extends UnaryOperator<
169178
*/
170179
#topK: TopK<TaggedValue<K, T>>
171180

181+
#limit: number
182+
172183
constructor(
173184
id: number,
174185
inputA: DifferenceStreamReader<[K, T]>,
@@ -177,7 +188,7 @@ export class TopKWithFractionalIndexOperator<K, T> extends UnaryOperator<
177188
options: TopKWithFractionalIndexOptions
178189
) {
179190
super(id, inputA, output)
180-
const limit = options.limit ?? Infinity
191+
this.#limit = options.limit ?? Infinity
181192
const offset = options.offset ?? 0
182193
const compareTaggedValues = (
183194
a: TaggedValue<K, T>,
@@ -193,7 +204,8 @@ export class TopKWithFractionalIndexOperator<K, T> extends UnaryOperator<
193204
const tieBreakerB = getTag(b)
194205
return tieBreakerA - tieBreakerB
195206
}
196-
this.#topK = this.createTopK(offset, limit, compareTaggedValues)
207+
this.#topK = this.createTopK(offset, this.#limit, compareTaggedValues)
208+
options.setSizeCallback?.(() => this.#topK.size)
197209
}
198210

199211
protected createTopK(

packages/db-ivm/src/operators/topKWithFractionalIndexBTree.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ class TopKTree<V> implements TopK<V> {
7575
this.#tree = new BTree(undefined, comparator)
7676
}
7777

78+
get size(): number {
79+
const offset = this.#topKStart
80+
const limit = this.#topKEnd - this.#topKStart
81+
const available = this.#tree.size - offset
82+
return Math.max(0, Math.min(limit, available))
83+
}
84+
7885
/**
7986
* Insert a *new* value.
8087
* Ignores the value if it is already present.

packages/db/src/indexes/auto-index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ export function ensureIndexForField<
2929
>(
3030
fieldName: string,
3131
fieldPath: Array<string>,
32-
collection: CollectionImpl<T, TKey, any, any, any>
32+
collection: CollectionImpl<T, TKey, any, any, any>,
33+
compareFn?: (a: any, b: any) => number
3334
) {
3435
if (!shouldAutoIndex(collection)) {
3536
return
@@ -49,6 +50,7 @@ export function ensureIndexForField<
4950
collection.createIndex((row) => (row as any)[fieldName], {
5051
name: `auto_${fieldName}`,
5152
indexType: BTreeIndex,
53+
options: compareFn ? { compareFn } : {},
5254
})
5355
} catch (error) {
5456
console.warn(`Failed to create auto-index for field "${fieldName}":`, error)

packages/db/src/indexes/base-index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { compileSingleRowExpression } from "../query/compiler/evaluators.js"
22
import { comparisonFunctions } from "../query/builder/functions.js"
3-
import type { BasicExpression } from "../query/ir.js"
3+
import type { BasicExpression, OrderByDirection } from "../query/ir.js"
44

55
/**
66
* Operations that indexes can support, imported from available comparison functions
@@ -56,6 +56,11 @@ export abstract class BaseIndex<
5656
abstract build(entries: Iterable<[TKey, any]>): void
5757
abstract clear(): void
5858
abstract lookup(operation: IndexOperation, value: any): Set<TKey>
59+
abstract take(
60+
n: number,
61+
direction?: OrderByDirection,
62+
from?: TKey
63+
): Array<TKey>
5964
abstract get keyCount(): number
6065

6166
// Common methods

packages/db/src/indexes/btree-index.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,35 @@ export class BTreeIndex<
230230
return result
231231
}
232232

233+
/**
234+
* Returns the next n items after the provided item or the first n items if no from item is provided.
235+
* @param n - The number of items to return
236+
* @param from - The item to start from (exclusive). Starts from the smallest item (inclusive) if not provided.
237+
* @returns The next n items after the provided key. Returns the first n items if no from item is provided.
238+
*/
239+
take(n: number, from?: any): Array<TKey> {
240+
const keysInResult: Set<TKey> = new Set()
241+
const result: Array<TKey> = []
242+
const nextKey = (k?: any) => this.orderedEntries.nextHigherKey(k)
243+
let key = from
244+
245+
while ((key = nextKey(key)) && result.length < n) {
246+
const keys = this.valueMap.get(key)
247+
if (keys) {
248+
const it = keys.values()
249+
let ks: TKey | undefined
250+
while (result.length < n && (ks = it.next().value)) {
251+
if (!keysInResult.has(ks)) {
252+
result.push(ks)
253+
keysInResult.add(ks)
254+
}
255+
}
256+
}
257+
}
258+
259+
return result
260+
}
261+
233262
/**
234263
* Performs an IN array lookup
235264
*/

packages/db/src/indexes/index-options.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export interface IndexOptions<TResolver extends IndexResolver = IndexResolver> {
88
indexType?: TResolver
99
options?: TResolver extends IndexConstructor<any>
1010
? TResolver extends new (
11-
id: string,
11+
id: number,
1212
expr: any,
1313
name?: string,
1414
options?: infer O
@@ -17,7 +17,7 @@ export interface IndexOptions<TResolver extends IndexResolver = IndexResolver> {
1717
: never
1818
: TResolver extends () => Promise<infer TCtor>
1919
? TCtor extends new (
20-
id: string,
20+
id: number,
2121
expr: any,
2222
name?: string,
2323
options?: infer O

packages/db/src/query/compiler/evaluators.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@ export type CompiledSingleRowExpression = (item: Record<string, unknown>) => any
2020
* Compiles an expression into an optimized evaluator function.
2121
* This eliminates branching during evaluation by pre-compiling the expression structure.
2222
*/
23-
export function compileExpression(expr: BasicExpression): CompiledExpression {
24-
const compiledFn = compileExpressionInternal(expr, false)
25-
return compiledFn as CompiledExpression
23+
export function compileExpression(
24+
expr: BasicExpression,
25+
isSingleRow: boolean = false
26+
): CompiledExpression | CompiledSingleRowExpression {
27+
const compiledFn = compileExpressionInternal(expr, isSingleRow)
28+
return compiledFn
2629
}
2730

2831
/**

packages/db/src/query/compiler/group-by.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,9 @@ export function processGroupBy(
166166
const mapping = validateAndCreateMapping(groupByClause, selectClause)
167167

168168
// Pre-compile groupBy expressions
169-
const compiledGroupByExpressions = groupByClause.map(compileExpression)
169+
const compiledGroupByExpressions = groupByClause.map((e) =>
170+
compileExpression(e)
171+
)
170172

171173
// Create a key extractor function using simple __key_X format
172174
const keyExtractor = ([, row]: [

0 commit comments

Comments
 (0)