- ネイティブ TCP プロトコルにおける
Data、Totals、Extremes、Log、ProfileEventsパケットの本体 (TableColumnsパケットは Native ブロックではありません。このパケットは 2 つのバイナリ文字列を格納するため、そのレイアウトはネイティブプロトコル仕様で扱います) - HTTP 経由で実行した
SELECT ... FORMAT Nativeの出力 INTO OUTFILE ... FORMAT Nativeで書き出したエクスポートファイル- サーバー間レプリケーションのペイロード
概要
wire 上で行を運ぶものはすべて Block です。Block は、行をカラムごとに格納した自己記述型の chunk です。まずカラム 1 のすべての値が並び、次にカラム 2 のすべての値が続き、以降も同様です。Block にはクエリが参照するカラムのみが含まれ、テーブル全体が含まれることはありません。 カラムのdata のレイアウトは、その型が属するファミリーによって決まります。ファミリーをデコーダの複雑さが低い順に挙げると、次のとおりです。

- 固定幅型は、
dataをbytes_per_value × num_rowsバイトの raw bytes として配置します。行ごとのフレーミングはありません。 - 複合型 (
Nullable、Array、Tuple、Map、Nested) は、型文字列から完全に導出できる再帰的な構造を持ちます。バージョンプレフィックスやブロック間の状態はありません。 - バージョン付き / ステートフル型 (
LowCardinality、JSON、Variant、Dynamic) は、空でない各ブロックの先頭にシリアライゼーションバージョン/state prefix を持ちます。Nativewire では、このプレフィックスおよび Dictionary はブロックごとに存在し、フォーマットがブロック間で状態を持ち越すことはありません (writer はブロックごとに新しいシリアル化状態を作成し、low_cardinality_max_dictionary_size = 0を設定します) 。ブロック間の状態は MergeTree のディスク上の表現に関わるものであり、Native のwireレイアウトとは関係ありません。
wire プリミティブ
Native フォーマットは、4 種類のプリミティブなエンコーディングで構成されています。VarUInt
LEB-128エンコーディングを使用する可変長の符号なし整数です。各バイトは、ビット位置0–6に7ビットのデータを、ビット位置7に1ビットの継続ビットを格納します。継続ビットは、後続のバイトがある場合は1、最後のバイトでは0になります。
値
300をエンコードする例:
0xAC 0x02 のデコード:
固定幅整数
例えば、UInt32 の値
1 は 01 00 00 00 に、Int32 の値 -1 は FF FF FF FF にエンコードされます。
String
長さプレフィックス付きのバイト列:0x00 としてエンコードされます。また、文字列には途中に NUL を含め、任意のバイト値を含めることができます。文字列 "ab" は 02 61 62 としてエンコードされます。デコードする際は、まず VarUInt の長さ (2) を読み取り、続いてその長さ分のバイトを読み取ります。
Bool
1 バイトで表されます。0x00 は false、0 以外の値はすべて true を表します (標準の表現は 0x01) 。
Block とカラムの構造
Block のwireレイアウト
BlockInfo プレフィックスの有無はチャネルによって異なります。これは、writer が リビジョン によってパラメータ化されているためです(client_protocol_version が出力専用であることも含めた詳細は プロトコルのリビジョンとNativeフォーマット を参照してください)。
-
ネイティブ TCP プロトコル では、サーバーは接続時にネゴシエートされたリビジョン(大きな値 —
DBMS_TCP_PROTOCOL_VERSION、src/Core/ProtocolDefines.hを参照)でブロックを書き込みます。BlockInfoはそのリビジョンが 0 より大きければ必ず書き込まれ、実際の接続では常にこの条件を満たします。各カラムのhas_custom_serializationバイト(カラムのwireレイアウト を参照)は、リビジョン54454以上で書き込まれます。 -
Native出力フォーマット — HTTP 経由のSELECT ... FORMAT Native、INTO OUTFILE ... FORMAT Native、およびclickhouse-clientが生成するNativeフォーマット — は、デフォルトでは リビジョン0でシリアライズされます。リビジョン0ではBlockInfoプレフィックスとhas_custom_serializationバイトがいずれも省略されるため、ブロックはnum_columns、num_rows、およびカラムだけで構成されます。 HTTP 経由の場合、このリビジョンは固定ではありません。クライアントは?client_protocol_version=<n>クエリパラメータでリビジョンを引き上げることができ、サーバーはその値をレスポンスのシリアライゼーションのリビジョンとして使用します。 十分に大きな値を指定すると、HTTP 出力にも TCP 経路とまったく同じように、BlockInfoプレフィックス(リビジョンが0より大きい場合に書き込まれる)とhas_custom_serializationバイト(リビジョン54454以上で書き込まれる)が含まれます。したがってクライアントは、HTTP のFORMAT Nativeペイロードがすべてリビジョン0であると想定してはいけません。
BlockInfo プレフィックスで始まるバイト例は、TCP の Data パケットのペイロードを示しています。同じクエリを FORMAT Native で実行した場合は、併記されている短い形式が生成されます。
BlockInfo
BlockInfo はフィールドの並びで、各フィールドの前には VarUInt のフィールド ID が置かれ、末尾はフィールド ID0 で終端されます。このwire形式は自己記述的ではありません。フィールド ID には値の長さや型がエンコードされていないため、リーダーは出現しうるすべてのフィールド ID の型をあらかじめ把握しておく必要があります。ClickHouse 自身のリーダーは、認識できないフィールド ID を破損とみなし、例外 (UNKNOWN_BLOCK_INFO_FIELD) を送出します。前方互換性は、その代わりにプロトコルリビジョンによって担保されています。送信側は、ネゴシエートされたリビジョンがそのフィールドの最小リビジョン以上である場合にのみフィールドを書き込むため、古い受信側が未知のフィールドを受け取ることはありません。
フィールド
1 と 2 は最小リビジョンが 0 であるため、BlockInfo が書き込まれる場合は必ず含まれます。フィールド 3 はリビジョン 54480 以上の場合にのみ書き込まれます。一般的なケース (リビジョンが 54480 未満) のwireレイアウトは次のとおりです。
カラムのwireレイアウト
Column は 1 つの Block 内にnum_columns 回出現します。
デコーダは
type 文字列に基づいて処理を振り分けます。型文字列には括弧付きのパラメータが含まれることが多く、デコーダは (...) サフィックスを取り除いて基本型を特定したうえで、パラメータをパースしてサイズ、スケール、内部型を決定します。Nested 型 (Array 内の Tuple など) を含むパラメータリストをパースする場合、, で単純に分割するのではなく、括弧のネストの深さを追跡しながらカンマで分割する処理が必要です。
バイナリ型エンコーディング
type フィールドがテキストの String になるのは、デフォルトのモードの場合だけです。クエリ設定 output_format_native_encode_types_in_binary_format = 1 が設定されている場合、このフィールドは バイナリ型エンコーディング になります。これは データ型のバイナリエンコーディング で説明しているものと同じタグベースのエンコーディングです。また、フラット化された Dynamic 型のリストでも、各型の名前に同じバイナリエンコーディングが使用されます。フィールド 2 を常に長さプレフィックス付きの文字列として読み取るデコーダは、最初のバイナリ型タグを文字列長と誤認して同期がずれてしまいます。そのため、デコーダはストリームがどちらのモードを使用しているかを把握しておく必要があります。query_kind = SECONDARY_QUERY のクエリ (remote、Distributed、並列レプリカのために別のサーバーから送信されたもの、または clickhouse-client --query_kind secondary_query で送信されたもの) では、送受信の双方が、そのクエリのブロックについて output_format_native_encode_types_in_binary_format と input_format_native_decode_types_in_binary_format を無視します。つまり、どちらの方向でもブロックにはテキストの型名が含まれます。このルールが導入される前のサーバーは、セカンダリクエリにも他のクエリと同様にこれらの設定を適用します。
kind_stack とスパースエンコーディング
kind_stack バイトは、デフォルト以外のカラムごとのシリアライゼーションを列挙します。
COMBINATION のペイロードは別の enum を使用します。 上記の 5 行は compact な 1 バイトコードです。COMBINATION (0x05) は、これらでカバーされない任意のスタックを表すための汎用エスケープであり、その後に VarUInt の count、さらに count 個の 1 バイトのエントリが続きます。これらのエントリは表の compact コードではなく、生の ISerialization::Kind 値です。
バイト値は compact コードとは異なります。
REPLICATED はこのネストされた enum では 0x03 ですが、compact コードでは 0x04 です。また DETACHED_OVER_SPARSE のエントリは存在せず、この組み合わせは SPARSE、DETACHED という 2 つの連続したエントリとして表されます。ネストされたバイトにも compact テーブルを適用し続けるデコーダは、0x03/0x04 を誤ってマッピングし、同期がずれます。
count は、すべてのスタックの先頭にある DEFAULT エントリを含めたスタック全体の長さです。エントリ数が 1 または 2 のスタックはすべて compact コードでカバーされているため、COMBINATION の count は常に 3 以上になります。
kind スタックの妥当性。 デコーダは不正なスタックを受け取った場合、それが記述するシリアライゼーションを構築せず、INCORRECT_DATA で拒否します。条件は次のとおりです。
- スタックは
DEFAULTで始まること。 - スタックは
DEFAULT、SPARSE、REPLICATED、DETACHEDの部分列であること — これは各 kind が互いをラップする順序です。SPARSEはREPLICATEDの内側に配置できますが、その逆はできません。またColumnBLOBはその下位にあるすべての要素のシリアル化された形式を保持するため、DETACHEDは常に最後になります。このことから同じ kind が 2 回出現することはなく、countの上限は kind の数となります。
ColumnSparse は、ラップされたままになります。
Tuple カラムの再帰的な kind_stack。 上記の kind_stack ペイロードは、1 つのカラム自身のシリアライゼーション情報を表すバイト (または COMBINATION シーケンス) です。Tuple は SerializationInfoTuple を持ち、まずタプル自身の kind スタックペイロードを書き込み、続いて各要素の完全な kind スタックペイロードを順に書き込みます。デコーダも同じ再帰構造で読み戻します。したがって Tuple(A, B, C) の場合、フィールド 4 のバイトは [tuple_kind][A_kind][B_kind][C_kind] となり、要素がさらに複合型であれば、その要素のペイロード自体も再帰的な構造になります。has_custom_serialization バイト (フィールド 3) は、タプル自身の情報またはいずれかの要素の情報がデフォルト以外であれば必ずセットされます。そのため、特殊な要素がスパースまたはレプリケートされた要素だけである Tuple でも、kind スタックペイロードがトリガーされます。Tuple に対して先頭の enum バイト 1 つだけを読み取るデコーダは、読み取りを途中で打ち切ってしまい、残りの要素 kind バイトをカラムデータとして誤読します。
スパースのwire形式。 kind_stack = 0x01 の場合、カラムの data は、単一の共有 TCP ストリーム内に連続して書き込まれる 2 つのストリームで構成されます。
- オフセットストリーム —
VarUIntの数列です。各値vは次のいずれかです。- ビット 62 (上位ビット) がクリアされている
v:(v & 0x3FFFFFFFFFFFFFFF)は、次の明示的な非デフォルト値より前にあるデフォルト位置の数を表します。この非デフォルト位置はcursor + group_sizeで、cursorは現在の位置を表します。その後、cursorはgroup_size + 1だけ進みます。 - ビット 62 がセットされている
v(END_OF_GRANULE_FLAG): フラグをクリアした値は、最後の非デフォルト値の後に続く末尾のデフォルト位置の数を表します。これは、そのブロックにおける オフセットストリーム の終端を示します。
- ビット 62 (上位ビット) がクリアされている
- Values stream — 内部型で密にエンコードされた
count個の非デフォルト値です。countは、上記で読み取った VarUInt のうち EOG 以外のものの数です。
0、String では ""、Date では 0 日など) で補完し、num_rows 個のエントリからなる密なカラムを再構築します。
スパースな Nullable(T) カラムは特殊なケースです。Nullable(T) のデフォルト値は NULL であるためです。スパースエンコーディングでは、通常の Nullable の null マップストリームは完全に省略されます。オフセットストリーム は非デフォルト (すなわち非 NULL) の位置を特定し、values ストリームはそれらの非 NULL 値のみを T で密に保持し、明示されていない位置はすべて NULL として再構築されます。したがって、デコーダは values ストリーム内で null マップを探してはならず、欠落位置を実在する値 0 で補完してもなりません。欠落位置は NULL で補完します。
Replicated のwire形式。 kind_stack = 0x04 の場合、カラムの data は Dictionary です。つまり、重複のない要素値のリストと、そのリストを指す行ごとの索引で構成されます (LowCardinality と同じルックアップ構造です)。内部型自体がバージョン付きである場合 (例: LowCardinality(T))、その state prefix は索引ストリームより前に書き込まれます。これは、replicated シリアライゼーションが num_rows を書き込む前に、プレフィックスフェーズを内部型に委譲するためです。プレフィックスが空の内部型 (リーフ型および単純な複合型) は、ここでは 1 バイトも出力しません。
i について elements[indexes[i]] を選択することで、密なカラムを再構築します。複合的な内部型は再帰的に処理されます。つまり、要素リストを内部型としてマテリアライズしてから、索引で参照します。サポート済みの内部型には、leaf 型、Nullable(T)、Array(T)、Tuple(...)、Map(K, V)、Nested(...) (各フィールドは Array と同様に展開されます) 、および LowCardinality(T) (共有 Dictionary はそのまま保持され、要素ごとのキーのみが索引で参照されます) があります。
デタッチされたwire形式。 DETACHED (0x02) と DETACHED_OVER_SPARSE (0x03) は、実際にwire上に出現します。純粋に内部でのみ使われる値ではありません。TCP 経路では、圧縮が有効で、かつネゴシエートされたリビジョンが DBMS_MIN_REVISON_WITH_PARALLEL_BLOCK_MARSHALLING (v54478) 以上の場合、カラムは次の 3 つのステップで処理されます。
- 対象となる各カラム (
constでもTupleでもなく、かつ 2 行以上を含むブロック内のもの) は、メインスレッド以外ですでにマーシャリングおよび圧縮済みのカラムを保持するColumnBLOBでラップされます。 - ラップされたカラムの kind stack に
DETACHEDが追加されます。 - カラムの
dataは、VarUInt形式のブロブサイズと、それに続くちょうどそのバイト数分のブロブバイトとして書き込まれます。
{DEFAULT, SPARSE, DETACHED} となり、DETACHED_OVER_SPARSE としてシリアライズされます。このようなカラムをデコードするクライアントは、ブロブの長さとバイト列を読み取った後、ブロブを展開して内部カラムのペイロードを復元します (圧縮の節にある ColumnBLOB に関する注記 を参照してください) 。
DETACHED には方向性があります。サーバーが送信する結果ブロック内でのみ、しかもトップレベルのカラム全体に対してのみ使用でき、Tuple の要素の kind の 1 つとして現れることはありません。他の kind とは異なり、DETACHED を指定すると、ランタイム上のカラムは宣言された型のカラムではなく ColumnBLOB になります。そのため受信側がこれを受け入れるのは、自身のパイプラインでブロブを元の型に変換し直せる場合に限られます。したがってサーバーは、クライアントが送信するブロックに DETACHED が含まれていれば、外部テーブルのデータ、スカラー、挿入データのいずれであっても、INCORRECT_DATA として拒否します。
Block のバリアント
Data 系のパケットはすべて、同一の Block wire形式を使用します。各バリアントの違いは、カラム数と行数のみです。バイトレベルの例
このセクションの例はすべて TCP の Data パケットのパス から取得したものであるため、BlockInfo プレフィックスと has_custom_serialization バイトが含まれています。FORMAT Native を使用する場合、同じブロックはより短くなります。理解の助けになる箇所には、同等の短い形式も併記しています。
空のブロック (BlockInfo 付き) 、合計 8 バイト:
SELECT 1 のヘッダーブロックは、UInt8 型の "1" という名前のカラム 1 つを、0 行で通知します。プロトコルバージョン 54454 以上では、has_custom_serialization バイトが含まれます。
FORMAT Native (リビジョン 0) を使用した場合、同じ結果ブロックには BlockInfo も has_custom_serialization バイトも含まれないため、SELECT 1 FORMAT Native は 11 バイトになります:
FORMAT Native では 1 バイトも出力されません。この出力フォーマットは空のブロックを出力しないためです。)
プロトコルのリビジョンとNativeフォーマット
Native のバイトストリームの構造を何よりも大きく左右するのは、ライターとリーダーが動作しているプロトコルのリビジョンです。リビジョンはバイト列自体のどこにも含まれておらず、wire 上にリビジョンを示すフィールドは存在しません。それにもかかわらず、いくつかの機能がそもそも現れるかどうかはリビジョンによって決まります。そのため、デコーダはペイロードをパースする前に、そのペイロードがどのリビジョンで書き込まれたかを把握しておく必要があります。リビジョンはストリームに含まれないため、リーダーとライターは別の手段でリビジョンを取り決めておかなければなりません。 リビジョンは単一のUInt64 値で、NativeWriter と NativeReader のどちらもコンストラクタの引数として受け取ります。ライター側では client_revision、リーダー側では server_revision という名前ですが、どちらも同じ値を指します。このリリースが認識している最新のリビジョンは DBMS_TCP_PROTOCOL_VERSION です (src/Core/ProtocolDefines.h を参照) 。
リビジョンによって制御される機能
各機能はDBMS_MIN_REVISION_WITH_* のしきい値によって制御されます。ライターはリビジョンがしきい値に達した場合にのみその機能を出力し、リーダーもまったく同じ規則に従ってその機能を探すため、両者は常に同期が保たれます。どちらか一方でリビジョンを誤ると、両者の同期が崩れます。Native フォーマット に関係するゲートは次のとおりです。
そのため、リビジョン
0 はほぼすべての点で最も保守的なエンコーディングとなります。ストリームには BlockInfo も has_custom_serialization バイトも含まれず、Dynamic/JSON は V1 となり、集約関数のバージョンも含まれず、DateTime はタイムゾーンパラメータが取り除かれた素の形になります。ただし、リビジョン 0 における集約関数の state バージョンは例外です。バージョンを導出する相手側が存在しない (ストリームは、それを生成した側自身が書き込み、読み戻すもの。たとえば StripeLog のデータファイル、Set/Join のバックアップ、Native フォーマットのファイルなど) ため、ライター側は state をバージョン 0 にダウングレードせず、型に固定されたバージョンをそのまま維持します (通常どおり型文字列内に明記します) 。バージョン 0 で書き込まれるのは、バージョンを固定していない型だけです。
LowCardinality は唯一の例外であり、しかも重要な例外です。ライター側のチェックは remove_low_cardinality || (client_revision && client_revision < DBMS_MIN_REVISION_WITH_LOW_CARDINALITY_TYPE) です。ポイントは先頭の client_revision && です。リビジョンがちょうど 0 の場合、条件全体が short-circuit により false になります。
したがって、リビジョン 0 (FORMAT Native のデフォルト) では、LowCardinality(T) は除去されません。型文字列とBlockごとの state prefix はストリーム内に残り、リビジョン 0 のリーダーはそれらをそのまま読み戻します。除去が行われるのは、リビジョンが 54405 未満かつゼロ以外の場合か、リビジョンに関係なく強制された場合に限られます。
この強制を担うのが remove_low_cardinality フラグです。FORMAT Native の出力がこのフラグを設定することはありませんが、ネイティブ TCP 経路では low_cardinality_allow_in_native_format = 0 (デフォルトは 1) のときに設定されます。つまり、この設定はネイティブ TCP の出力には影響しますが、FORMAT Native には一切影響しません。
実務上の要点として、デフォルトの FORMAT Native ストリームに LowCardinality が含まれることは正当にありえます。リビジョン 0 では存在しない機能として扱わないようにしてください。
データの転送経路ごとのリビジョンの決まり方
同じ Native バイト列でも、ネイティブ TCP プロトコル、HTTP リクエスト、ディスク上のファイルなど、さまざまな経路で転送されます。リビジョンの決まり方は経路ごとに異なります。注意点として、読み取り側と書き込み側はそれぞれ個別に設定されるため、両者のリビジョンが一致しない場合があります。ネイティブ TCP プロトコル — ネゴシエーションあり、双方向
ネイティブ TCP プロトコルでは、リビジョンは Hello ハンドシェイクで決まります。クライアントがDBMS_TCP_PROTOCOL_VERSION を送信すると、サーバーは自身の値を返します。以降、双方はそれぞれ相手側が通知したリビジョンでシリアライズを行います。サーバーは client_tcp_protocol_version に基づいて NativeReader/NativeWriter を構築し、クライアントは受信した server_revision を使用します。明示的な min は存在しませんが、どちらの側も自身が実装していない機能を送出することはできないため、各方向の上限は実質的に 2 つのピアのうち古い方によって決まります。
両方のピアが同じ最新ビルドであれば、両方向とも同じリビジョン (DBMS_TCP_PROTOCOL_VERSION。src/Core/ProtocolDefines.h を参照) に揃い、すべてのゲートが有効になります。これが一般的なケースですが、常にそうなるとは限りません。バージョンが混在している場合やサードパーティ製のピアが関わる場合は、方向ごとにリビジョンが異なることがあるため、ゲートは方向ごとに判断する必要があります。BlockInfo は 0 以外であればどのリビジョンでも含まれますが、それ以外のフィールド (has_custom_serialization を含む) は、その方向の実効リビジョンが各しきい値に達して初めて含まれるようになります。例えば、54454 未満のリビジョンを通知するピアは、has_custom_serialization バイトを送信も受信もしません。
FORMAT Native 出力 — デフォルトはリビジョン 0、HTTP 経由で引き上げ可能
Native の出力フォーマットは、デフォルトでリビジョン 0 を使用します。対象となるのは、HTTP 経由の SELECT ... FORMAT Native、INTO OUTFILE ... FORMAT Native、および clickhouse-client が書き出す Native 出力です。いずれの場合も、出力ファクトリは FormatSettings::client_protocol_version をそのまま NativeWriter に渡します。この値によって、BlockInfo / has_custom_serialization のフレーミングとオフセット String レイアウトの両方が決まります。そのため、client_protocol_version を引き上げた (≥ 54492) HTTP の SELECT ... FORMAT Native はオフセットレイアウトで書き出され、デフォルトのリビジョン 0 では移植性のある値ごとのレイアウトが維持されます。
ただし HTTP の場合、話はこのデフォルトだけでは終わりません。クライアントは ?client_protocol_version=<n> クエリパラメータでこの値を引き上げられます。HTTP ハンドラーはこれを SQL の設定ではなく予約されたパラメータとして扱い、クエリコンテキストに格納します。その後、フォーマット層がこの値を FormatSettings にコピーします。十分に大きな値を指定すると、HTTP の FORMAT Native 出力にも TCP 経路と同様に BlockInfo プレフィックスと has_custom_serialization バイトが含まれるようになります。したがって、HTTP の FORMAT Native ペイロードが常にリビジョン 0 であるとは限りません。
このパラメータはクエリコンテキスト全体に格納されるため、結果ストリームだけでなく、そのリクエストで生成されるすべての NativeWriter に影響します。たとえば、?client_protocol_version= を引き上げた HTTP リクエストで INSERT INTO FUNCTION file('x.native', 'Native', ...) SELECT ... のようなサーバー側の書き込みを行うと、ファイルはそのリビジョンで書き出されます。これは以前からある動作で (String のオフセットシリアライゼーションの導入以前から存在し、BlockInfo と has_custom_serialization にも適用されます) 、しかも非対称です。読み取り側 (後述) は同じ方法では引き上げられないため、このようにして書き出したファイルは file(...) で読み戻せません。後で再度読み込むデータを書き出すリクエストでは、client_protocol_version を指定しないでください。なお、INTO OUTFILE およびローカルの clickhouse-client 出力にはこのような調整手段がなく、常に 0 のままです。
FORMAT Native 入力 — 常にリビジョン 0
Native 入力フォーマットは、常にリビジョン 0 で NativeReader を構築します。BlockInfo プレフィックスを想定せず、has_custom_serialization バイトも読み取らず、常にデフォルトのシリアライゼーションを前提とします。これは INSERT ... FORMAT Native のボディをパースする場合も、Native ファイルを読み取る場合も、スキーマを推論する場合も同様です。?client_protocol_version=<n> を指定しても入力側のリビジョンは引き上げられません (このパラメータを読み取るのは出力側のみです) 。
入力リビジョンは常に 0 であるため、入力フォーマットは常に値ごとの String レイアウトで読み取ります。そのため、オフセットレイアウトで書き込まれたストリーム (ネイティブ TCP プロトコル経由のもの、または client_protocol_version を引き上げた SELECT ... FORMAT Native で出力されたもの) は、FORMAT Native 入力では読み戻せません。これは BlockInfo のフレーミングと同じ非対称性です。
往復変換への影響
FORMAT Native では、デフォルトのリビジョンは両端とも 0 です。リビジョン 0 で SELECT ... FORMAT Native によって書き出されたデータは、そのまま INSERT ... FORMAT Native で問題なく読み戻せます。
出力リビジョンを引き上げて (SELECT に ?client_protocol_version=<n> を指定して) 生成したストリームには、BlockInfo と has_custom_serialization のフレーミング、およびオフセット String レイアウトが含まれます。しかし入力フォーマットは常にリビジョン 0 で動作するため、これらはいずれも読み戻せません。したがって、client_protocol_version を引き上げるのはネイティブ TCP プロトコルを扱うコンシューマー向けの場合に限られ、HTTP 経由で FORMAT Native を往復変換する用途には向きません。
ファイルの場合は非対称になります。サーバー側の INSERT INTO FUNCTION file(...) (または s3、url) で ?client_protocol_version= を引き上げると、ファイルはそのリビジョンで書き込まれますが、file(...) による読み取りは常にリビジョン 0 でパースされます。そのため、このようなファイルは往復変換できません。書き込みリクエストに client_protocol_version を指定せず、ファイルを通常のリビジョン 0 で書き出すか、ネイティブ TCP プロトコル経由でデータを移動してください。ネイティブ TCP プロトコルでは、各方向でハンドシェイクによりネゴシエートされたリビジョンが使用されます。
プロトコルリビジョンとシリアル化バージョンの違いプロトコルリビジョンをシリアル化バージョンと混同しないでください。ここでいうリビジョンは接続単位またはリクエスト単位で適用され、バイト列には現れません。一方、シリアル化バージョンはカラムごとに存在し、バージョン付き型によって保持され、空でないすべてのBlockに書き込まれます。リビジョンはある機能がそもそも含まれるかどうかを決め、シリアル化バージョンは、バージョン付きカラムの内部で、その型のエンコーディングのうちどのバリアントが続くかを決めます。
データ型
このセクションでは、Native フォーマットがカラムのdata 内に保持できる型のワイヤエンコーディングについて説明します。各型は、デコーダの実装の複雑さが低いものから順に 4 つのファミリーに分類されています。AggregateFunction(func, ...) と QBit(T, N[, stride]) の 2 つの型は Native のカラム型として有効ですが、関数や型に固有のペイロードを持つため、本ドキュメントでは扱いません。ただし、別名と誤解されやすい箇所では、以下でその旨を明記します。
固定長型
各値は一定のバイト数を占めます。M 行のカラムは、wire 上でちょうど bytes_per_row × M バイトを占め、各値は区切り文字やパディングなしで連結されます。
整数型
UInt8–UInt256 および Int8–Int256 は、整数値をそのままバイナリエンコードしたものです。デコーダは bytes_per_row × num_rows バイトを読み取り、型に応じて解釈します。
[1, 256, 65536] を格納する UInt32 カラムの例:
[-1, 42] を格納する Int32 カラム:
Float32 と Float64
標準的な IEEE 754 のバイナリ浮動小数点数で、単精度 (binary32) は 4 バイト、倍精度 (binary64) は 8 バイトです。いずれもリトルエンディアンで格納されます。NaN、±Infinity、±0.0、および非正規化数は、いずれも正規化されることなくそのまま往復変換されます。
Float32 の値 1.5 (0x3FC00000):
Float64 の値 1.5 (0x3FF8000000000000) :
BFloat16
brain-floating-point フォーマットです。IEEE 754Float32 の上位 16 ビットに相当し、符号 1 ビット、指数 8 ビット、仮数 7 ビットで構成されます。各値は 2 バイトのリトルエンディアンで、生の 16 ビットパターンを保持します。数値を復元するには、パターンを上位半分に配置して下位半分をゼロで埋め、Float32 に拡張します (bits << 16 を Float32 として再解釈) 。拡張後の値は、Float32 と同じテキストフォーマットで表現されます。
BFloat16 の値 1.5 (パターン 0x3FC0、Float32 0x3FC00000 の上位半分) :
Bool
UInt8 とワイヤ形式で互換性があります。1 行あたり 1 バイトで、0x00 = false、0x01 = true です。ただし、wire 上の型文字列は UInt8 ではなく、文字どおり Bool です。そのため、型文字列に基づいて処理を振り分けるデコーダでは、これを別途認識する必要があります。
Bool カラム [true, false, true]:
Date と Date32
どちらも日付を、Unix エポック1970-01-01 からの日数を表す整数としてエンコードします。いずれも時刻部分は持ちません。
Date 型の値 1970-01-02 (1 日) :
Date32 の値 1900-01-01 (-25567 日) :
DateTime
UInt32 とワイヤ形式で互換性があります。秒単位のUnixタイムスタンプを、4バイトのリトルエンディアンで表現します。型は DateTime または DateTime('Timezone') と表記されます。タイムゾーンは表示にのみ影響し、wire 上の値には含まれません。タイムゾーンパラメータが異なる2つの DateTime カラムでも、同じ時点であれば同一のバイト列になります。デコーダは (...) のパラメータ接尾辞を取り除き、そのカラムを UInt32 として処理します。
DateTime('UTC') の値 2024-03-15 14:30:00 UTC (タイムスタンプ 1710513000) :
DateTime64(scale[, timezone])
8 バイトのリトルエンディアン Int64 で、Unix エポックからの経過時間を10^-scale 秒単位のティック数として表します。scale パラメータ (0〜9) は型文字列に含まれ、時間の単位を決定します。
この型は
DateTime64(s) (サーバーのデフォルトのタイムゾーンを暗黙的に使用) または DateTime64(s, 'TimezoneName') (タイムゾーンを明示的に指定。表示にのみ影響) として表記されます。負の値はエポックより前のティックを表します。
DateTime64(3, 'UTC') の値 2024-01-15 12:30:45.123 UTC (1705321845123 ms):
DateTime64(0) の値 2024-01-15 12:30:45 UTC (1705321845 秒) :
Time と Time64(scale)
特定の時点ではなく、経過時間を表します。Time は符号付きの秒数で、4 バイトのリトルエンディアン Int32 です。Time64(scale) は指定された 10 進スケール (0–9) における符号付きのティック数で、8 バイトのリトルエンディアン Int64 です。これは DateTime64 と同じ wire 形式です。
テキスト形式は [-]HH:MM:SS[.fraction] ですが、DateTime とは異なり、時間フィールドは 24 時間で折り返されません。この値は時間の総数を表し、23 を超えることもあります。表示される値の大きさの上限は 999:59:59 (3599999 秒) です。これを超える値は、秒の部分が上限値で表示され、格納された小数部はそのまま維持されます。たとえば、ティック数 3700000.25 は 999:59:59.250000 と表示されます。CAST は格納値を、秒単位ではなく、ターゲットのスケールで表現可能な最大のティック (Time64(6) の場合は 999:59:59.999999) に飽和させます。一方、算術演算ではカラム内に範囲外のティック数が残る場合があり、その値は表示時にのみ上限に丸められます。いずれの場合も wireバイトには影響せず、wireバイトは単なる符号付き整数のままです。
Time の値 45296 (12:34:56):
Time64(3) の値 45296789 ティック (12:34:56.789) :
Time と Time64 は実験的な機能であり、使用するにはサーバーで allow_experimental_time_time64_type = 1 を設定する必要があります。インターバル
Interval<Unit> — IntervalSecond、IntervalMinute、IntervalHour、IntervalDay、IntervalWeek、IntervalMonth、IntervalQuarter、IntervalYear、IntervalNanosecond など。すべての単位で同一のワイヤエンコーディングが使われ、カウント値は符号付き 8 バイトのリトルエンディアン Int64 として表現されます。単位は型文字列にのみ含まれ、wireバイトにもテキスト形式 (単なる整数) にも影響しません。そのため、すべての単位を単一のデコーダパスで処理できます。
IntervalDay の値 5:
UUID
値1つあたり16バイトです。ワイヤエンコーディングは、標準形式の16バイトのビッグエンディアン表現ではありません。8バイトずつの前半と後半が、それぞれ独立してバイト順を反転されます。 論理モデルは、標準テキスト形式xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx で表される128ビットの識別子で、そのバイトは慣例的にビッグエンディアンで表記されます。wireモデルでは、この標準形式の16バイトを8バイトずつ2つに分割し、それぞれをリトルエンディアンで書き込みます。
- wireバイト 0..7 = 標準形式のバイト 0..7 を逆順にしたもの
- wireバイト 8..15 = 標準形式のバイト 8..15 を逆順にしたもの
550e8400-e29b-41d4-a716-446655440000:
IPv4 と IPv6
互いに関連はあるものの、エンコード方式が異なる 2 つのアドレス型です。IPv4 は 4 バイトで、標準の 32 ビットアドレス (a.b.c.d から得られる値 (a << 24) | (b << 16) | (c << 8) | d) を保持するリトルエンディアンの UInt32 としてエンコードされます。wireバイトは、ネットワークバイトオーダーのバイト列を逆順に並べたものです。
192.168.1.10 (標準の 32 ビット値は 0xC0A8010A) :
IPv6 は 16 バイトで、バイトの入れ替えを行わずネットワークバイトオーダーのまま書き込まれます。これは inet_pton(AF_INET6, ...) と同じバイト順序です。
2001:db8::1:
u32 として格納されますが、IPv6 は多くのネットワーク API で一般的なネットワークバイトオーダーのレイアウトをそのまま保持します。
Enum8 と Enum16
それぞれInt8 および Int16 とワイヤ形式で互換性があります。1 行あたり 1 バイトまたは 2 バイトで、16 ビット版は 2 の補数のリトルエンディアンで表現されます。値と名前の対応関係はすべて型文字列に含まれています:
(...) パラメータの接尾辞を取り除き、Int8 / Int16 としてディスパッチしてもかまいません。wireバイトは単なる整数のインデックスにすぎません。ラベルを表示するクライアントは、型文字列から 'name' = value のマップをパースし、カラムと併せて保持します。整数だけではラベルを復元できないためです。テキスト指向の出力では、インデックスではなくラベル (active) を表示し、enum が複合型の内部にネストされている場合はシングルクォートで囲みます ('active')。マップは整数カラムから復元できないため、Array(Enum8(...)) や Map(Enum16(...), V) のようにネストされた enum の場合も、マップを保持しておく必要があります。
Enum8('active' = 1, 'inactive' = 2) カラム [active, inactive, active]:
Enum16(...) の値 30000 の場合:
Decimal(P, S)
10 のべき乗でスケーリングされた符号付き整数です。整数のバイト幅は 精度P によって決まり、スケール S は負の指数 (小数点以下の桁数) を表します。どちらも型文字列に含まれます。
ワイヤエンコーディングは、基になる整数をリトルエンディアンの 2 の補数で表現したものです。論理的な 10 進数値は
wire_integer × 10^(-S) で求められます。
ClickHouse は、型の宣言方法にかかわらず、常に Decimal(P, S) を出力します。Decimal32(S)、Decimal64(S) などはすべて、wire 上では Decimal(P, S) に正規化されます (P には各幅で取り得る最大値である 9、18、38、76 が設定されます) 。そのため、Decimal(P, S) だけを認識するデコーダであれば、サーバーが出力するすべての表記に対応できます。
Decimal(9, 4) の値 123.4567 → 基になる整数 1234567:
Decimal(18, 1) の値 -1.5 → 基になる整数 -15:
Decimal(38, 4) 型の値 123.4567 (合計 16 バイト) :
Nothing
Nothing 型は値を一切保持しません。実際には Nullable(Nothing) の内部型としてのみ使われます。これは、SELECT NULL のように、有効な値が「値が存在しないこと」だけである式に対してサーバーが返す型です。概念的にはユニット型にあたります。
wire 上では、1 行あたりちょうど 1 バイトのプレースホルダーバイトを占めます。サーバーは ASCII 文字 '0' (0x30) を出力しますが、デシリアライザはこれらのバイトを無視します。内容は未定義であり、デコーダは特定の値に依存してはなりません。書き込まれるバイト数は num_rows × 1 であるため、消費すべきバイト数はカラムヘッダーの num_rows だけで決まります。
1 行につき 1 バイトを割り当てることで、Block の不変条件が保たれます。つまり、すべてのカラムの長さは num_rows から導出できるため、デコーダはセルごとの長さプレフィックスがなくても順に読み進められます。外側の Nullable は常にすべての位置を NULL として報告するため、プレースホルダーの中身が参照されることはありません。
3 行 (すべて NULL) の Nullable(Nothing) カラム:
Nullable のフレーミングです (Nullable を参照) 。内側の 3 バイトは Nothing のペイロードで、デコーダはこれを読み飛ばします。
可変長型
各値は、wire 上で自身の長さ情報を保持します。String
型文字列:String。String カラムには 2 種類のワイヤレイアウトがあり、どちらを使うかはプロトコルのリビジョンによって決まります (リビジョンによって制御される内容 を参照) 。ここでいうリビジョンとは、ネイティブ TCP プロトコルではネゴシエートされたリビジョン、FORMAT Native 出力では client_protocol_version (デフォルトは 0) を指します。FORMAT Native 入力は常にリビジョン 0 であり、Buffers フォーマットは常に値単位レイアウトを使用します。
値単位レイアウト — DBMS_MIN_REVISION_WITH_STRING_WITH_SIZE_STREAM_SERIALIZATION (54492) 未満の場合 (FORMAT Native のデフォルトであるリビジョン 0 を含む) 。長さプレフィックス付きのバイト列を num_rows 個並べた形式です:
0x00 で表されます。カラムが占める総バイト数は、全行についての Σ (varuint_size(len_i) + len_i) です。
3 つの文字列 ["ab", "", "c"] からなるカラム (合計 6 バイト) :
54492 以降:2つのストリームを連結した形式です。まず累積バイトオフセットが配置され、その後にすべてのデータが続きます:
Array がネイティブプロトコル上でオフセットを送信する方式と同じです。オフセット i はデータブロブ内における値 i の終端位置を表すため、値 i のサイズは offset[i] - offset[i-1] (ただし offset[-1] = 0) となり、最後のオフセットはデータブロブの合計サイズと一致します。デコーダは 8 × num_rows バイト分のオフセットを読み取った後、最後のオフセットを正確な長さとして用いてデータブロブ全体を一括で読み取ります。これにより、行ごとにパースすることなく、バッファを正確なサイズで事前に割り当てて一括コピーできるうえ、サイズからオフセットへの変換も不要です。num_rows = 0 のカラムは、どちらのストリームにもバイトを出力しません。
同じ 3 つの文字列 ["ab", "", "c"] (合計 27 バイト) の場合:
String に適用されます。複合型 (Array(String)、Nullable(String)、Map、Tuple、Variant、Dynamic、JSON) の内部にネストされた String も対象です。ただし、例外が 1 つあります。LowCardinality カラムの dictionary は常にデフォルトのネストされたシリアライゼーションで書き込まれるため、その String 値はどのリビジョンでも値ごとのレイアウトのままです。これは LowCardinality(String)、LowCardinality(Nullable(String))、および String を含むその他すべての LowCardinality dictionary に当てはまります。
いずれのレイアウトでも、ClickHouse の String はテキスト指向ではなくバイト指向です。UTF-8 として有効かどうかはチェックされず、値には埋め込みの NUL を含む任意のバイトが含まれる場合があります。UTF-8 文字列型にデコードするデコーダは、読み取り時に検証を行うか、raw bytes をそのまま呼び出し元に渡すかのいずれかとなります。
FixedString(N)
型文字列:FixedString(N)。N は正の整数です (例: FixedString(16)) 。カラムは正確に N × num_rows バイトの raw bytes で構成され、長さプレフィックスや区切り文字は含まれません。デコーダは型文字列から N をパースし、1 行ごとにそのバイト数を消費します。
SQL で N バイトより短い値を挿入した場合 (例: CAST('abc' AS FixedString(5))) 、サーバーは宣言された長さになるまで値の右側を NUL バイト (0x00) でパディングします。これらのパディングバイトは格納された値の一部であり、wire 上でもそのまま送信されます。トリミングはクライアント側で対処すべき事項です。String と同様に、FixedString(N) はテキストというよりバイト配列に近い型で、通常は固定幅の識別子、アドレスのバイト列、ハッシュダイジェストなどに使用されます。
2 つの FixedString(3) 値 ["abc", "de\0"] (合計 6 バイト) :
複合型
複合型は 1 つ以上の内部型をラップするもので、1 カラムあたり複数のストリームという共通のwireモデルを持ちます。1 つの論理カラムは、それぞれ独立して読み取られる 2 つ以上のバイト列を連結した形でエンコードされます。 複合型には、次の 3 つの構造的特性が共通しています。- 形状はスキーマごとに固定される。 構造はデコード時の型文字列だけで決まります。
Array(UInt32)のストリームレイアウトは、どのブロックでも常に同じです。 - 独自のバージョンプレフィックスを持たない。 複合型のラッパー自体はバージョンバイトを付加しません。そのフレーミング (オフセット、null マップ、要素ストリーム) は ClickHouse のリリース間で変わりません。ただし、これはラッパーについてのみ言えることです。内部のバージョン付き型については、後述のプレフィックスフェーズに関する注記を参照してください。
- 独自のブロック間状態を持たない。 ラッパーのフレーミングはブロック単位で完全に自己記述的です。ブロック間状態に関わる問題が生じるとすれば、それはラッパーではなく内部のバージョン付き型によるものです。
SerializationArray は配列オフセットを書き込む前に内部型のプレフィックスフェーズを実行し、Tuple、Map、Nested、Nullable も同様に要素のシリアライゼーションを通じてこれを行います (Nullable は null マップより前に内部プレフィックスを実行します) 。
そのため、複合型がバージョン付き/ステートフルな型 (LowCardinality、Variant、Dynamic、JSON) をラップしている場合、その内部型のバージョン/状態プレフィックスは、ラッパーのオフセットや要素ペイロードよりも先に出力されます。例えば Array(LowCardinality(String)) のレイアウトは、オフセットが先頭に来るのではなく、[LowCardinality state prefix] → [array offsets] → [flattened LowCardinality element payload] の順になります。
内部プレフィックスフェーズを実行せずに先にオフセットを読み取るデコーダは、LowCardinality、Variant、Dynamic、JSON のいずれかを含む複合型で同期がずれます。内部型がすべて単純なリーフ型か、バージョンを持たない別の複合型である場合は、プレフィックスフェーズでバイトは出力されないため、以下で説明するオフセット先頭の記述がそのまま当てはまります。
Nullable(T)
型文字列:Nullable(InnerType)。例: Nullable(UInt32)、Nullable(String)、Nullable(FixedString(16))、Nullable(DateTime('UTC'))。
他の複合型と同様に、Nullable は null マップを書き込む前に、プレフィックスフェーズを内部型のシリアライゼーションに委譲します。内部型がバージョン付きの場合は、内部型の state prefix が最初に出力されます。したがって、Nullable(Tuple(LowCardinality(String))) は null マップからではなく、LowCardinality の state prefix から始まります。内部型が leaf 型、またはその他のバージョンなしの型である場合、プレフィックスフェーズでは 1 バイトも出力されません。
ワイヤレイアウトは、内部型のプレフィックスフェーズ (内部型がバージョン付きでない限り空) の後に、2 つのストリームを連結したものが続く構成です。先頭は null マップです:
num_rows バイトで、1 行につき 1 バイトです。
values ストリームには、null の位置も含めた すべての
num_rows 行について、内部型の標準エンコーディングが格納されます。デコーダは、ストリームを読み進めるために null の位置にあるプレースホルダーのバイトも読み取る必要がありますが、個々の値を解釈する前には必ず null マップを参照しなければなりません。送信側は null の位置に任意のバイトを書き込むことができるため、デコーダは特定のプレースホルダー値を前提としてはなりません。
内部型ファミリーごとのプレースホルダー値:
Nullable(T) は Array、Tuple、Map、Nested の内部に記述でき、Array(Nullable(T)) や Tuple(Nullable(T1), T2) はよく使われます。ただし Nullable を入れ子にすることはできず、Nullable(Nullable(T)) はサーバーによって拒否されます。
3 行 [5, NULL, 9] を持つ Nullable(UInt8) の例 (合計 6 バイト) :
["hello", NULL, "world"] からなる Nullable(String)(合計 15 バイト):
Array(T)
型文字列:Array(InnerType)。例: Array(UInt32)、Array(String)、Array(Nullable(UInt32))、Array(Array(UInt8))。
ワイヤレイアウトは、内部型のプレフィックスフェーズ (内部型がバージョン付きでない限り空) と、それに続く連結された 2 つのストリームで構成されます。ストリームはオフセットが先です:
num_rows 個のリトルエンディアン UInt64 値で構成されます。各値は、その行の要素を書き込んだ後の values ストリームにおける累積終了位置を表します。
- 行
Nの要素開始インデックス =offsets[N - 1](N == 0の場合は0)。 - 行
Nの要素終了インデックス(この位置自体は含まない)=offsets[N]。 - 行
Nの要素数 =offsets[N] - offsets[N - 1]。
offsets[num_rows - 1] は全行の要素の総数となり、values ストリームにはその数だけの内部値が隙間なく連結されて格納されます。
オフセットは単調非減少です。連続するオフセットが等しい場合は空の行を表します。デコーダは、単調非減少でないオフセットを破損とみなして拒否する必要があります。空のカラム(num_rows == 0)では 1 バイトも書き込まれず、offsets ストリームも values ストリームも存在しません。内部型には他の複合型を含む任意の型を指定できます。Array(Array(T))、Array(Tuple(...))、Array(Nullable(T)) はいずれも有効です。
行 [[10, 20, 30], [], [40, 50]] を持つ Array(UInt32) の例(合計 44 バイト):
0) です。連続するオフセットが同じ値の場合、その行は空になります。

