Skip to main content

Описание

Формат RowBinary разбирает данные по строкам в бинарном формате. Строки и значения идут подряд, без разделителей. Поскольку данные представлены в бинарном формате, разделитель после FORMAT RowBinary должен строго иметь следующий вид:
  • Любое количество пробельных символов:
    • ' ' (пробел — код 0x20)
    • '\t' (табуляция — код 0x09)
    • '\f' (перевод страницы — код 0x0C)
  • Затем ровно одна последовательность перевода строки:
    • в стиле Windows "\r\n"
    • или в стиле Unix '\n'
  • Сразу после неё следуют бинарные данные.
Этот формат менее эффективен, чем формат Native, поскольку он основан на строках.

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

Большинство запросов из примеров можно выполнить с помощью curl, сохранив вывод в файл.
Затем данные можно просмотреть в hex-редакторе.

Беззнаковый LEB128 (Little Endian Base 128)

Кодирование беззнакового целого числа переменной длины в формате little-endian, используемое для кодирования длины типов данных переменного размера, таких как String, Array и Map. Пример реализации можно найти на странице LEB128 в Википедии.

(U)Int8, (U)Int16, (U)Int32, (U)Int64, (U)Int128, (U)Int256

Все целочисленные типы кодируются соответствующим количеством байтов в порядке little-endian. Знаковые типы (Int8Int256) используют представление в дополнительном коде. В большинстве языков такие целые числа можно извлекать из массивов байтов либо встроенными средствами, либо с помощью широко известных библиотек. Для Int128/Int256 и UInt128/UInt256, которые превышают встроенные размеры целых чисел в большинстве языков, может потребоваться собственная десериализация.

Bool

Логические значения кодируются одним байтом и могут быть десериализованы так же, как UInt8.
  • 0false
  • 1true

Float32, Float64

Числа с плавающей запятой в порядке байтов little-endian, закодированные в 4 байта для Float32 и в 8 байт для Float64. Как и в случае с целыми числами, в большинстве языков есть подходящие инструменты для десериализации этих значений.

BFloat16

BFloat16 (Brain Floating Point) — это 16-битный формат чисел с плавающей точкой с диапазоном Float32 и пониженной точностью, что делает его удобным для задач машинного обучения. Формат передачи данных по сути представляет собой старшие 16 бит значения Float32. Если ваш язык не поддерживает его на нативном уровне, проще всего читать и записывать его как UInt16, преобразуя в Float32 и обратно: Чтобы преобразовать BFloat16 в Float32 (псевдокод):
Для преобразования Float32 в BFloat16 (псевдокод):
Примеры внутренних значений для BFloat16:

Decimal32, Decimal64, Decimal128, Decimal256

Типы Decimal представлены в виде целых чисел little-endian с соответствующей битовой разрядностью.
  • Decimal32 - 4 байта, или Int32.
  • Decimal64 - 8 байт, или Int64.
  • Decimal128 - 16 байт, или Int128.
  • Decimal256 - 32 байта, или Int256.
При десериализации значения Decimal целую и дробную части можно определить с помощью следующего псевдокода:
Где trunc выполняет усечение к нулю (а не деление с округлением вниз, которое для отрицательных значений даёт другой результат), а scale — это количество цифр после десятичной точки. Например, для Decimal(10, 2) (эквивалент Decimal32(2)) scale равен 2, а значение 12345 будет представлено как (123, 45). Для сериализации требуется выполнить обратную операцию:
Подробнее см. в документации ClickHouse по типам Decimal.

String

Строки в ClickHouse — это произвольные последовательности байтов. Они не обязаны быть корректным UTF-8. Префикс длины — это длина в байтах, а не число символов. Кодирование состоит из двух частей:
  1. Целое число переменной длины (LEB128), указывающее длину строки в байтах.
  2. Сырые байты строки.
Например, строка foobar кодируется семью байтами следующим образом:

FixedString

