パーツマージ
ClickHouse におけるパートマージとは?
ClickHouse は、クエリだけでなくインサートも高速です。その理由は、ストレージレイヤ にあり、これは LSM tree と同様の動作をします。
① (MergeTree engine ファミリーの)テーブルへのインサートは、ソート済みで不変の data parts を生成します。
② すべてのデータ処理は バックグラウンドパートマージ にオフロードされます。
これにより、データ書き込みは軽量で、非常に効率的になります。
テーブルごとの ^^parts^^ の数を制御し、上記 ② を実現するために、ClickHouse はバックグラウンドで継続的に(パーティション単位で)小さな ^^parts^^ を大きなものへとマージし、圧縮後のサイズがおよそ ~150 GB に達するまで続けます。
次の図は、このバックグラウンドマージ処理の概要を示しています。

merge level は、追加のマージが行われるたびに 1 ずつ増加します。0 のレベルは、そのパートが新しく、まだマージされていないことを意味します。より大きな ^^parts^^ にマージされた元の ^^parts^^ は inactive とマークされ、最終的には 設定可能 な時間(デフォルトでは 8 分)後に削除されます。時間の経過とともに、これによりマージされた ^^parts^^ の 木構造 が形成されます。これが merge tree テーブルという名称の由来です。
マージの監視
テーブルパーツとは何かの例では、ClickHouse がすべてのテーブル^^パーツ^^を parts システムテーブルで追跡していることを示しました。例に用いたテーブルについて、アクティブな各パーツごとのマージレベルと保存されている行数を取得するために、次のクエリを使用しました。
前に説明したクエリ結果から、サンプルテーブルにはアクティブな ^^parts^^ が4つあり、それぞれは最初に挿入された ^^parts^^ を1回マージして作成されたものであることが分かります。
クエリを実行すると 、4 つの ^^parts^^ が、その後 1 つの最終 part にマージされたことがわかります(テーブルにこれ以上 insert が行われていない場合):
ClickHouse 24.10 では、組み込みの monitoring dashboards に新しい merges dashboard が追加されました。OSS と Cloud の両方で /merges HTTP ハンドラー経由で利用でき、このダッシュボードを使ってサンプルテーブルに対するすべてのパートマージを可視化できます。

上のダッシュボードは、最初のデータ挿入から最終的に 1 つのパートへマージされるまでの全プロセスを捉えています。
① アクティブな ^^parts^^ の数。
② パートマージ。ボックスで視覚的に表現されており(サイズはパートの大きさを反映しています)。
同時マージ
1 つの ClickHouse サーバーでは、複数のバックグラウンドマージスレッドを使用して、パーツのマージを並行して実行します。

各マージスレッドは次のループを実行します。
① 次にどの ^^パーツ^^ をマージするかを決定し、それらの ^^パーツ^^ をメモリに読み込みます。
② メモリ上で ^^パーツ^^ をマージして、より大きな 1 つのパーツにします。
③ マージされたパーツをディスクに書き込みます。
① に戻る
CPU コア数と RAM 容量を増やすことで、バックグラウンドマージのスループットを高めることができます。
メモリ最適化されたマージ
ClickHouse は、前の例で示したように、マージ対象となるすべての ^^parts^^ を必ずしも一度にメモリへ読み込むわけではありません。いくつかの要因に応じて、メモリ消費量を削減する(マージ速度を犠牲にする)ために、いわゆる垂直マージを用い、^^parts^^ を一度にではなく、ブロックのチャンクごとに読み込んでマージします。
マージの仕組み
下の図は、ClickHouse における単一のバックグラウンドマージスレッドが、(デフォルトではバーティカルマージなしで)^^パーツ^^をどのようにマージするかを示しています:

パーツのマージは、次のステップで行われます:
① 解凍と読み込み: マージ対象の ^^パーツ^^ から取得した圧縮済みバイナリカラムファイルを解凍し、メモリに読み込みます。
② マージ: データをより大きなカラムファイルにマージします。
③ インデックス作成: マージ後のカラムファイルに対して、新しい疎なプライマリインデックスを生成します。
④ 圧縮と保存: 新しいカラムファイルとインデックスを圧縮し、マージ後のデータパーツを表す新しいディレクトリに保存します。
セカンダリのデータスキップインデックス、カラム統計情報、チェックサム、min-max インデックスなどのデータパーツ内の追加メタデータも、マージ後のカラムファイルに基づいて再生成されます。ここでは説明を簡潔にするため、これらの詳細は省略しています。
ステップ ② の動作は、使用している特定の MergeTree エンジンに依存します。エンジンごとにマージ処理の方法が異なり、たとえば行が集約されたり、古い行が置き換えられたりすることがあります。前述のとおり、このアプローチによりすべてのデータ処理がバックグラウンドのマージにオフロードされ、書き込み処理を軽量かつ効率的に保つことで、極めて高速な挿入を実現します。
次に、^^MergeTree^^ ファミリー内の特定のエンジンにおけるマージの仕組みについて、簡単に概観します。
標準的なマージ
以下の図は、標準的な MergeTree テーブルにおいて ^^パーツ^^ がどのようにマージされるかを示しています。

上の図にある DDL 文は、MergeTree テーブルを作成し、^^ソートキー^^ (town, street) を指定しています。これは、ディスク上のデータがこれらのカラムでソートされ、それに応じて疎なプライマリインデックスが生成されることを意味します。
① 解凍され事前にソート済みのテーブル列が、テーブルの ^^ソートキー^^ によって定義されるテーブル全体のソート順序を維持したまま ② マージされ、③ 新しい疎なプライマリインデックスが生成され、④ マージされた列ファイルとインデックスが圧縮されて、新しいデータパーツとしてディスクに保存されます。
置換マージ
ReplacingMergeTree テーブルにおけるパーツのマージ処理は 標準のマージ と同様に動作しますが、各行について最新バージョンのみが保持され、古いバージョンは破棄されます。

上図の DDL ステートメントは、^^ソートキー^^ (town, street, id) を持つ ReplacingMergeTree テーブルを作成します。これは、ディスク上のデータがこれらのカラムでソートされ、そのソートに基づいてスパースなプライマリインデックスが生成されることを意味します。
② のマージは標準的な MergeTree テーブルと同様に動作し、グローバルなソート順を維持しながら、解凍済みで事前ソートされたカラムを結合します。
ただし、ReplacingMergeTree は同じ ^^ソートキー^^ を持つ重複行を削除し、それぞれを含むパーツの作成タイムスタンプに基づいて、最新の行のみを保持します。
集計マージ
数値データは、SummingMergeTree テーブルの ^^パーツ^^ がマージされる際に、自動的に集計されます。

上の図の DDL ステートメントは、town を ^^ソートキー^^ とする SummingMergeTree テーブルを定義しています。これは、ディスク上のデータがこの列でソートされ、その列に基づいて疎な primary index が作成されることを意味します。
② のマージ処理ステップでは、ClickHouse は同じ ^^ソートキー^^ を持つすべての行を 1 行に集約し、数値列の値を合計します。
集約マージ
上の SummingMergeTree テーブルの例は、AggregatingMergeTree テーブルの特殊なバリアントであり、パーツのマージ時に 90 以上 の任意の集約関数を適用することで、自動インクリメンタルなデータ変換 を可能にします。

上の図の DDL ステートメントは、town を ^^ソートキー^^ とする AggregatingMergeTree テーブルを作成します。これにより、ディスク上でこの列に基づいてデータがソートされ、対応する疎なプライマリインデックスが生成されます。
② のマージ中、ClickHouse は同じ ^^ソートキー^^ を持つすべての行を、部分集約状態(例: avg() 向けの sum と count)を格納する 1 つの行に置き換えます。これらの状態によって、バックグラウンドでのインクリメンタルなマージを通じて正確な結果が保証されます。