[["a", "bb"], []] を持つ Array(String) (合計 20 バイト) :
Array(Array(UInt32)) に行 [[[1,2]], [], [[3], [4,5]]] を格納すると、同じ構造が入れ子になります。
- 外側のオフセット:
[1, 1, 3]— 行 0 は内側の配列を 1 個、行 1 は 0 個、行 2 は 2 個持ちます。 - 中間の
Array(UInt32)は、オフセット[2, 3, 5]を使って 3 行をデコードします。 - 最も内側の
UInt32は、5 個の値[1, 2, 3, 4, 5]をデコードします。
Tuple(T1, T2, …)
型文字列:Tuple(T1, T2, ..., Tn)。例: Tuple(UInt32, String)、Tuple(Int32)、Tuple(Array(UInt32), String)、Tuple(UInt8, Tuple(Int32, String))。ClickHouse は Tuple(a UInt32, b String) の形式で名前付きタプルもサポートしています。名前は単なるメタデータであり、ワイヤ形式には影響しません。
ワイヤレイアウトは、まず各要素のプレフィックスフェーズ (バージョン付きの要素はそれぞれ宣言順に自身の state prefix を出力し、バージョンなしの要素では空) があり、その後に要素型ごとに 1 つずつ、宣言順に連結された N 個のストリームが続きます:
num_rows 個の値をエンコードします。長さプレフィックスやoffsets ストリームはなく、ストリーム間の区切り文字もありません。空のカラム (num_rows == 0) では、各ストリームに 0 バイトが書き込まれます。要素型には他の複合型を含め任意の型を使用できます。Tuple(Tuple(...), ...)、Tuple(Array(...), ...)、Tuple(Nullable(T1), T2) はいずれも有効です。
要素数 0 のタプル Tuple() も有効で、SELECT tuple() や CAST(x AS Tuple()) などの式から生成されます。要素ストリームを持たないため、代わりに Nothing と同様の方法でシリアライズされます。つまり、行ごとに 1 バイトのプレースホルダーバイト (0x30、ASCII '0') が書き込まれ、デシリアライザーはこれを読み捨てます。行数は Nothing の場合と同じく、ブロックヘッダーから取得されます。
3 行 (1,4), (2,5), (3,6) を持つ Tuple(UInt8, UInt8):
[1, 2, 3]、要素 1 には [4, 5, 6] が得られます。
2 行 (10, "a")、(20, "bb") を持つ Tuple(UInt32, String) (合計 13 バイト) :
Map(K, V)
型文字列:Map(KeyType, ValueType)。例:Map(String, UInt32)、Map(String, Array(UInt32))、Map(UInt8, Tuple(Int32, String))、Map(Array(String), Int8)。ワイヤ形式ではどちらの型にも制約はなく、K と V には複合型を含め、サポートされている任意の型を指定できます。 (ClickHouse の SQL レベルで許容されるキー型のルールはリリースによって異なるため、対象とするサーバーバージョンの SQL ドキュメントを参照してください。)
ワイヤレイアウトは Array(Tuple(K, V)) とバイト単位で同一です。そのため、先頭には内部のプレフィックスフェーズが置かれます (K または V がバージョン付きの型でない限り、これは空です) :
total_pairs = offsets[num_rows - 1](num_rows == 0 の場合は 0)です。offsets ストリームのセマンティクスは Array と同じです。キーは値と位置的に対応しており、ペア i は (keys[i], values[i]) となります。
ClickHouse における Map カラムのインメモリ表現はタプルの配列です。型システム上は、SQL で扱いやすくするため(m['key']、mapKeys、mapValues)に独立した型として提供されています。ワイヤ形式はこのストレージをそのままシリアライズしたものであるため、Map と Array(Tuple(K, V)) はバイト単位で互換性があります。
offsets は単調非減少であり、keys ストリームと values ストリームはいずれもちょうど total_pairs 個の値を含みます。空のカラムでは 1 バイトも書き込まれません。1 つの行の中でキーは通常一意ですが、これはセマンティクス上の規則であり、ワイヤ形式で強制されるものではありません。ワイヤ形式では重複キーもそのまま往復変換でき、重複はサーバー側のセマンティクスにより、Map 対応の関数がその行を消費する時点で初めて解決されます。
2 行 {1:10, 2:20}、{3:30} を持つ Map(UInt8, UInt8)(合計 22 バイト):
i は、keys[i] と values[i] を組み合わせて読み取ることで復元されます。
{'a':1, 'b':2} の 1 行を持つ Map(String, UInt32) (合計 20 バイト) :
Nested(name1 T1, name2 T2, …)
Nested のワイヤ上での表現はサーバー側の flatten_nested 設定によって異なり、次の 2 つのケースがあります。