В отличие от String, FixedString имеет фиксированную длину, заданную в схеме. Он кодируется как последовательность байтов и дополняется нулевыми байтами в конце, если значение короче N.
При чтении FixedString нулевые байты в конце могут быть как байтами заполнения, так и фактическими символами \0 в данных; в wire format их невозможно различить. Сам ClickHouse сохраняет все N байт без изменений.
Пустой FixedString(3) содержит только нулевые байты заполнения:
Непустое значение FixedString(3), содержащее строку hi:
Непустое значение FixedString(3), содержащее строку bar:
В последнем примере дополнение не требуется, поскольку используются все три байта.

Date

Хранится как UInt16 (два байта), обозначающий количество дней с 1970-01-01. Поддерживаемый диапазон значений: [1970-01-01, 2149-06-06]. Примеры внутренних значений для Date:

Date32

Хранится как Int32 (четыре байта), который представляет количество дней до или после 1970-01-01. Поддерживаемый диапазон значений: [1900-01-01, 2299-12-31]. Примеры внутренних значений для Date32:
Дата до начала эпохи:

DateTime

Хранится как UInt32 (четыре байта), обозначающий количество секунд с 1970-01-01 00:00:00 UTC. Синтаксис:
Например, DateTime или DateTime('UTC').
Бинарное значение всегда представляет собой смещение относительно эпохи UTC. Часовой пояс не меняет кодирование. Однако часовой пояс действительно влияет на то, как строковые значения интерпретируются при вставке: вставка '2024-01-15 10:30:00' в столбец DateTime('America/New_York') сохраняет другое значение эпохи, чем вставка той же строки в столбец DateTime('UTC'), поскольку строка интерпретируется как локальное время в часовом поясе столбца. На уровне протокола оба варианта — это просто секунды эпохи в формате UInt32.
Поддерживаемый диапазон значений: [1970-01-01 00:00:00, 2106-02-07 06:28:15]. Примеры внутренних значений для DateTime:

DateTime64

Хранится как Int64 (восемь байт), где хранится количество тиков до или после 1970-01-01 00:00:00 UTC. Разрешение тика задаётся параметром precision, см. синтаксис ниже:
Где precision — целое число от 0 до 9. Обычно используются только следующие значения: 3 (миллисекунды), 6 (микросекунды), 9 (наносекунды). Примеры корректных определений DateTime64: DateTime64(0), DateTime64(3), DateTime64(6, 'UTC') или DateTime64(9, 'Europe/Amsterdam').
Как и в случае с DateTime, бинарное значение всегда представляет собой смещение относительно эпохи UTC. Часовой пояс влияет на то, как строковые значения интерпретируются при вставке (см. примечание о DateTime), но само кодирование всегда представляет собой Int64-тики, отсчитываемые от эпохи UTC.
Внутреннее значение Int64 для типа DateTime64 можно интерпретировать как количество следующих единиц времени до или после эпохи UNIX:
  • DateTime64(0) - секунды.
  • DateTime64(3) - миллисекунды.
  • DateTime64(6) - микросекунды.
  • DateTime64(9) - наносекунды.
Поддерживаемый диапазон значений: [0000-01-01 00:00:00, 9999-12-31 23:59:59.999999999] (для точности до 7; для точности 8 и 9 диапазон уже, см. примечание ниже). Примеры внутренних значений для DateTime64:
  • DateTime64(3): значение 1546300800000 соответствует 2019-01-01 00:00:00 UTC.
  • DateTime64(6): значение 1705314600123456 соответствует 2024-01-15 10:30:00.123456 UTC.
  • DateTime64(9): значение 1705314600123456789 соответствует 2024-01-15 10:30:00.123456789 UTC.
Поскольку диапазон тиков базового Int64 при более высокой точности уже, максимальное поддерживаемое значение уменьшается: при точности 8 это 4892-10-07, а при точности 9 (наносекунды) — 2262-04-11 23:47:16 в UTC.

Time

