メインコンテンツへスキップ
メインコンテンツへスキップ

Native

入力出力エイリアス

説明

Native フォーマットは、カラムを行に変換しないという意味で真の「列指向」であり、ClickHouse において最も効率的なフォーマットです。

このフォーマットでは、データはバイナリ形式で ブロック ごとに書き込みおよび読み取りが行われます。各ブロックについて、行数、カラム数、カラム名と型、およびブロック内の各カラムのデータが順番に記録されます。

このフォーマットは、サーバー間のやり取りに使われるネイティブインターフェイス、コマンドラインクライアント、および C++ クライアントで使用されます。

ヒント

このフォーマットを使用して、ClickHouse DBMS でのみ読み取ることができるダンプを高速に生成できます。 ただし、このフォーマットを手作業で扱うのは現実的ではない場合があります。

データ型のワイヤーフォーマット

データは列指向フォーマットで送信されます。つまり、各カラムは個別に送信され、 1 つのカラム内のすべての値が 1 つの配列としてまとめて送信されます。

ブロック内の各カラムには、RowBinaryWithNamesAndTypes と同様のヘッダーが含まれます。

注記

ネイティブ TCP バイナリプロトコルを使用する場合 (または HTTP エンドポイントが ?client_protocol_version=<n> を受け取る場合) 、 カラム数と行数の前に BlockInfo 構造体が書き込まれます。このセクションの例では、 プロトコルバージョンを指定しない通常の HTTP インターフェイスを使用しているため、BlockInfo は省略されます。

ブロック構造

次のクエリは、numberstr の 2 つのカラムを持つ 3 行を返します。

curl -XPOST "http://localhost:8123?default_format=Native" --data-binary "SELECT number, toString(number) AS str FROM system.numbers LIMIT 3" > out.bin

出力データは1つのClickHouseブロックに収まり、次のようになります。

const data = new Uint8Array([
  // --- Block Header ---
  0x02,                   // 2 columns
  0x03,                   // 3 rows
  // -- Column 1 Header --
  0x06,                   // LEB128 - column name 'number' has 6 bytes
  0x6e, 0x75, 0x6d,       
  0x62, 0x65, 0x72,       // column name: 'number'
  0x06,                   // LEB128 - column type 'UInt64' has 6 bytes
  0x55, 0x49, 0x6e,
  0x74, 0x36, 0x34,       // 'UInt64'
  0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, // 0 as UInt64
  0x01, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, // 1 as UInt64
  0x02, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, // 2 as UInt64
  0x03,                   // LEB128 - column name 'str' has 3 bytes
  0x73, 0x74, 0x72,       // column name: 'str'
  0x06,                   // LEB128 - column type 'String' has 6 bytes
  0x53, 0x74, 0x72, 
  0x69, 0x6e, 0x67,       // 'String'
  0x01,                   // LEB128 - the string has 1 byte
  0x30,                   // '0' as String
  0x01,                   // LEB128 - the string has 1 byte
  0x31,                   // '1' as String
  0x01,                   // LEB128 - the string has 1 byte
  0x32,                   // '2' as String
])

複数のブロック

