RowBinary
| Вход | Выход | Псевдоним |
|---|---|---|
| ✔ | ✔ |
Описание
Формат RowBinary разбирает данные по строкам в двоичном виде.
Строки и значения идут последовательно, без разделителей.
Поскольку данные представлены в двоичном формате, разделитель после FORMAT RowBinary строго задан следующим образом:
- Произвольное количество пробельных символов:
' '(пробел — код0x20)'\t'(табуляция — код0x09)'\f'(form feed — код0x0C)
- После чего следует ровно одна последовательность перевода строки:
- в стиле Windows
"\r\n" - или в стиле Unix
'\n'
- в стиле Windows
- Сразу после этого идут двоичные данные.
Этот формат менее эффективен, чем формат 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. Для знаковых типов (Int8–Int256) используется представление в дополнительном коде. В большинстве языков такие целые числа можно извлекать из массивов байтов с помощью встроенных средств или широко известных библиотек. Для Int128/Int256 и UInt128/UInt256, которые превышают размер встроенных целочисленных типов в большинстве языков, может потребоваться собственная десериализация.
Bool
Логические значения кодируются одним байтом и могут быть десериализованы так же, как UInt8.
0—false1—true
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-последовательностью. Префикс длины — это длина в байтах, а не количество символов.
Строка кодируется в двух частях:
- Целое число переменной длины (LEB128), которое указывает длину строки в байтах.
- Необработанные байты строки.
Например, строка foobar будет закодирована с использованием семи байтов следующим образом:
FixedString
В отличие от String, FixedString имеет фиксированную длину, которая задаётся в схеме. Он кодируется как последовательность байтов и дополняется завершающими нулевыми байтами, если значение короче N.
При чтении FixedString завершающие нулевые байты могут быть как заполнением, так и фактическими символами \0 в данных — при передаче их невозможно отличить друг от друга. Сам 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)- наносекунды.
Поддерживаемый диапазон значений: [1900-01-01 00:00:00, 2299-12-31 23:59:59.99999999].
Примеры базовых значений для 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.
Точность максимального значения составляет 8 знаков. Если используется максимальная точность в 9 цифр (наносекунды), максимальное поддерживаемое значение в UTC — 2262-04-11 23:47:16.
Time
Хранится в виде Int32, представляющего значение времени в секундах. Отрицательные значения допустимы.
Поддерживаемый диапазон значений: [-999:59:59, 999:59:59] (то есть [-3599999, 3599999] секунд).
На данный момент для использования Time или Time64 необходимо установить значение 1 для настройки enable_time_time64_type.
внутренний значение для 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) определяет единицу измерения хранимого значения. Кодирование на уровне wire format всегда одинаково.
Примеры внутренних значений:
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 byte order (старший байт — первым). Примеры внутренних значений для IPv6:
Nullable
Тип данных Nullable кодируется следующим образом:
- Один байт, указывающий, является ли значение
NULL:0x00означает, что значение неNULL.0x01означает, что значениеNULL.
- Если значение не
NULL, базовый тип данных кодируется как обычно. Если значениеNULL, для базового типа дополнительные байты не записываются.
Например, значение Nullable(UInt32):
LowCardinality
В формате RowBinary маркер низкой кардинальности не влияет на формат передачи. Например, LowCardinality(String) кодируется так же, как обычный String.
Это относится только к RowBinary. В формате Native LowCardinality использует другое кодирование на основе словаря.
Столбец можно определить как LowCardinality(Nullable(T)), но его нельзя определить как Nullable(LowCardinality(T)) — это всегда приводит к ошибке сервера.
При тестировании параметр allow_suspicious_low_cardinality_types можно установить в 1, чтобы разрешить использование большинства типов данных внутри LowCardinality для более полного покрытия.
Массив
Массив кодируется следующим образом:
- Целое число переменной длины (LEB128), указывающее количество элементов в массиве.
- Элементы массива, закодированные таким же образом, как и базовый тип данных.
Например, массив со значениями UInt32:
Чуть более сложный пример:
Массив может содержать значения типа Nullable, но сам массив не может иметь тип Nullable.
Следующее допустимо:
И будет закодировано следующим образом:
Пример работы с многомерными массивами приведён в разделе Geo.
Кортеж
Кортеж кодируется как все его элементы, следующие друг за другом в соответствующем им формате wire, без какой-либо дополнительной метаинформации или разделителей.
Строковое представление типа данных tuple создаёт проблемы, аналогичные тем, что возникают с типом Enum, например необходимость отслеживать экранированные символы и специальные знаки; в случае с Tuple также требуется отслеживать открывающие и закрывающие круглые скобки. Кроме того, обратите внимание, что самые сложные Tuple могут содержать другие вложенные Tuple, Arrays, Maps и даже enum.
Например, в следующей table tuple содержит enum с апострофом и круглой скобкой в имени, что может вызвать проблемы при разборе, если обработать это неправильно:
Map
Map можно рассматривать как Array(Tuple(K, V)), где K — тип ключа, а V — тип значения. Map кодируется следующим образом:
- Целое число переменной длины (LEB128), указывающее количество элементов в Map.
- Элементы 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 — это один байт, идентифицирующий тип. Индексы типов и параметры см. в справочнике здесь.
Значение NULL типа Dynamic кодируется с помощью BinaryTypeIndex 0x00 (тип Nothing) без дополнительных байтов:
Примеры:
JSON
Тип JSON кодирует данные в двух различных категориях:
- Типизированные пути — пути, объявленные в схеме с явным указанием типов (например,
JSON(user_id UInt32, name String)) - Динамические Path/пути переполнения при превышении лимита динамических путей - Path, обнаруженные во время выполнения, хранятся с типом
Dynamic. Кодированию значения предшествует определение типа.
Формат передачи данных и правила для этих двух категорий различаются.
| Категория Path | Включается в сериализацию | Кодирование значения | Допускается Variant/Nullable |
|---|---|---|---|
| Типизированные пути | Всегда (даже при NULL) | Типозависимый двоичный формат | Да |
| Динамические пути | Только если не NULL | Динамическое | Нет |
Пути сериализуются в трёх группах, записываемых последовательно: типизированные пути, динамические пути, затем пути общих данных (overflow). Типизированные и динамические пути записываются в порядке, определяемом реализацией (определяется итерацией по внутреннему хеш-map), тогда как пути общих данных записываются в алфавитном порядке. Не следует полагаться на какой-либо конкретный порядок путей. Десериализатор обрабатывает каждый путь по имени, а не по позиции.
Каждая JSON-строка в формате RowBinary сериализуется следующим образом:
Примеры:
1. Простой JSON только с типизированными путями:
Schema: JSON(user_id UInt32, active Bool)
Строка: {"user_id": 42, "active": true}
Двоичное кодирование (hex с аннотациями):
2. Простой JSON с типизированными и динамическими путями:
Schema: JSON(user_id UInt32, active Bool)
Строка: {"user_id": 42, "active": true, "name": "Alice"}
Двоичное кодирование (hex с аннотациями):
3. Обработка значений NULL:
С типизированным Nullable столбцом вы получаете null:
Schema: JSON(score Nullable(Int32))
Строка: {"score": null }
Двоичное кодирование (hex с аннотациями):
Для типизированного non-nullable столбца возвращается значение по умолчанию:
Схема: JSON(name String)
Строка: {"name": null}
Двоичное кодирование:
При динамическом пути параметр игнорируется:
Schema: JSON(id UInt64)
Строка: {"id": 100, "metadata": null}
Двоичное кодирование:
Note: Путь metadata со значением NULL не включается, поскольку динамические пути сериализуются только при ненулевых значениях. Это ключевое отличие от типизированных путей.
4. Вложенные объекты JSON:
Схема: JSON()
Строка: {"user": {"name": "Bob", "age": 30}}
Бинарное кодирование (hex, с аннотациями):
Примечание: Вложенные объекты разворачиваются в пути, разделённые точками (например, user.name вместо вложенной структуры).
Альтернатива: режим JSON в виде строки
При использовании настройки output_format_binary_write_json_as_string=1 JSON-столбцы сериализуются как одна текстовая строка JSON, а не в структурированном двоичном формате. Для записи в JSON-столбцы есть соответствующая настройка — input_format_binary_read_json_as_string. Выбор настройки здесь сводится к тому, хотите ли вы разбирать JSON на стороне клиента или на стороне сервера.
Типы Geo
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 — это тип Variant, который может содержать любой из перечисленных выше геотипов. В бинарном формате он кодируется точно так же, как Variant: байт дискриминанта указывает, какой геотип следует далее.
Индексы дискриминанта для Geometry:
| Index | Type |
|---|---|
| 0 | LineString |
| 1 | MultiLineString |
| 2 | MultiPolygon |
| 3 | Point |
| 4 | Polygon |
| 5 | Ring |
Структура бинарного формата:
Пример представления 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 базового типа элемента (Float32, Float64 или BFloat16). Оптимизация побитового транспонирования для хранения выполняется на стороне сервера, а не в протоколе RowBinary.
Синтаксис:
Где element_type — это Float32, Float64 или BFloat16, а dimension — фиксированная размерность вектора.
Формат представления в памяти: идентичен Array(element_type):
Пример кодирования QBit(Float32, 4), содержащего значения [1.0, 2.0, 3.0, 4.0]:
Параметры формата
Следующие настройки общие для всех форматов типа RowBinary.
| Setting | Description | Default |
|---|---|---|
format_binary_max_string_size | Максимально допустимый размер значения типа String в формате RowBinary. | 1GiB |
output_format_binary_encode_types_in_binary_format | Позволяет записывать типы в заголовке с использованием binary encoding вместо строк с именами типов в формате вывода RowBinaryWithNamesAndTypes. | false |
input_format_binary_decode_types_in_binary_format | Позволяет читать типы в заголовке с использованием binary encoding вместо строк с именами типов в формате ввода RowBinaryWithNamesAndTypes. | false |
output_format_binary_write_json_as_string | Позволяет записывать значения типа данных JSON как строковые значения JSON (типа String) в формате вывода RowBinary. | false |
input_format_binary_read_json_as_string | Позволяет читать значения типа данных JSON как строковые значения JSON (типа String) в формате ввода RowBinary. | false |