Перейти к основному содержанию
ВводВыводПсевдоним

Описание

Формат Native — самый эффективный формат в ClickHouse, поскольку он действительно является «столбцовым»: он не преобразует столбцы в строки. В этом формате данные записываются и читаются блоками в бинарном формате. Для каждого блока последовательно записываются количество строк, количество столбцов, имена и типы столбцов, а также части столбцов в блоке. Этот формат используется в нативном интерфейсе для взаимодействия между серверами, в клиенте командной строки и в клиентах на C++.
Этот формат можно использовать для быстрого создания дампов, которые может читать только СУБД ClickHouse. Однако работать с этим форматом напрямую не всегда удобно.

Формат передачи данных для типов данных

Данные передаются в столбцовом формате: каждый столбец передаётся отдельно, а все его значения отправляются вместе как единый массив. Каждый столбец в блоке содержит заголовок, аналогичный RowBinaryWithNamesAndTypes.
При использовании нативного бинарного протокола TCP (или когда конечная точка HTTP получает ?client_protocol_version=<n>), структура BlockInfo записывается перед количеством столбцов и строк. В примерах этого раздела используется обычный HTTP-интерфейс без версии протокола, поэтому BlockInfo не записывается.

Структура блока

Следующий запрос возвращает два столбца, number и str, из трёх строк:
curl -XPOST "http://localhost:8123?default_format=Native" --data-binary "SELECT number, toString(number) AS str FROM system.numbers LIMIT 3" > out.bin
Выходные данные помещаются в один блок ClickHouse и будут выглядеть так:
const data = new Uint8Array([
  // --- Заголовок блока ---
  0x02,                   // 2 столбца
  0x03,                   // 3 строки
  // -- Заголовок столбца 1 --
  0x06,                   // LEB128 - имя столбца 'number' занимает 6 байт
  0x6e, 0x75, 0x6d,       
  0x62, 0x65, 0x72,       // имя столбца: 'number'
  0x06,                   // LEB128 - тип столбца 'UInt64' занимает 6 байт
  0x55, 0x49, 0x6e,
  0x74, 0x36, 0x34,       // 'UInt64'
  0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, // 0 как UInt64
  0x01, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, // 1 как UInt64
  0x02, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, // 2 как UInt64
  0x03,                   // LEB128 - имя столбца 'str' занимает 3 байта
  0x73, 0x74, 0x72,       // имя столбца: 'str'
  0x06,                   // LEB128 - тип столбца 'String' занимает 6 байт
  0x53, 0x74, 0x72, 
  0x69, 0x6e, 0x67,       // 'String'
  0x01,                   // LEB128 - строка занимает 1 байт
  0x30,                   // '0' как String
  0x01,                   // LEB128 - строка занимает 1 байт
  0x31,                   // '1' как String
  0x01,                   // LEB128 - строка занимает 1 байт
  0x32,                   // '2' как String
])

Несколько блоков

Однако во многих случаях данные не помещаются в один блок, и ClickHouse отправляет их в виде нескольких блоков. Рассмотрим следующий запрос, который извлекает две строки при уменьшенном размере блока, чтобы принудительно разбить данные так, что в каждом блоке будет по одной строке:
curl -XPOST "http://localhost:8123?default_format=Native" --data-binary "SELECT number, toString(number) AS str                FROM system.numbers LIMIT 2                 SETTINGS max_block_size=1" \  > out.bin
Результат:
const data = new Uint8Array([
 
  // ----- Блок 1 ----- 
  0x02,                   // 2 столбца
  0x01,                   // 1 строка
  0x06,                   // LEB128 - имя столбца 'number' занимает 6 байт
  0x6E, 0x75, 0x6D, 
  0x62, 0x65, 0x72,       // имя столбца: 'number' 
  0x06,                   // LEB128 - тип столбца 'UInt64' занимает 6 байт
  0x55, 0x49, 0x6E, 
  0x74, 0x36, 0x34,       // 'UInt64' 
  0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, // 0 в формате UInt64
  0x03,                   // LEB128 - имя столбца 'str' занимает 3 байта
  0x73, 0x74, 0x72,       // имя столбца: 'str'
  0x06,                   // LEB128 - тип столбца 'String' занимает 6 байт
  0x53, 0x74, 0x72, 
  0x69, 0x6E, 0x67,       // 'String'
  0x01,                   // LEB128 - строка занимает 1 байт
  0x30,                   // '0' в формате String
  
  // ----- Блок 2 -----
  0x02,                   // 2 столбца
  0x01,                   // 1 строка
  0x06,                   // LEB128 - имя столбца 'number' занимает 6 байт
  0x6E, 0x75, 0x6D,  
  0x62, 0x65, 0x72,       // имя столбца: 'number'
  0x06,                   // LEB128 - тип столбца 'UInt64' занимает 6 байт
  0x55, 0x49, 0x6E,  
  0x74, 0x36, 0x34,       // 'UInt64'
  0x01, 0x00, 0x00, 0x00,  
  0x00, 0x00, 0x00, 0x00, // 1 в формате UInt64
  0x03,                   // LEB128 - имя столбца 'str' занимает 3 байта
  0x73, 0x74, 0x72,       // имя столбца: 'str'
  0x06,                   // LEB128 - тип столбца 'String' занимает 6 байт
  0x53, 0x74, 0x72,  
  0x69, 0x6E, 0x67,       // 'String'
  0x01,                   // LEB128 - строка занимает 1 байт
  0x31,                   // '1' в формате String
]);

