6.4 Cross-Shard Fan-Out

Event queries execute against every database in the event store directory — active shards and historical ones alike (§3.3). Log and metric queries touch one database each and need none of this.

Shards carry no meaning for the query path (§2.3). A shard holds whatever CPUs routed to it during whatever lifetimes wrote it, so there is no shard a query can skip on the basis of its contents, and a predicate on event.cpu scans all of them.

6.4.1 Merging #

How results combine depends on the query.

Queries in the default order. A non-aggregating query with no SORT reads every shard at once, each newest first through idx_events_timestamp, and the coordinator takes whichever shard's next row comes first under the tiebreakers of §6.2: an N-way merge of the sorted streams. Each row that passes access control and every predicate is sent as soon as it is taken. Once SKIP + TAKE rows have passed, the merge stops. What the query holds is one row per shard, the merge frontier, whatever the size of its result.

Sorted queries. A SORT on any other field reads the shards in turn and gathers the visible rows. With TAKE it keeps only the best SKIP + TAKE of them, trimming to that whenever it holds twice as many or a thousand, whichever is more. Without TAKE it holds every visible row. Either way it holds them within MaxQueryHeldBytes (§6.5).

Aggregating queries. Rows from every shard fold into one set of groups as they are read, and no row is kept. Each group keeps:

QueryEach group keeps
COUNT BY, TOP N BY, GROUP … COUNTa count; at the end the groups are sorted by count descending and TAKE applies
GROUP … SUMthe exact integer sum while every value is an integer and it fits, and the binary64 sum
GROUP … AVGthe binary64 sum and count, divided once at the end
GROUP … MIN / MAXthe extreme so far
DISTINCTthe value

GROUP … SUM, AVG, MIN and MAX groups are sorted by their keys ascending, in the order the fields are named, before SKIP and TAKE apply. An explicit SORT overrides that order, with group-key order breaking its ties (PSPU §3.21).

AVG is the one that cannot be composed from its own output: averaging partial averages weights each part equally, regardless of how many rows it held, so the sum and the count are kept and divided once.

A group is found by hashing a key that every pair of language-equal values shares — numbers by their nearest binary64, text with ASCII case folded — and then by the language's own equality against each group's representative in the order the groups appeared (§6.2). The hash only narrows the search; equality decides.

What an aggregation holds is bounded by the number of its groups rather than by its rows, within MaxQueryHeldBytes (§6.5).

6.4.2 The unbounded case #

A non-aggregating query without TAKE has no implicit row limit. EVENTS SINCE 7d ago may match millions of rows, all of which pass through the merge.

In the default order that costs time and no memory, so the query timeout is the only backstop (§6.5). The timeout covers sending as well as reading, so a client that reads slowly can run out of time on a result the merge would have finished.

6.4.3 Descriptors #

An event query opens a read-only connection per database, and each SQLite connection holds one or two descriptors for the database and its write-ahead log. With many historical shards this adds up quickly across concurrent queries.

Active shard writer connections stay open for the process lifetime and are not negotiable. Historical shard read connections are the pool worth bounding — opened when a query touches them, closed after a period of inactivity (§C).

Edit this page