flatten_nested = 1 (サーバーのデフォルト) 。 テーブルがデフォルト設定で作成されている場合、Nested は ワイヤ型ではありません。サーバーはこのカラムを、ドット区切りの名前 (outer.field1、outer.field2 など) を持つ N 個の並列な Array(T_i) カラムとして格納し、提示します。フォーマット層にとって新たに考慮すべき点はなく、ドット区切りの各カラムは通常の Array として扱われます。
flatten_nested = 0。 テーブルが flatten_nested = 0 で作成されている場合、このカラムは wire 上では型文字列 Nested(name1 T1, name2 T2, ...) を持つ単一のカラムとして表現され、型文字列以降のレイアウトは、内側の プレフィックスフェーズ も含めて Array(Tuple(T1, T2, ..., Tn)) とバイト単位で同一 です。そのため、バージョン付きのフィールド T_i がある場合は、オフセットより先にその state prefix が出力されます。以下の例ではバージョンなしのフィールドを使用しているため、プレフィックスフェーズは空になります。
Nested はフィールド名 (a、b) を保持しますが、Array(Tuple) はこれらを名前付きスロットとして保持しません。
Case B の型文字列は、(名前, 型) のペアをカンマで区切って並べたリストです。名前と型は最初の空白文字で区切られます。型自体にも空白文字、カンマ、括弧が含まれる場合があるため、パースには Tuple と同じく、ネストの深さを考慮した分割処理が必要です。ワイヤレイアウトは次のとおりです。
total_elements = offsets[num_rows - 1] (num_rows == 0 の場合は 0) です。オフセットは単調非減少であり、各フィールドのストリームには常に total_elements 個の値が格納されます。サーバーは INSERT 時に、1 つの行内のすべてのフィールドが同じ数の要素を持つよう強制します。空のカラムでは何も書き込まれません (0 バイト) 。
[(10,'x'),(20,'y')] と [(30,'z')] の 2 行からなる Nested(a UInt8, b String) の例 (型文字列に続く 25 バイト) :
型の別名
いくつかの型は純粋な別名です。サーバーはカラムヘッダーで別名を送信しますが、その後に続くバイト列は基になる型のものです。デコーダは別名をその型に対応付け、その型のコーデックを再利用します。新たなワイヤ形式は関係しません。 地理型は、ネストされた配列およびタプルの別名です。
したがって、
Point カラムは Tuple(Float64, Float64) とまったく同じようにデコードされ ((1,2) と表示されます) 、Ring は Array(Tuple(Float64, Float64)) ([(0,0),(1,1)]) としてデコードされます。階層の上位の型も同様です。
Geometry も別名ですが、ネストされた配列ではなく Variant の別名です。そのペイロードは、上記 7 つの Geo 型からなるバリアントです。カラムヘッダーには型文字列 Geometry のみが含まれ、バリアントの構成は明記されません。そのため、デコーダ側で展開する必要があります。ユーザーが記述した Variant とは異なり、Geometry の判別子は名前のソート順に従いません。判別子は互換性のために固定されており、新しい Geo 型は常に末尾に追加されます。対応は 0 = LineString、1 = MultiLineString、2 = MultiPolygon、3 = Point、4 = Polygon、5 = Ring、6 = MultiPoint です (MultiPoint は 26.7 で追加。古いサーバーからのストリームに判別子 6 が含まれることはありません) 。選択された各値は、上記の対応する Geo 別名としてデコードされます (NULL には Variant の NULL 判別子 255 が使用されます) 。
SimpleAggregateFunction(func, T) は、その値の型 T の別名です。最終化済みの集約値を格納するため、ワイヤ上の形式と表示は T とまったく同じです (SimpleAggregateFunction(sum, UInt64) は UInt64 としてデコードされます) 。このように別名として扱われるのは、値の型を 1 つだけ持つ形式に限られます。なお、基になる型自体は複合型でもかまいません。
関連する 2 つの型は別名ではありません。これらは有効な
Native のカラム型であり、たとえばクライアントは -State コンビネータや分散集約から AggregateFunction カラムを受け取ることがあります。ただし、いずれも独自の専用ペイロードを持っており、その詳細は本ページの対象範囲外です:AggregateFunction(func, ...)は中間集約状態 (確定済みの値ではない) を保持します。そのバイナリレイアウトは集約関数とバージョンごとに固有です。関数名にはパラメータを付けることができ、各パラメータはAggregateFunction(topK(3), UInt64)のように SQL リテラルとして出力されます。一部の集約関数では、AggregateFunction(topK('3'::Decimal32(0)), UInt64)のように、パラメータに明示的な::Typeサフィックスが付きます。これは、Decimalやワイド整数の値には、そのまま出力できる素のリテラル形式が存在しないためです。この挙動はフォーマットではなく各関数の性質によるものであるため、パラメータを検査するデコーダは両方の表記を受け入れる必要があります。引数の型のみを必要とするデコーダには影響しません。バージョン付きの集約関数では、0 以外の状態バージョンが型文字列の第 1 引数として明記されます (AggregateFunction(1, quantileDeterministic, UInt64, UInt64)) 。一方、バージョン0は出力されないため、型文字列はAggregateFunction(quantileDeterministic, UInt64, UInt64)のままです。ネゴシエーション済みの接続 (リビジョンが0より大きい) では、writer はプロトコルリビジョンのみに基づいてバージョンを決定し、ローカルの型にたまたま固定されているバージョンを使うことはありません。バージョン0は、wire 上では「バージョンが通知されていない」状態と区別できないためです。そのため、同じ関数でも接続ごとに異なるバージョンを通知することがあり、reader は最新バージョンを前提とせず、型文字列からバージョンを取得する必要があります。リビジョン0では、バージョンを導出する基準となるピアが存在しません。ストリームは自己記述的であり、書き込んだ側自身が読み戻すからです。したがって、型に固定されたバージョンは0にダウングレードされることなく、ストリーム (および通知される型文字列) にそのまま残ります。quantileDeterministic(quantilesDeterministicおよびmedianDeterministicの表記を含む) は、リビジョン54491からバージョン付きになります。バージョン1では状態の末尾にUInt8 skip_degreeが追加されますが、それより前の部分はバージョン0とバイト単位で同一です。このリビジョン未満では writer はバージョン0の状態を出力するため、古いピアには影響しません。uniqは、リビジョン54493からバージョン付きになります。バージョン1では、バージョン0のレイアウトの先頭にフラグバイトが追加されます。さらにフラグのビット0が設定されている場合は、完全な 64 ビットハッシュから成る小規模な追加サンプルが末尾に付加されます (各値は定数0x9E3779B97F4A0000と XOR されており、UInt8 skip_degree、VarUInt count、8 バイトの値の順に並びます) 。これにより、カーディナリティが 20 億を超えても推定精度が保たれます。バージョン0の状態は 32 ビットのメインサンプルのみを保持します。フラグのビット1は、64 ビットサンプルが不完全であることを示します。これは、状態がバージョン0の状態を取り込んだ場合に該当します (バージョン0の状態の要素は、64 ビットサンプルに一度も渡されていないためです) 。この場合、64 ビットサンプルは書き込まれず、reader は 32 ビットのメインサンプルのみから推定する必要があります (また、以降に書き込む状態でも引き続きこのビットを設定する必要があります) 。したがって 2 つのビットは相互に排他的であり、reader は両方が設定された状態を拒否する必要があります。その他のビットは定義されておらず、reader はいずれかが設定された状態を拒否する必要があります。以上の内容はすべて、コンテナ型の内部にネストされた状態にも同様に当てはまります。対象となるのはArray、Tuple、Map、Nullable、Variant(代替型に明記されたバージョンによって型文字列は変わりますが、判別子の順序は変わりません) 、およびNested/SimpleAggregateFunctionラッパーです。集約関数のコンビネータを適用した場合も同様です。バージョン付きの関数に対するパススルー型のコンビネータ (uniqIf、uniqArray、uniqForEach、uniqMap、uniqArgMin/uniqArgMax、uniqOrNull/uniqOrDefault、uniqResample、および暗黙的なNullable引数アダプタ) は、ペイロードに含まれるネストされた状態にバージョンを引き継ぎます。また、新たに生成された状態の型文字列には、通常の関数とまったく同じようにネストされた関数の現在のバージョンを明記し (AggregateFunction(1, uniqIf, UInt64, UInt8)) 、ネゴシエーション済みの接続では同じくプロトコルリビジョンからバージョンを導出します。ラッパーのカスタム名には引数の型のコピーがもう 1 つ含まれているため、writer はそのコピーも、ネゴシエーション済みの同じ集約状態バージョンで再構築する必要があります。そうしないと、バージョン0のペイロードに対して、ヘッダーがバージョン1を通知してしまう可能性があります。Variantには、wire 上でまったく表現できないコーナーケースが 1 つあります。明記された状態バージョンだけが異なる 2 つの代替型 (型文字列が異なるため、カラム型としては有効) がある場合、ネゴシエーション済みのリビジョンに合わせて両方のバージョンを付け直すと、同一の代替型に統合されてしまいます。そのため送信側は、ペイロードの実際の書き込みバージョンと異なるバージョンを通知するのではなく、転送を失敗させる必要があります。Dynamicは、リビジョンに基づくバージョン決定の意図的な例外です。Dynamicカラムは、自身の型リストと バイナリ型エンコーディング (その明示的なバージョンフィールドに、明記された状態バージョンが格納されます。共有バリアントの値は、保存時でもこの方法で型を埋め込みます) を通じて、ネストされた型を自ら通知します。これらの通知はカラム自体のデータの一部であり、どのピア・どの媒体でも同一であるため、ネゴシエーション済みのリビジョンに合わせて書き換えられることはありません。したがって、Dynamic値の内部にネストされた状態は、どの接続・媒体においても、その型に明記されたバージョン (-Stateクエリの結果はバージョンを明記するため、新たに生成された状態はこれを保持しています) で書き込まれ、型にバージョンが明記されていない場合はバージョン0で書き込まれます。QBit(T, N[, stride])は、ベクトル検索ワークロード向けに、ビットプレーンを転置した形でベクトルを格納します。その wire 上のストリームレイアウト (グループ優先のFixedStringビットプレーンストリーム。明示的なstrideを指定した場合はelement_size * (N / stride)個) およびバイナリ型エンコーディング (タグ0x36、またはstride != Nの場合は0x37QBitWithStride) は、QBitデータ型のページ および バイナリ型エンコーディング リファレンスに記載されています。そのため、Nativereader が C++ のソースコードから仕様を読み解く必要はありません。
バージョン付き型
バージョン付き型は、wire 上でシリアル化バージョンのプレフィックスを持ち、これによって後続するエンコーディングがどのバリアントであるかを示します。また、(複合型と同様に)複数のストリームを使用する場合もあります。Native の wire では、プレフィックスおよび辞書はブロックごとに送られるため、これらの型がブロックをまたいで状態を保持することはありません(後述のブロック単位のプレフィックスに関する注記を参照)。ブロックをまたぐシリアル化状態が存在するのは、MergeTree のディスク上のストリームのみです。
これらの型は固定構造の複合型に比べてかなり複雑なため、単純な分析クエリを主な用途とするクライアントであれば、対応を後回しにしても構いません。
シリアル化バージョン: 概念
シリアル化バージョンとは、型ごと・カラムごとに付与される on-wire のバージョン番号で、送信側がその型のエンコーディングのうちどのバリアントを使用しているかを示します。これはカラムの state prefix の先頭に置かれるため、デコーダはまずこれを読み取り、カラムの残りの部分を処理する適切なパーサーにディスパッチします。 シリアル化バージョンはプロトコルバージョンとは別物です:
バージョン付きの型の多くは、他の state prefix データより前に、バージョンを リトルエンディアン の UInt64 として書き込みます。一部の型では VarUInt または UInt8 を使用します。デコーダはまずバージョンを読み取り、未知の値であれば拒否する必要があります。より大きいバージョンは、デコーダが解釈できない新しい送信側フォーマットであることを意味し、誤ってパースすると以降のすべてのバイトが壊れてしまいます。
state prefix は、行数が 0 より大きいすべてのブロックの先頭、つまりそのブロックのペイロードの直前に出力されます。
Native の writer と reader は、ブロックをまたいでシリアル化状態を保持しません。
NativeWriter は空でないカラムブロックを書き込むたびに新しいシリアライズ状態を作成して state prefix を書き込み、NativeReader は空でないブロックを読み取るたびに新しいデシリアライズ状態を作成して state prefix を読み取ります (どちらも rows == 0 の場合は prefix を完全にスキップします)。
そのため、ヘッダーブロック (rows = 0) と空のブロックでは何も出力されず、デコーダは空でないブロックごとに、その先頭で state prefix を改めて読み取る必要があります。prefix を一度しか読み取らず、以降のブロックをペイロードのみとして扱うデコーダは、次のブロックの prefix をデータとして読み取ってしまい、同期がずれてしまいます:

シリアル化バージョンリファレンス
この表について、注意すべき点がいくつかあります。
- 値は連続していません。
Dynamicは1、2、3、4を使用し、V3が4、FLATTENEDが3に割り当てられています。数値が大きいほど新しいとは限りません。 - 一部の値は Native フォーマット専用です。
Object::STRING、Object::FLATTENED、Dynamic::FLATTENEDは、Object/Dynamic を完全には実装していないクライアントとのネイティブプロトコル互換性を保つために存在します。これらが MergeTree のディスク上のストレージに現れることはありません。 V3はディスク上専用です。Object::V3とDynamic::V3を記載しているのは、デコーダがこれらを受け取る可能性があるからではなく、値が割り当て済みで再利用してはならないためです。ClickHouse がこれらをNativeで送信することはなく、Native上で受け取った場合はINCORRECT_DATAで拒否します。これらが保持する情報 (Object::V3の shared-data サブバージョンや、そのバケットおよびグラニュールのレイアウト) はすべてBlockではなくパーツを記述するものであるため、本ページの対象外です。ネイティブ TCP プロトコルを消費するクライアントが受け取るのはV1、V2、STRING、FLATTENEDのいずれかです。
LowCardinality(T)
最も単純なバージョン付き型です。N 個の内部値からなるカラムを、一意な値からなる小さな辞書と、その辞書を指す N 個の索引で置き換えます。
型文字列: LowCardinality(InnerType)。例: LowCardinality(String)、LowCardinality(FixedString(4))、LowCardinality(Nullable(String))。
sharedDictionariesWithAdditionalKeys を表します。その他の値は予約されています。
Blockごとのメタデータ UInt64 はビットフィールドです。
カラムごとにデータBlockが 1 つだけの一般的なクエリレスポンスでは、メタデータは
0x600 (HasAdditionalKeys + NeedUpdateDictionary) になります。
dict の値は、内部型 T でエンコードされた dict_size 個の値です。Dictionary は特殊な値のために先頭のスロットを予約します。非 Nullable のカラムでは 1 つを予約し (dict[0] には内部型のデフォルト値が格納されます。例: String の場合は "") 、実際の個別値は dict[1] から始まります。
LowCardinality(Nullable(T)) の場合も dict は通常の T としてエンコードされます (null マップのストリームはありません) が、予約されるスロットは 2 つ になります。dict[0] は NULL マーカー、dict[1] は内部型のデフォルト値 (例: String の場合は "") で、実際の個別値は dict[2] から始まります。NULL の行のキーは dict[0] を指し、このスロットは wire 上では内部型のデフォルト値のバイト列として書き込まれます。
キーは dict への索引です。各索引のサイズは 1 << key_type_code バイト (1、2、4、8 のいずれか) で、値 N は dict[keys[N]] として復元されます。
keys_count は 現在の再帰レベル における LowCardinality の値の数であり、Blockの行数と一致するとは限りません。トップレベルの LowCardinality カラムであれば両者は一致します。しかし LowCardinality が複合型の内側にある場合は、複合型から渡されるフラット化後の値の数になります。たとえば、3 行で合計 5 個の要素を持つ Array(LowCardinality(String)) では、keys_count は 3 ではなく 5 です。Map(K, LowCardinality(V)) ではペアの総数になり、他の型も同様です。デコーダはBlockの行数を前提にせず、このフィールドから keys_count を読み取る必要があります。フラット化後の数がゼロの場合 (たとえば、すべての配列が空のBlock) 、LowCardinality のデータフェーズでは 何も書き込まれません。存在するのは state prefix (複合型のプレフィックスフェーズで出力される) のみで、その後にメタデータ、Dictionary、keys_count は続きません。
state prefix は、行数が 0 より大きいすべてのBlockの先頭で読み取られます。ヘッダーBlock (rows = 0) および空のBlockでは何も出力されません。Block内では、keys_count は行数に等しく、dict_size は dict ストリーム内の値の数に等しくなります。また、各キーは 1 << key_type_code バイトに収まります。
Native フォーマットでは、各Blockが自己完結したBlockローカルな辞書を個別に持ちます。Blockをまたいで保持される辞書の状態はありません。Native writer は low_cardinality_max_dictionary_size = 0 を設定するため、SerializationLowCardinality が共有辞書を構築することはありません。空でないすべてのBlockは、NeedGlobalDictionaryBit を設定しない状態 (メタデータ 0x600) で、キーをBlockローカルな追加キーとして書き込みます。また、Native reader は native_format が true の場合、NeedGlobalDictionaryBit を拒否します。したがって、デコーダはBlockごとに辞書をリセットし、そのBlockに含まれる dict_size 個のエントリを読み取る必要があります。前のBlockの辞書を引き継ぐと、次のBlockのキーを誤って解釈してしまいます。 (Blockをまたいだ LC 辞書の永続化は MergeTree のディスク上の表現に関わる事柄であり、Native のワイヤレイアウトとは無関係です。)['a', 'b', 'a', 'c', 'b'] の LowCardinality(String):
dict[1], dict[2], dict[1], dict[3], dict[2] = ["a", "b", "a", "c", "b"]。
値が ['a', NULL, '', 'b'] の LowCardinality(Nullable(String)) の例では、2 つの予約されたスロットが両方とも使われています。dict[0] は NULL 用、dict[1] はデフォルト値である空文字列用です。
dict[2] = "a"、dict[0] = NULL、dict[1] = ""、dict[3] = "b"、つまり ["a", NULL, "", "b"] となります。dict[0] と dict[1] はいずれも wire 上では空のバイト列です。NULL かどうかはバイト列の内容ではなく、キーがスロット 0 を指しているかどうかで決まります。
JSON (Tier 1: String fallback)
ClickHouse のJSON 型には複数のワイヤエンコーディングがあります (シリアル化バージョンのリファレンスを参照) 。Tier 1 は最も単純な方式です。クエリ単位の設定 output_format_native_write_json_as_string = 1 が指定されている場合、サーバーはすべての JSON 値をシリアル化されたテキストに平坦化し、そのカラムをstate prefixマーカー付きの String として出力します。
型文字列: JSON。
1 です。その他の値は、それぞれ異なる JSON/Object のエンコーディングを表します。0 = V1、2 = V2 (ネイティブ TCP プロトコルでのデフォルト)、3 = FLATTENED、4 = V3 です (シリアル化バージョンリファレンスを参照)。ここで 1 以外の値を検出した場合、デコーダが読み取っているのは String fallback ではありません。プレフィックスは行数 > 0 のすべての Block の先頭で読み取られ、values ストリームは num_rows 行分の標準的な String カラムです。
JSON 値 '{"a":1}' (1 行):
{"a":1}) として出力され、整数は整数のまま保持されます。このテキストは単なる String 値にすぎないため、クライアントは JSON を中身を解釈しないデータとして受け渡すことはできますが、個々のパスとその ClickHouse の型を復元することはできません。パスごとの型を正確に保持するには、後述の Tier 2 エンコーディングが必要です。
Variant(T1, T2, …)
判別共用体 (discriminated union) です。各行は、Variant 型のいずれか 1 つの型の値、または NULL を保持します。各行には型を選択する 1 バイトの グローバル判別子 があり、型ごとの値は Variant 型ごとに 1 つの連続した領域にまとめて密に格納されます。 型文字列:Variant(T1, T2, ...)。サーバーは型の順序を標準形に正規化します (Variant 型は名前順にソートされます) 。そのため、受信した型文字列では、型がすでに グローバル判別子の順序 で並んでいます。判別子 0 は最初の型、1 は 2 番目の型を選択し、以降も同様です。255 (NULL_DISCRIMINATOR) は、その行が NULL であることを示します。Variant の要素が Nullable になることはありません。NULL は判別子で表現されます。例: Variant(String, UInt64)、Variant(Array(UInt8), String)。
state prefix には UInt64 LE の discriminator モードが格納されます。0 = BASIC (すべての行の判別子をそのまま書き込む) 、1 = COMPACT (グラニュール単位のランレングス符号化) です。ネイティブプロトコルでは、サーバーはデフォルトで BASIC を使用します (use_compact_variant_discriminators_serialization = false) 。本仕様では BASIC のみを規定します。
d (≠ 255) を持つ行 r は、Variant 型 d の値の並びから索引 counter[d] の位置にある値を取り出し、その後 counter[d] をインクリメントします。判別子が 255 の行は NULL であり、どの並びからも値を消費しません。そのため、型ごとのカウンターの合計は非 NULL 行の数と一致します。
state prefix (モードを表す UInt64) は、行数が 0 より大きいすべてのBlockの先頭で読み取られます。ヘッダーBlockおよび空のBlockは何も出力しません。非 NULL の判別子はいずれも Variant 型の数より小さく、Variant 型 i は正確に count[i] 行分デコードされます。
Variant の要素のうち、それ自体がステートフルなもの (
LowCardinality、Variant、Dynamic、JSON) は、要素ごとの state prefix フェーズで、モードを表す UInt64 の後に独自の state prefix を出力します。leaf 型および単純な複合型 (leaf 型からなる Array、Tuple、Map) は state prefix が空であるため、自由に組み合わせることができます。[42, 'hi', NULL] を持つ Variant(String, UInt64) の例 (標準の順序では String が UInt64 より前にソートされるため、判別子 0 = String、1 = UInt64 となります) :
42、row 1 = String run[0] = "hi"、row 2 = NULL。
判別子ストリームが索引の役割を果たします。非 NULL の各判別子は、対応する型の密なランから次の値を取り出します。一方、255 (NULL) は何も消費しません。Dynamic も同じ走査で復元でき、違いは NULL のエンコード方法のみです:

Dynamic
値の型が実行時に判明するカラムです。各行は、実行時に決定される型の集合のいずれかに属する値、または NULL を保持します。Variant とは異なり、型の集合はカラムの 型文字列 には含まれず、state prefix によって伝達されます。
型文字列: Dynamic または Dynamic(max_types=N)。max_types パラメータは、カラムが追跡する異なる型の数の上限を指定しますが、以下のワイヤ形式には影響しません。
Dynamic には V1 = 1、V2 = 2、FLATTENED = 3、V3 = 4 の 4 つのエンコーディングがあります。サーバーがどれを出力するかは、チャネルとクエリ設定によって決まります。
clickhouse-clientおよび HTTP のFORMAT Native経由では、writer のリビジョンは (client_protocol_versionで引き上げない限り)0であるため、デフォルトは V1 です。- ネイティブ TCP プロトコル経由では、ネゴシエートされたリビジョンにおいてデフォルトは V2 です。
Nativewriter は統計情報を無効のままにするため、デフォルトのV2ペイロードにはバリアントごとの統計情報が含まれず、型リスト の直後にネストされたVariantのプレフィックスとデータが続きます。 (バリアントごとの統計情報は MergeTree のディスク上の表現に関わるものであり、Native の wire には含まれません。) - クエリ設定
output_format_native_use_flattened_dynamic_and_json_serialization = 1を指定すると上記の両方がオーバーライドされ、リビジョンに関係なく FLATTENED (version 3) が出力されます。
対象範囲このページでは
FLATTENED レイアウトのみを規定します。フラット化されていない V1/V2/V3 のバイナリレイアウトは内部表現/ディスク上の表現 (バイナリエンコードされた 型リスト、バリアントごとの統計情報) であり、ここでは規定しません。このページに基づいて Dynamic をデコードするクライアントは、output_format_native_use_flattened_dynamic_and_json_serialization = 1 を設定して FLATTENED を要求する必要があります。以下のレイアウトはこの設定を前提としています。プレフィックスの先頭にバージョンバイトがあるため、デコーダは実際に受信したエンコーディングを判別でき、FLATTENED のみを実装している場合は V1/V2/V3 を拒否できます。num_types 個の型と NULL 用のスロットを合わせて索引付けできる最小の符号なし整数型です。num_types ≤ 255 の場合は UInt8 となり、それを超えると順に UInt16、UInt32、UInt64 が使われます。NULL は判別子の値 num_types そのもので表されます。この点は、NULL が固定値 255 で表される Variant とは異なります。復元処理は Variant と同じく密な走査で行います。型ごとにカウンターを保持し、判別子 d (≠ num_types) を持つ行 r は、型 d の並びから counter[d] 番目の値を取得します。
state prefix (バージョン + 型リスト) は、行数 > 0 のすべてのBlockの先頭で読み取られます。ヘッダーBlockおよび空のBlockでは何も出力されません。
不正なカウント値
num_types は、型名よりも前にストリームから読み取られます。デコーダはこの値を信頼できないものとして扱う必要があり、割り当てのサイズ決定に直接使用してはなりません。これは、SIZE_MAX に近いカウント値 (中間演算でオーバーフローを起こしたり、DB::Exception 以外の例外をスローしたりする可能性があります) であっても、100000000 のように表現可能ではあるが大きいカウント値 (vector の max_size() よりはるかに小さいものの、型名を 1 つも読み取らないうちに数ギガバイトを割り当ててしまいます) であっても同様です。ClickHouse は、上限付きの事前割り当てヒントのみを使って型リストを 1 エントリずつ読み取ります。そのため、破損した num_types は INCORRECT_DATA (カウント値がコンテナーに格納可能な数を超える場合は “Dynamic column has too many types”) として、またはストリーム内の型エントリが尽きた時点で通常の読み取りエラーとして拒否され、メモリ不足による失敗になることは決してありません。ただし、フラット化された num_types を ColumnDynamic::MAX_DYNAMIC_TYPES_LIMIT (ClickHouse では 254) で制限してはなりません。フラット化された型リストには、共有バリアントにあふれた型も含め、すべての異なるランタイム型が含まれるため、有効なフラット化Blockがこの値をはるかに超える数の型を列挙することも正当にあり得ます。MAX_DYNAMIC_TYPES_LIMIT による制限が適用されるのは、非フラットな V1/V2/V3 プレフィックス内の num_dynamic_types カウントのみです。このカウントは通常バリアントのスロット数を表し、この上限によって制限されます (ClickHouse は、共有バリアント分の + 1 を加える前の段階でこの値を検証します) 。途中で切れたストリーム判別子の並びにはちょうど
num_rows 個のエントリが含まれます。デコーダは、ストリームに含まれていた分をそのまま受け入れるのではなく、その数だけ読み取れたことを検証する必要があります。並びが短くても、後続のデータとの矛盾は生じないためです。型ごとの値のカウントは実際に読み取られた判別子から導出されるので値の並びと整合したままであり、すべて NULL マーカーで構成された並びであれば値のバイトはまったく必要ありません。ClickHouse は、短い判別子の並びを、型ごとのチェックを行う前に INCORRECT_DATA (“Mismatch in flattened Dynamic column: N rows are expected, but the indexes stream contains only M rows”) として拒否します。これは、再構築されたカラムが実際のサイズにかかわらず num_rows 行分として扱われるためです。シリアライゼーションがステートフルなランタイム型 (
LowCardinality、Variant、Dynamic、JSON) は、型名リストの後にネストされた state prefix を持ちます。Variant の正規化に従います。つまり、通常バリアントのスロットは DataTypeVariant (型名) 順に書き込まれるため、wire上の順序は挿入順とは一致しません。ただし、常に全体がソートされているとは限りません。共有バリアントにあふれた型 (例えば Dynamic(max_types=N) の場合) は、通常スロットの後ろに初出順で追記されるため、リストの末尾では型名順が崩れることがあります。したがってデコーダは、送信された型リストを判別子の割り当ての基準として扱い、独自に再ソートしてはなりません。一方、リストに重複した型や Nothing が含まれていてはなりません。等しい 2 つのエントリは 1 つの判別子に対応付けられ、Nothing はそもそもバリアントではないため、いずれの場合もリストの一部に再構築先が存在しなくなるからです。ClickHouse はどちらも INCORRECT_DATA として拒否します。行 [42::UInt64, "hi", NULL] の場合、2 つの型は String と UInt64 で、"String" は "UInt64" より前にソートされるため、判別子は 0 = String、1 = UInt64、2 = NULL となります。
42、行 1 = String run[0] = "hi"、行 2 = NULL。型ごとの run は、型リストと同じwire上の順序 (String が UInt64 より先) で並びます。
JSON (Tier 2: FLATTENED Object)
よりリッチな JSON エンコーディングです。すべての値をテキストにフラット化する (Tier 1) 代わりに、カラムを JSON パスごとに 1 つのサブカラムへ分割します。このエンコーディングは、フラット化シリアライゼーションのフラグを有効にした状態 (output_format_native_use_flattened_dynamic_and_json_serialization = 1) で、Tier 1 へのフォールバックを要求しない (output_format_native_write_json_as_string = 0) ことで選択されます。このとき、サーバーはシリアライゼーションバージョン 3 を出力します。
パスには 2 種類あります。
- 型付きパス は型文字列内で宣言され (例:
JSON(a UInt32, b String))、宣言された型でデコードされます。ドットを含むパス名は、型文字列内でバッククォートで囲みます。 - 動的パス は実行時に検出され、それぞれ Dynamic カラムとしてデコードされます。
num_rows 個の値を持つ完全なカラムです。
Dynamic プレフィックス (プレフィックスフェーズ内) は、そのデータ (データフェーズ内) とは離れた位置に置かれます。state prefix は行数 > 0 のすべての Block の先頭で読み取られ、すべてのパスカラム (型付き・動的を問わず) はちょうど num_rows 個の値を保持します。行 r のオブジェクトは、各パスの索引 r の値を読み取ることで組み立てられます。その行で Dynamic 判別子が NULL である動的パスは、キーを生成しません。動的パス名はそれぞれ一意でなければならず、また型文字列ですでに型付きとして宣言されているパスと重複してはなりません。いずれの場合も、組み立てられたオブジェクトの1つのキーに2つの値が存在することになるため、ClickHouse はどちらも INCORRECT_DATA として拒否します。非フラットエンコーディングの動的パスリストにも同じことが当てはまります。
不正なカウントここで説明する FLATTENED レイアウトの
num_dynamic_paths、および非フラットな V1/V2 エンコーディングにおける動的パス数は、パス名より前にストリームから読み取られます (非フラットのプレフィックスには、フラット化パス用の独立したフィールドはありません。保持するのは動的パス数のみで、V1 ではこれに加えて、読み取られた後に破棄される max_dynamic_paths の値が含まれます) 。Dynamic と同様に、デコーダはこれらのカウントを信頼できない値として扱い、その値をそのまま割り当てサイズに使用してはなりません。これは SIZE_MAX 系のカウントに限らず、表現可能な範囲内の大きなカウントについても同様です。ClickHouse は上限付きの事前割り当てヒントのみを用いてパス名を1エントリずつ読み取るため、破損したカウントは INCORRECT_DATA (コンテナーが保持できる量を超える場合は「JSON/Object column has too many paths」) として、またはストリーム中のパス名が尽きた時点で通常の読み取りエラーとして拒否されます。途中で切れたストリームパスカラムごとの
num_rows 個の値は上限ではなく、厳密に満たすべき要件です。デコーダは、ストリームが本来の位置で終わっていると信頼するのではなく、型付き・動的を問わずすべてのパスカラムを Block の行数と照合する必要があります。組み立てられたオブジェクトには照合に使える独立したサイズがなく、その行数はフラット化の解除結果に依存するだけです。そのため、値が不足しているパスカラムは後から検出できず、その行 r を読み取ると末尾を越えてしまいます。ClickHouse はこれを INCORRECT_DATA (「Unexpected size of typed path / flattened path p in flattened Object column」) として拒否します。動的パスの場合、このチェックはその Dynamic カラム内部での同等のチェックに加えて行われます。JSON 値 {"a": 42, "b": "hi"} (1行、両パスとも動的) 。JSON の整数は Int64 として推論されます。
JSON 非フラット (V1/V2)
非フラット化されたObject エンコーディングは、フラット化フラグがオフのときにサーバーが wire 上で送出する形式です。clickhouse-client / HTTP FORMAT Native (リビジョン 0) では V1、ネイティブ TCP プロトコルでは V2 が使われます。 (3 つ目の非フラットエンコーディングである V3 は MergeTree パーツへの書き込み専用であり、wire 上では拒否されます。) これらは shared-data カラムを含み、本ページでは仕様を規定していません。なお、これらは Native wire 上ではパスごとの統計情報を含みません。NativeWriter は統計情報を無効のままにするため、Object の構造プレフィックスには統計情報セクションがなく、その直後には typed/dynamic/shared-data の各プレフィックスとデータがそのまま続きます。統計情報が現れるのは、統計情報を有効にしている MergeTree のディスク上のパスに限られます。本ページの仕様に従って JSON カラムをデコードするには、クライアントは文書化されたいずれかのティアを選択する必要があります。String フォールバックを使う場合は output_format_native_write_json_as_string = 1 を設定し、FLATTENED Object レイアウトを使う場合は output_format_native_use_flattened_dynamic_and_json_serialization = 1 を (output_format_native_write_json_as_string = 0 とあわせて) 設定します。
圧縮フレーム
ClickHouse は、Native ストリームのカラムデータを内部のフレームフォーマットで圧縮できます。以下のフレームレイアウトはトランスポートに依存しません。ネイティブ TCP プロトコルでも HTTP でも同じフレームが使われます。ただし、圧縮の要求方法や、フレームの前後に付く要素はトランスポートによって異なります。
- ネイティブ TCP プロトコル。 圧縮はクエリ単位のオプトイン方式で、Query パケットの
compressionフラグで有効にします。有効な場合、Data、Totals、Extremes、Log、ProfileEventsの各パケットのボディ (table_name文字列より後のバイト列) がフレームフォーマットでラップされます。パケットエンベロープ自体、つまりパケットタイプコードとtable_name文字列は圧縮されず、サーバーはこれらを生のストリームにそのまま書き込みます。NativeWriterが出力する内容はすべて圧縮ストリームに入るため、フレーム内ではBlockInfoプレフィックスが先頭に置かれ、その後に次元とカラムが続きます。したがって、クライアントはBlockInfoを読み取る前にフレームを伸長する必要があります。 - HTTP。
SELECT ... FORMAT Native&compress=1は、FORMAT Nativeのバイトストリーム全体を同じフレームでラップします (サーバーは同じ内部CompressedWriteBufferを使用します) 。また?decompress=1を指定すると、Nativeの入力ボディが同じフレーム形式であることを前提に、対応するCompressedReadBufferでデコードします。この経路には TCP のパケットタイプ、table_name、パケットエンベロープはなく、圧縮ペイロード全体はフレーム化されたNativeブロックだけで構成されます (BlockInfoプレフィックスが含まれるのは、上記の非圧縮レイアウトと同様、ネゴシエートされたリビジョンが0より大きい場合のみです) 。この内部のcompress/decompressフレーミングは、HTTP トランスポート圧縮 (Content-Encoding: gzip/zstd。enable_http_compressionで有効化) とは別のものです。後者は HTTP レイヤーでレスポンスをラップする仕組みであり、以下のフレームフォーマットとは異なります。
FORMAT Native レイアウトしか実装していないクライアントでも、圧縮された HTTP の Native レスポンスを読み取ったり、decompress=1 のリクエストボディを送信したりするには、このフレームレイヤーを追加で実装する必要があります。
フレームフォーマット
16 + compressed_size = 16 + 9 + body_size = 25 + body_size です。ここでは 2 つの範囲の違いに注意してください。チェックサムの対象は 9 バイトのヘッダーとボディであり、compressed_size もヘッダーとボディを含みますが、チェックサム自体は含みません。

