挿入された行の自動 upsert
ALTER や DELETE ステートメントを使わなくても、同じ行の複数のコピーを挿入し、そのうち 1 つを最新バージョンとして示すことで、更新操作を適用できます。これに対し、バックグラウンドプロセスが同じ行の古いバージョンを非同期に削除し、不変な insert を使って更新操作を効率的に再現します。
これは、テーブルエンジンが重複行を識別できることに依存しています。これは、一意性の判定に ORDER BY 句を使うことで実現されます。つまり、2 つの行が ORDER BY で指定されたカラムに対して同じ値を持つ場合、それらは重複と見なされます。テーブル定義時に指定する version カラムにより、2 つの行が重複と識別された場合に最新バージョンの行を保持できます。つまり、version の値が最も大きい行が保持されます。
このプロセスを以下の例で示します。ここでは、行は A カラム (このテーブルの ORDER BY) によって一意に識別されます。これらの行は 2 つの batches として挿入され、その結果、ディスク上に 2 つのパーツが作成されたと仮定します。その後、非同期のバックグラウンドプロセスで、これらのパーツがマージされます。
ReplacingMergeTree では、さらに deleted カラムも指定できます。このカラムには 0 または 1 を設定でき、値が 1 の場合はその行 (およびその重複) が削除済みであることを示し、それ以外では 0 を使用します。注意: 削除済みの行はマージ時には削除されません。
このプロセスでは、パーツマージ中に次のことが起こります。
- A カラムの値 1 で識別される行には、version 2 の更新行と version 3 の削除行 (deleted カラムの値は 1) の両方があります。したがって、削除済みとしてマークされた最新の行が保持されます。
- A カラムの値 2 で識別される行には、2 つの更新行があります。新しい方の行が保持され、price カラムの値は 6 になります。
- A カラムの値 3 で識別される行には、version 1 の行と version 2 の削除行があります。この削除行が保持されます。
削除済みの行は決して削除されない点に注意してください。これらは
OPTIMIZE table FINAL CLEANUP によって強制的に削除できます。これには実験的な設定 allow_experimental_replacing_merge_with_cleanup=1 が必要です。これは、以下の条件下でのみ実行してください。
- クリーンアップで削除対象となる古いバージョンを持つ行が、操作の実行後に挿入されないことを確実にできること。これらが挿入されると、削除済みの行がすでに存在しないため、誤って保持されます。
- クリーンアップを実行する前に、すべてのレプリカが同期していることを確認してください。これは次のコマンドで実現できます。
ReplacingMergeTree で deletes を処理する方法は、上記の条件でクリーンアップ期間を確保できる場合を除き、delete の割合が少量から中程度 (10%未満) の table にのみ推奨されます。
ヒント: 今後変更されない一部のパーティションに対して OPTIMIZE FINAL CLEANUP を実行できる場合もあります。
プライマリ/重複排除キーの選択
ORDER BY のカラムの値は、変更をまたいでも行を一意に識別できなければなりません。そのため、Postgres のようなトランザクションデータベースから移行する場合は、元の Postgres の主キーを ClickHouse の ORDER BY 句に含める必要があります。
ClickHouse の利用者であれば、テーブルの ORDER BY 句に含めるカラムを選び、クエリパフォーマンスを最適化することには慣れているでしょう。一般に、これらのカラムは よく実行されるクエリに基づいて選択し、カーディナリティの低い順に並べるべきです。重要なのは、ReplacingMergeTree では追加の制約があることです。つまり、これらのカラムは不変でなければなりません。たとえば Postgres からレプリケーションする場合は、基になる Postgres のデータで変化しないカラムだけをこの句に追加してください。他のカラムは変更されてもかまいませんが、これらのカラムは行を一意に識別するため、常に同じ値である必要があります。
分析ワークロードでは、通常、Postgres の主キーはあまり役に立ちません。特定の 1 行を対象にしたルックアップを行うことはほとんどないためです。カラムはカーディナリティの低い順に並べることを推奨しており、さらに ORDER BY の前方にあるカラムに一致する条件のほうが通常は高速 であることから、Postgres の主キーは ORDER BY の末尾に追加するべきです (分析上の価値がある場合を除きます) 。Postgres で複数のカラムが主キーを構成している場合は、カーディナリティとクエリ上の有用性を考慮しつつ、それらを ORDER BY に追加する必要があります。また、MATERIALIZED カラムで値を連結して、一意の主キーを生成する方法も検討できます。
Stack Overflow データセットの posts テーブルを考えてみましょう。
ORDER BY キーとして (PostTypeId, toDate(CreationDate), CreationDate, Id) を使用します。各投稿で一意な Id カラムにより、行を重複排除できます。必要に応じて、Version カラムと Deleted カラムをスキーマに追加します。
ReplacingMergeTree のクエリ
ORDER BY カラムの値を一意の識別子として使って重複した行を特定し、最も大きいバージョンだけを残すか、最新バージョンが削除を示している場合は重複した行をすべて削除します。ただし、これで保証されるのは最終的な整合性だけであり、行が必ず重複排除されるわけではないため、これに依存すべきではありません。
重複排除されたデータを読み取るには FINAL を使用する重複排除は background merges の間にのみ行われるため、通常の
SELECT でも重複した行や削除済みの行が返されることがあります。クエリ時に正しい結果を取得するには、FINAL modifier を使用してください。これにより、クエリの実行時に重複排除と削除行の除去が完了します。INSERT INTO SELECT でシミュレートできます。
INSERT INTO SELECT で再現できます。
FINAL を適用すると、正しい結果が得られます。
FINAL のパフォーマンス
FINAL 演算子は、クエリにわずかなパフォーマンス上のオーバーヘッドをもたらします。
これは、クエリが主キーカラムでフィルタリングされていない場合に特に顕著です。
その結果、より多くのデータが読み取られ、重複排除のオーバーヘッドも増加します。WHERE 条件でキーカラムを
フィルタリングに使用すると、読み込まれて重複排除に回されるデータ量を減らせます。
WHERE 条件でキーカラムを使用しない場合、ClickHouse は現在、FINAL 使用時に PREWHERE 最適化を利用しません。この最適化は、フィルタされていないカラムについて読み取る行数を減らすことを目的としています。この PREWHERE を再現し、パフォーマンスを改善できる可能性がある例は、こちらで確認できます。
ReplacingMergeTree でパーティションを活用する
FINAL クエリのパフォーマンスを向上させるために、do_not_merge_across_partitions_select_final=1 という設定を使用できます。この設定により、FINAL 使用時にはパーティションごとに個別にマージと処理が行われます。
次の posts テーブルでは、パーティション化を使用していません。
FINAL が実際に処理を行う必要があるように、重複した行を insert して AnswerCount を増やし、100万行を更新します。
FINAL を使用して、年ごとの回答の合計を計算します:
do_not_merge_across_partitions_select_final=1 を指定して上記のクエリをもう一度実行します。
マージ動作に関する考慮事項
マージ選択ロジック
大きなパーツにおけるマージ動作
max_bytes_to_merge_at_max_space_in_pool のしきい値に達すると、min_age_to_force_merge_seconds が設定されていても、以降のマージ対象には選ばれなくなります。その結果、継続的なデータ挿入に伴って蓄積する可能性がある重複の除去を、自動マージに頼ることはできなくなります。
これに対処するには、OPTIMIZE FINAL を実行して、パーツを手動でマージし、重複を削除できます。自動マージとは異なり、OPTIMIZE FINAL は max_bytes_to_merge_at_max_space_in_pool のしきい値を回避し、利用可能なリソース、特にディスク容量のみに基づいてパーツをマージし、各パーティションでパーツが 1 つだけ残るまで処理を進めます。ただし、この方法は大規模なテーブルではメモリ消費が大きくなる可能性があり、新しいデータが追加されるたびに繰り返し実行が必要になる場合があります。
パフォーマンスを維持しつつ、より持続可能な解決策とするには、テーブルのパーティション化を推奨します。これにより、パーツがマージの最大サイズに達するのを防ぎやすくなり、継続的な手動最適化の必要性も減らせます。
パーティション化とパーティションをまたぐマージ
クエリパフォーマンスを向上させるためのマージの調整
min_age_to_force_merge_seconds と min_age_to_force_merge_on_partition_only はそれぞれ 0 と false に設定されており、これらの機能は無効です。この構成では、ClickHouse はパーティションの経過時間に基づいてマージを強制せず、標準のマージ動作を適用します。
min_age_to_force_merge_seconds に値を指定すると、ClickHouse は指定した期間より古いパーツに対して、通常のマージヒューリスティクスを無視します。これは一般に、パーツの総数を最小限に抑えることが目的の場合にのみ効果的ですが、ReplacingMergeTree ではクエリ実行時にマージ対象となるパーツ数を減らすことで、クエリパフォーマンスの向上につながる場合があります。
この挙動は、min_age_to_force_merge_on_partition_only=true を設定することでさらに調整できます。この設定では、積極的なマージを行う条件として、パーティション内のすべてのパーツが min_age_to_force_merge_seconds で指定した時間より古くなっている必要があります。この構成により、古いパーティションは時間の経過とともに 1 つのパーツまでマージされ、データが集約されることでクエリパフォーマンスを維持できます。
推奨設定
min_age_to_force_merge_seconds には低い値、つまりパーティション期間より大幅に短い値を設定するのが望ましいです。これによりパーツ数を最小限に抑え、FINAL 演算子によるクエリ実行時の不要な マージ を防げます。
たとえば、すでに単一のパーツに マージ 済みの月次パーティションを考えてみてください。このパーティション内で小さな想定外の insert により新しいパーツが作成されると、マージ が完了するまで ClickHouse は複数のパーツを読み取る必要があるため、クエリパフォーマンスが低下することがあります。min_age_to_force_merge_seconds を設定すると、これらのパーツが積極的に マージ されるようになり、クエリパフォーマンスの低下を防げます。