| Ввод | Вывод | Псевдоним |
|---|---|---|
| ✔ | ✔ |
Описание
Полная официальная спецификация формата Native доступна здесь, а сопутствующая спецификация протокола Native — TCP-протокола, по которому он передается, — доступна здесь.
Формат Native — самый эффективный формат в ClickHouse, поскольку он действительно является «столбцовым»:
он не преобразует столбцы в строки.
В этом формате данные записываются и читаются блоками в бинарном формате. Для каждого блока последовательно записываются количество строк, количество столбцов, имена и типы столбцов, а также части столбцов в блоке.
Этот формат используется в нативном интерфейсе для взаимодействия между серверами, в клиенте командной строки и в клиентах на C++.
Формат передачи данных для типов данных
Данные передаются в столбцовом формате: каждый столбец передаётся отдельно, а все его значения отправляются вместе как единый массив.
Каждый столбец в блоке содержит заголовок, аналогичный RowBinaryWithNamesAndTypes.
Структура блока
Следующий запрос возвращает два столбца, 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 использует словарное столбцовое кодирование. Столбец кодируется как префикс версии, затем словарь уникальных значений и массив целочисленных индексов этого словаря.
Префикс версии — это 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 // 12Variant
В отличие от RowBinary, где каждая строка содержит собственный байт дискриминатора, за которым сразу следует встроенное значение, формат Native отделяет дискриминаторы от данных.
Столбец 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 // 3Dynamic
В отличие от 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: 3JSON
В отличие от RowBinary, где каждая строка содержит имена путей и значения и потому является самоописывающейся, формат Native сериализует JSON в столбцовой структуре. Схема кодирования сложна и зависит от версии: она включает префикс структуры с версией сериализации, имена динамических путей и структуру общих данных, после чего следуют типизированные пути (каждый в виде отдельного столбца), динамические пути (каждый как столбец Dynamic) и общие данные для overflow-путей.
Для более простой совместимости можно использовать настройку output_format_native_write_json_as_string=1, которая сериализует JSON-столбцы как обычные текстовые строки JSON (по одному String на строку).