Хранится как Int32, представляющий значение времени в секундах. Допускаются отрицательные значения. Поддерживаемый диапазон значений: [-999:59:59, 999:59:59] (то есть [-3599999, 3599999] секунд).
На данный момент для использования Time или Time64 параметр enable_time_time64_type должен быть установлен в 1.
Примеры внутренних значений для Time:

Time64

Внутренне хранится как Decimal64 (который, в свою очередь, хранится как Int64) и представляет значение времени с дробной частью секунд и настраиваемой точностью. Отрицательные значения допустимы. Синтаксис:
Где precision — целое число от 0 до 9. Распространённые значения: 3 (миллисекунды), 6 (микросекунды), 9 (наносекунды). Поддерживаемый диапазон значений: [-999:59:59.xxxxxxxxx, 999:59:59.xxxxxxxxx].
В настоящее время для использования Time или Time64 параметр enable_time_time64_type должен быть установлен в 1.
Внутреннее значение Int64 представляет дробные секунды, масштабированные с коэффициентом 10^precision. Примеры внутренних значений для Time64:

Типы Interval

Все типы Interval хранятся как Int64 (восемь байт, little-endian). Значение представляет собой количество соответствующих единиц времени. Допустимы отрицательные значения. Типы Interval: IntervalNanosecond, IntervalMicrosecond, IntervalMillisecond, IntervalSecond, IntervalMinute, IntervalHour, IntervalDay, IntervalWeek, IntervalMonth, IntervalQuarter, IntervalYear.
Имя типа Interval (например, IntervalSecond или IntervalDay) определяет единицу измерения хранимого значения. Кодирование при передаче всегда одинаковое.
Внутреннее значение:

Enum8, Enum16

Хранится как один байт (Enum8 == Int8) или два байта (Enum16 == Int16), представляя индекс значения enum в его определении. Обратите внимание, что тип хранения — знаковый; значения enum могут быть отрицательными (например, Enum8('a' = -128, 'b' = 0)). Enum можно определить простым способом, например так:
Для определённого выше Enum8 на стороне клиента будет использоваться следующее сопоставление значений:
Или более сложным образом, например так:
Для определённого выше Enum16 на стороне клиента значения будут следующими:
Для парсера типов данных основная сложность — отслеживать экранированные символы в определении enum, такие как \', а также специальные символы вроде =, которые могут встречаться внутри строк в кавычках.

UUID

Представлен как последовательность из 16 байт. UUID хранится как два значения UInt64 в порядке little-endian: первые 8 байт стандартного представления UUID записываются в обратном порядке, и вторые 8 байт также независимо записываются в обратном порядке. Например, для UUID 61f0c404-5cb3-11e7-907b-a6006ad3dba0:
  • Стандартное байтовое представление: 61 f0 c4 04 5c b3 11 e7 | 90 7b a6 00 6a d3 db a0
  • Первая половина в обратном порядке (LE UInt64): e7 11 b3 5c 04 c4 f0 61
  • Вторая половина в обратном порядке (LE UInt64): a0 db d3 6a 00 a6 7b 90
Пример внутреннего значения для UUID:
  • 61f0c404-5cb3-11e7-907b-a6006ad3dba0 представлен как:
  • UUID 00000000-0000-0000-0000-000000000000 по умолчанию представляется в виде 16 нулевых байтов:
Его можно использовать, если была вставлена новая запись, но значение UUID не было указано.

IPv4

Хранится в четырёх байтах в виде UInt32 с порядком байтов little-endian. Обратите внимание, что это отличается от традиционного сетевого порядка байтов (big-endian), который обычно используется для IP-адресов. Примеры внутренних значений для IPv4:

IPv6

Хранится в 16 байтах в порядке байтов big-endian / network (старший значащий байт первым). Примеры внутренних значений для IPv6:

Nullable

Тип данных Nullable кодируется следующим образом:
  1. Один байт, который показывает, является ли значение NULL:
    • 0x00 означает, что значение не NULL.
    • 0x01 означает, что значение NULL.
  2. Если значение не NULL, базовый тип данных кодируется как обычно. Если значение NULL, для базового типа не записываются дополнительные байты.
