5.2 Series and Samples
The metric store is a single SQLite database, and unlike the event and log stores it is organised around series rather than records. Individual samples are appended to a series that already has an identity.
5.2.1 The series table #
| Column | Type | Contents |
|---|---|---|
id | INTEGER PRIMARY KEY | Series identifier; the foreign key samples uses. |
name | TEXT NOT NULL | The metric name. |
labels | TEXT NOT NULL | Canonical label representation. Empty string for no labels. |
type | INTEGER NOT NULL | 0 counter, 1 gauge, 2 histogram. |
label_hash | INTEGER NOT NULL | Hash of the canonical label string. |
boundaries_hash | INTEGER | Hash of the canonical boundary blob. Null for counters and gauges. |
boundaries | BLOB | Canonical boundary blob. Null for counters and gauges. |
5.2.1.1 The canonical label string #
Labels are sorted by key in unsigned UTF-8 byte order, each pair written
key=value, and the pairs joined with commas: core=0,host=server1.
The empty label set is the empty string.
No escaping is performed and none is needed, because ingestion rejects
= and , inside a key or a value (PSPU §3.10). That prohibition
exists precisely to make this encoding unambiguous, and it is the reason
the constraint binds the producer rather than being handled internally.
5.2.1.2 The boundaries blob #
A fixed binary encoding, not MessagePack:
boundary_count,u32little-endian- that many IEEE-754
f64values, each little-endian, in the validated order the producer sent
It exists only to identify histogram series and to resolve hash collisions, and is never returned in a query result.
5.2.1.3 Hashes narrow, they do not decide #
label_hash and boundaries_hash are 64-bit FNV-1a over the exact
bytes of the canonical string or blob, with offset basis
0xcbf29ce484222325 and prime 0x100000001b3. The high bit is cleared
before storage, hash & 0x7fff_ffff_ffff_ffff, so the value always fits
SQLite's signed INTEGER.
A lookup always verifies the full labels string, and for a histogram
the full boundaries blob, after narrowing by hash. A hash is an index
key, never an identity: two label sets that collide are still two
series.
5.2.1.4 Uniqueness #
The table carries UNIQUE(name, labels, boundaries_hash).
For counters and gauges boundaries_hash is null, and SQLite treats
nulls as distinct in a unique constraint — so the constraint does not
enforce uniqueness for them. What does is the single-writer resolution
logic (§5.3), which checks before inserting. The constraint is a
defensive backstop against a future change that introduces a second
write path, not the primary mechanism.
type is not part of the identity. A record resolving to an
existing series with a different type resolves successfully and is then
dropped for the mismatch (§5.1).
5.2.2 The samples table #
| Column | Type | Contents |
|---|---|---|
id | INTEGER PRIMARY KEY | Internal row identifier; the tiebreaker for samples sharing a series and timestamp. |
series_id | INTEGER NOT NULL | References series(id). |
boot_id | BLOB NOT NULL | 16-byte boot ID GUID. |
timestamp | INTEGER NOT NULL | Nanoseconds since the Unix epoch. |
value | REAL NOT NULL | The raw value for counters and gauges. Stores 0 for histograms. |
histogram_data | BLOB | Canonical MessagePack histogram sample map. Null for counters and gauges. |
For a histogram, histogram_data is a canonical MessagePack map
(PSPU §3.5) with exactly four keys: boundaries, an array of float64
in the producer's order; counts, an array of unsigned integers;
total_count; and sum, a finite float64.
value is a placeholder for histogram rows and is never returned as a
metric query value. Storing 0 rather than null keeps the column
NOT NULL and keeps the row layout uniform.
Canonical encoding is required here because a stored sample map must be byte-stable: two equal histograms encode identically, which is what makes them comparable without decoding.
boot_id is per sample and is not part of the series identity, so a
series stays continuous across a reboot (§3.7).
5.2.3 Ordering #
Query execution order within a series is always (timestamp, id)
ascending, never insertion order alone. Duplicate timestamps are
permitted and id gives them a stable order.
id is internal. It is never exposed as a query field, a result field,
an access-control field, or a reserved label key (PSPU §3.28).
5.2.4 The rollups table #
The database also holds rollups, defined in §5.6. Rollups are
boot-agnostic scalar aggregates and carry no boot_id, which is why a
boot-filtered metric query is never served from one.
5.2.5 Write-time indexes #
idx_samples_series_timestamponsamples(series_id, timestamp, id)— the dominant pattern is "samples for series X over range Y in deterministic order", and this one composite index serves the series lookup, the range filter and the(timestamp, id)ordering in a single scan.idx_series_nameonseries(name)— name lookups.idx_series_label_hashonseries(label_hash)— series resolution on the ingestion path.idx_rollups_series_function_windowonrollups(series_id, function, window_seconds, window_start).
5.2.6 Schema version #
A metadata table with the same structure as the other stores' (§3.1).
Version 1 comprises series, samples, rollups and metadata; the
current value is in §B. eventd checks it at startup, applies the
lifecycle rules of §5.4, and does not migrate.