Skip to content
ClickHouse Docs
ClickHouse DocsClickHouse Docs

RowBinary

入力 出力 エイリアス

説明

RowBinary フォーマットは、バイナリ形式のデータを行単位で解析します。 行と値は、区切り文字なしで連続して並びます。 データがバイナリ形式であるため、FORMAT RowBinary の後の区切り文字は以下のとおり厳密に定められています。

  • 任意の数の空白文字:
    • ' ' (スペース - コード 0x20)
    • '\t' (タブ - コード 0x09)
    • '\f' (フォームフィード - コード 0x0C)
  • その後に、ちょうど 1 つの改行シーケンス:
    • Windows 形式 "\r\n"
    • または Unix 形式 '\n'
  • その直後にバイナリデータ。

データ型のワイヤ形式

その後、データはヘックスエディタで確認できます。

符号なし LEB128 (リトルエンディアン Base 128)

StringArrayMap などの可変サイズのデータ型の長さを符号化するために使われる、符号なしリトルエンディアンの可変長整数エンコーディングです。実装例は LEB128 の Wikipedia ページで確認できます。

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

すべての整数型は、適切なバイト数で リトルエンディアン 形式にエンコードされます。符号付き型 (Int8 から Int256) では、2 の補数 表現が使われます。ほとんどの言語では、組み込み機能または広く使われているライブラリを利用して、このような整数をバイト配列から取り出せます。大半の言語のネイティブな整数サイズを超える Int128/Int256 および UInt128/UInt256 については、独自にデシリアライズする必要がある場合があります。

Bool

ブール値は1バイトでエンコードされ、UInt8と同様にデシリアライズできます。

  • 0false
  • 1true

Float32, Float64

リトルエンディアン形式の浮動小数点数で、Float32 は4バイト、Float64 は8バイトでエンコードされます。整数と同様、ほとんどの言語にはこれらの値を適切にデシリアライズするための手段が用意されています。

BFloat16

BFloat16 (Brain Floating Point) は、Float32 と同じ範囲を持ちながら精度を抑えた 16 ビット浮動小数点フォーマットで、機械学習のワークロードに適しています。ワイヤ形式は、基本的に Float32 値の上位 16 ビットです。使用している言語がこれをネイティブでサポートしていない場合は、UInt16 として読み書きし、Float32 との間で相互変換するのが最も簡単です。

BFloat16 を Float32 に変換するには (疑似コード) :

// 2バイトをリトルエンディアンのUInt16として読み取る
// 16ビット左シフトしてFloat32のビット列を得る
bfloat16Bits = readUInt16()
float32Bits = bfloat16Bits << 16
floatValue = reinterpretAsFloat32(float32Bits)

Float32 を BFloat16 に変換するには (擬似コード) :

// Float32のビットを右に16ビットシフトしてBFloat16に切り詰める
float32Bits = reinterpretAsUInt32(floatValue)
bfloat16Bits = float32Bits >> 16
writeUInt16(bfloat16Bits)

BFloat16 の内部値の例:

SELECT CAST(1.25, 'BFloat16')
0xA0, 0x3F, // BFloat16 における 1.25

Decimal32, Decimal64, Decimal128, Decimal256

Decimal 型は、それぞれのビット幅に対応するリトルエンディアンの整数として表現されます。

  • Decimal32 - 4 バイト、つまり Int32
  • Decimal64 - 8 バイト、つまり Int64
  • Decimal128 - 16 バイト、つまり Int128
  • Decimal256 - 32 バイト、つまり Int256

Decimal 値をデシリアライズする際は、整数部と小数部を次の擬似コードで求められます。

let scale_multiplier = 10 ** scale
let whole_part = trunc(value / scale_multiplier)  // ゼロ方向に切り捨て
let fractional_part = value % scale_multiplier
let result = Decimal(whole_part, fractional_part)

ここで trunc は 0 に向かって切り捨てを行い (負の値では結果が異なる床除算ではなく) 、scale は小数点以下の桁数を表します。たとえば、Decimal(10, 2) (Decimal32(2) と同等) では、scale2 で、値 12345(123, 45) として表現されます。

シリアライゼーションでは、この逆の操作が必要です:

let scale_multiplier = 10 ** scale
let result = whole_part * scale_multiplier + fractional_part

詳細は、ClickHouseドキュメントのDecimal型を参照してください。

String

ClickHouse の文字列は、任意のバイト列です。有効な UTF-8 である必要はありません。長さのプレフィックスはバイト長であり、文字数ではありません。

エンコードは次の 2 つの部分で構成されます。

  1. 文字列の長さをバイト単位で示す可変長整数 (LEB128) 。
  2. 文字列の生のバイト列。

たとえば、文字列 foobar は次のように 7 バイトでエンコードされます。

0x06, // 文字列のLEB128長 (6)
0x66, // 'f'
0x6f, // 'o'
0x6f, // 'o'
0x62, // 'b'
0x61, // 'a'
0x72, // 'r'

FixedString

String とは異なり、FixedString はスキーマで定義される固定長を持ちます。バイト列としてエンコードされ、値が N より短い場合は末尾がゼロバイトで埋められます。

空の FixedString(3) にはパディングのゼロだけが含まれます:

0x00, 0x00, 0x00

文字列 hi が入った空でない FixedString(3):

0x68, // 'h'
0x69, // 'i'
0x00, // パディングゼロ

文字列 bar を含む、空でない FixedString(3):

0x62, // 'b'
0x61, // 'a'
0x72, // 'r'

最後の例では、3 バイトすべてを使用しているため、パディングは不要です。

Date

1970-01-01 からの日数を表す UInt16 (2 バイト) として格納されます。

サポートされる値の範囲: [1970-01-01, 2149-06-06]

Date の内部値の例:

SELECT CAST('2024-01-15', 'Date') AS d
0x19, 0x4D, // UInt16(リトルエンディアン)で19737 = 1970-01-01から19737日

Date32

1970-01-01前後の日数を表す Int32 (4バイト) として格納されます。

サポートされる値の範囲: [0000-01-01, 9999-12-31]

Date32 の内部値の例:

SELECT CAST('2024-01-15', 'Date32') AS d
0x19, 0x4D, 0x00, 0x00, // 19737 as Int32 (little-endian) = 19737 days since 1970-01-01

エポック以前の日時:

SELECT CAST('1900-01-01', 'Date32') AS d
0x21, 0x9C, 0xFF, 0xFF, // -25567 as Int32 (little-endian) = 25567 days before 1970-01-01

DateTime

1970-01-01 00:00:00 UTC からの 経過秒数を表す UInt32 (4バイト) として格納されます。

構文:

DateTime([timezone])

たとえば、DateTimeDateTime('UTC') です。

サポートされる値の範囲: [1970-01-01 00:00:00, 2106-02-07 06:28:15]

DateTime の内部値の例:

SELECT CAST('2024-01-15 10:30:00', 'DateTime(\'UTC\')') AS d
0x28, 0x09, 0xA5, 0x65, // 1705314600 をUInt32として(リトルエンディアン)

DateTime64

1970-01-01 00:00:00 UTCを基準としたティック数を表すInt64 (8バイト) として格納されます。値はその前後のいずれも取り得ます。ティックの精度はprecisionパラメータで定義されます。以下の構文を参照してください。

DateTime64(precision, [timezone])

precision0 から 9 までの整数です。通常使われるのは、3 (ミリ秒) 、6 (マイクロ秒) 、 9 (ナノ秒) のみです。

有効な DateTime64 定義の例: DateTime64(0)DateTime64(3)DateTime64(6, 'UTC')DateTime64(9, 'Europe/Amsterdam')

DateTime64 型の基になる Int64 値は、UNIX エポックの前後における以下の単位数として解釈できます。

  • DateTime64(0) - 秒。
  • DateTime64(3) - ミリ秒。
  • DateTime64(6) - マイクロ秒。
  • DateTime64(9) - ナノ秒。

サポートされる値の範囲: [0000-01-01 00:00:00, 9999-12-31 23:59:59.999999999] (precision が 7 までの場合。precision 8 および 9 ではより狭くなります。詳細は以下の注記を参照してください)。

DateTime64 の内部値の例:

  • DateTime64(3): 値 15463008000002019-01-01 00:00:00 UTC を表します。
  • DateTime64(6): 値 17053146001234562024-01-15 10:30:00.123456 UTC を表します。
  • DateTime64(9): 値 17053146001234567892024-01-15 10:30:00.123456789 UTC を表します。

Time

秒単位の時刻値を表す Int32 として格納されます。負の値も有効です。

サポートされる値の範囲は [-999:59:59, 999:59:59] (つまり [-3599999, 3599999] 秒) です。

Time の内部値の例:

SET enable_time_time64_type = 1;
SELECT CAST('15:32:16', 'Time') AS t
0x80, 0xDA, 0x00, 0x00, // 55936 Seconds = 15:32:16

Time64

内部的には Decimal64 (Int64 として格納) で保存され、小数秒を含む時刻値を表します。精度は設定可能です。負の値も有効です。

構文:

Time64(precision)

ここで、precision0 から 9 までの整数です。一般的な値は、3 (ミリ秒) 、6 (マイクロ秒) 、9 (ナノ秒) です。

サポートされる値の範囲は [-999:59:59.xxxxxxxxx, 999:59:59.xxxxxxxxx] です。

基になる Int64 の値は、秒の小数部分を 10^precision 倍した値を表します。

Time64 の内部値の例:

SET enable_time_time64_type = 1;
SELECT CAST('15:32:16.123456', 'Time64(6)') AS t
0x40, 0x82, 0x0D, 0x06,
0x0D, 0x00, 0x00, 0x00, // Int64としての55936123456
// 55936123456 / 10^6 = 55936.123456 Seconds = 15:32:16.123456

インターバル型

すべてのインターバル型は Int64 (8バイト、リトルエンディアン) として格納されます。値は、それぞれの時間単位の個数を表します。負の値も有効です。

インターバル型は次のとおりです: IntervalNanosecondIntervalMicrosecondIntervalMillisecondIntervalSecondIntervalMinuteIntervalHourIntervalDayIntervalWeekIntervalMonthIntervalQuarterIntervalYear

内部値の例:

SELECT INTERVAL 5 SECOND   AS a,
     INTERVAL 10 DAY     AS b,
     INTERVAL -7 DAY     AS c,
     INTERVAL 3 YEAR     AS d,
     INTERVAL 500 MICROSECOND AS e
// IntervalSecond: 5
0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// IntervalDay: 10
0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// IntervalDay: -7
0xF9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
// IntervalYear: 3
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// IntervalMicrosecond: 500
0xF4, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

Enum8、Enum16

enum 定義内での enum 値のインデックスを表す 1 バイト (Enum8 == Int8) または 2 バイト (Enum16 == Int16) として格納されます。ストレージ型は signed である点に注意してください。つまり、enum 値には負の値も指定できます (例: Enum8('a' = -128, 'b' = 0)) 。

Enum は次のように簡単に定義できます。

SELECT 1 :: Enum8('hello' = 1, 'world' = 2) AS e;
   ┌─e─────┐
1. │ hello │
   └───────┘

上で定義したEnum8は、クライアント側では次の値にマッピングされます。

Map<Int8, String> {
  1: 'hello',
  2: 'world'
}

あるいは、次のような、より複雑な方法もあります:

SELECT 42 :: Enum16('f\'' = 1, 'x =' = 2, 'b\'\'' = 3, '\'c=4=' = 42, '4' = 1234) AS e;
   ┌─e─────┐
1. │ 'c=4= │
   └───────┘

上で定義した Enum16 は、クライアント側では次の値にマッピングされます:

Map<Int16, String> {
  1:    'f\'',
  2:    'x =',
  3:    'b\'',
  42:   '\'c=4=',
  1234: '4'
}

データ型パーサーにとっての主な課題は、\' のような enum 定義内のエスケープされた記号や、引用符で囲まれた文字列内に現れる可能性のある = のような特殊記号を正しく追跡することです。

UUID

16 バイトの並びとして表現されます。UUID は、2 つのリトルエンディアン UInt64として格納されます。標準的な UUID 表現の最初の 8 バイトはバイト順が逆になり、後半の 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 は次のように表現されます:
0xE7, 0x11, 0xB3, 0x5C, 0x04, 0xC4, 0xF0, 0x61,
0xA0, 0xDB, 0xD3, 0x6A, 0x00, 0xA6, 0x7B, 0x90,
  • デフォルトの UUID 00000000-0000-0000-0000-000000000000 は、16 個のゼロバイトで表されます:
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

新しいレコードが挿入されたが、UUIDの値が指定されていない場合に使用できます。

IPv4

4バイトの UInt32 として、リトルエンディアン のバイト順で格納されます。これは、IP アドレスで一般的に使用される従来のネットワークバイトオーダー (ビッグエンディアン) とは異なる点に注意してください。IPv4 の内部値の例:

SELECT    
  CAST('0.0.0.0',         'IPv4') AS a,
  CAST('127.0.0.1',       'IPv4') AS b,
  CAST('192.168.0.1',     'IPv4') AS c,
  CAST('255.255.255.255', 'IPv4') AS d,
  CAST('168.212.226.204', 'IPv4') AS e
0x00, 0x00, 0x00, 0x00, // 0.0.0.0
0x01, 0x00, 0x00, 0x7f, // 127.0.0.1
0x01, 0x00, 0xa8, 0xc0, // 192.168.0.1
0xff, 0xff, 0xff, 0xff, // 255.255.255.255
0xcc, 0xe2, 0xd4, 0xa8, // 168.212.226.204

IPv6

ビッグエンディアン / ネットワークバイトオーダー (MSB 先頭) で 16 バイトに格納されます。IPv6 の内部値の例:

SELECT
    CAST('2a02:aa08:e000:3100::2',        'IPv6') AS a,
    CAST('2001:44c8:129:2632:33:0:252:2', 'IPv6') AS b,
    CAST('2a02:e980:1e::1',               'IPv6') AS c
// 2a02:aa08:e000:3100::2
0x2A, 0x02, 0xAA, 0x08, 0xE0, 0x00, 0x31, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
// 2001:44c8:129:2632:33:0:252:2
0x20, 0x01, 0x44, 0xC8, 0x01, 0x29, 0x26, 0x32, 
0x00, 0x33, 0x00, 0x00, 0x02, 0x52, 0x00, 0x02,
// 2a02:e980:1e::1
0x2A, 0x02, 0xE9, 0x80, 0x00, 0x1E, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,

Nullable

Nullable データ型は、次のようにエンコードされます。

  1. 値が NULL かどうかを示す 1 バイト:
    • 0x00 は、値が NULL ではないことを示します。
    • 0x01 は、値が NULL であることを示します。
  2. 値が NULL でない場合は、基になるデータ型が通常どおりエンコードされます。値が NULL の場合、基になる型については 追加のバイトは一切 書き込まれません。

たとえば、Nullable(UInt32) 型の値:

SELECT    
   CAST(42,   'Nullable(UInt32)') AS a,
   CAST(NULL, 'Nullable(UInt32)') AS b
0x00,                   // NULLではない - 値が続く
0x2A, 0x00, 0x00, 0x00, // UInt32(42)
0x01,                   // NULL - 後続データなし

LowCardinality

RowBinary フォーマットでは、low-cardinality マーカーはワイヤ形式に影響しません。たとえば、LowCardinality(String) は通常の String と同じようにエンコードされます。