Например, значение Nullable(UInt32):

LowCardinality

В формате RowBinary маркер low-cardinality не влияет на формат передачи данных. Например, LowCardinality(String) кодируется так же, как обычный String.
Это относится только к RowBinary. В Native format LowCardinality использует другое кодирование на основе словаря.
Столбец можно определить как LowCardinality(Nullable(T)), но нельзя определить как Nullable(LowCardinality(T)) — это всегда будет приводить к ошибке сервера.
При тестировании параметр allow_suspicious_low_cardinality_types можно установить в 1, чтобы разрешить использование большинства типов данных внутри LowCardinality для более полного покрытия.

Array

Массив кодируется следующим образом:
  1. Целое число переменной длины (LEB128), указывающее число элементов в массиве.
  2. Элементы массива, закодированные так же, как и значения базового типа данных.
Например, массив со значениями UInt32:
Немного более сложный пример:
Массив может содержать значения типа Nullable, но сам массив не может иметь тип Nullable.
Допустим следующий вариант:
И будет закодирован следующим образом:
Пример работы с многомерными массивами можно найти в разделе Geo.

Tuple

Кортеж кодируется как все его элементы, следующие друг за другом в соответствующем им формате передачи данных, без какой-либо дополнительной метаинформации или разделителей.
Строковое представление типа данных Tuple сопряжено с теми же сложностями, что и тип Enum, например с отслеживанием экранированных символов и специальных знаков; кроме того, в случае Tuple нужно также отслеживать открывающие и закрывающие круглые скобки. Также обратите внимание, что наиболее сложные Tuple могут содержать другие вложенные Tuple, Arrays, Maps и даже enum. Например, в следующей таблице tuple содержит enum с апострофом и круглой скобкой в имени, что при неправильной обработке может вызвать проблемы при парсинге:

Map

Map можно представить как Array(Tuple(K, V)), где K — тип ключа, а V — тип значения. Map кодируется следующим образом:
  1. Целое число переменной длины (LEB128), указывающее количество элементов в Map.
  2. Элементы Map в виде пар ключ-значение, закодированные в соответствии с их типами.
Например, Map с ключами типа String и значениями типа UInt32:
Возможны значения типа Map с глубоко вложенными структурами, например Map(String, Map(Int32, Array(Nullable(String)))), которые кодируются аналогично описанному выше.

Variant

Этот тип представляет собой объединение других типов данных. Тип Variant(T1, T2, ..., TN) означает, что каждая строка этого типа содержит значение либо типа T1, либо T2, либо …, либо TN, либо не содержит ни одного из них (значение NULL).
Хотя для конечного пользователя Variant(T1, T2) означает ровно то же самое, что и Variant(T2, T1), порядок типов в определении важен для формата передачи данных: типы в определении всегда сортируются по алфавиту, и это важно, поскольку конкретный вариант кодируется с помощью “дискриминант” — индекса типа данных в определении.
Рассмотрим следующий пример:
Для кодирования значения NULL используется байт дискриминанта 0xFF:
Настройку allow_suspicious_variant_types можно использовать, чтобы разрешить более всестороннее тестирование типа Variant.

Dynamic

Тип Dynamic может содержать значения любого типа, определяемого во время выполнения. В формате RowBinary каждое значение самодостаточно: первая часть — это спецификация типа в таком формате. Затем следует содержимое со значением, закодированным так, как описано в этом документе. Поэтому, чтобы разобрать значение, достаточно использовать индекс типа для выбора подходящего парсера, а затем повторно использовать уже имеющийся у вас код разбора RowBinary.
Где BinaryTypeIndex — это один байт, обозначающий тип. Индексы типов и параметры см. в справочнике здесь. Значение Dynamic со значением NULL кодируется с BinaryTypeIndex 0x00 (тип Nothing) без дополнительных байтов:
Примеры:

JSON

