Native

// even though we still might have a proper value for this number 
  // in the block, it should be still returned as NULL to the user!
  0x01, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00,       // Row #1: NULL
  
  0x02, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00,       // Row #2: 2 as UInt64
  
  0x03, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00,       // Row #3: NULL, similar to Row #1
  
  0x04, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00,       // Row #4: 4 as UInt64
]);
```

It works similarly with `Nullable(String)`. The null indicator always comes from the nullable mask byte —
a mask value of `0x01` means the row is `NULL` regardless of the string content. For `NULL` rows,
the underlying string is stored as an empty string (LEB128 length `0`). Note that a non-`NULL` empty
string also has LEB128 length `0`, so only the mask byte distinguishes the two cases. For example, the following query:

```
curl -XPOST "http://localhost:8123?default_format=Native" \  --data-binary "SELECT if(number % 2 = 0, toString(number), NULL) :: Nullable(String) AS maybe_str                 FROM system.numbers LIMIT 5" \  > out.bin
```

The output will look like this:

```
const data = new Uint8Array([
  // --- Block Header ---
  0x01, // LEB128 - 1 column
  0x05, // LEB128 - 5 rows

  // -- Column Header --
  0x09, // LEB128 - column name has 9 bytes
  0x6d,
  0x61,
  0x79,
  0x62,
  0x65,
  0x5f,
  0x73,
  0x74,
  0x72, // column name: 'maybe_str'

  0x10, // LEB128 - column type has 16 bytes
  0x4e,
  0x75,
  0x6c,
  0x6c,
  0x61,
  0x62,
  0x6c,
  0x65,
  0x28,
  0x53,
  0x74,
  0x72,
  0x69,
  0x6e,
  0x67,
  0x29, // column type: 'Nullable(String)'

  // -- Nullable mask --
  0x00, // Row 0 is NOT NULL
  0x01, // Row 1 is NULL
  0x00, // Row 2 is NOT NULL
  0x01, // Row 3 is NULL
  0x00, // Row 4 is NOT NULL

  // -- String values --
  0x01,
  0x30, // Row 0: LEB128 == 1, '0' as String
  0x00, // Row 1: LEB128 == 0, NULL
  0x01,
  0x32, // Row 2: LEB128 == 1, '2' as String
  0x00, // Row 3: LEB128 == 0, NULL
  0x01,
  0x34, // Row 4: LEB128 == 1, '4' as String
])
```

#### LowCardinality[​](#lowcardinality "Direct link to LowCardinality")

Unlike [RowBinary](/docs/interfaces/formats/RowBinary#lowcardinality) where `LowCardinality` is transparent, the Native format uses a dictionary-based columnar encoding. A column is encoded as a version prefix, then a dictionary of unique values, and an array of integer indexes into that dictionary.

Note

A column can be defined as `LowCardinality(Nullable(T))`, but it is not possible to define it as `Nullable(LowCardinality(T))` — it will always result in an error from the server.

The version prefix is a `UInt64(LE)` with value `1`, written once per column. Then, per block, the following is written:

- `UInt64(LE)` — `IndexesSerializationType` bitfield. Bits 0–7 encode the index width (0 = UInt8, 1 = UInt16, 2 = UInt32, 3 = UInt64). Bit 8 (`NeedGlobalDictionaryBit`) is never set in Native format (the server throws an exception if it is encountered). Bit 9 indicates additional dictionary keys are present. Bit 10 indicates the dictionary should be reset.
- `UInt64(LE)` — number of dictionary keys, followed by the keys bulk-serialized using the inner type encoding.
- `UInt64(LE)` — number of rows, followed by index values bulk-serialized using the appropriate UInt width.

The dictionary always contains a default value at index 0 (e.g. empty string for `String`, 0 for numeric types). For `LowCardinality(Nullable(T))`, index 0 represents `NULL`, and the keys are serialized without the `Nullable` wrapper.

For example, `LowCardinality(String)` with 5 rows `['foo', 'bar', 'baz', 'foo', 'bar']`:

```
// Version prefix
01 00 00 00 00 00 00 00    // UInt64(LE) = 1

// IndexesSerializationType: UInt8 indexes, has keys, update dictionary
00 06 00 00 00 00 00 00    // UInt64(LE) = 0x0600

04 00 00 00 00 00 00 00    // 4 dictionary keys
00                          // key 0: "" (default)
03 66 6f 6f                 // key 1: "foo"
03 62 61 72                 // key 2: "bar"
03 62 61 7a                 // key 3: "baz"

