ClickHouse向けの強力な新しいJSONデータ型を開発
(clickhouse.com)- ClickHouseは、JSONドキュメントを文字列として格納して毎回パースするボトルネックを避けるため、JSONパスごとの値を真のカラム指向ストレージに配置する新しいJSON型を導入した
- 実装の中核はVariant型とDynamic型で、同じJSONパスに整数・文字列・配列のような異なる型が入ってきても、最小共通型へ無理に統合しない
max_dynamic_pathsのデフォルト値は1024、max_dynamic_typesのデフォルト値は32で、サブカラム数と型別ファイル数を制限し、ファイルディスクリプタやマージコストの増加を抑える- 型ヒント、
SKIP、SKIP REGEXPでパスごとの保存方法を調整でき、値はC.a.bのようなサブカラム構文で読み出せる - 新しい型は廃止予定の
Object('json')の置き換えを目指しており、JSONキーパスをプライマリキーやdata-skipping indexで使う改善もロードマップに残っている
JSONをカラム指向ストレージに適合させる課題
- JSONは、ログ、オブザーバビリティ、リアルタイムデータストリーミング、モバイルアプリのストレージ、機械学習パイプラインで半構造化・非構造化データを扱う共通形式として使われている
- ClickHouseは真のカラム指向データベースで、テーブルをディスク上のカラムデータファイル群として保存し、圧縮やベクトル化されたフィルタ・集計を行う
- JSONでも同じ性能を出すには、ドキュメントを文字列カラムに保存して後からパースするのではなく、各一意のJSONパスの値をカラムのように保存する必要がある
新しいJSON型が扱う4つの制約
-
パスごとのカラム指向保存
- JSONパスごとの値も、数値型の通常カラムのように圧縮し、ベクトル化された方式でフィルタ・集計できなければならない
-
動的に変わる型
- 同じJSONパス
aに、整数、浮動小数、配列のような異なる型が入ることがある - ClickHouseはこれを事前には把握できず、型同士に互換性がない場合もあるため、最小共通型へまとめると情報が失われる可能性がある
- 同じJSONパス
-
カラムファイルの爆発的増加防止
- 新しいJSONパスごとに新規カラムファイルを作ると、一意キーの多いデータではディスク上のファイル数が急増する
- ファイルディスクリプタはメモリを消費し、処理対象ファイルが増えるとマージ性能にも影響する
-
疎なキーの高密度保存
- 一意ではあるが疎なJSONキーが多い場合、値のない行ごとに
NULLやデフォルト値を繰り返し保存してはならない - 実際の値だけを高密度に保存してこそ、PB規模の分析でもスケールできる
- 一意ではあるが疎なJSONキーが多い場合、値のない行ごとに
Variant型: 型を無理に統合しない基盤
- Variantデータ型はJSONとは別に使える独立機能で、1つのテーブルカラム内に異なるデータ型の値を保存・読み出しできる
- 既存のClickHouseカラムは固定型を持ち、挿入値はその型であるか、暗黙変換される
Nullableカラムは、値ファイルとは別にNULLマスクファイルを使うArrayは配列サイズを別ファイルに保存し、それによってオフセットを計算する
- Variantカラムは、同じ具体型の値を型ごとのサブカラムに保存する
- 例: すべての
Int64値はC.Int64.binに、すべてのString値はC.String.binに保存される
- 例: すべての
- 各行がどの型を使うかは、
UInt8のdiscriminatorカラムで追跡する- discriminator値は、ソート済みの型名一覧におけるインデックスである
- discriminatorの
255はNULL用の予約値である - この設計により、Variantは最大255個の具体型を持てる
- 型別データファイルは、値のある行だけを保持する高密度保存構造になっている
- 型別ファイルには
NULL値を保存しない - discriminator行から実際の型ファイル内の行位置を見つけるため、メモリ上の
UInt64オフセットカラムを使う - このオフセットはディスクには保存されず、discriminatorカラムファイルからその場で生成できる
- 型別ファイルには
- Variantは任意のネストをサポートする
Variant(T1, T2)とVariant(T2, T1)は、型の順序という点では同じ意味を持つ- Variantの内部にさらにVariantを入れることもできる
- 特定のネスト型の値は、型名をサブカラムのように付けて読み出す
- 例:
C.Int64
- 例:
Dynamic型: 型の一覧を事前に知らなくても保存
- Dynamic型はVariantの上に実装された独立機能で、JSONの文脈外でも使える
- DynamicはVariantに2つの機能を追加する
- 1つのカラム内に任意の型の値を保存でき、型一覧を事前に指定しなくてよい
- 別カラムデータファイルとして保存する型数を制限できる
- 内部保存方式はVariantと同じだが、
C.dynamic_structure.binファイルが追加される- このファイルには、サブカラムとして保存された型一覧と、型別カラムデータファイルのサイズ統計が入る
- このメタデータはサブカラム読み出しとデータパートのマージに使われる
Dynamic(max_types=N)は、別ファイルとして保存する型数を制限する0 <= N < 255- デフォルト値は32
- 上限に達すると、残りの型の値は
C.SharedVariant.binのような単一カラムファイルに保存される- このファイルの型は
Stringである - 各行は
<binary_encoded_data_type><binary_value>構造の文字列値を持つ - 複数型の値を1つのカラムファイル内に保存して再び読み出せる
- このファイルの型は
- DynamicもVariantと同様に、型名をサブカラムとして使って特定型の値を読み出せる
- 例:
C.Int64
- 例:
JSON型の宣言と保存構造
- 新しいJSON型は、任意構造のJSONオブジェクトを保存し、各JSON値をパスベースのサブカラムとして読み出せるようにする
- 型宣言にはオプション引数とヒントを含められる
<column_name> JSON(
max_dynamic_paths=N,
max_dynamic_types=M,
some.path TypeName,
SKIP path.to.skip,
SKIP REGEXP 'paths_regexp')
max_dynamic_paths- デフォルト値は1024
- 別サブカラムとして保存するJSONキーパス数を指定する
- 上限を超えたパスは、特殊構造の単一サブカラムにまとめて保存される
max_dynamic_types- デフォルト値は32
- 値の範囲は
0から254 - 1つのJSONキーパスカラムで、別カラムデータファイルとして保存するデータ型数を指定する
- 上限を超える新しい型は、特殊構造の単一カラムデータファイルにまとめて保存される
some.path TypeName- 特定JSONパスに対する型ヒントである
- そのパスは指定型のサブカラムとして常に保存され、性能保証を提供する
SKIP path.to.skip- 特定JSONパスをパース中にスキップする
- そのパスはJSONカラムに保存されない
- 指定パスがネストしたJSONオブジェクトなら、そのネストオブジェクト全体がスキップされる
SKIP REGEXP 'path_regexp'- 正規表現に一致するパスをJSONパース中にスキップする
- 一致したパスはJSONカラムに保存されない
JSONパスをカラムのように読み出す仕組み
- JSONカラムの各一意なleafパス値は、ディスク上で2つの方法のいずれかで保存される
- 型ヒントのあるパスは通常のカラムデータファイルとして保存される
- 型が動的に変わりうるパスはDynamicサブカラムとして保存される
- JSON型は
object_structureという特殊ファイルを使う- 動的パスに関するメタデータを保持する
- 各動的パスのnon-null値の統計を保持する
- サブカラム読み出しとデータパートのマージに使われる
- カラムファイル爆発は2段階の制限で制御される
max_dynamic_typesは、1つのJSONキーパス内で別ファイルとして保存する型数を制限するmax_dynamic_pathsは、別サブカラムとして保存するJSONキーパス数を制限する
max_dynamic_pathsの上限を超える追加の動的JSONパスは、shared dataとして保存される- 例のファイルは
C.object_shared_data.size0.bin、C.object_shared_data.paths.bin、C.object_shared_data.values.bin object_shared_data.valuesはString型である- 各エントリは
<binary_encoded_data_type><binary_value>構造を持つ
- 例のファイルは
- shared dataについても
object_structure.binに追加統計が保存される- 現在shared dataカラムに保存されているパスのうち、最初の10000件のパスについてnon-null値の統計を保存する
JSONパス構文とネストオブジェクト
- JSON型は、各パスのleaf値をパス名ベースのサブカラムとして読み出せる
- 例:
C.a.b
- 例:
- 型ヒントが指定されていないパスの値は、常にDynamic型を持つ
- 例:
C.a.dの型はDynamic
- 例:
- Dynamic型の下位型サブカラムは特殊なJSON構文で読み出す
- 例:
C.a.d.:Int64
- 例:
- ネストしたJSONオブジェクトは
JSON_column.^some.path構文でJSON型サブカラムのように読み出せる- 例:
C.^a
- 例:
- 現時点では、ドット構文は性能上の理由からネストオブジェクトを読み出さない
- パスごとのリテラル値の読み出しには、現在の保存構造が効率的である
- パスごとのサブオブジェクト全体を読み出すには、より多くのデータを読む必要があり遅くなる可能性がある
- オブジェクトを返すには
.^構文が必要である - ClickHouseは2つの
.構文を統合する計画である
Compact discriminatorシリアライズ
- 多くの動的JSONパスでは、値の型がほとんど同じである場合がある
- 一意だが疎なJSONパスが多いと、各パスのdiscriminatorファイルは主に
255、つまりNULL値を含むことになる - こうしたファイルは圧縮効率は高いが、すべての行で値が同じであっても依然として重複が多い
- ClickHouseは、discriminatorシリアライズ向けにcompact formatを実装した
- 通常のように
UInt8discriminator値をすべて書く代わりに、対象granuleのdiscriminatorがすべて同じなら、3つの値だけをシリアライズする - compact granule formatインジケータ
- そのgranuleの値数インジケータ
- discriminator値
- 通常のように
- この最適化はMergeTree設定
use_compact_variant_discriminators_serializationで制御される- デフォルトで有効になっている
- 通常のindex granularity基準では8192個の値の代わりに3つの値だけを保存するケースがある
リリース状況と次の段階
- 新しいJSON型は、廃止予定のObject('json')型を置き換えるよう設計されている
- 実装はClickHouse 24.08リリースで、テスト目的のexperimental機能として提供されている
- JSONロードマップには、JSONキーパスをテーブルのプライマリキーやdata-skipping index内で使う改善が含まれている
- VariantやDynamicのような構成要素は、JSON以外にもXMLやYAMLのような追加の半構造化型サポートの基盤になる
- ClickHouse Cloudユーザーが新しいJSONデータ型をテストするには、ClickHouseサポートチームに連絡してprivate previewへのアクセスをリクエストする必要がある
まだコメントはありません。