Простые типы данных

Формат передачи данных отдельного значения одного из простых типов данных аналогичен RowBinary/RowBinaryWithNamesAndTypes. Полный список типов, соответствующих этому описанию:
  • (U)Int8, (U)Int16, (U)Int32, (U)Int64, (U)Int128, (U)Int256
  • Float32, Float64
  • Bool
  • String
  • FixedString(N)
  • Date
  • Date32
  • DateTime
  • DateTime64
  • IPv4
  • IPv6
  • UUID
Подробнее см. описания перечисленных выше типов в разделе “Формат передачи данных типов данных RowBinary”.

Сложные типы данных

Кодирование следующих типов отличается от кодирования в RowBinary и RowBinaryWithNamesAndTypes.
  • Nullable
  • LowCardinality
  • Array
  • Map
  • Variant
  • Dynamic
  • JSON

Nullable

В формате Native перед самими данными для столбца с типом Nullable записывается количество байтов, равное числу строк в блоке. Каждый из этих байтов указывает, равно ли значение NULL или нет. Например, в этом запросе каждое нечётное число будет NULL:
curl -XPOST "http://localhost:8123?default_format=Native" \  --data-binary "SELECT if(number % 2 = 0, number, NULL) :: Nullable(UInt64) AS maybe_null                 FROM system.numbers LIMIT 5" \  > out.bin
Результат будет выглядеть так:
const data = new Uint8Array([
  // --- Заголовок блока ---
  0x01,                         // LEB128 - 1 столбец
  0x05,                         // LEB128 - 5 строк
  
  // -- Заголовок столбца --
  0x0A,                         // LEB128 - имя столбца занимает 10 байт
  0x6D, 0x61, 0x79, 0x62, 0x65, 
  0x5F, 0x6E, 0x75, 0x6C, 0x6C, // имя столбца: 'maybe_null'
  
  0x10,                         // LEB128 - тип столбца занимает 16 байт
  0x4E, 0x75, 0x6C, 0x6C, 
  0x61, 0x62, 0x6C, 0x65, 
  0x28, 0x55, 0x49, 0x6E, 
  0x74, 0x36, 0x34, 0x29,       // тип столбца: 'Nullable(UInt64)'
  
  // -- Маска Nullable --
  0x00,                         // Строка 0 — NOT NULL
  0x01,                         // Строка 1 — NULL
  0x00,                         // Строка 2 — NOT NULL
  0x01,                         // Строка 3 — NULL
  0x00,                         // Строка 4 — NOT NULL
  
  // -- Значения UInt64 --
  0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00,       // Строка 0: 0 как UInt64

  // даже если в блоке хранится корректное значение для этого числа,
  // пользователю оно всё равно должно быть возвращено как NULL!
  0x01, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00,       // Строка #1: NULL
  
  0x02, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00,       // Строка #2: 2 как UInt64
  
  0x03, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00,       // Строка #3: NULL, аналогично строке #1
  
  0x04, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00,       // Строка #4: 4 как UInt64
]);
С Nullable(String) это работает аналогично. Индикатор NULL всегда задаётся байтом маски nullable — значение маски 0x01 означает, что строка равна NULL независимо от содержимого строки. Для строк со значением NULL базовая строка хранится как пустая строка (длина LEB128 0). Обратите внимание, что пустая строка с не-NULL значением также имеет длину LEB128 0, поэтому эти два случая различаются только байтом маски. Например, следующий запрос:
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
Результат будет выглядеть так:
const data = new Uint8Array([
  // --- Заголовок блока ---
  0x01, // LEB128 - 1 столбец
  0x05, // LEB128 - 5 строк

  // -- Заголовок столбца --
  0x09, // LEB128 - имя столбца занимает 9 байт
  0x6d,
  0x61,
  0x79,
  0x62,
  0x65,
  0x5f,
  0x73,
  0x74,
  0x72, // имя столбца: 'maybe_str'

  0x10, // LEB128 - тип столбца занимает 16 байт
  0x4e,
  0x75,
  0x6c,
  0x6c,
  0x61,
  0x62,
  0x6c,
  0x65,
  0x28,
  0x53,
  0x74,
  0x72,
  0x69,
  0x6e,
  0x67,
  0x29, // тип столбца: 'Nullable(String)'

  // -- Маска Nullable --
  0x00, // Строка 0 — NOT NULL
  0x01, // Строка 1 — NULL
  0x00, // Строка 2 — NOT NULL
  0x01, // Строка 3 — NULL
  0x00, // Строка 4 — NOT NULL

  // -- Строковые значения --
  0x01,
  0x30, // Строка 0: LEB128 == 1, '0' как String
  0x00, // Строка 1: LEB128 == 0, NULL
  0x01,
  0x32, // Строка 2: LEB128 == 1, '2' как String
  0x00, // Строка 3: LEB128 == 0, NULL
  0x01,
  0x34, // Строка 4: LEB128 == 1, '4' как String
])