テスト時には、カバレッジを広げるために、allow_suspicious_low_cardinality_types1 に設定すると、LowCardinality 内でほとんどの data types を許可できます。

Array

Array は次のようにエンコードされます。

  1. 配列の要素数を示す 可変長整数 (LEB128)
  2. 配列の各要素。基になるデータ型と同じ方法でエンコードされます。

たとえば、UInt32 型の値を持つ配列は次のとおりです。

SELECT CAST(array(1, 2, 3), 'Array(UInt32)') AS arr
0x03,                   // LEB128 - Arrayの要素数は3
0x01, 0x00, 0x00, 0x00, // UInt32(1)
0x02, 0x00, 0x00, 0x00, // UInt32(2)
0x03, 0x00, 0x00, 0x00, // UInt32(3)

やや複雑な例:

SELECT array('foobar', 'qaz') AS arr
0x02,             // LEB128 - 配列の要素数は2
0x06,             // LEB128 - 1番目の文字列は6バイト
0x66, 0x6f, 0x6f, 
0x62, 0x61, 0x72, // 'foobar'
0x03,             // LEB128 - 2番目の文字列は3バイト
0x71, 0x61, 0x7a, // 'qaz'

以下は有効です。

SELECT CAST([NULL, 'foo'], 'Array(Nullable(String))') AS arr;
   ┌─arr──────────┐
1. │ [NULL,'foo'] │
   └──────────────┘

そして、次のようにエンコードされます。

0x02,             // LEB128  - 配列には2つの要素がある
0x01,             // NULLである - この要素に続くデータはない
0x00,             // NULLでない - データが続く
0x03,             // LEB128  - 文字列は3バイト
0x66, 0x6f, 0x6f, // 'foo'

多次元配列の扱いの例は、Geo セクションに記載されています。

Tuple

タプルは、追加のメタ情報や区切り文字を使わず、各要素をそれぞれ対応するワイヤ形式で順に並べてエンコードされます。

CREATE OR REPLACE TABLE foo
(
    `t` Tuple(
           UInt32,
           String,
           Array(UInt8)
        )
)
ENGINE = Memory;
INSERT INTO foo VALUES ((42, 'foo', array(99, 144)));
0x2a, 0x00, 0x00, 0x00, // UInt32 として 42
0x03,                   // LEB128 - 文字列は 3 バイト
0x66, 0x6f, 0x6f,       // 'foo'
0x02,                   // LEB128 - 配列は 2 要素
0x63,                   // UInt8 として 99
0x90,                   // UInt8 として 144

Tuple データ型の文字列エンコーディングでは、エスケープされた記号や特殊文字を追跡する必要があるなど、Enum 型と同様の課題があります。さらに、Tuple では開き括弧と閉じ括弧も追跡する必要があります。加えて、複雑な Tuple には、ネストされた別の Tuple、Array、Map、さらには enum が含まれることもある点に注意してください。

たとえば次の表では、Tuple に名前の中にバッククォートと括弧を含む enum が含まれており、適切に処理しないとパースの問題を引き起こす可能性があります。

CREATE OR REPLACE TABLE foo
(
   `t` Tuple(
          Enum8('f\'()' = 0),
          Array(Nullable(Tuple(UInt32, String)))
       )
) ENGINE = Memory;

Map

map は Array(Tuple(K, V)) とみなすことができ、ここで K はキーの型、V は値の型です。map は次のようにエンコードされます。

  1. map 内の要素数を示す 可変長整数 (LEB128)
  2. map の各要素を、対応する型でエンコードされたキー・バリューのペアとして格納します。

たとえば、キーが String、値が UInt32 の map は次のとおりです。

SELECT CAST(map('foo', 1, 'bar', 2), 'Map(String, UInt32)') AS m
0x02,                   // LEB128 - マップの要素数は2
0x03,                   // LEB128 - 最初のキーは3バイト
0x66, 0x6f, 0x6f,       // 'foo'
0x01, 0x00, 0x00, 0x00, // UInt32(1)
0x03,                   // LEB128 - 2番目のキーは3バイト
0x62, 0x61, 0x72,       // 'bar'
0x02, 0x00, 0x00, 0x00, // UInt32(2)

Variant

この型は、ほかのデータ型のユニオンを表します。型 Variant(T1, T2, ..., TN) は、この型の各行が T1T2、…、TN のいずれかの型、またはそのどれにも該当しない値 (NULL 値) を持つことを意味します。

次の例を見てみましょう。

SET allow_experimental_variant_type = 1,
    allow_suspicious_variant_types = 1;
