Описание
RowBinary разбирает данные по строкам в бинарном формате.
Строки и значения идут подряд, без разделителей.
Поскольку данные представлены в бинарном формате, разделитель после FORMAT RowBinary должен строго иметь следующий вид:
- Любое количество пробельных символов:
' '(пробел — код0x20)'\t'(табуляция — код0x09)'\f'(перевод страницы — код0x0C)
- Затем ровно одна последовательность перевода строки:
- в стиле Windows
"\r\n" - или в стиле Unix
'\n'
- в стиле Windows
- Сразу после неё следуют бинарные данные.
Этот формат менее эффективен, чем формат Native, поскольку он основан на строках.
Формат передачи данных для типов данных
Беззнаковый LEB128 (Little Endian Base 128)
String, Array и Map. Пример реализации можно найти на странице LEB128 в Википедии.
(U)Int8, (U)Int16, (U)Int32, (U)Int64, (U)Int128, (U)Int256
Int8–Int256) используют представление в дополнительном коде. В большинстве языков такие целые числа можно извлекать из массивов байтов либо встроенными средствами, либо с помощью широко известных библиотек. Для Int128/Int256 и UInt128/UInt256, которые превышают встроенные размеры целых чисел в большинстве языков, может потребоваться собственная десериализация.
Bool
UInt8.
0—false1—true
Float32, Float64
Float32 и в 8 байт для Float64. Как и в случае с целыми числами, в большинстве языков есть подходящие инструменты для десериализации этих значений.
BFloat16
BFloat16:
Decimal32, Decimal64, Decimal128, Decimal256
Decimal32- 4 байта, илиInt32.Decimal64- 8 байт, илиInt64.Decimal128- 16 байт, илиInt128.Decimal256- 32 байта, илиInt256.
trunc выполняет усечение к нулю (а не деление с округлением вниз, которое для отрицательных значений даёт другой результат), а scale — это количество цифр после десятичной точки. Например, для Decimal(10, 2) (эквивалент Decimal32(2)) scale равен 2, а значение 12345 будет представлено как (123, 45).
Для сериализации требуется выполнить обратную операцию:
String
- Целое число переменной длины (LEB128), указывающее длину строки в байтах.
- Сырые байты строки.
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
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 можно определить простым способом, например так:
enum, такие как \', а также специальные символы вроде =, которые могут встречаться внутри строк в кавычках.
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 нулевых байтов:
IPv4
UInt32 с порядком байтов little-endian. Обратите внимание, что это отличается от традиционного сетевого порядка байтов (big-endian), который обычно используется для IP-адресов. Примеры внутренних значений для IPv4:
IPv6
IPv6:
Nullable
- Один байт, который показывает, является ли значение
NULL:0x00означает, что значение неNULL.0x01означает, что значениеNULL.
- Если значение не
NULL, базовый тип данных кодируется как обычно. Если значениеNULL, для базового типа не записываются дополнительные байты.
Nullable(UInt32):
LowCardinality
LowCardinality(String) кодируется так же, как обычный String.
Столбец можно определить как
LowCardinality(Nullable(T)), но нельзя определить как Nullable(LowCardinality(T)) — это всегда будет приводить к ошибке сервера.1, чтобы разрешить использование большинства типов данных внутри LowCardinality для более полного покрытия.
Array
- Целое число переменной длины (LEB128), указывающее число элементов в массиве.
- Элементы массива, закодированные так же, как и значения базового типа данных.
UInt32:
Массив может содержать значения типа Nullable, но сам массив не может иметь тип Nullable.
Tuple
Map
Array(Tuple(K, V)), где K — тип ключа, а V — тип значения. Map кодируется следующим образом:
- Целое число переменной длины (LEB128), указывающее количество элементов в Map.
- Элементы Map в виде пар ключ-значение, закодированные в соответствии с их типами.
String и значениями типа UInt32:
Возможны значения типа Map с глубоко вложенными структурами, например
Map(String, Map(Int32, Array(Nullable(String)))), которые кодируются аналогично описанному выше.Variant
Variant(T1, T2, ..., TN) означает, что каждая строка этого типа содержит значение либо типа T1, либо T2, либо …, либо TN, либо не содержит ни одного из них (значение NULL).
Рассмотрим следующий пример:
NULL используется байт дискриминанта 0xFF:
Variant.
Dynamic
Dynamic может содержать значения любого типа, определяемого во время выполнения. В формате RowBinary каждое значение самодостаточно: первая часть — это спецификация типа в таком формате. Затем следует содержимое со значением, закодированным так, как описано в этом документе. Поэтому, чтобы разобрать значение, достаточно использовать индекс типа для выбора подходящего парсера, а затем повторно использовать уже имеющийся у вас код разбора RowBinary.
BinaryTypeIndex — это один байт, обозначающий тип. Индексы типов и параметры см. в справочнике здесь.
Значение Dynamic со значением NULL кодируется с BinaryTypeIndex 0x00 (тип Nothing) без дополнительных байтов:
JSON
- Типизированные пути - пути, объявленные в схеме с явным указанием типов (например,
JSON(user_id UInt32, name String)) - Динамические пути/пути переполнения при превышении лимита динамических путей — пути, обнаруженные во время выполнения и хранящиеся как тип
Dynamic. Перед кодированием значения указывается определение типа.
Пути сериализуются в трёх группах, записываемых последовательно: типизированные пути, динамические пути, затем пути общих данных (overflow). Типизированные и динамические пути записываются в порядке, определяемом реализацией (определяется порядком итерации по внутренней хэш-карте), тогда как пути общих данных записываются в алфавитном порядке. Не следует полагаться на какой-либо конкретный порядок путей. Десериализатор обрабатывает каждый путь по имени, а не по позиции.
Каждая JSON-строка в формате RowBinary сериализуется следующим образом:
JSON(user_id UInt32, active Bool)
Строка: {"user_id": 42, "active": true}
Двоичное кодирование (hex с аннотациями):
JSON(user_id UInt32, active Bool)
Строка: {"user_id": 42, "active": true, "name": "Alice"}
Двоичное кодирование (hex с аннотациями):
JSON(score Nullable(Int32))
Строка: {"score": null }
Двоичное кодирование (hex с аннотациями):
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: на стороне клиента или на стороне сервера.
Гео-типы
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))).
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 показывает столбцы в развёрнутом виде:
flatten_nested = 0
flatten_nested = 0 Nested сохраняется как один столбец типа Array(Tuple(...)). Имя столбца не содержит точек:
DESCRIBE TABLE foo возвращает один столбец:
Array(Tuple(String, Int32)): сначала префикс длины массива, затем поля кортежа каждого элемента по порядку:
SimpleAggregateFunction
SimpleAggregateFunction(func, T) кодируется так же, как базовый тип данных T. Имя агрегатной функции не влияет на формат передачи данных.
Например, SimpleAggregateFunction(max, UInt32) кодируется так же, как обычный UInt32:
SimpleAggregateFunction(max, UInt32), но фактическое значение в формате передачи данных — просто UInt32:
AggregateFunction
AggregateFunction(func, T) хранит полное промежуточное состояние агрегатной функции. В отличие от SimpleAggregateFunction, которая тоже хранит промежуточное состояние, но кодирует его так же, как и базовый тип данных, AggregateFunction хранит непрозрачный бинарный blob, формат которого зависит от конкретной агрегатной функции.
Внутренний формат зависит от функции. Несколько простых примеров:
countState — хранит счётчик как VarUInt (LEB128):
sumState — хранит накопленную сумму в целочисленном значении фиксированной разрядности. Разрядность зависит от типа аргумента (UInt64 для целочисленных аргументов):
minState / maxState — хранит байт флага, за которым следует значение соответствующего базового типа. Флаг равен 0x00 для пустого состояния (значения не встречались) или 0x01, если значение присутствует:
Более сложные функции, такие как
uniq, quantile или groupArray, используют форматы, специфичные для конкретной реализации. Если вам нужно читать или записывать эти состояния, обратитесь к исходному коду ClickHouse для соответствующей функции.QBit
QBit — это векторный тип для эффективного поиска с различными уровнями точности. Внутренне он хранится в транспонированном виде. При передаче QBit представляет собой просто Array базового типа элемента (Int8, Float32, Float64 или BFloat16). Оптимизация хранения за счёт транспонирования битов выполняется на стороне сервера, а не в протоколе RowBinary.
Синтаксис:
element_type — Int8, Float32, Float64 или BFloat16, а dimension — фиксированная размерность вектора. Необязательный параметр stride управляет только тем, как битовые плоскости группируются в потоки хранения на стороне сервера; он не влияет на формат передачи данных RowBinary, который всегда представляет собой полный массив из dimension элементов.
Формат передачи данных: идентичен Array(element_type):
QBit(Float32, 4) для [1.0, 2.0, 3.0, 4.0]:
Настройки формата
RowBinary.