LowCardinality

В отличие от RowBinary, где LowCardinality прозрачен, формат Native использует словарное столбцовое кодирование. Столбец кодируется как префикс версии, затем словарь уникальных значений и массив целочисленных индексов этого словаря.
Столбец можно определить как LowCardinality(Nullable(T)), но определить его как Nullable(LowCardinality(T)) нельзя — это всегда приводит к ошибке сервера.
Префикс версии — это UInt64(LE) со значением 1, который записывается один раз для каждого столбца. Затем для каждого блока записывается следующее:
  • UInt64(LE) — битовое поле IndexesSerializationType. Биты 0–7 кодируют ширину индекса (0 = UInt8, 1 = UInt16, 2 = UInt32, 3 = UInt64). Бит 8 (NeedGlobalDictionaryBit) никогда не устанавливается в формате Native (сервер генерирует исключение, если он встречается). Бит 9 указывает на наличие дополнительных ключей словаря. Бит 10 указывает, что словарь нужно сбросить.
  • UInt64(LE) — количество ключей словаря, после которого сами ключи массово сериализуются с использованием кодирования внутреннего типа.
  • UInt64(LE) — количество строк, после которого значения индексов массово сериализуются с использованием соответствующей разрядности UInt.
Словарь всегда содержит значение по умолчанию по индексу 0 (например, пустую строку для String, 0 для числовых типов). Для LowCardinality(Nullable(T)) индекс 0 соответствует NULL, а ключи сериализуются без обёртки Nullable. Например, LowCardinality(String) с 5 строками ['foo', 'bar', 'baz', 'foo', 'bar']:
// Префикс версии
01 00 00 00 00 00 00 00    // UInt64(LE) = 1

// IndexesSerializationType: индексы UInt8, есть ключи, обновить словарь
00 06 00 00 00 00 00 00    // UInt64(LE) = 0x0600

04 00 00 00 00 00 00 00    // 4 ключа словаря
00                          // ключ 0: "" (по умолчанию)
03 66 6f 6f                 // ключ 1: "foo"
03 62 61 72                 // ключ 2: "bar"
03 62 61 7a                 // ключ 3: "baz"

05 00 00 00 00 00 00 00    // 5 строк
01 02 03 01 02              // индексы → "foo", "bar", "baz", "foo", "bar"
В LowCardinality(Nullable(String)) индекс 0 — NULL:
01 00 00 00 00 00 00 00    // версия
00 06 00 00 00 00 00 00    // IndexesSerializationType
03 00 00 00 00 00 00 00    // 3 ключа
00                          // ключ 0: NULL
00                          // ключ 1: "" (по умолчанию)
03 79 65 73                 // ключ 2: "yes"
05 00 00 00 00 00 00 00    // 5 строк
02 00 02 00 02              // индексы → "yes", NULL, "yes", NULL, "yes"

Array

В отличие от RowBinary, где каждому массиву предшествует число элементов в кодировке LEB128, формат Native кодирует массивы как два столбцовых подпотока:
  • N накопительных смещений UInt64 (little-endian, по 8 байт каждое). Строка i содержит offset[i] - offset[i-1] элементов, при этом offset[-1] неявно считается равным 0.
  • Все вложенные элементы из всех строк, сериализованные подряд в один непрерывный блок.