CREATE OR REPLACE TABLE foo
(
  -- It does not matter what is the order of types in the user input;
  -- the types are always sorted alphabetically in the wire format.
  `var` Variant(
           Array(Int16),
           Bool,
           Date,
           FixedString(6),
           Float32, Float64,
           Int128, Int16, Int32, Int64, Int8,
           String,
           UInt128, UInt16, UInt32, UInt64, UInt8
       )
)
ENGINE = MergeTree
ORDER BY ();
INSERT INTO foo VALUES (true), ('foobar' :: FixedString(6)), (100.5 :: Float64), (100 :: Int128), ([1, 2, 3] :: Array(Int16));
SELECT * FROM foo FORMAT RowBinary;
0x01,                               // type index -> Bool
 0x01,                               // true
 0x03,                               // type index -> FixedString(6)
 0x66, 0x6F, 0x6F, 0x62, 0x61, 0x72, // 'foobar' 
 0x05,                               // type index -> Float64
 0x00, 0x00, 0x00, 0x00, 
 0x00, 0x20, 0x59, 0x40,             // 100.5 as Float64
 0x06,                               // type index -> Int128
 0x64, 0x00, 0x00, 0x00, 
 0x00, 0x00, 0x00, 0x00, 
 0x00, 0x00, 0x00, 0x00, 
 0x00, 0x00, 0x00, 0x00,             // 100 as Int128
 0x00,                               // type index -> Array(Int16)
 0x03,                               // LEB128 - the array has 3 elements
 0x01, 0x00,                         // 1 as Int16
 0x02, 0x00,                         // 2 as Int16
 0x03, 0x00,                         // 3 as Int16

NULL 値は、判別値バイト 0xFF でエンコードされます:

SELECT NULL :: Variant(UInt32, String)
0xFF, // discriminant = NULL

allow_suspicious_variant_types 設定を使用すると、Variant 型をより網羅的にテストできます。

Dynamic

Dynamic 型は、実行時に決まる任意の型の値を保持できます。RowBinary フォーマットでは、各値は自己記述的です。最初の部分には、このフォーマットで表される型指定が入ります。続いて、このドキュメントで説明している値エンコードに従った内容が続きます。したがって、値をパースするには、型の索引を使って適切なパーサーを判断し、あとはすでに別の場所で使っている RowBinary のパース処理を再利用するだけです。

[BinaryTypeIndex][type-specific parameters...][value]

ここで、BinaryTypeIndex は型を識別する 1 バイトの値です。型インデックスとパラメータについては、こちら のリファレンスを参照してください。

NULL の Dynamic 値は、BinaryTypeIndex 0x00 (Nothing 型) としてエンコードされ、追加のバイトはありません:

SELECT NULL::Dynamic
00                        # BinaryTypeIndex: Nothing (0x00)、NULLを表す

例:

SELECT 42::Dynamic
0a                        # BinaryTypeIndex: Int64 (0x0A)
2a 00 00 00 00 00 00 00   # Int64 value: 42
SELECT toDateTime64('2024-01-15 10:30:00', 3, 'America/New_York')::Dynamic
14                        # BinaryTypeIndex: DateTime64WithTimezone (0x14)
03                        # UInt8: 精度
10                        # VarUInt: タイムゾーン名の長さ
41 6d 65 72 69 63 61 2f   # "America/"
4e 65 77 5f 59 6f 72 6b   # "New_York"
c0 6c be 0d 8d 01 00 00   # Int64: timestamps

JSON

JSON型はデータを2つの異なるカテゴリにエンコードします:

  1. 型付きパス - スキーマ内で型を明示して宣言されたパス (例: JSON(user_id UInt32, name String))
  2. 動的パス/動的パスの上限を超えた場合のオーバーフローパス - Dynamic 型として保存される実行時検出パス。値のエンコーディングの前に型定義が付きます。

この2つのカテゴリでは、ワイヤ形式とルールが異なります。

パスのカテゴリ シリアライゼーションに含まれるか 値のエンコーディング Variant/Nullable を使用可能か
型付きパス 常に含まれる (NULL の場合も含む) 型固有のバイナリ形式 はい
動的パス NULL でない場合のみ 動的 いいえ

パスは3つのグループに分けてシリアライズされ、typed paths、dynamic paths、shared data (オーバーフロー) pathsの順に書き込まれます。typed pathsとdynamic pathsは実装定義の順序 (内部ハッシュマップのイテレーションによって決定) で書き込まれ、shared data pathsはアルファベット順で書き込まれます。読み取り側は特定のパス順序に依存しないでください。デシリアライザは各パスを位置ではなく名前によってディスパッチします。

RowBinary フォーマットにおける各 JSON 行は、次のようにシリアライズされます:

[VarUInt: number_of_paths]
[String: path_1][value_1]
[String: path_2][value_2]
...

例:

1. 型付きパスのみを含むシンプルなJSON:

スキーマ: JSON(user_id UInt32, active Bool)

行: {"user_id": 42, "active": true}

バイナリエンコーディング (注釈付き16進数) :

02                              # VarUInt: 合計2パス

# 型付きパス "active"
06 61 63 74 69 76 65            # String: "active" (長さ6 + バイト列)
01                              # Bool/UInt8 値: true (1)

# 型付きパス "user_id"
07 75 73 65 72 5F 69 64         # String: "user_id" (長さ7 + バイト列)
2A 00 00 00                     # UInt32 値: 42 (リトルエンディアン)

2. 型付きパスと動的パスを持つシンプルなJSON:

スキーマ: JSON(user_id UInt32, active Bool)

行: {"user_id": 42, "active": true, "name": "Alice"}

バイナリエンコーディング (注釈付き16進数) :

03                              # VarUInt: 合計3パス

# 型付きパス "active"
06 61 63 74 69 76 65            # String: "active" (長さ6 + バイト列)
01                              # Bool/UInt8 値: true (1)

