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:
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).