Например, Array(UInt32) с 3 строками [[0, 10], [1, 11], [2, 12]]:
// Смещения
02 00 00 00 00 00 00 00    // 2 (строка 0: 2 элемента)
04 00 00 00 00 00 00 00    // 4 (строка 1: 2 элемента)
06 00 00 00 00 00 00 00    // 6 (строка 2: 2 элемента)

// Вложенные значения UInt32 (всего 6)
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
Пустой массив имеет то же смещение, что и в предыдущей строке. Например, Array(String) с 4 строками [[], ['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(K, V) кодируется как Array(Tuple(K, V)) — сначала идут смещения массива, затем все ключи, а потом все значения. Это отличается от RowBinary, где ключи и значения чередуются в каждой записи. Например, Map(String, UInt64) с 3 строками [{'a':0,'b':10}, {'a':1,'b':11}, {'a':2,'b':12}]:
// Смещения массива
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

// Все ключи (6 строк)
01 61                       // "a"
01 62                       // "b"
01 61                       // "a"
01 62                       // "b"
01 61                       // "a"
01 62                       // "b"

// Все значения (6 UInt64)
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

В отличие от RowBinary, где каждая строка содержит собственный байт дискриминатора, за которым сразу следует встроенное значение, формат Native отделяет дискриминаторы от данных.
Как и в RowBinary, типы в определении всегда сортируются по алфавиту, а дискриминатор — это индекс в этом отсортированном списке. 0xFF (255) обозначает NULL.
Столбец Variant кодируется следующим образом:
  • Префикс режима дискриминаторов UInt64(LE) (0 = BASIC, 1 = COMPACT). Вывод в формате Native обычно использует BASIC (0); режим COMPACT может встречаться при чтении данных, сохраненных с включенным use_compact_variant_discriminators_serialization.
  • N дискриминаторов UInt8, по одному на строку.
  • Данные каждого типа варианта в виде отдельного столбца с массовыми данными, содержащего только соответствующие строки, в порядке дискриминаторов.
Например, Variant(String, UInt32) с 5 строками [0::UInt32, 'hello', NULL, 3::UInt32, 'hello'] (в отсортированном списке: String = 0, UInt32 = 1):
00 00 00 00 00 00 00 00    // режим дискриминаторов = BASIC
01 00 ff 01 00              // UInt32, String, NULL, UInt32, String

// String (2 значения, строки 1 и 4)
05 68 65 6c 6c 6f          // "hello"
05 68 65 6c 6c 6f          // "hello"

// UInt32 (2 значения, строки 0 и 3)
00 00 00 00                 // 0
03 00 00 00                 // 3

Dynamic

В отличие от RowBinary, где каждое значение является самоописывающимся (префикс типа + значение), формат Native сериализует Dynamic как префикс структуры, за которым следует столбец Variant. Префикс структуры содержит версию сериализации UInt64(LE), затем количество динамических типов (в формате VarUInt), а затем имена типов в виде строк. В версии V1 количество типов для совместимости записывается дважды. Следующие за ним данные представляют собой столбец Variant, список типов которого включает динамические типы и внутренний тип SharedVariant, отсортированные по алфавиту. Например, Dynamic с 5 строками [0::UInt32, 'hello', NULL, 3::UInt32, 'hello']:
// Префикс структуры (V1)
01 00 00 00 00 00 00 00    // версия = V1
02                          // кол-во типов (V1 записывает дважды)
02                          // кол-во типов
06 53 74 72 69 6e 67       // "String"
06 55 49 6e 74 33 32       // "UInt32"

// Данные Variant: Variant(SharedVariant, String, UInt32)
// дискриминанты: SharedVariant=0, String=1, UInt32=2
00 00 00 00 00 00 00 00    // режим дискриминаторов = BASIC
02 01 ff 02 01              // UInt32, String, NULL, UInt32, String
// SharedVariant: 0 значений
05 68 65 6c 6c 6f          // String: "hello"
05 68 65 6c 6c 6f          // String: "hello"
00 00 00 00                 // UInt32: 0
03 00 00 00                 // UInt32: 3

JSON

В отличие от RowBinary, где каждая строка содержит имена путей и значения и потому является самоописывающейся, формат Native сериализует JSON в столбцовой структуре. Схема кодирования сложна и зависит от версии: она включает префикс структуры с версией сериализации, имена динамических путей и структуру общих данных, после чего следуют типизированные пути (каждый в виде отдельного столбца), динамические пути (каждый как столбец Dynamic) и общие данные для overflow-путей. Для более простой совместимости можно использовать настройку output_format_native_write_json_as_string=1, которая сериализует JSON-столбцы как обычные текстовые строки JSON (по одному String на строку).
Последнее изменение 10 июня 2026 г.