ただし、多くの場合、データは1つのブロックには収まらず、ClickHouse は複数のブロックに分けてデータを送信します。 次のクエリでは、ブロックサイズを小さくして、2行のデータが1ブロックにつき1行となるよう強制的に分割しています。

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([
 
  // ----- Block 1 ----- 
  0x02,                   // 2 columns
  0x01,                   // 1 row
  0x06,                   // LEB128 - column name 'number' has 6 bytes
  0x6E, 0x75, 0x6D, 
  0x62, 0x65, 0x72,       // column name: 'number' 
  0x06,                   // LEB128 - column type 'UInt64' has 6 bytes
  0x55, 0x49, 0x6E, 
  0x74, 0x36, 0x34,       // 'UInt64' 
  0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, // 0 as UInt64
  0x03,                   // LEB128 - column name 'str' has 3 bytes
  0x73, 0x74, 0x72,       // column name: 'str'
  0x06,                   // LEB128 - column type 'String' has 6 bytes
  0x53, 0x74, 0x72, 
  0x69, 0x6E, 0x67,       // 'String'
  0x01,                   // LEB128 - the string has 1 byte
  0x30,                   // '0' as String
  
  // ----- Block 2 -----
  0x02,                   // 2 columns
  0x01,                   // 1 row
  0x06,                   // LEB128 - column name 'number' has 6 bytes
  0x6E, 0x75, 0x6D,  
  0x62, 0x65, 0x72,       // column name: 'number'
  0x06,                   // LEB128 - column type 'UInt64' has 6 bytes
  0x55, 0x49, 0x6E,  
  0x74, 0x36, 0x34,       // 'UInt64'
  0x01, 0x00, 0x00, 0x00,  
  0x00, 0x00, 0x00, 0x00, // 1 as UInt64
  0x03,                   // LEB128 - column name 'str' has 3 bytes
  0x73, 0x74, 0x72,       // column name: 'str'
  0x06,                   // LEB128 - column type 'String' has 6 bytes
  0x53, 0x74, 0x72,  
  0x69, 0x6E, 0x67,       // 'String'
  0x01,                   // LEB128 - the string has 1 byte
  0x31,                   // '1' as 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 data types wire format" にある上記の型の説明を参照してください。

複合データ型

以下の型のエンコード形式は、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([
  // --- Block Header ---
  0x01,                         // LEB128 - 1 column
  0x05,                         // LEB128 - 5 rows
  
  // -- Column Header --
  0x0A,                         // LEB128 - column name has 10 bytes
  0x6D, 0x61, 0x79, 0x62, 0x65, 
  0x5F, 0x6E, 0x75, 0x6C, 0x6C, // column name: 'maybe_null'
  
  0x10,                         // LEB128 - column type has 16 bytes
  0x4E, 0x75, 0x6C, 0x6C, 
  0x61, 0x62, 0x6C, 0x65, 
  0x28, 0x55, 0x49, 0x6E, 
  0x74, 0x36, 0x34, 0x29,       // column type: 'Nullable(UInt64)'
  
  // -- Nullable mask --
  0x00,                         // Row 0 is NOT NULL
  0x01,                         // Row 1 is NULL
  0x00,                         // Row 2 is NOT NULL
  0x01,                         // Row 3 is NULL
  0x00,                         // Row 4 is NOT NULL
  
  // -- UInt64 values --
  0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00,       // Row 0: 0 as UInt64

  // even though we still might have a proper value for this number 
  // in the block, it should be still returned as NULL to the user!
  0x01, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00,       // Row #1: NULL
  
  0x02, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00,       // Row #2: 2 as UInt64
  
  0x03, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00,       // Row #3: NULL, similar to Row #1
  
  0x04, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00,       // Row #4: 4 as UInt64
]);

Nullable(String) でも同様です。null インジケーターは常に nullable マスクバイトから取得され、 マスク値が 0x01 の場合は、文字列の内容に関係なく、その行は NULL です。NULL の行では、 基になる文字列は空文字列 (LEB128 長 0) として格納されます。なお、NULL ではない空 文字列も LEB128 長は 0 であるため、この 2 つのケースを区別できるのはマスクバイトだけです。例えば、次のクエリです:

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([
  // --- Block Header ---
  0x01, // LEB128 - 1 column
  0x05, // LEB128 - 5 rows

  // -- Column Header --
  0x09, // LEB128 - column name has 9 bytes
  0x6d,
  0x61,
  0x79,
  0x62,
  0x65,
  0x5f,
  0x73,
  0x74,
  0x72, // column name: 'maybe_str'

  0x10, // LEB128 - column type has 16 bytes
  0x4e,
  0x75,
  0x6c,
  0x6c,
  0x61,
  0x62,
  0x6c,
  0x65,
  0x28,
  0x53,
  0x74,
  0x72,
  0x69,
  0x6e,
  0x67,
  0x29, // column type: 'Nullable(String)'

  // -- Nullable mask --
  0x00, // Row 0 is NOT NULL
  0x01, // Row 1 is NULL
  0x00, // Row 2 is NOT NULL
  0x01, // Row 3 is NULL
  0x00, // Row 4 is NOT NULL

  // -- String values --
  0x01,
  0x30, // Row 0: LEB128 == 1, '0' as String
  0x00, // Row 1: LEB128 == 0, NULL
  0x01,
  0x32, // Row 2: LEB128 == 1, '2' as String
  0x00, // Row 3: LEB128 == 0, NULL
  0x01,
  0x34, // Row 4: LEB128 == 1, '4' as String
])

LowCardinality

LowCardinality が透過的な RowBinary とは異なり、Native フォーマットでは辞書ベースの列指向エンコーディングを使用します。カラムは、バージョンプレフィックス、一意な値の辞書、その辞書を参照する整数の索引配列の順にエンコードされます。

注記

カラムは LowCardinality(Nullable(T)) として定義できますが、Nullable(LowCardinality(T)) として定義することはできません。これは常にサーバーエラーになります。

バージョンプレフィックスは値 1UInt64(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 ラッパーなしでシリアライズされます。

たとえば、5 行 ['foo', 'bar', 'baz', 'foo', 'bar'] を持つ LowCardinality(String) の場合:

// Version prefix
01 00 00 00 00 00 00 00    // UInt64(LE) = 1

// IndexesSerializationType: UInt8 indexes, has keys, update dictionary
00 06 00 00 00 00 00 00    // UInt64(LE) = 0x0600

04 00 00 00 00 00 00 00    // 4 dictionary keys
00                          // key 0: "" (default)
03 66 6f 6f                 // key 1: "foo"
03 62 61 72                 // key 2: "bar"
03 62 61 7a                 // key 3: "baz"

05 00 00 00 00 00 00 00    // 5 rows
01 02 03 01 02              // indexes → "foo", "bar", "baz", "foo", "bar"

LowCardinality(Nullable(String)) では、インデックス 0 は NULL になります:

01 00 00 00 00 00 00 00    // version
00 06 00 00 00 00 00 00    // IndexesSerializationType
03 00 00 00 00 00 00 00    // 3 keys
00                          // key 0: NULL
00                          // key 1: "" (default)
03 79 65 73                 // key 2: "yes"
05 00 00 00 00 00 00 00    // 5 rows
02 00 02 00 02              // indexes → "yes", NULL, "yes", NULL, "yes"

Array

各配列の先頭に LEB128 形式の要素数が付く RowBinary とは異なり、Native フォーマットでは配列は次の 2 つの列指向サブストリームとしてエンコードされます。

  • 累積 UInt64 オフセットが N 個 (リトルエンディアン、各 8 バイト) 。行 i の要素数は offset[i] - offset[i-1] で、offset[-1] は暗黙的に 0 とみなされます。
  • すべての行にまたがるネストされた全要素を、連続した領域にまとめてシリアライズします。

たとえば、3 行の [[0, 10], [1, 11], [2, 12]] を持つ Array(UInt32) の場合:

// Offsets
02 00 00 00 00 00 00 00    // 2 (row 0: 2 elements)
04 00 00 00 00 00 00 00    // 4 (row 1: 2 elements)
06 00 00 00 00 00 00 00    // 6 (row 2: 2 elements)

// Nested UInt32 values (6 total)
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

空の配列のオフセットは、前の行と同じです。たとえば、4 行の Array(String)[[], ['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 とは異なります。

たとえば、3 行の Map(String, UInt64) [{'a':0,'b':10}, {'a':1,'b':11}, {'a':2,'b':12}] は次のとおりです。

// Array offsets
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

// All keys (6 Strings)
01 61                       // "a"
01 62                       // "b"
01 61                       // "a"
01 62                       // "b"
01 61                       // "a"
01 62                       // "b"

// All values (6 UInt64s)
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    // 12

Variant

各行がそれぞれ識別バイトを持ち、その直後に値がインラインで続く RowBinary とは異なり、Native フォーマットでは識別子とデータが分離されています。

注意

RowBinary と同様に、定義内の型は常にアルファベット順にソートされ、識別子はそのソート済みリストにおけるインデックスです。0xFF (255) は NULL を表します。

Variant カラムは次のようにエンコードされます。

  • UInt64(LE) の識別子モードプレフィックス (0 = BASIC、1 = COMPACT) 。Native フォーマットの出力では通常 BASIC (0) が使用されます。COMPACT モードは、use_compact_variant_discriminators_serialization を有効にして保存されたデータを読み込む場合に現れることがあります。
  • N 個の UInt8 識別子。各行につき 1 つです。
  • 各 variant 型のデータ。該当する行のみを含む個別のバルクカラムとして、識別子の順序で格納されます。

たとえば、5 行の [0::UInt32, 'hello', NULL, 3::UInt32, 'hello'] を持つ Variant(String, UInt32) の場合 (ソート順: String = 0、UInt32 = 1) :

00 00 00 00 00 00 00 00    // discriminators mode = BASIC
01 00 ff 01 00              // UInt32, String, NULL, UInt32, String

// String (2 values, rows 1 and 4)
05 68 65 6c 6c 6f          // "hello"
05 68 65 6c 6c 6f          // "hello"

// UInt32 (2 values, rows 0 and 3)
00 00 00 00                 // 0
03 00 00 00                 // 3

Dynamic

各値が自己記述的 (型プレフィックス + 値) である RowBinary とは異なり、Native フォーマットでは Dynamic は構造プレフィックスの後に続く Variant カラムとしてシリアライズされます。

構造プレフィックスには、まず UInt64(LE) のシリアル化バージョン、次に動的型の数 (VarUInt として) 、その後に型名が文字列として含まれます。バージョン V1 では、互換性のため型の数が 2 回書き込まれます。後続のデータは Variant カラムであり、その型リストは動的型に内部の SharedVariant 型を加えたもので、アルファベット順に並べられます。

たとえば、5 行の Dynamic [0::UInt32, 'hello', NULL, 3::UInt32, 'hello'] は次のようになります:

// Structure prefix (V1)
01 00 00 00 00 00 00 00    // version = V1
02                          // num types (V1 writes twice)
02                          // num types
06 53 74 72 69 6e 67       // "String"
06 55 49 6e 74 33 32       // "UInt32"

// Variant data: Variant(SharedVariant, String, UInt32)
// discriminants: SharedVariant=0, String=1, UInt32=2
00 00 00 00 00 00 00 00    // discriminators mode = BASIC
02 01 ff 02 01              // UInt32, String, NULL, UInt32, String
// SharedVariant: 0 values
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: 3

JSON

各行がパス名と値を含む自己記述的な RowBinary とは異なり、Native フォーマットでは JSON は列指向構造でシリアライズされます。このエンコードは複雑で、バージョンにも依存します。内容は、シリアル化バージョン、動的パス名、共有データのレイアウトを含む構造プレフィックスに続き、型付きパス (各パスは一括カラム) 、動的パス (各パスは Dynamic カラム) 、およびオーバーフローパス用の共有データで構成されます。

よりシンプルに相互運用したい場合は、設定 output_format_native_write_json_as_string=1 の使用を検討してください。これにより、JSON カラムはプレーンな JSON テキスト文字列 (各行に String を 1 つ) としてシリアライズされます。