Storage format
This page describes, byte by byte, what muscles writes to disk. It is for an engineer who wants to read the data without the server, or who wants to know what a release writes before trusting it with data. You do not need it to run muscles.
This is the beta format: from the beta on, every release reads the files that an earlier release wrote. A section marked Work in progress describes what the beta writes; its layout is settled, and the muscles code that writes it is being finished against this page.
Every structure has a table of offsets, sizes and meanings, and the worked examples at the end show real bytes with real checksums. The page is written so that a reader can be built from it, and a standalone reader is being written against it.
Conventions
- Every integer is little-endian.
u8,u16,u32,u64are unsigned,i8toi64,i128andi256are two's complement,f32andf64are IEEE 754. var_uintis unsigned LEB128: seven bits per byte, low bits first, the high bit set when another byte follows, at most 10 bytes, and the tenth byte is00or01so that the value fits au64, and a writer emits the shortest form.- A length or a count in metadata is a
var_uint. Offsets and counts inside column data are fixed width, so a reader can index them without a scan. - A name is a
var_uintlength followed by UTF-8 bytes. - The checksum everywhere is CRC32C: polynomial
0x1EDC6F41, reflected, initial value and final xor0xFFFFFFFF. The check value iscrc32c("123456789") = 0xE3069283. Where one file records the checksum of another file that ends in a checksum, it records that file's trailer value, because the CRC of a file over its own CRC is always the constant0x48674BC7. - Every file, object and record starts with a 4-byte magic and a 1-byte version. A reader that meets a version it does not know refuses the file. A file is exactly one envelope and bytes after the trailer are an error, except in the S3 bundle, whose files follow its envelope by design.
- Every length, count and offset is checked against the bytes held and against the limits before a reader allocates or indexes.
- A time value is a UTC instant. A time zone in a type is a display attribute, so a change of the zone touches metadata and no stored byte.
The envelope Work in progress
Every file that is not a stream of records uses one envelope. A stream, such as a write-ahead log segment, uses it once at the head and its records follow the envelope's checksum.
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 4 | bytes | magic | one per format, listed in the sections below |
| 4 | 1 | u8 | version | the layout of the fixed body; changes only when that layout changes |
| 5 | 1 | u8 | flags | bit 0: the body is a codec block; bit 1: an optional-field section follows the body; bits 2 to 7 are reserved, must be 0, and a reader refuses the file when one is set |
| 6 | 4 | u32 | body_len | bytes of the body as stored |
| 10 | body_len | bytes | body | the format's fixed layout |
| 10 + body_len | 4 | u32 | section_len | present when flags bit 1 is set: the bytes of the entries that follow |
| 14 + body_len | section_len | entries | optional fields | each entry is tag var_uint, len var_uint, len bytes; at most 256 entries, with tags in strictly ascending order |
| end − 4 | 4 | u32 | crc32c | over every byte before it |
An optional field's tag carries the must-understand bit in bit 0. A reader skips an entry with an even tag it does not know and refuses a file with an odd tag it does not know. Tags 0 and 1 are invalid. Each format assigns its own tags, and a field whose absence would change the meaning of the data gets an odd tag.
The codec block
Compressed bytes always sit behind this 20-byte header: every stream of a column block, a compressed write-ahead log insert record, and a compressed envelope body. The header names the chain that produced the payload, so a reader needs no other source for it.
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 1 | u8 | magic | 0xC6 |
| 1 | 1 | u8 | version | 1 |
| 2 | 1 | u8 | n_stages | 1 to 4 |
| 3 | 1 | u8 | reserved | 0 |
| 4 | 8 | 4 × (u8, u8) | stages | codec id and parameter per stage; unused pairs are 0 |
| 12 | 4 | u32 | raw_len | bytes after decoding, checked against the limit before allocation |
| 16 | 4 | u32 | stored_len | bytes of the payload |
| 20 | stored_len | bytes | payload |
Stages run in order on encode and in reverse on decode. A general compressor (lz4 or zstd) is the last stage and there is at most one; none is always the only stage; the payload of each stage is:
| id | codec | parameter | payload |
|---|---|---|---|
| 0 | none | 0 | the raw bytes |
| 1 | lz4 | 0 | one LZ4 block in the block format, without a frame |
| 2 | zstd | the level, 1 to 22 | one zstd frame that carries its content size |
| 3 | rle | 0 | pairs of byte, run, with the run from 1 to 255 |
| 4 | delta | the lane width, 1, 2, 4 or 8 | the same length as the input, which is a multiple of the width: lane 0 is v[0], lane i is v[i] − v[i − 1] with wrapping arithmetic |
| 5 | double_delta | the lane width; 8 at reader level 1 | 8-byte lanes. u32 value count, then a bit stream packed most significant bit first: v[0] as 64 bits, then for each next value the delta of deltas, (v[i] − v[i − 1]) − (v[i − 1] − v[i − 2]), with a prefix: 0 when it is zero; 10 then 8 bits; 110 then 16 bits; 1110 then 32 bits; 1111 then 64 bits, each two's complement. The low bits of the last byte are zero padding. |
| 6 | gorilla | the lane width; 8 at reader level 1 | 8-byte lanes. u32 value count, then a bit stream packed most significant bit first: v[0] as 64 raw bits, then for each next value the xor with the previous bit pattern: 0 when it is zero; 10 then the meaningful bits in the previous window; 11 then 6 bits of leading-zero count, 6 bits of the meaningful length minus one, then the meaningful bits |
| 7 | t64 | reserved; no writer emits it and a reader refuses it like an unknown id | |
| 8 | retired | never reused | |
| 9 to 127 | later codecs | ||
| 128 to 255 | experiments | never in a shipped file |
When a general compressor follows a stage whose output length the header does not record (rle, double_delta, gorilla), its payload starts with u32 intermediate_len, the length of the bytes it decodes to; after delta, which keeps the length, that length is raw_len. The width parameter of double_delta and gorilla lets a later reader level add 4-byte lanes; a level-1 reader refuses any value but 8.
A reader that meets an id it does not know refuses that stream with an unsupported-codec error. A new codec therefore costs an older reader one stream of one column in one granule, never the part.
The type descriptor
A column type is written in the ClickHouse binary type encoding: one id byte, the type's parameters, and the descriptors of its children for a composite. The ids are ClickHouse's own, so a type read from a ClickHouse Native export decodes with the same code. The table lists the ids the beta writer emits.
| type | bytes |
|---|---|
Nothing | 00 |
UInt8, UInt16, UInt32, UInt64, UInt128, UInt256 | 01 to 06 |
Int8, Int16, Int32, Int64, Int128, Int256 | 07 to 0C |
Float32, Float64 | 0D, 0E |
Date, Date32, DateTime | 0F, 10, 11 |
DateTime('tz') | 12 var_uint name length, name |
DateTime64(P) | 13 u8 P |
DateTime64(P, 'tz') | 14 u8 P, var_uint name length, name |
String | 15 |
FixedString(N) | 16 var_uint N |
Enum8 | 17 var_uint count, then per member: name, i8 value |
Enum16 | 18 var_uint count, then per member: name, i16 value |
Decimal32(P, S), Decimal64, Decimal128, Decimal256 | 19 to 1C, then u8 P, u8 S |
UUID | 1D |
Array(T) | 1E T |
Tuple(T1, ...) | 1F var_uint count, T1 ... |
Tuple(a T1, ...) (named) | 20 var_uint count, then per element: name, T |
Nullable(T) | 23 T |
LowCardinality(T) | 26 T |
Map(K, V) | 27 K V |
IPv4, IPv6 | 28, 29 |
Bool | 2D |
| aggregate state | F0 01, then a ClickHouse AggregateFunction descriptor: 25, var_uint version (0), name, var_uint parameter count, parameters as ClickHouse Field values, var_uint argument count, argument descriptors |
The prefix F0 followed by a var_uint extension id marks a muscles extension. Extension 1 wraps a ClickHouse AggregateFunction descriptor and says that the cells hold muscles aggregate states. No other extension is defined.
A reader that meets any other ClickHouse id, among them Variant, Dynamic, JSON, Nested and QBit, reports an unsupported type for that column, skips its bytes by the stream lengths, and reads the rest of the part. A later revision of this page defines their layouts; adding one adds an id and bumps no version.
Time types and their ranges: Date as u16 days ends on 2149-06-06 and DateTime as u32 seconds on 2106-02-07; Date32 and DateTime64 reach far past both. For Iceberg interchange, DateTime64(6) maps to timestamp, DateTime64(6, 'UTC') to timestamptz, and Date and Date32 to date.
The column encoding Work in progress
A column block holds n rows of one column as a list of streams, and each stream is its own codec block. A compression transform therefore sees exactly the bytes it expects: the values of a UInt64 column are a stream of 8n bytes with nothing in front of them. The same block appears in write-ahead log insert records, in the granules of a column file, in the dictionary file and in the primary index.
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 1 | u8 | flags | bit 0: the top-level validity stream is omitted and every row holds a value; bit 1: every LowCardinality dictionary of the block is inline, before its indexes; bit 2: compact streams; bits 3 to 7 are reserved, must be 0, and a reader refuses the block when one is set |
| 1 | .. | var_uint | stream_count | |
| .. | .. | streams | streams | per stream: kind u8, then one codec block; in the compact form, kind u8, var_uint length, then the raw bytes |
The compact form is for small log inserts, where a 20-byte codec header per stream would outweigh the values; a part never uses it. Stream kinds: 0 validity, 1 values, 2 offsets, 3 data, 4 indexes. A reader refuses a kind from 5 to 127 and may skip one from 128 to 255. The streams of a type come in a fixed order, depth first, and every count follows from a stream's raw_len, so there is no count field:
| type | streams |
|---|---|
Nullable(T) | a validity stream unless block flags bit 0 is set, then the streams of T. The bitmap is ceil(n / 64) × 8 bytes; bit r, least significant bit first within each byte, is 1 when row r holds a value. NULL rows hold zero bytes in the other streams (0, an empty string, an empty array). A nested Nullable always writes its bitmap. |
| fixed width | a values stream of n × width bytes. Width 1: UInt8, Int8, Bool, Enum8. Width 2: UInt16, Int16, Date, Enum16. Width 4: UInt32, Int32, Float32, Date32, DateTime, IPv4, Decimal32. Width 8: UInt64, Int64, Float64, DateTime64, Decimal64. Width 16: UInt128, Int128, UUID, IPv6, Decimal128. Width 32: UInt256, Int256, Decimal256. |
String | an offsets stream of u32[n + 1] with offsets[0] = 0 and non-decreasing values, then a data stream of offsets[n] bytes. Cell r is data[offsets[r] .. offsets[r + 1]]. |
FixedString(N) | a values stream of n × N bytes |
Array(T) | an offsets stream in elements, then the streams of T over offsets[n] elements |
Map(K, V) | as Array(Tuple(K, V)): the offsets, the streams of K, then the streams of V |
Tuple(T1, ..., Tk) | the streams of T1, then T2, and so on |
LowCardinality(T) | when block flags bit 1 is set, which is always the case in a log record: the streams of T over the dictionary, then an indexes stream; otherwise the indexes stream alone, into the part's dictionary file for this stream path. The index width is raw_len / n: 1, 2 or 4; when n is 0 the stream is empty and the width is 1. A NULL row points at a dictionary entry whose validity bit is 0. |
| aggregate state | as String; each cell is one MAS1 record |
Nothing | no streams |
Values of the fixed-width types: integers two's complement; Date days since 1970-01-01 as u16, Date32 as i32; DateTime seconds since the epoch as u32; DateTime64(P) ticks of 10−P seconds as i64; a decimal as its scaled integer; IPv4 as the address value in a u32; IPv6 and UUID as the 16 bytes in the order their text form prints them; Bool as 0 or 1. There is no padding or alignment inside a stream.
Every stream has a path: the top level is empty, a tuple element is its ordinal, a map's key and value are key and value, an array's child is elem, and a nullable's child or a dictionary's element adds nothing. A LowCardinality nested in a composite has its own dictionary file, col-<id>.<path>.dict: Map(LowCardinality(String), LowCardinality(String)) in column 7 has col-7.key.dict and col-7.value.dict. The names col-<id>.<path>.bin are reserved for a later layout that stores one stream in its own file, so that a reader can fetch a map's keys or a null map alone. The beta writes every stream of a column into one file, and a reader ignores a stream file it does not expect.
Identities Work in progress
- A table has a 128-bit
table_uuid, a UUID version 7 assigned atCREATE TABLEand kept across renames. Its lowercase 32-digit hex is the table's directory name. - A column has a
column_id u32, assigned from 1 when it is created and never reused by the table; the schema record carries the next id to hand out. Files, blocks and manifests name columns by id, so a rename touches no data file. - A part has a 128-bit
part_uuid, a UUID version 7 assigned when the writer opens it. Every file of the part carries it, so a file from another part fails the check even when the row counts agree. - A row is
(part_uuid, position), with the position au64below the part's row count. A part holds at most 236 rows and 226 granules, the most its marks, primary index and deletion vector can hold within their limits, and a merge that would pass either is not planned. Parts never change, so the pair holds until a merge rewrites the part, and the merge journal names the inputs. No row id is stored in the data. - Every file and replication object names its writer, as a text such as
muscles 0.1.0-beta.1, and amin_reader u16. Level 1 is this page. A writer raises the level only in a release that changes what existing bytes mean; an addition takes a new id or field instead, so a raised level is rare, and a file that exceeds a level-1 limit carries the level that allows it. A merged part carries the highest level among its inputs and the writer. A reader whose level is belowmin_readerrefuses the file; a reader that knows a writer's defect can skip what that writer got wrong.
Write-ahead log Work in progress
Each table has its own log directory, <wal_dir>/<table_uuid>/, of append-only segments named wal-<seq>.wal with a 16-digit zero-padded sequence, so lexicographic order is segment order. A segment starts with the envelope, magic MWAL, version 1, with this body.
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 8 | u64 | segment_seq | the sequence in the file name |
| 8 | 8 | u64 | lsn_floor | no record in this segment carries a lower log sequence number |
| 16 | 8 | u64 | created_at | Unix time in nanoseconds |
| 24 | 16 | bytes | table_uuid | |
| 40 | .. | name | writer | the program and version that wrote the segment |
| .. | 2 | u16 | min_reader | 1 for the beta |
| .. | 4 | u32 | max_insert_raw_bytes | the largest raw columns section the writer emitted; a reader allocates up to it, within the format limit |
Record framing
Records follow the envelope's checksum, each in one frame.
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 4 | u32 | len | bytes of the body |
| 4 | len | bytes | body | kind u8, flags u8, the kind's fixed fields, then an optional-field section in the envelope's form when flags bit 1 is set |
| 4 + len | 8 | u64 | synced | how far the segment's last completed fsync reached when this frame was written; never past the frame's own start |
| 12 + len | 4 | u32 | crc32c | over the body and the synced stamp |
A write is acknowledged after the frame is on disk and synced. On replay, an incomplete frame at the end of the last segment, or a frame with a bad checksum that no later frame's synced stamp covers, is a torn tail: the reader truncates there. A bad frame at offset p is corruption when a later intact frame carries synced > p, because a completed sync covered it; a reader reports it and does not skip it. A last segment whose envelope is incomplete or fails its checksum holds no acknowledged record and is read as empty; the same in an earlier segment is corruption. The directory is synced after a segment is created, renamed or deleted. A kind from 0 to 127 must be understood, and a reader refuses a segment with one it does not know. A kind from 128 to 255 may be skipped.
Insert record, kind 1
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 1 | u8 | kind | 1 |
| 1 | 1 | u8 | flags | bit 0: the columns section is one codec block; bit 1: optional fields follow it |
| 2 | 8 | u64 | first_lsn | row i of the record has sequence number first_lsn + i |
| 10 | 4 | u32 | row_count | |
| 14 | 2 | u16 | column_count | |
| 16 | .. | columns | columns section | per column: column_id var_uint, the descriptor as a var_uint length and bytes, then one column block of row_count rows |
Below 4096 bytes the columns section is stored raw and its blocks use compact streams; from 4096 bytes on, flags bit 0 is set and the section is one LZ4 codec block whose raw bytes hold every stream as a codec block under none; a table setting selects ZSTD instead. Optional field 2 (may be skipped) is the 16-byte insert token that lets a client retry a batch. Optional field 5 (must be understood) is present when one statement spans frames: statement_uuid (16 bytes), seq u32, last u8; replay applies such a statement only when its last frame is present.
Checkpoint, kind 2
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 1 | u8 | kind | 2 |
| 1 | 1 | u8 | flags | bit 1: optional fields follow |
| 2 | 8 | u64 | flushed_lsn | every row at or below this sequence number is in a part; replay skips inserts up to it |
Optional field 3 is a Kafka position, partition i32 and offset i64, the point from which ingest resumes. Its tag is odd: a reader that resumes ingest must understand it.
Schema record, kind 3
The record is the whole table definition. A later record with a higher epoch replaces an earlier one, so a reader keeps the one with the highest schema_epoch.
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 1 | u8 | kind | 3 |
| 1 | 1 | u8 | flags | bit 1: optional fields follow |
| 2 | 8 | u64 | schema_epoch | rises with every change to the table |
| 10 | 16 | bytes | table_uuid | |
| 26 | 4 | u32 | next_column_id | the id the next new column takes; never lower than any id ever used |
| 30 | .. | name | table name | |
| .. | .. | var_uint | column_count | |
| .. | .. | columns | columns | per column: column_id u32; name; descriptor; col_flags u8 (bit 0: an initial default follows as a ClickHouse Field; bit 1: a default expression follows as text); the codec chain as n_stages u8 and (id, param) pairs; a per-column section as a var_uint length and optional-field entries in the envelope's form (even tags: 2 ALIAS expression, 4 MATERIALIZED expression, 6 column TTL, 8 comment) |
| .. | .. | sort key | sort key | var_uint count, then per entry: column_id u32 (0 for an expression), direction u8 (0 ascending, 1 descending), nulls_first u8, the expression as text (empty for a plain column), and the key's descriptor |
| .. | 1 | u8 | engine | 0 plain, 1 replacing, 2 aggregating, 3 collapsing |
| .. | .. | engine key | engine key | var_uint count and column_id u32 per column, then version_column u32 and is_deleted_column u32, 0 when absent |
A col_flags bit a reader does not know refuses the record. When older segments are deleted, the newest schema record is carried forward into the oldest kept segment, so a log always starts with its schema. The initial default is the value a row written before the column existed reads; the default expression is what an insert that omits the column computes. Where a part's descriptor differs from the schema's, the part's descriptor says what the bytes are and a reader casts the values to the schema's type. The optional fields with even-numbered tags carry what a reader of the data does not need: tag 2 the TTL expression, tag 4 the skip-index declarations, tag 6 the Kafka source, tag 8 the table settings. A ClickHouse Field is the value encoding ClickHouse uses for aggregate function parameters: a tag byte (00 NULL, 01 var_uint, 02 zigzag var_int, 07 f64, 0C string as var_uint length and bytes), then the value.
Delete record, kind 4
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 1 | u8 | kind | 4 |
| 1 | 1 | u8 | flags | bit 1: optional fields follow |
| 2 | 16 | bytes | statement_uuid | the DELETE statement this frame belongs to |
| 18 | 4 | u32 | seq | the frame's number within the statement, from 0 |
| 22 | 1 | u8 | last | 1 on the statement's last frame; replay applies a statement only when its last frame is present |
| 23 | .. | var_uint | part_count | |
| .. | .. | parts | parts | per part: part_uuid, generation u64 of the deletion vector this produces, form u8, then the rows: form 0, var_uint count and u64 positions ascending; form 1, var_uint count and (start u64, length u64) ranges; form 2, row_count u64 and a bitmap of u64 words |
Dedup record, kind 5
kind u8 = 5, flags u8, var_uint count, then 16-byte insert tokens that the table still remembers after older segments were deleted.
Parts Work in progress
A part is an immutable directory under <parts_dir>/<table_uuid>/, named <table_uuid>-<seq>.part when a memtable flush wrote it and <table_uuid>-m<seq>.part when a merge did. The writer builds it under a .tmp name, syncs every file and the directory, and publishes it with one atomic rename. A directory whose name ends in .tmp is not a part.
| file | magic | content |
|---|---|---|
part.manifest | MUS1 v1 | row count, columns, zone maps, checksums of the other files, the merge journal |
col-<id>.bin | MCOL v1 | one column: a 32-byte header, then one column block per granule |
col-<id>.mrk | MMRK v1 | where each granule of that column starts, its length and its checksum |
primary.idx | MPIX v1 | the first row of each granule and the sort key of that row |
col-<id>.dict, col-<id>.<path>.dict | MDCT v1 | the dictionary of a LowCardinality column or of one nested in a composite |
rows.del | MDLV v1 | the deletion vector; absent when no row is deleted |
minmax-<id>.mski, set-<id>.sski, bloom-<id>.bski, textbloom-<id>.tbki | MSKI, SSKI, BSKI, TBKI | skip indexes; a reader may ignore them |
mapkeys-<id>.mkd, sidx-<id>.mftx, sidx-<id>.mvix | MKD1, MFTX, MVIX | the map-key digest, the full-text index and the vector index; a reader may ignore them |
part.tombstone, part.deleting, part.cold, part.moved, part.replicated | MTMB, MDEL, MCLD, MMOV, MREP | markers, described under directories and markers |
A file in a part directory that this table does not name is ignored. The column data is always authoritative: a damaged or missing index costs a reader speed and never a wrong row.
The manifest
The envelope's magic is MUS1 and the version is 1. The body:
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 16 | bytes | part_uuid | |
| 16 | 16 | bytes | table_uuid | |
| 32 | .. | name | writer | the program and version that wrote the part |
| .. | 2 | u16 | min_reader | 1 for the beta |
| .. | 8 | u64 | row_count | physical rows, including deleted ones |
| .. | 4 | u32 | granule_rows | the most rows a granule holds; a granule may hold fewer |
| .. | 4 | u32 | granule_count | |
| .. | 8 | u64 | block | install sequence on the node that wrote the part |
| .. | 8 | u64 | content_id | a hash of the rows, used to recognise an identical part |
| .. | 8 | u64 | first_lsn | the first log sequence number the part covers, plus one; 0 when the part covers none |
| .. | 8 | u64 | last_lsn | the last one, plus one |
| .. | 8 | u64 | schema_epoch | the schema the part was written under |
| .. | 8 | u64 | written_at | Unix time in nanoseconds |
| .. | 4 | u32 | max_block_raw_bytes | the largest raw stream in the part; a reader allocates up to it, within the format limit |
| .. | .. | name | table name | |
| .. | .. | sort key | sort key | var_uint count, then entries as in the schema record |
| .. | 12 | u64, u32 | primary index | the length of primary.idx and its checksum (its trailer) |
| .. | .. | var_uint | column_count | |
| .. | .. | columns | columns | per column: column_id u32; name; descriptor; col_flags u8; file_len u64 and file_crc u32 over every byte of col-<id>.bin; marks_len u64 and marks_crc u32, the length and trailer of col-<id>.mrk; when col_flags bit 1 is set, a var_uint count and per dictionary file its stream path, file_len u64 and trailer u32; the zone map as a var_uint length and bytes, length 0 when there is none; a per-column section as a var_uint length and optional-field entries in the envelope's form |
| .. | .. | var_uint | replaces_count | the merge journal |
| .. | .. | inputs | replaces | per input part this part replaced: part_uuid, name, masked_rows u64 |
Column flags: bit 0 the column holds at least one NULL, bit 1 dictionary files follow; bits 2 to 7 are reserved, and a reader refuses the manifest when one is set. The per-column section takes even tags: 2 null_count u64, 4 raw_bytes u64 (the sum of the column's raw stream lengths), 8 the initial default as a ClickHouse Field. The manifest's own optional fields: 3 a Kafka position (partition i32, offset i64), 4 the list of sidecar files with their checksums, 6 the value-sorted flag of a collapsing merge key.
Zone maps
Every column has a zone map: the least and greatest non-NULL value in the part, so a WHERE can skip the part before it reads anything. The bytes start with kind u8 and bound_flags u8, then the minimum and the maximum unless bit 3 is set. A kind or a flag bit a reader does not know means there is no zone map and the column is scanned.
- Kind 1, fixed width: each bound at the column's lane width. The order is the type's own, so a reader compares
UInt64values unsigned andInt64values signed and never compares the bytes. Floats compare numerically, with−0equal to+0; as in Parquet, a writer stores a zero minimum as−0and a zero maximum as+0. NaN is left out of the bounds, and bit 2 says the column holds a NaN, so a predicate that could match NaN does not prune. - Kind 2, prefix, for strings: each bound as a
var_uintlength and up to 64 bytes, ordered as unsigned bytes. A minimum that was cut is a prefix of the true minimum and still a lower bound; bit 0 says it was cut. A maximum that was cut would not be an upper bound, so the writer increments the last byte of the prefix that is notFFand drops the bytes after it; when every byte isFFthere is no upper bound and bit 1 says so. Bit 4 says the maximum was cut: it is an upper bound and not the true maximum, so a reader never answersmaxfrom it. - Bit 3 says there are no bounds, because every row is NULL or NaN.
Marks and the primary index
A column file is split into granules: runs of up to granule_rows rows, each a column block of its own. Each column has a marks file, magic MMRK version 1, so a reader fetches the marks of the columns it needs and no others:
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 16 | bytes | part_uuid | |
| 16 | 4 | u32 | column_id | |
| 20 | 4 | u32 | granule_count | equal to the manifest's |
| 24 | 16 per granule | marks | marks | per granule: offset u64 of the column block in the column file, length u32 of the block, crc32c u32 of the block |
The primary index, magic MPIX version 1, holds what every column shares: where each granule starts, and the sort key of its first row.
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 16 | bytes | part_uuid | |
| 16 | 4 | u32 | granule_count | |
| 20 | 8 per granule | u64 | row_index | the first row of each granule |
| .. | .. | var_uint | key_count | the number of sort-key entries |
| .. | .. | blocks | keys | per sort-key entry, one column block of granule_count values: the key of the first row of each granule |
A granule's row count is the difference of adjacent row_index values, and the last granule's against row_count. The stored length in the marks lets a reader fetch one granule from S3 with one range request. The key blocks are typed by the sort key's descriptors, so a 128-bit, decimal or string key needs nothing special.
Column files
A column file starts with a 32-byte header: the magic MCOL, version u8 = 1, flags u8 = 0, reserved u16 = 0, part_uuid, column_id u32, reserved u32 = 0. Then the granule blocks follow back to back, each a column block whose streams are codec blocks. The marks give the offset, the length and the checksum of each block; the manifest gives the length and checksum of the whole file.
A LowCardinality column also has col-<id>.dict: an envelope, magic MDCT version 1, whose body is part_uuid, column_id u32, dict_count u32, then one column block of the element type with dict_count rows. Every granule of the column stores indexes into it, and a nested LowCardinality has a file of its own, named by its stream path, with that path in the body. A column whose dictionary would pass the dictionary limit is written with an inline dictionary in every granule instead.
The deletion vector
DELETE never rewrites a published part. It writes rows.del next to it: an envelope, magic MDLV version 1, with the body below. A reader drops the masked rows from every scan; a later merge leaves them out and the new part has no vector.
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 16 | bytes | part_uuid | must equal the manifest's |
| 16 | 8 | u64 | generation | rises with every DELETE on the part; the highest generation is the current vector |
| 24 | 8 | u64 | row_count | equal to the manifest's |
| 32 | 8 | u64 | deleted_count | the number of deleted rows, a cross-check |
| 40 | 1 | u8 | form | 0 bitmap, 1 ranges, 2 Roaring |
| 41 | .. | rows | rows | form 0: 8 × ceil(row_count / 64) bytes of u64 words, bit r of word r / 64 is 1 when row r is deleted; allowed up to 233 rows. Form 1: u64 count, then (start u64, length u64) ranges, ascending and disjoint. Form 2: a 64-bit Roaring bitmap in the portable serialization of the CRoaring library, the form Iceberg version 3 uses. A reader supports all three; a writer picks the smallest allowed form. |
Skip indexes
A skip index summarises blocks of granularity granules of one column so that a scan can skip a block before it reads it. Each is an envelope whose body starts with the same header, then one entry per block, then nothing else. A reader cross-checks part_uuid, row_count and granule_rows against the manifest and ignores the file on a mismatch.
| offset | size | type | field | meaning |
|---|---|---|---|---|
| 0 | 16 | bytes | part_uuid | |
| 16 | 4 | u32 | column_id | |
| 20 | 4 | u32 | granule_rows | as in the manifest |
| 24 | 4 | u32 | granularity | granules per block, at least 1 |
| 28 | 8 | u64 | row_count | as in the manifest |
| 36 | 4 | u32 | block_count | |
| 40 | .. | entries | blocks | per kind, below |
MSKI, minmax: per blockflags u8(bit 0 the block holds a NULL, bit 1 the bounds are unusable and the block always survives), then a zone map in the manifest's form.SSKI, set: per blockflags u8(bit 0 has NULL, bit 1 the set overflowed and the block always survives),u32count, then the distinct values at lane width, or asvar_uintlength and bytes for strings.BSKI, bloom filter: after the headerm_bits u32(a multiple of 8),k u8,seed u64; per blockflags u8(bit 0 has NULL) andm_bits / 8bytes. A value's hashhis the 64-bit wyhash withseedover its canonical bytes; withh1 = h mod 232andh2 = (h >> 32) | 1, probeiofktests bit(h1 + i × h2) mod m_bits, computed in 64-bit unsigned arithmetic.TBKI, text bloom filter: asBSKIwithsubtype u8(0 n-gram, 1 token) andn u8beforem_bits; each token or n-gram of a cell is hashed as a string.
The map-key digest, the full-text index and the vector index are rebuilt from the column data by the server, so their bodies are not frozen by this page, and a reader ignores a sidecar version it does not know. The full-text index records its tokenizer: id 0 splits on ASCII punctuation and folds ASCII case; id 1 is ngram, whose spec holds min_gram u8 and max_gram u8, the lengths in code points of the terms it indexes. A query side that does not know the tokenizer id scans instead.
The S3 bundle
A part moved to the cold tier is one object: an envelope, magic MPBL version 1, whose body is part_uuid, table_uuid and a file table (var_uint count; per file a name, offset u64, length u64, crc32c u32), followed after the envelope's checksum by the files back to back, with offsets relative to the end of the envelope. The stub part.cold left in the part directory carries part_uuid, the object key relative to the configured prefix, the object length and a copy of the table, so a reader can fetch one granule with one ranged request.
Aggregate states Work in progress
A column of the aggregate-state type holds one state per row, as the result of a -State combinator, a rollup rung or an aggregating materialized view. Each cell is one record: an envelope with magic MAS1, version 1, body kind u8 then the kind's payload. A reader that copies parts can carry a state as opaque bytes; to finish or merge it, it needs the payload. A kind id is never redefined: a changed payload takes a new id.
A held value inside a payload is a tag u8 followed by the value: 0 nothing, 1 i64, 2 f64, 3 a string as var_uint length and bytes, 4 u64, 5 i128, 6 u128, 7 sixteen raw bytes (UUID, IPv6), 8 thirty-two raw bytes (256-bit integers and decimals).
Canonical bytes, for every hash in this format: a signed integer of any width, and a decimal as its scaled integer, as i64 little-endian with wyhash seed 1, or as its 16 or 32 raw bytes with seed 4 when it does not fit; an unsigned one as u64 with seed 2; a float as the f64 bit pattern with seed 3; a string, FixedString, UUID, IPv6 and the wide integers as their raw bytes with seed 4. Hashing the widened value keeps a sketch valid when a column is widened within its class, signed to wider signed, unsigned to wider unsigned, Float32 to Float64; a change of class, such as UInt32 to Int64, rebuilds the sketches. The hash is wyhash final version 4 as the Zig standard library implements it. Test vectors: wyhash(1, i64 1) = 0x736c1462d905b0f3, wyhash(2, u64 1) = 0x3816548725c03c86, wyhash(3, f64 1.5) = 0xc76a38da4e33d8ee, wyhash(4, "ann") = 0x2733844f6ddc4d99, wyhash(0, "") = 0x0409638ee2bde459.
| kind | function | payload |
|---|---|---|
| 0 | count | u64 |
| 1 | sum | lane u8 (1 integer, 2 float, 4 unsigned), i256 integer sum, f64 float sum |
| 2 | avg | the sum payload, then u64 count |
| 3, 4 | min, max | one held value; tag 0 for an empty group |
| 5 | uniqExact | u32 count and i64 keys, then u32 count and string keys |
| 6, 7 | argMin, argMax | have u8, the key as a held value, the value as a held value |
| 8 | groupArray | elem u8 (0 none, 1 integer, 2 float, 3 string), u32 count, then the items as i64, f64 or strings |
| 9 | uniq | precision u8 (the HyperLogLog has 2precision registers; 14 in the beta), form u8: 0 sparse, then u32 count and u32 entries of register << 8 | rank in ascending register order; 1 dense, then u32 length and the register bytes. The register is the top precision bits of the canonical hash; the rank is one plus the leading zeros of the remaining bits, with a guard bit so that it never exceeds 64 − precision + 1. |
| 10 | topK | u32 k, elem u8 (as groupArray, plus 4 unsigned), u32 count, per item: the item, u64 count, u64 error |
| 11 | quantile | a t-digest: f64 compression, n_levels u8, f64 levels, f64 total weight, u32 centroid count, then (f64 mean, f64 weight) per centroid |
| 12 | quantileExact | n_levels u8, f64 levels, u32 count, f64 values |
| 13 to 16 | varPop, varSamp, stddevPop, stddevSamp | u64 n, f64 mean, f64 m2 |
| 17 to 20 | covarPop, covarSamp, corr, simpleLinearRegression | u64 n, f64 mean x, mean y, co-moment, m2 x, m2 y |
| 21, 22 | stochasticLinearRegression, stochasticLogisticRegression | loss u8, f64 learning rate, u32 batch size, u32 max features, u64 n, u32 feature count, then feature count plus one f64 weights |
| 23 | quantileExactInterpolated | as 12 |
| 24 to 26 | groupBitAnd, groupBitOr, groupBitXor | u64 accumulator, seen u8, unsigned u8 |
| 27 | groupBitmap | u32 count, u64 keys |
| 28 | deltaSum | elem u8, i64 sum, first, last, f64 sum, first, last |
| 29, 30 | any, anyLast | have u8, one held value |
| 31 | uniqUpTo | u32 threshold, u32 count, u32 item count, then held values |
| 32 to 34 | sumMap, minMap, maxMap | key_kind u8, val_kind u8, u32 count, then the integer keys as i64 each with its value (i64 or f64 by val_kind), then the string keys each with its value |
| 35 | windowFunnel | i64 window, events u8, flags u8, sorted u8, u32 count, then (i64 ts, u8 event) |
| 36 | retention | events u8, u32 mask |
| 37, 38 | sequenceMatch, sequenceCount | events u8, sorted u8, u32 conditions met, u32 count, then (i64 ts, u32 mask) |
| 39 to 42 | skewPop, skewSamp, kurtPop, kurtSamp | f64 m0, m1, m2, m3, m4 |
| 43 | entropy | u32 count of (i64 key, u64 n), u32 count of (u64 float bits, u64 n), u32 count of (string, u64 n) |
| 44, 45 | studentTTest, welchTTest | f64 nx, ny, x1, x2, y1, y2 |
| 46, 47 | mannWhitneyUTest, kolmogorovSmirnovTest | u32 count and f64 x values, u32 count and f64 y values |
| 48 | avgWeighted | f64 numerator, f64 denominator |
| 49, 50 | first_value, last_value | as 6 and 7: the key is the row's time, the value is x; on a tie in time last_value keeps the greater x and first_value the smaller, with NULL as the least |
The exact kinds carry a cap on what they hold: uniqExact 220 keys, groupArray 220 items, quantileExact and quantileExactInterpolated 220 values, groupBitmap 220 keys, entropy 220 keys, mannWhitneyUTest and kolmogorovSmirnovTest 220 samples per side, uniqUpTo its threshold and topK its k, so every state stays below the cell limit. A merge of two states can still pass a cap, and the rule is that no value is ever dropped: a part merge that would pass a cap leaves the inputs uncollapsed and reports it, and a query that needs the collapsed value fails with an error that names the aggregate. The sketch kinds, uniq, quantile and the moment and regression families, are bounded in size whatever the input and always merge.
Replication objects Work in progress
Three replication objects outlive a release and follow the same rules as the files above: the commit record in the Kafka topic, the shard manifest and the schema snapshot in S3, each of them an envelope.
MRCLversion 1, a commit record:kind u8(1 part added, 2 part removed, 3 part masked, 4 generation barrier, 5 DDL set, 6 DDL drop, 7 DDL snapshot), the writer,min_reader u16,generation u64,emitter u64,shard_id u32,schema_epoch u64, then the kind's fields. A part kind carriestable_uuid,part_uuid,block u64,content_id u64,row_count u64and the part name, and the bundle key isparts/<name>-<content_id>.part; an added part also carries its insert tokens, and a masked partmasked_rows u64. A DDL kind carries the object kind as a word, the name, the SHA-256 of the text, the text (empty when withheld) and, for a table, the body of its schema record, so a replica takes the table id, every column id and the next column id from the primary. A snapshot carrieswatermark i64, the SHA-256 and the object key. Optional field 3 is the Ed25519 signature over the body.MSHMversion 3, the shard manifest: the writer,min_reader u16,watermark i64,generation u64,shard_id u32,key_fingerprint u64,u32count, then per live parttable_uuid,part_uuid,content_id u64,block u64,row_count u64and the part name. The bundle key isparts/<name>-<content_id>.part, relative to the configured prefix.MSCMversion 3, the schema snapshot: the writer,min_reader u16,watermark i64,generation u64,shard_id u32,schema_epoch u64,key_fingerprint u64, the topic name,u32count, then per entry the object kind as a word,flags u8,created_epoch u64, the name, the text and, for a table, its schema record body, with the signature as optional field 3.
Directories, catalogs and markers
A data directory has two roots, set by MUSCLES_WAL_DIR and MUSCLES_PARTS_DIR. Under each, one directory per table carries the table's id in hex. A reader needs no catalog file to read the data: the table list is the directory list, the table name, the column names and types and the sort key are in the latest schema record of the table's log and in each part's manifest.
Which part directories are live follows from the markers. A directory with a part.tombstone or a part.deleting marker is not live. A part.cold stub means the column files are in the S3 bundle it names. A part.moved redirect names the disk the part was moved to. A part.replicated marker means the part was installed from another node. Each marker is an envelope with the magic in the parts table and part_uuid in its body; the tombstone, deleting and replicated markers carry nothing else.
The definitions of views, rollups, dictionaries, policies, recording rules and search indexes are envelope files that hold SQL text. A reader of the data does not need them.
Versioning and extension rules
- Every file, object and record starts with a magic and a version. The version changes only when the fixed body changes, and a reader refuses a version it does not know.
- Every file names its writer and a minimum reader level, and a reader below that level refuses the file.
- Every durable record carries a CRC32C: the envelope over the whole file, the log frame over its body and sync stamp, the marks over each granule block, the manifest over each column file, each marks file and the primary index.
- Reserved flag bits are 0 and a reader refuses a set one. A later version can therefore give a bit a must-understand meaning.
- An optional field with an even tag may be skipped and one with an odd tag must be understood. A new piece of information that a reader may ignore is an even field; one that changes the meaning of the data is an odd field.
- Column ids, table ids and part ids are stable: a rename, a dropped column or a merge never changes an existing id, and a dropped column's id is never reused.
- A new column type is a new ClickHouse id or a new extension id in the descriptor. An older reader reports an unsupported type for that column, skips its streams, and reads the others.
- A new codec is a new id in the block header. An older reader refuses that stream and reads the others.
- A new stream kind from 128 up may be skipped by an older reader; one below 128 is refused. The same rule holds for a new log record kind.
- A new sidecar is a new file in the part directory. A reader ignores files it does not know.
- A limit that a writer may raise is stored in the file it applies to, and a reader honours the stored value up to the format's own limit. A format limit changes only with the reader level, never with a version.
- All integers are little-endian, everywhere, including inside the ClickHouse type descriptor.
Limits a reader enforces
A reader checks each of these before it allocates or indexes, and refuses a file that passes one. They are fixed for reader level 1; the two writer settings, max_insert_raw_bytes and max_block_raw_bytes, are stored in the files and bounded by the raw stream limit.
| what | limit |
|---|---|
| envelope body | 256 MiB unless a row below says otherwise; a manifest 64 MiB |
col-<id>.mrk | 1 GiB, which holds 226 granules |
primary.idx | 2 GiB |
rows.del | 2 GiB; the bitmap form up to 233 rows, the range or Roaring form above |
| dictionary file | 1 GiB and 224 entries; past either, the column uses inline dictionaries |
| optional-field section | 256 entries, 16 MiB |
var_uint | 10 bytes, the tenth at most 01 |
| type descriptor | 64 KiB, nesting depth 32, 65536 enum members, names of 256 bytes, time zone names of 64 bytes |
| log frame body | 1 GiB plus 64 KiB; a segment 1 GiB plus one frame |
| insert record | 1,048,576 rows, 4096 columns, a columns section of 1 GiB raw |
| raw stream | 1 GiB |
| part | 236 rows and 226 granules; granule_rows from 1 to 1,048,576; a merge whose output would pass either is not planned |
| string or state cell | 128 MiB |
Worked examples
The examples use one table, events, with two columns: id UInt32 (column id 1) and name Nullable(String) (column id 2), sorted by id. Three rows are inserted: (1, 'ann'), (2, NULL), (3, 'bob'). The table id is 0193b2f4-e5c0-7a1b-8c3d-4e5f60718293 and the writer is muscles 0.1.0-beta.1. Every checksum below is computed over the bytes shown, and a script that walks every structure on this page and recomputes every checksum is run before the page is published.
The log segment header
The segment starts with the envelope MWAL, version 1, flags 0, body length 67: sequence 1, floor 0, created_at, the table id, the writer, the reader level and the writer's insert limit, then the checksum, 81 bytes in all.
0000 4d 57 41 4c 01 00 43 00 00 00 "MWAL", version 1, flags 0, body_len 67
000a 01 00 00 00 00 00 00 00 segment_seq = 1
0012 00 00 00 00 00 00 00 00 lsn_floor = 0
001a 00 40 ac c6 3d 28 74 18 created_at
0022 01 93 b2 f4 e5 c0 7a 1b 8c 3d 4e 5f 60 71 82 93 table_uuid
0032 14 6d 75 73 63 6c 65 73 20 30 2e 31 2e 30 2d 62 writer: 20 bytes, "muscles 0.1.0-b
0042 65 74 61 2e 31 eta.1"
0047 01 00 min_reader = 1
0049 00 00 00 40 max_insert_raw_bytes = 1 GiB
004d 64 ff 58 77 crc32c = 0x7758ff64
A log insert record
The columns section is 61 bytes, below the compression threshold, so flags bit 0 is clear, the section is stored raw and its blocks use compact streams. The frame is 93 bytes, and its sync stamp says the 81 bytes of the header were synced before it was written.
0000 4d 00 00 00 len = 77
0004 01 kind = 1, insert
0005 00 flags = 0
0006 00 00 00 00 00 00 00 00 first_lsn = 0
000e 03 00 00 00 row_count = 3
0012 02 00 column_count = 2
0014 01 column_id = 1
0015 01 03 descriptor: 1 byte, 03 = UInt32
0017 04 block flags = 4: compact streams
0018 01 stream_count = 1
0019 01 0c stream kind 1 = values, length 12
001b 01 00 00 00 02 00 00 00 03 00 00 00 values 1, 2, 3
0027 02 column_id = 2
0028 02 23 15 descriptor: 2 bytes, 23 15 = Nullable(String)
002b 04 block flags = 4: compact streams, validity present
002c 03 stream_count = 3
002d 00 08 stream kind 0 = validity, length 8
002f 05 00 00 00 00 00 00 00 bits 0 and 2 set: rows 0 and 2 hold a value
0037 02 10 stream kind 2 = offsets, length 16
0039 00 00 00 00 03 00 00 00 03 00 00 00 06 00 00 00 offsets 0, 3, 3, 6
0049 03 06 stream kind 3 = data, length 6
004b 61 6e 6e 62 6f 62 data "ann" "bob"
0051 51 00 00 00 00 00 00 00 synced = 81
0059 e4 6d 9a 36 crc32c over bytes 4 to 0x58 = 0x369a6de4
The column files of the flushed part
After the flush, the part has one granule. col-1.bin is 67 bytes: the 32-byte header, then a 35-byte column block with the same stream as the log record, as a codec block.
0000 4d 43 4f 4c 01 00 00 00 "MCOL", version 1, flags 0, reserved
0008 01 93 b3 01 6f 3e 7b 2c 9d 4e 5f 60 71 82 93 04 part_uuid
0018 01 00 00 00 column_id = 1
001c 00 00 00 00 reserved
0020 00 01 01 column block: flags 0, 1 stream, kind values
0023 c6 01 01 00 00 00 00 00 00 00 00 00 codec block header, chain none
002f 0c 00 00 00 0c 00 00 00 raw_len = 12, stored_len = 12
0037 01 00 00 00 02 00 00 00 03 00 00 00 values 1, 2, 3
col-2.bin is 127 bytes: the header, then the 95-byte block of the name column.
0000 4d 43 4f 4c 01 00 00 00 "MCOL", version 1, flags 0, reserved
0008 01 93 b3 01 6f 3e 7b 2c 9d 4e 5f 60 71 82 93 04 part_uuid
0018 02 00 00 00 column_id = 2
001c 00 00 00 00 reserved
0020 00 03 column block: flags 0, 3 streams
0022 00 stream kind 0 = validity
0023 c6 01 01 00 00 00 00 00 00 00 00 00 codec block header, chain none
002f 08 00 00 00 08 00 00 00 raw_len = 8, stored_len = 8
0037 05 00 00 00 00 00 00 00 bits 0 and 2 set
003f 02 stream kind 2 = offsets
0040 c6 01 01 00 00 00 00 00 00 00 00 00 codec block header, chain none
004c 10 00 00 00 10 00 00 00 raw_len = 16, stored_len = 16
0054 00 00 00 00 03 00 00 00 03 00 00 00 06 00 00 00 offsets 0, 3, 3, 6
0064 03 stream kind 3 = data
0065 c6 01 01 00 00 00 00 00 00 00 00 00 codec block header, chain none
0071 06 00 00 00 06 00 00 00 raw_len = 6, stored_len = 6
0079 61 6e 6e 62 6f 62 data "ann" "bob"
The marks and the primary index
col-1.mrk records the one granule of column 1: its block starts at byte 32 of the column file, is 35 bytes long, and has the checksum shown. col-2.mrk has the same shape with column id 2, length 95 and checksum 80 a9 1d b5; its trailer is 7d 5b 3f 94.
0000 4d 4d 52 4b 01 00 28 00 00 00 "MMRK", version 1, flags 0, body_len 40
000a 01 93 b3 01 6f 3e 7b 2c 9d 4e 5f 60 71 82 93 04 part_uuid
001a 01 00 00 00 column_id = 1
001e 01 00 00 00 granule_count = 1
0022 20 00 00 00 00 00 00 00 granule 0: offset 32
002a 23 00 00 00 length 35
002e cd 63 bd fd crc32c of the block = 0xfdbd63cd
0032 26 2b 40 20 crc32c = 0x20402b26
0000 4d 4d 52 4b 01 00 28 00 00 00 "MMRK", version 1, flags 0, body_len 40
000a 01 93 b3 01 6f 3e 7b 2c 9d 4e 5f 60 71 82 93 04 part_uuid
001a 02 00 00 00 column_id = 2
001e 01 00 00 00 granule_count = 1
0022 20 00 00 00 00 00 00 00 granule 0: offset 32
002a 5f 00 00 00 length 95
002e 80 a9 1d b5 crc32c of the block = 0xb51da980
0032 7d 5b 3f 94 crc32c = 0x943f5b7d
primary.idx says the granule starts at row 0 and that the sort key of that row is id = 1.
0000 4d 50 49 58 01 00 38 00 00 00 "MPIX", version 1, flags 0, body_len 56
000a 01 93 b3 01 6f 3e 7b 2c 9d 4e 5f 60 71 82 93 04 part_uuid
001a 01 00 00 00 granule_count = 1
001e 00 00 00 00 00 00 00 00 granule 0 starts at row 0
0026 01 key_count = 1
0027 00 01 01 column block: flags 0, 1 stream, kind values
002a c6 01 01 00 00 00 00 00 00 00 00 00 codec block header, chain none
0036 04 00 00 00 04 00 00 00 raw_len = 4, stored_len = 4
003e 01 00 00 00 first key of granule 0 = 1
0042 41 2b e8 44 crc32c = 0x44e82b41
The part manifest
The manifest is 303 bytes. The part id is 0193b301-6f3e-7b2c-9d4e-5f6071829304; the log coverage is sequence numbers 0 to 2, stored plus one. The checksums it holds for the marks files and the primary index are those files' trailers; the checksums for the column files cover every byte of them.
0000 4d 55 53 31 01 00 21 01 00 00 "MUS1", version 1, flags 0, body_len 289
000a 01 93 b3 01 6f 3e 7b 2c 9d 4e 5f 60 71 82 93 04 part_uuid
001a 01 93 b2 f4 e5 c0 7a 1b 8c 3d 4e 5f 60 71 82 93 table_uuid
002a 14 6d 75 73 63 6c 65 73 20 30 2e 31 2e 30 2d 62 writer: 20 bytes, "muscles 0.1.0-b
003a 65 74 61 2e 31 eta.1"
003f 01 00 min_reader = 1
0041 03 00 00 00 00 00 00 00 row_count = 3
0049 00 20 00 00 granule_rows = 8192
004d 01 00 00 00 granule_count = 1
0051 01 00 00 00 00 00 00 00 block = 1
0059 60 4e 7d 2b 3f 9e 1a 5c content_id
0061 01 00 00 00 00 00 00 00 first_lsn = 0 + 1
0069 03 00 00 00 00 00 00 00 last_lsn = 2 + 1
0071 01 00 00 00 00 00 00 00 schema_epoch = 1
0079 00 0a 47 02 3e 28 74 18 written_at
0081 00 00 00 02 max_block_raw_bytes = 32 MiB
0085 06 65 76 65 6e 74 73 table name "events"
008c 01 sort key: 1 entry
008d 01 00 00 00 00 00 00 01 03 column_id 1, ascending, NULLs last, no expression, UInt32
0096 46 00 00 00 00 00 00 00 41 2b e8 44 primary.idx: 70 bytes, checksum 0x44e82b41
00a2 02 column_count = 2
00a3 01 00 00 00 column_id = 1
00a7 02 69 64 name "id"
00aa 01 03 descriptor UInt32
00ac 00 col_flags = 0
00ad 43 00 00 00 00 00 00 00 67 1e f0 67 col-1.bin: 67 bytes, crc32c 0x67f01e67
00b9 36 00 00 00 00 00 00 00 26 2b 40 20 col-1.mrk: 54 bytes, checksum 0x20402b26
00c5 0a 01 00 01 00 00 00 03 00 00 00 zone map, 10 bytes: kind 1, flags 0, min 1, max 3
00d0 14 02 08 00 00 00 00 00 00 00 00 section, 20 bytes: tag 2 null_count = 0,
00db 04 08 0c 00 00 00 00 00 00 00 tag 4 raw_bytes = 12
00e5 02 00 00 00 column_id = 2
00e9 04 6e 61 6d 65 name "name"
00ee 02 23 15 descriptor Nullable(String)
00f1 01 col_flags: bit 0, the column holds a NULL
00f2 7f 00 00 00 00 00 00 00 bf 4b f3 ec col-2.bin: 127 bytes, crc32c 0xecf34bbf
00fe 36 00 00 00 00 00 00 00 7d 5b 3f 94 col-2.mrk: 54 bytes, checksum 0x943f5b7d
010a 0a 02 00 03 61 6e 6e 03 62 6f 62 zone map, 10 bytes: kind 2, flags 0, min "ann", max "bob"
0115 14 02 08 01 00 00 00 00 00 00 00 section, 20 bytes: tag 2 null_count = 1,
0120 04 08 1e 00 00 00 00 00 00 00 tag 4 raw_bytes = 30
012a 00 replaces_count = 0
012b d5 75 6c ab crc32c = 0xab6c75d5
A deletion vector and an aggregate state
A DELETE of the row with the NULL name writes rows.del in the bitmap form with bit 1 set, 63 bytes:
0000 4d 44 4c 56 01 00 31 00 00 00 "MDLV", version 1, flags 0, body_len 49
000a 01 93 b3 01 6f 3e 7b 2c 9d 4e 5f 60 71 82 93 04 part_uuid
001a 01 00 00 00 00 00 00 00 generation = 1
0022 03 00 00 00 00 00 00 00 row_count = 3
002a 01 00 00 00 00 00 00 00 deleted_count = 1
0032 00 form 0 = bitmap
0033 02 00 00 00 00 00 00 00 word 0: bit 1 set, row 1 is deleted
003b 04 77 3f 7e crc32c = 0x7e3f7704
The state of avg(id) over the three rows, as a rollup stores it, is a 64-byte record: the integer sum 6 as 32 bytes, the float sum 0.0, and the count 3.
0000 4d 41 53 31 01 00 32 00 00 00 "MAS1", version 1, flags 0, body_len 50
000a 02 kind 2 = avg
000b 01 lane 1 = integer
000c 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 sum_int = 6 as i256
001c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
002c 00 00 00 00 00 00 00 00 sum_float = 0.0
0034 03 00 00 00 00 00 00 00 count = 3
003c 77 ed 2b 34 crc32c = 0x342bed77