# 動的パス "name"
04 6E 61 6D 65                  # String: "name" (長さ4 + バイト列)
15                              # BinaryTypeIndex: String (0x15)
05 41 6C 69 63 65               # String 値: "Alice" (長さ5 + バイト列)

# 型付きパス "user_id"
07 75 73 65 72 5F 69 64         # String: "user_id" (長さ7 + バイト列)
2A 00 00 00                     # UInt32 値: 42 (little-endian)

3. NULLの処理:

型付きNullableカラムの場合、nullが返されます:

スキーマ: JSON(score Nullable(Int32))

行: {"score": null }

バイナリエンコーディング (注釈付き16進数) :

01                              # VarUInt: パス合計1件

# 型付きパス "score" (Nullable)
05 73 63 6f 72 65               # String: "score" (長さ5 + バイト列)
01                              # Nullableフラグ: 1 (NULLのため、後続の値なし)

型付き非Nullableカラムの場合、デフォルト値が返されます:

Schema: JSON(name String)

行: {"name": null}

バイナリエンコーディング:

01                              # VarUInt: 1 path(動的なNULLパスはスキップされます!)

04 6e 61 6d 65  # "name"
00              # String の長さ 0(空文字列)

動的パスの場合、これは無視されます:

スキーマ: JSON(id UInt64)

行: {"id": 100, "metadata": null}

バイナリエンコーディング:

01                              # VarUInt: パス1個(動的NULLパスはスキップされます!)

# 型付きパス "id"
02 69 64                        # String: "id" (length 2 + bytes)
64 00 00 00 00 00 00 00         # UInt64 値: 100(little-endian)

注意: NULL 値を持つ metadata パスは、動的パスが非 null の場合にのみシリアライズされるため、含まれません。これは型付きパスとの重要な相違点です。

4. ネストされたJSONオブジェクト:

スキーマ: JSON()

行: {"user": {"name": "Bob", "age": 30}}

バイナリエンコーディング (注釈付き16進表記) :

02                              # VarUInt: パス数 2(ネストされたオブジェクトはフラット化される)

# 動的パス "user.age"
08 75 73 65 72 2E 61 67 65      # String: "user.age"(長さ 8 + バイト列)
0A                              # BinaryTypeIndex: Int64(0x0A)
1E 00 00 00 00 00 00 00         # Int64 値: 30(リトルエンディアン)

# 動的パス "user.name"
09 75 73 65 72 2E 6E 61 6D 65   # String: "user.name"(長さ 9 + バイト列)
15                              # BinaryTypeIndex: String(0x15)
03 42 6F 62                     # String 値: "Bob"(長さ 3 + バイト列)

注: ネストされたオブジェクトは、ネスト構造ではなくドット区切りのパスにフラット化されます (例: user.name) 。

代替: JSONをStringとして扱うモード

設定 output_format_binary_write_json_as_string=1 を有効にすると、JSONカラムは構造化されたバイナリフォーマットではなく、1つのJSONテキスト文字列としてシリアライズされます。JSONカラムへの書き込みには、対応する設定 input_format_binary_read_json_as_string もあります。ここでどちらの設定を使うかは、JSONをクライアント側でパースするか、サーバー側でパースするかによって決まります。

Geo 型

