説明
Native フォーマットの完全な公式仕様は こちら で公開されており、それに対応する Native プロトコル — これを伝送する TCP wire protocol — の仕様は こちら で公開されています。
両方の仕様は、ClickHouse のソースコードをもとに LLM によって生成されたものです。コードが引き続き主要な正しい情報源であり、仕様とコードに食い違いがある場合は、コードが正しいものとします。
Native フォーマットは、カラムを行に変換しない真の「列指向」フォーマットであるため、ClickHouse で最も効率的なフォーマットです。
このフォーマットでは、データはバイナリフォーマットで ブロック 単位に読み書きされます。
各ブロックについて、行数、カラム数、カラム名と型、さらにブロック内の各カラムのデータ部分が順に記録されます。
これは、サーバー間のやり取りに使われるネイティブインターフェイス、コマンドラインクライアント、C++ クライアントで使用されるフォーマットです。
データ型のワイヤ形式
データはワイヤ上で列指向フォーマットで送信されます。つまり、各カラムは個別に送信され、 1 つのカラム内のすべての値が単一の配列としてまとめて送信されます。 ブロック内の各カラムには、RowBinaryWithNamesAndTypes と同様のヘッダーがあります。ネイティブ TCP バイナリプロトコルを使用する場合 (または HTTP エンドポイントが
?client_protocol_version=<n> を受信する場合) 、
カラム数と行数の前に BlockInfo 構造体が書き込まれます。このセクションの例では、
プロトコルバージョンを指定しない通常の HTTP インターフェイスを使用しているため、BlockInfo は省略されます。ブロック構造
次のクエリは、number と str の 2 つのカラムを持つ 3 行を返します:
複数のブロック
ただし、多くの場合、データは1つのブロックには収まらず、ClickHouse は複数のブロックに分けて送信します。 データが1ブロックにつき1行に分割されるよう、ブロックサイズを小さくして2行を取得する次のクエリを見てみましょう。単純なデータ型
これらの比較的単純なデータ型における個々の値のワイヤ形式は、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 および RowBinaryWithNamesAndTypes のものとは異なります。
- Nullable
- LowCardinality
- Array
- Map
- Variant
- Dynamic
- JSON
Nullable
Nativeフォーマットでは、Nullableカラムでは実際のデータの前に、ブロック内の行数と同じだけのバイト列が置かれます。各バイトは、その値が NULL かどうかを示します。たとえば、このクエリでは、代わりに各奇数が NULL になります。
Nullable(String) でも同様です。null インジケーターは常に nullable のマスクバイトで決まり、
マスク値が 0x01 の場合は、文字列の内容に関係なくその行は NULL になります。NULL の行では、
実際の文字列は空文字列として格納されます (LEB128 の長さは 0) 。なお、NULL ではない空
文字列も LEB128 の長さは 0 なので、この 2 つのケースを区別できるのはマスクバイトだけです。たとえば、次のクエリです。
LowCardinality
LowCardinality が透過的に扱われる RowBinary とは異なり、Nativeフォーマット では辞書ベースの列指向エンコーディングを使用します。カラムは、バージョンプレフィックス、一意な値の辞書、その辞書を参照する整数の索引の Array としてエンコードされます。
カラムは
LowCardinality(Nullable(T)) として定義できますが、Nullable(LowCardinality(T)) として定義することはできません。常にサーバーエラーになります。1 の UInt64(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 幅を使って一括シリアライゼーションされます。
String では空文字列、数値型では 0) 。LowCardinality(Nullable(T)) の場合、索引 0 は NULL を表し、キーは Nullable ラッパーなしでシリアライゼーションされます。
たとえば、5 行 ['foo', 'bar', 'baz', 'foo', 'bar'] を持つ LowCardinality(String) は次のように表されます。
LowCardinality(Nullable(String)) では、索引 0 は NULL になります:
Array
各配列の先頭に LEB128 形式の要素数が付く RowBinary とは異なり、Nativeフォーマットでは、配列は次の 2 つの列指向サブストリームとしてエンコードされます。- 累積
UInt64オフセットが N 個 (リトルエンディアン、各 8 バイト) 。行iの要素数はoffset[i] - offset[i-1]で、offset[-1]は暗黙的に 0 とみなされます。 - 全行にわたるすべてのネスト要素を、連続した領域にまとめて一括シリアライズします。
[[0, 10], [1, 11], [2, 12]] を持つ Array(UInt32) の場合:
Array(String) [[], ['0'], ['0','1'], ['0','1','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}] は次のようにエンコードされます。
Variant
各行がそれぞれ自身の discriminant バイトを持ち、その直後に値がインラインで続く RowBinary とは異なり、Nativeフォーマット では discriminant とデータが分離されています。Variant カラムは次のようにエンコードされます。
UInt64(LE)の discriminator モード prefix (0= BASIC、1= COMPACT) 。Nativeフォーマット の出力では通常 BASIC (0) が使用されます。COMPACT モードは、use_compact_variant_discriminators_serializationを有効にして保存されたデータを読み取る際に現れることがあります。- N 個の
UInt8discriminant。各行に 1 つずつ対応します。 - 各 Variant 型のデータは、それに一致する行だけを含む個別のバルクカラムとして、discriminant の順に格納されます。
[0::UInt32, 'hello', NULL, 3::UInt32, 'hello'] を持つ Variant(String, UInt32) の場合 (ソート順: String = 0、UInt32 = 1) :
Dynamic
各値が自己記述的 (型プレフィックス + 値) である RowBinary とは異なり、Nativeフォーマット ではDynamic は構造プレフィックスに続く Variant カラムとしてシリアライズされます。
構造プレフィックスには、UInt64(LE) のシリアル化バージョン、動的型の数 (VarUInt として) 、続いて文字列としての型名が含まれます。バージョン V1 では、互換性のため型の数が 2 回書き込まれます。後続のデータは Variant カラムで、その型リストは動的型に内部 SharedVariant 型を加えたものをアルファベット順に並べたものです。
たとえば、5 行の [0::UInt32, 'hello', NULL, 3::UInt32, 'hello'] を持つ Dynamic の場合:
JSON
各行がパス名と値を含み自己記述的である RowBinary とは異なり、Nativeフォーマット ではJSON は列指向の構造でシリアライズされます。このエンコーディングは複雑で、バージョンにも依存します。具体的には、シリアル化バージョン、動的パス名、共有データレイアウトを含む構造プレフィックスに続いて、型付きパス (それぞれをバルクカラムとして格納) 、動的パス (それぞれを Dynamic カラムとして格納) 、さらにオーバーフローパス用の共有データで構成されます。
より簡単に相互運用できるようにするには、JSON カラムを通常の JSON テキスト文字列 (各行につき 1 つの String) としてシリアライズする設定 output_format_native_write_json_as_string=1 の使用を検討してください。