05 00 00 00 00 00 00 00    // 5 rows
01 02 03 01 02              // indexes → "foo", "bar", "baz", "foo", "bar"
```

With `LowCardinality(Nullable(String))`, index 0 is `NULL`:

```
01 00 00 00 00 00 00 00    // version
00 06 00 00 00 00 00 00    // IndexesSerializationType
03 00 00 00 00 00 00 00    // 3 keys
00                          // key 0: NULL
00                          // key 1: "" (default)
03 79 65 73                 // key 2: "yes"
05 00 00 00 00 00 00 00    // 5 rows
02 00 02 00 02              // indexes → "yes", NULL, "yes", NULL, "yes"
```

#### Array[​](#array "Direct link to Array")

Unlike [RowBinary](/docs/interfaces/formats/RowBinary#array) where each array is prefixed with a LEB128 element count, the Native format encodes arrays as two columnar sub-streams:

- N cumulative `UInt64` offsets (little-endian, 8 bytes each). Row `i` has `offset[i] - offset[i-1]` elements, with `offset[-1]` implicitly 0.
- All nested elements across all rows, bulk-serialized contiguously.

For example, `Array(UInt32)` with 3 rows `[[0, 10], [1, 11], [2, 12]]`:

```
// Offsets
02 00 00 00 00 00 00 00    // 2 (row 0: 2 elements)
04 00 00 00 00 00 00 00    // 4 (row 1: 2 elements)
06 00 00 00 00 00 00 00    // 6 (row 2: 2 elements)

// Nested UInt32 values (6 total)
00 00 00 00                 // 0
0a 00 00 00                 // 10
01 00 00 00                 // 1
0b 00 00 00                 // 11
02 00 00 00                 // 2
0c 00 00 00                 // 12
```

An empty array has the same offset as the previous row. For example, `Array(String)` with 4 rows `[[], ['0'], ['0','1'], ['0','1','2']]`:

```
00 00 00 00 00 00 00 00    // 0 (empty)
01 00 00 00 00 00 00 00    // 1
03 00 00 00 00 00 00 00    // 3
06 00 00 00 00 00 00 00    // 6
01 30                       // "0"
01 30                       // "0"
01 31                       // "1"
01 30                       // "0"
01 31                       // "1"
01 32                       // "2"
```

#### Map[​](#map "Direct link to Map")

A `Map(K, V)` is encoded as `Array(Tuple(K, V))` — array offsets followed by all keys, then all values. This differs from [RowBinary](/docs/interfaces/formats/RowBinary#map) where keys and values are interleaved per entry.

For example, `Map(String, UInt64)` with 3 rows `[{'a':0,'b':10}, {'a':1,'b':11}, {'a':2,'b':12}]`:

```
// Array offsets
02 00 00 00 00 00 00 00    // 2
04 00 00 00 00 00 00 00    // 4
06 00 00 00 00 00 00 00    // 6

// All keys (6 Strings)
01 61                       // "a"
01 62                       // "b"
01 61                       // "a"
01 62                       // "b"
01 61                       // "a"
01 62                       // "b"

// All values (6 UInt64s)
00 00 00 00 00 00 00 00    // 0
0a 00 00 00 00 00 00 00    // 10
01 00 00 00 00 00 00 00    // 1
0b 00 00 00 00 00 00 00    // 11
02 00 00 00 00 00 00 00    // 2
0c 00 00 00 00 00 00 00    // 12
```

#### Variant[​](#variant "Direct link to Variant")

Unlike [RowBinary](/docs/interfaces/formats/RowBinary#variant) where each row carries its own discriminant byte followed by the value inline, the Native format separates discriminators from data.

Note

As with RowBinary, the types in the definition are always sorted alphabetically, and the discriminant is the index in that sorted list. `0xFF` (255) represents `NULL`.

A `Variant` column is encoded as:

- `UInt64(LE)` discriminators mode prefix (`0` = BASIC, `1` = COMPACT). Native format output typically uses BASIC (`0`); COMPACT mode may appear when reading data stored with `use_compact_variant_discriminators_serialization` enabled.
- N `UInt8` discriminators, one per row.
- Each variant type's data as a separate bulk column containing only the matching rows, in discriminant order.

For example, `Variant(String, UInt32)` with 5 rows `[0::UInt32, 'hello', NULL, 3::UInt32, 'hello']` (sorted: `String` = 0, `UInt32` = 1):

```
00 00 00 00 00 00 00 00    // discriminators mode = BASIC
01 00 ff 01 00              // UInt32, String, NULL, UInt32, String