Geo は、地理データを表現するデータ型のカテゴリです。次の型が含まれます。

  • Point - Tuple(Float64, Float64) として表されます。
  • MultiPoint - Array(Point) または Array(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 フォーマットのヘッダーには、これらの型の別名 (たとえば PointMultiPointRingPolygonMultiPolygonLineStringMultiLineString) が含まれます。

SELECT    (1.0, 2.0)                                       :: Point           AS point,
    [(3.0, 4.0), (5.0, 6.0)]                         :: Ring            AS ring,
    [[(7.0, 8.0), (9.0, 10.0)], [(11.0, 12.0)]]      :: Polygon         AS polygon,
    [[[(13.0, 14.0), (15.0, 16.0)], [(17.0, 18.0)]]] :: MultiPolygon    AS multi_polygon,
    [(19.0, 20.0), (21.0, 22.0)]                     :: LineString      AS line_string,
    [[(23.0, 24.0), (25.0, 26.0)], [(27.0, 28.0)]]   :: MultiLineString AS multi_line_string
// Point - または Tuple(Float64, Float64)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, // Point.X
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // Point.Y
// Ring - または Array(Tuple(Float64, Float64))
0x02, // LEB128 - "ring" 配列には 2 点が含まれる
   // Ring - 点 #1
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x40, 
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x40, 
   // Ring - 点 #2
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x40, 
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x40, 
// Polygon - または Array(Array(Tuple(Float64, Float64)))
0x02, // LEB128 - "polygon" 配列には 2 つの Ring が含まれる
   0x02, // LEB128 - 最初の Ring には 2 点が含まれる
      // Polygon - Ring #1 - 点 #1
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x40, 
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40,
      // Polygon - Ring #1 - 点 #2
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x40, 
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 
  0x01, // LEB128 - 2 番目の Ring には 1 点が含まれる
      // Polygon - Ring #2 - 点 #1 (唯一の点)
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x26, 0x40, 
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x40, 
// MultiPolygon - または Array(Array(Array(Tuple(Float64, Float64))))
0x01, // LEB128 - "multi_polygon" 配列には 1 つの Polygon が含まれる
   0x02, // LEB128 - 最初の Polygon には 2 つの Ring が含まれる
      0x02, // LEB128 - 最初の Ring には 2 点が含まれる
         // MultiPolygon - Polygon #1 - Ring #1 - 点 #1
         0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, 0x40, 
         0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2C, 0x40,
         // MultiPolygon - Polygon #1 - Ring #1 - 点 #2
         0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x40, 
         0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x40, 
      0x01, // LEB128 - 2 番目の Ring には 1 点が含まれる
        // MultiPolygon - Polygon #1 - Ring #2 - 点 #1 (唯一の点)
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x40, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x40, 
 // LineString - または Array(Tuple(Float64, Float64))
 0x02, // LEB128 - LineString には 2 点が含まれる
    // LineString - 点 #1
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x40, 
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x40,
    // LineString - 点 #2
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x35, 0x40, 
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x40, 
 // MultiLineString - または Array(Array(Tuple(Float64, Float64)))
 0x02, // LEB128 - MultiLineString には 2 つの LineString が含まれる
   0x02, // LEB128 - 最初の LineString には 2 点が含まれる
     // MultiLineString - LineString #1 - 点 #1
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x40, 
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x40, 
     // MultiLineString - LineString #1 - 点 #2
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0x40, 
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3A, 0x40, 
   0x01, // LEB128 - 2 番目の LineString には 1 点が含まれる
     // MultiLineString - LineString #2 - 点 #1 (唯一の点)
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3B, 0x40, 
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x40,

Geometry

Geometry は、上記に挙げた任意の Geo 型を保持できる Variant 型です。ワイヤ上では Variant とまったく同じようにエンコードされ、後続する Geo 型を示す 判別値 バイトが付加されます。

Geometry の 判別値 インデックスは次のとおりです。

インデックス
0 LineString
1 MultiLineString
2 MultiPolygon
3 Point
4 Polygon
5 Ring
6 MultiPoint

ワイヤ形式の構造:

// 1 byte discriminant (0-6)
// followed by the corresponding geo type data

PointGeometry としてエンコードした例:

SELECT ((1.0, 2.0)::Point)::Geometry
0x03,                                           // discriminant = 3 (Point)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, // Point.X = 1.0 as Float64
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // Point.Y = 2.0 as Float64

RingGeometry としてエンコードした例:

0x05,       // discriminant = 5 (Ring)
0x02,       // LEB128 - array has 2 points
// Point #1
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x40, // X = 3.0
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x40, // Y = 4.0
// Point #2
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x40, // X = 5.0
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x40, // Y = 6.0

Nested

Nested のワイヤ形式は、flatten_nested 設定に依存します。

flatten_nested = 1 (デフォルト)

デフォルト設定では、Nested は独立した配列にフラット化されます。各サブカラムは、ドット区切りの名前を持つ個別の Array カラムになります。

CREATE OR REPLACE TABLE foo
(
    n Nested(a String, b Int32)
) ENGINE = MergeTree ORDER BY ();
-- flatten_nested=1 はデフォルト値
INSERT INTO foo VALUES (['foo', 'bar'], [42, 144]);

DESCRIBE TABLE foo を実行すると、フラット化されたカラムが表示されます。

   ┌─name─┬─type──────────┐
1. │ n.a  │ Array(String) │
2. │ n.b  │ Array(Int32)  │
   └──────┴───────────────┘

各配列は、Array セクションで説明しているとおり、それぞれ独立してシリアライズされます。

0x02,                   // LEB128 - 最初の配列 (n.a) の String 要素数は 2
 0x03,                   // LEB128 - 最初の文字列は 3 バイト
 0x66, 0x6F, 0x6F,       // 'foo'
 0x03,                   // LEB128 - 2 番目の文字列は 3 バイト
 0x62, 0x61, 0x72,       // 'bar'
0x02,                   // LEB128 - 2 番目の配列 (n.b) の Int32 要素数は 2
 0x2A, 0x00, 0x00, 0x00, // Int32 としての 42
 0x90, 0x00, 0x00, 0x00, // Int32 としての 144

flatten_nested = 0

flatten_nested = 0 の場合、NestedArray(Tuple(...)) 型の単一のカラムとして保持されます。カラム名はドット区切りではありません。

SET flatten_nested = 0;
CREATE OR REPLACE TABLE foo
(
    n Nested(a String, b Int32)
) ENGINE = MergeTree ORDER BY ();
INSERT INTO foo VALUES ([('foo', 42), ('bar', 144)]);

DESCRIBE TABLE foo では、1 つのカラムが表示されます。

   ┌─name─┬─type───────────────────────┐
1. │ n    │ Nested(a String, b Int32)  │
   └──────┴────────────────────────────┘

エンコーディングは Array(Tuple(String, Int32)) です。最初に配列長のプレフィックスがあり、続いて各要素のタプルのフィールドが順に並びます。

0x02,                   // LEB128 - 配列の要素数: 2
 0x03,                   // LEB128 - 1番目のタプル、フィールド a: 3バイト
 0x66, 0x6F, 0x6F,       // 'foo'
 0x2A, 0x00, 0x00, 0x00, // 1番目のタプル、フィールド b: Int32 として 42
 0x03,                   // LEB128 - 2番目のタプル、フィールド a: 3バイト
 0x62, 0x61, 0x72,       // 'bar'
 0x90, 0x00, 0x00, 0x00, // 2番目のタプル、フィールド b: Int32 として 144

フィールドが、フラット化された表現のようにカラムごとにまとめられている (a₁, a₂, b₁, b₂) のではなく、要素ごとに交互に並んでいる (a₁, b₁, a₂, b₂) ことに注目してください。

SimpleAggregateFunction

SimpleAggregateFunction(func, T) は、基になるデータ型 T と同一の形式でエンコードされます。集約関数名はワイヤ形式に影響しません。

たとえば、SimpleAggregateFunction(max, UInt32) は通常の UInt32 と同じ形式でエンコードされます。

CREATE TABLE test_saf
(
    key UInt32,
    val SimpleAggregateFunction(max, UInt32)
) ENGINE = AggregatingMergeTree ORDER BY key;

INSERT INTO test_saf VALUES (1, 42);
SELECT val FROM test_saf;

RowBinaryWithNamesAndTypes のヘッダーでは型は SimpleAggregateFunction(max, UInt32) と示されますが、実際にワイヤ上で流れる値は単なる UInt32 です:

0x2A, 0x00, 0x00, 0x00, // UInt32 として 42

AggregateFunction

AggregateFunction(func, T) は、aggregate function の完全な中間状態を格納します。中間状態を格納する点は同じでも、それを基になるデータ型と同じ形式でエンコードする SimpleAggregateFunction とは異なり、AggregateFunction は各 aggregate function 固有のフォーマットを持つ不透明なバイナリブロブを格納します。

内部フォーマットは関数ごとに異なります。簡単な例をいくつか示します。

countState — カウントを VarUInt (LEB128) として格納します。

SELECT countState(number) FROM numbers(5)
0x05, // VarUInt: 5

sumState — 累積された合計を固定サイズの整数に格納します。ビット幅は引数の型に依存します (整数型の引数の場合は UInt64) :

SELECT sumState(toUInt32(number)) FROM numbers(5) -- 合計 = 0+1+2+3+4 = 10
0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // UInt64として10

minState / maxState — フラグバイトに続けて、基になる型の値を格納します。フラグは、空の状態 (値が1つもない) の場合は 0x00、値がある場合は 0x01 です:

SELECT maxState(toUInt32(number)) FROM numbers(5) -- 最大値 = 4
0x01,                   // フラグ: 値あり
0x04, 0x00, 0x00, 0x00, // UInt32 としての 4

空の状態 (集計された行がない) :

SELECT minState(toUInt32(number)) FROM numbers(0)
0x00, // フラグ: 値なし

QBit

QBit は、異なる精度レベルで効率的にルックアップできるベクトル型です。内部的には転置フォーマットで保存されます。転送時には、QBit は単に基底要素型 (Int8Float32Float64、または BFloat16) の Array です。保存時のビット転置の最適化は RowBinary プロトコルではなく、サーバー側で行われます。

構文:

QBit(element_type, dimension[, stride])

ここで、element_typeInt8Float32Float64、または BFloat16 で、dimension は固定のベクトル次元です。省略可能な stride は、サーバー側でビットプレーンを保存ストリームにどうグループ化するかだけを制御するもので、RowBinary のワイヤ形式には影響しません。ワイヤ形式は常に dimension 個の要素からなる完全な配列です。

ワイヤ形式: Array(element_type) と同一です。

// LEB128 length
// followed by `length` elements of `element_type`

[1.0, 2.0, 3.0, 4.0] を含む QBit(Float32, 4) のエンコード例:

SELECT [1.0, 2.0, 3.0, 4.0]::QBit(Float32, 4)
0x04,                   // LEB128 - array has 4 elements
0x00, 0x00, 0x80, 0x3F, // 1.0 as Float32
0x00, 0x00, 0x00, 0x40, // 2.0 as Float32
0x00, 0x00, 0x40, 0x40, // 3.0 as Float32
0x00, 0x00, 0x80, 0x40, // 4.0 as Float32

フォーマット設定

以下の設定は、すべての RowBinary 系フォーマットに共通です。

設定 説明 デフォルト
format_binary_max_string_size RowBinary フォーマットで許可される String の最大サイズです。 1GiB
output_format_binary_encode_types_in_binary_format RowBinaryWithNamesAndTypes 出力フォーマットで、ヘッダー内の型を、型名の文字列ではなく binary encoding を使って書き込めるようにします。 false
input_format_binary_decode_types_in_binary_format RowBinaryWithNamesAndTypes 入力フォーマットで、ヘッダー内の型を、型名の文字列ではなく binary encoding を使って読み取れるようにします。 false
output_format_binary_write_json_as_string RowBinary 出力フォーマットで、JSON データ型の値を JSON String 値として書き込めるようにします。 false
input_format_binary_read_json_as_string RowBinary 入力フォーマットで、JSON データ型の値を JSON String 値として読み取れるようにします。 false
Navigation