Тип JSON кодирует данные в двух различных категориях:
  1. Типизированные пути - пути, объявленные в схеме с явным указанием типов (например, JSON(user_id UInt32, name String))
  2. Динамические пути/пути переполнения при превышении лимита динамических путей — пути, обнаруженные во время выполнения и хранящиеся как тип Dynamic. Перед кодированием значения указывается определение типа.
Формат передачи данных и правила для этих двух категорий различаются. Пути сериализуются в трёх группах, записываемых последовательно: типизированные пути, динамические пути, затем пути общих данных (overflow). Типизированные и динамические пути записываются в порядке, определяемом реализацией (определяется порядком итерации по внутренней хэш-карте), тогда как пути общих данных записываются в алфавитном порядке. Не следует полагаться на какой-либо конкретный порядок путей. Десериализатор обрабатывает каждый путь по имени, а не по позиции. Каждая JSON-строка в формате RowBinary сериализуется следующим образом:
Примеры: 1. Простой JSON только с типизированными путями: Схема: JSON(user_id UInt32, active Bool) Строка: {"user_id": 42, "active": true} Двоичное кодирование (hex с аннотациями):
2. Простой JSON с типизированными и динамическими путями: Схема: JSON(user_id UInt32, active Bool) Строка: {"user_id": 42, "active": true, "name": "Alice"} Двоичное кодирование (hex с аннотациями):
3. Обработка NULL: С типизированным столбцом Nullable вы получаете null: Схема: JSON(score Nullable(Int32)) Строка: {"score": null } Двоичное кодирование (hex с аннотациями):
Для типизированного non-nullable столбца возвращается значение по умолчанию: Схема: JSON(name String) Строка: {"name": null} Двоичное кодирование:
При динамическом пути это игнорируется: Схема: JSON(id UInt64) Строка: {"id": 100, "metadata": null} Двоичное кодирование:
Примечание: путь metadata со значением NULL не включается, так как динамические пути сериализуются только при ненулевых значениях. Это ключевое отличие от типизированных путей. 4. Вложенные JSON-объекты: Схема: JSON() Строка: {"user": {"name": "Bob", "age": 30}} Двоичное кодирование (в шестнадцатеричном виде, с аннотациями):
Примечание: вложенные объекты разворачиваются в пути с разделением точками (например, user.name вместо вложенной структуры). Альтернатива: режим JSON как String При использовании настройки output_format_binary_write_json_as_string=1 JSON-столбцы сериализуются как единая текстовая строка JSON, а не в структурированном бинарном формате. Для записи в JSON-столбцы есть соответствующая настройка — input_format_binary_read_json_as_string. Выбор здесь зависит от того, где вы хотите разбирать JSON: на стороне клиента или на стороне сервера.

Гео-типы

Geo — это категория типов данных для представления географических данных. Она включает:
  • Point - в виде Tuple(Float64, Float64).
  • Ring - в виде Array(Point) или Array(Tuple(Float64, Float64)).
  • Polygon - в виде Array(Ring) или Array(Array(Tuple(Float64, Float64))).
  • MultiPolygon - в виде Array(Polygon) или Array(Array(Array(Tuple(Float64, Float64)))).
  • LineString - в виде Array(Point) или Array(Tuple(Float64, Float64)).
  • MultiLineString - в виде Array(LineString) или Array(Array(Tuple(Float64, Float64))).
Формат передачи данных для значений Geo в точности такой же, как у Tuple и Array. Заголовки формата RowBinaryWithNamesAndTypes будут содержать псевдонимы этих типов, например Point, Ring, Polygon, MultiPolygon, LineString и MultiLineString.

Geometry

Geometry — это тип Variant, который может содержать любой из перечисленных выше гео-типов. В формате передачи данных он кодируется точно так же, как Variant: байт дискриминант указывает, какой гео-тип идёт следующим. Индексы дискриминант для Geometry: Структура формата передачи данных:
Пример кодирования Point в виде Geometry:
Пример кодирования Ring в виде Geometry:

Nested