メソッドバイトの値
以下の 3 つのコーデックは、ストリーム全体に対するNative フレーミングでサーバーが生成するものです。HTTP の compress=1 出力ではサーバーのデフォルトコーデック (ZSTD(3)) が使用され、ネイティブ TCP プロトコルでは network_compression_method に応じて LZ4、ZSTD、NONE のいずれかが使用されます。汎用的な Native クライアントは、これらを生成・処理できれば十分です。
メソッドバイトは カラムレベルのコーデック も表します。これらはストリーム全体のフレーミングではなく、MergeTree のディスク上のパスでカラムごとに適用されるものです。ただし、
decompress=1 の HTTP 入力パスでは各フレームのメソッドバイトからコーデックを判別するため、これらのバイトはいずれも正当な入力として現れる可能性があります。したがって、仕様に準拠するデコーダは割り当て済みの値の範囲全体を認識し、実装していないバイトについては、ボディを誤って解釈せずに拒否する必要があります。これらのボディの形式はコーデックごとに異なり、この汎用的なフレーム規約の対象外です。
DoubleDelta (0x94) は、2 階差分を MSB ファーストのプレフィックス/符号/絶対値ビットとして格納します。ワード単位でデコードしても、このエンコーディング、要素数、最終バイトのパディングは保持されます。
0x9c (ALP) は、Float32 および Float64 向けの ベータ版 の可逆コーデック (アダプティブな可逆浮動小数点圧縮) です。CODEC(ALP) を指定したテーブルは enable_alp_codec 設定が有効な場合にのみ作成できますが、既存のデータを引き続き読み取れるよう、伸長時にはこのメソッドバイトが常に受け付けられます。0x9d (SZ3) は、Float32、Float64、およびそれらの型の Array 向けの 実験的 な誤差制限付き非可逆コーデックです。CODEC(SZ3) を指定したテーブルは enable_sz3_codec 設定が有効な場合にのみ作成できますが、既存のデータを引き続き読み取れるよう、伸長時にはこのメソッドバイトが常に受け付けられます。0x9f (ZXC) は 実験的 な非対称 LZ コーデックで、圧縮は低速ですが伸長は非常に高速であり、圧縮率は LZ4 と ZSTD の中間です。CODEC(ZXC) を指定したテーブルは enable_zxc_codec 設定が有効な場合にのみ作成できますが、既存のデータを引き続き読み取れるよう、伸長時にはこのメソッドバイトが常に受け付けられます。バイト 0x99 (DeflateQpl) と 0x9b (ZSTD_QPL) は、すでに削除されたコーデックに割り当てられていたもので、予約済みとして再利用されません。
0x9e (Quantized) は、密なベクトルカラム (Array(Float32) など) 向けの 実験的 なカラムコーデックです。NONE と同様にパススルーであり、フル精度のボディはそのまま格納されます。ただし、このコーデックを指定すると、ベクトル検索を高速化するためのコンパクトな量子化済み補助ストリームを書き込むシリアライゼーションが付与されます。CODEC(Quantized(...)) を指定したテーブルは enable_quantized_codec 設定が有効な場合にのみ作成でき、伸長時にはこのメソッドバイトが常に受け付けられます。
チェックサム
ClickHouse が使用しているのは CityHash v1.0.2 (旧来のバリアント) であり、現行の Google CityHash ではありません。両者の出力は異なります。 チェックサムは、9 バイトのヘッダー (method + compressed_size + uncompressed_size) と N バイトのボディ、つまりチェックサムの直後からフレームの末尾までのすべてを対象に計算されます。16 バイトの CityHash128 出力のうち、先頭 8 バイトが下位半分 (LE) 、続く 8 バイトが上位半分 (LE) です。デコーダは受信したヘッダーとボディから CityHash128 を再計算し、先頭の 16 バイトと比較します。一致しない場合はデータ破損とみなされ、デコーダはエラーになります。ブロック単位の境界
Block の圧縮ペイロードは1 つ以上のフレームからなるストリームであり、単一のフレームであるとは限りません。送信側は、シリアル化されたブロックをCompressedWriteBuffer 経由で書き込みます。CompressedWriteBuffer は、内部バッファが満杯になるたび (約 1 MB、DBMS_DEFAULT_BUFFER_SIZE) にフレームを出力し、ブロックが flush された時点で最後のフレームを出力します。そのため、小さなブロックは 1 つのフレームに、大きなブロックは連続する複数のフレームになります。
この不変条件は一方向にしか成り立ちません。送信側は各ブロックの終端で圧縮バッファを flush するため、ブロックの終端は必ずフレーム境界と一致しますが、その逆は成り立ちません。ブロックの途中でバッファが満杯になって生じた中間のフレーム境界は、ブロックの途中に位置しており、ブロック境界ではありません。したがって、デコーダはブロック自身の次元 (num_columns/num_rows) を使ってブロックの終端を特定する必要があり、1 つのフレームが 1 つの完全なブロックであると想定してはいけません。
受信側はフレームを順次読み込んで処理します。まず 16 + 9 バイトを読み取り、続いてボディを正確に compressed_size - 9 バイト読み取り、正確に uncompressed_size バイトに伸長して、そのバイト列をブロックデコーダに渡します。デコーダが現在のフレームに含まれる以上のデータを必要とする場合は、次のフレームを取得します。送信側はブロックごとに flush するため、ブロックのデコードが完了した時点でフレームバッファは空になっており、次のブロックは新しいフレームの先頭から始まります。
ネイティブ TCP プロトコルでは、パケットエンベロープ (パケットタイプを表す VarUInt と table_name 文字列) は圧縮ペイロードの外側、つまり生ストリームに書き込まれ、フレーム化されるのはブロックのボディ (BlockInfo + カラム) だけです。HTTP の compress/decompress パスにはこうしたエンベロープは存在せず、ストリーム全体がフレーム化されたブロックで構成されます。
ネゴシエーション
ネイティブ TCP プロトコルでは、圧縮は接続単位ではなくクエリ単位で行われます。Query パケットのcompression: bool フィールドを使うと、そのクエリに限って圧縮をリクエストできます。サーバーはこのリクエストに応じ、クエリが終了するまでの間、圧縮された Data/Totals/Extremes/Log/ProfileEvents ボディを送出します (Log/ProfileEvents は v54481 以降のみ) 。また、クライアントから送信される Data ブロック (外部テーブル、データ終端を示す空のマーカー、INSERT の行) も同じ形式でフレーム化されていることを前提とします。同じ接続上の後続のクエリでは、圧縮の有無が異なっていても構いません。
HTTP には Query パケットがありません。代わりに、compress=1 クエリパラメータでそのリクエストの出力をフレーム化するよう指定し、decompress=1 でリクエストボディがフレーム化されていることを宣言します。compress=1 の出力は、network_compression_method ではなくサーバーのデフォルトコーデック (ZSTD(3)) で書き込まれます。一方、decompress=1 の読み取り側は各フレームのメソッドバイトからコーデックを判別するため、入力ではどのコーデックでも受け付けられます。
圧縮が有効な場合、サーバーは 2 行以上を含むブロックについて、カラムを並列ブロックマーシャリング /
ColumnBLOB パス (PARALLEL_BLOCK_MARSHALLING、v54478) 経由で処理することもあります。INSERT データを圧縮する実装では、ストリームの同期ずれを防ぐため、このパスに対応しておく (または明示的にオプトアウトする) 必要があります。用語集
Block — Native フォーマットにおけるデータ交換の単位です。カラム指向で格納された、自己記述的な行のまとまりです。ブロックとカラムの構造を参照してください。 BlockInfo — TCP の Data パケット経路で Block の前に置かれるメタデータヘッダーです (接続のリビジョンが 0 より大きい場合は常に書き込まれます)。リビジョンによって有無が決まり、フィールド ID でタグ付けされたフィールドの並びで構成されます。リビジョン0 でシリアライズする Native 出力フォーマットでは省略されます。BlockInfoを参照してください。
カラムボディ — カラムヘッダー (名前、型、has_custom_serialization バイト) に続く、実際の値を保持するカラムのバイト列です。レイアウトは型によって異なります。カラムの wire レイアウトを参照してください。
複合型 — 1 つ以上の内部型から構成される型で、カラムごとに複数のストリームとしてエンコードされます。wire フォーマットは安定しており、バージョン管理の対象ではありません。複合型を参照してください。
Dictionary (LowCardinality) — LowCardinality(T) カラムが整数インデックスを介して参照する、一意な値の配列です。LowCardinalityを参照してください。
空ブロック — num_columns = 0 かつ num_rows = 0 の Block です。番兵として使用され、クライアント側では入力終了マーカー、サーバー側ではストリーム境界マーカーとして機能します。ブロックのバリアントを参照してください。
ヘッダーブロック — num_columns > 0 かつ num_rows = 0 の Block で、クエリ応答の最初の Data パケットとしてサーバーから送信されます。結果のスキーマを通知する役割を持ちます。ブロックのバリアントを参照してください。
内部型 — 複合型がラップする型です。Array(UInt32) の内部型は UInt32、Nullable(T) の内部型は T です。
オフセットストリーム — Array、Map、Nested が行ごとの要素の境界を区切るために使用する、累積終了位置を表す UInt64 の配列です。Arrayを参照してください。
プレースホルダー値 — Nullable(T) カラムの values ストリームで、NULL の位置に書き込まれるバイト列です。デコーダはストリームを読み進めるためにこれを読み取りますが、内容は無視します。Nullableを参照してください。
結果ブロック — num_rows > 0 で、実際のクエリ結果の行を格納した Block です。ブロックのバリアントを参照してください。
スキーマブロック — ヘッダーブロックの別名で、INSERT フェーズの説明で使用されます。スキーマブロックは、想定されるカラムの形状をクライアントに伝えます。
シリアル化バージョン — バージョン付きの型が、後続するエンコーディングのバリアントを示すために使用する、型ごとの wire 上のバージョン番号です。プロトコルバージョンとは別物です。シリアル化バージョン: 概念を参照してください。
ステートプレフィックス — バージョン付きの型で、ブロックごとのペイロードの前に置かれるバイト列です。シリアル化バージョンと、(LowCardinality の場合は) ブロックごとの Dictionary メタデータを含みます。行数が 0 より大きいすべてのブロックの先頭で出力され、ブロックをまたいで引き継がれることはありません。
ストリーム — カラムボディ内の連続したバイト列で、1 つの論理的なサブコンポーネント (null マップ、オフセット配列、values ストリーム) をエンコードします。マルチストリーム型では、カラムごとに 2 つ以上のストリームが連結されます。