Формат передачи данных для Nested зависит от настройки flatten_nested.
Все массивы компонентов в одной строке должны иметь одинаковую длину. Это ограничение контролируется сервером. Если длины не совпадают, возникнет ошибка вставки.

flatten_nested = 1 (по умолчанию)

При значении по умолчанию Nested разворачивается в независимые массивы. Каждый подстолбец становится отдельным столбцом Array с именем, части которого разделены точками:
DESCRIBE TABLE foo показывает столбцы в развёрнутом виде:
Каждый массив сериализуется отдельно, как описано в разделе Array:

flatten_nested = 0

При flatten_nested = 0 Nested сохраняется как один столбец типа Array(Tuple(...)). Имя столбца не содержит точек:
DESCRIBE TABLE foo возвращает один столбец:
Кодировка имеет вид Array(Tuple(String, Int32)): сначала префикс длины массива, затем поля кортежа каждого элемента по порядку:
Обратите внимание, что поля чередуются внутри каждого элемента (a₁, b₁, a₂, b₂), а не группируются по столбцам (a₁, a₂, b₁, b₂), как в сглаженном представлении.

SimpleAggregateFunction

SimpleAggregateFunction(func, T) кодируется так же, как базовый тип данных T. Имя агрегатной функции не влияет на формат передачи данных. Например, SimpleAggregateFunction(max, UInt32) кодируется так же, как обычный UInt32:
В заголовке RowBinaryWithNamesAndTypes тип указан как SimpleAggregateFunction(max, UInt32), но фактическое значение в формате передачи данных — просто UInt32:

AggregateFunction

AggregateFunction(func, T) хранит полное промежуточное состояние агрегатной функции. В отличие от SimpleAggregateFunction, которая тоже хранит промежуточное состояние, но кодирует его так же, как и базовый тип данных, AggregateFunction хранит непрозрачный бинарный blob, формат которого зависит от конкретной агрегатной функции.
Состояния агрегатных функций в RowBinary не имеют префикса длины. Парсер должен понимать внутренний формат сериализации каждой конкретной агрегатной функции, чтобы определить, сколько байтов нужно прочитать. На практике большинство клиентов рассматривают состояния агрегатных функций как непрозрачные и используют комбинаторы *State / *Merge, чтобы сервер сам выполнял сериализацию.
Внутренний формат зависит от функции. Несколько простых примеров: countState — хранит счётчик как VarUInt (LEB128):
sumState — хранит накопленную сумму в целочисленном значении фиксированной разрядности. Разрядность зависит от типа аргумента (UInt64 для целочисленных аргументов):
minState / maxState — хранит байт флага, за которым следует значение соответствующего базового типа. Флаг равен 0x00 для пустого состояния (значения не встречались) или 0x01, если значение присутствует:
Пустое состояние (не агрегирована ни одна строка):
Более сложные функции, такие как uniq, quantile или groupArray, используют форматы, специфичные для конкретной реализации. Если вам нужно читать или записывать эти состояния, обратитесь к исходному коду ClickHouse для соответствующей функции.

QBit

QBit — это векторный тип для эффективного поиска с различными уровнями точности. Внутренне он хранится в транспонированном виде. При передаче QBit представляет собой просто Array базового типа элемента (Int8, Float32, Float64 или BFloat16). Оптимизация хранения за счёт транспонирования битов выполняется на стороне сервера, а не в протоколе RowBinary. Синтаксис:
Где element_typeInt8, Float32, Float64 или BFloat16, а dimension — фиксированная размерность вектора. Необязательный параметр stride управляет только тем, как битовые плоскости группируются в потоки хранения на стороне сервера; он не влияет на формат передачи данных RowBinary, который всегда представляет собой полный массив из dimension элементов. Формат передачи данных: идентичен Array(element_type):
Пример кодирования QBit(Float32, 4) для [1.0, 2.0, 3.0, 4.0]:

Настройки формата

Следующие настройки общие для всех форматов типа RowBinary.
Последнее изменение 23 июля 2026 г.