概要
更新処理の扱いに関しては、分析用データベースとトランザクションデータベースは、その基盤となる設計思想と想定ユースケースの違いにより、大きく異なります。 たとえば PostgreSQL は、行指向で ACID 準拠のリレーショナルデータベースであり、堅牢でトランザクション性のある更新および削除操作をサポートし、Multi-Version Concurrency Control(MVCC)などの仕組みによってデータの一貫性と完全性を保証します。 これにより、高い同時実行性の環境でも安全かつ信頼性の高い変更が可能になります。
一方、ClickHouse は、読み取り中心のアナリティクスと高スループットな追記専用処理に最適化されたカラム指向データベースです。 インプレースの更新と削除をネイティブにサポートしていますが、大量の I/O を招かないよう注意して使用する必要があります。 別の方法として、テーブルを再構成し、削除や更新を追記型の操作に変換して、非同期および/または読み取り時に処理させることもできます。これにより、リアルタイムなデータ操作よりも、高スループットなデータインジェストと効率的なクエリパフォーマンスに重点を置くという設計方針が反映されます。
このガイドでは、ClickHouse で利用可能な更新方法の概要を示し、ユースケースに応じて適切な更新戦略を選択できるようにします。
更新戦略の選択
ClickHouse のデータを更新するアプローチは、基本的に 2 つに大別されます。
- 専用のテーブルエンジンを使用し、挿入として更新を処理する方法
UPDATE ... SETやALTER TABLE ... UPDATE文のような、宣言的な更新を使用する方法
上記 2 つのカテゴリそれぞれの中に、データを更新するためのいくつかの手法があります。 それぞれに利点とパフォーマンス特性があり、データモデルと更新対象のデータ量に基づいて、適切な手法を選択する必要があります。
専用テーブルエンジンを使用するタイミング
大量の更新、頻繁な行レベルの変更、あるいは更新および削除イベントの連続的なストリームを処理する必要がある場合は、専用テーブルエンジンを使用する方が適しています。
一般的に利用されるエンジンは次のとおりです。
| Engine | Syntax | When to use |
|---|---|---|
| ReplacingMergeTree | ENGINE = ReplacingMergeTree | 大量のデータを更新する場合に使用します。このテーブルエンジンは、マージ時のデータ重複排除に最適化されています。 |
| CoalescingMergeTree | ENGINE = CoalescingMergeTree | データが断片的に到着し、行全体の置き換えではなくカラムレベルでの統合が必要な場合に使用します。 |
| CollapsingMergeTree | ENGINE = CollapsingMergeTree(Sign) | 個々の行を頻繁に更新する場合、または時間とともに変化するオブジェクトの最新状態を保持する必要があるシナリオで使用します。たとえば、ユーザーアクティビティや記事の統計を追跡する場合などです。 |
MergeTree ファミリーのテーブルエンジンはバックグラウンドでデータパーツをマージするため、結果整合性 を提供します。マージが完了するまでの間もテーブルをクエリする際に適切な重複排除を行うには、FINAL キーワードを使用する必要があります。
他にも engine types は存在しますが、ここで挙げたものが最も一般的に使用されるものです。
宣言的な更新を使用するタイミング
宣言的な UPDATE 文は、重複排除ロジックを管理する複雑さなしにシンプルな更新操作を行う場合には、より分かりやすくなることがありますが、一般的には、専用エンジンを使う場合と比べて、少量の行をそれほど頻繁ではない頻度で更新する用途により適しています。
| Method | Syntax | When to use |
|---|---|---|
| Update mutation | ALTER TABLE [table] UPDATE | データを即座にディスク上で更新する必要がある場合に使用します(例: コンプライアンス目的)。SELECT パフォーマンスに悪影響を与えます。 |
| On-the-fly updates | ALTER TABLE [table] UPDATE | 少量のデータを更新する場合に使用します。以降のすべての SELECT クエリで更新後のデータを即座に返しますが、ディスク上では当初は内部的に更新済みとしてマークされるだけです。SET apply_mutations_on_fly = 1; を設定して有効化します。 |
| Lightweight updates | UPDATE [table] SET ... WHERE | 少量のデータ(テーブルの約 10% 程度まで)を更新する場合に使用します。カラム全体を書き換えることなく、即時可視性のためのパッチパーツを作成します。SELECT クエリにオーバーヘッドを追加しますが、レイテンシは予測可能です。 |
専用テーブルエンジンを利用した更新
ReplacingMergeTree
ReplacingMergeTree は、バックグラウンドでのマージ処理中に、同じソートキーを持つ行の重複を解消し、最新バージョンのみを保持します。
このエンジンは、安定したキーで更新が識別される、個々の行への高頻度での更新に最適です。 ベンチマークでは、単一行の更新において mutations より最大 4,700 倍高速になり得ることが示されています。
行を更新するには、同じソートキーの値を持ち、より高いバージョン番号を持つ新しいバージョンを挿入するだけです。古いバージョンはバックグラウンドマージの際に削除されます。重複排除は遅延的/最終的にのみ行われ(マージ時にだけ実行される)ため、正しく重複排除された結果を得るには、FINAL 修飾子または同等のクエリロジックを使用する必要があります。FINAL 修飾子は、データに応じて 21〜550% のクエリオーバーヘッドを追加します。
ReplacingMergeTree はソートキーの値を更新できません。また、論理削除のための Deleted カラムもサポートしています。
詳細: ReplacingMergeTree guide | ReplacingMergeTree reference
CoalescingMergeTree
CoalescingMergeTree は、マージ処理の際に各カラムごとに最新の null 以外の値を保持することで、スパースなレコードを統合します。これにより、行全体を置き換えるのではなく、カラムレベルでのアップサートが可能になります。
このエンジンは、データが複数のソースから断片的に到着する場合や、異なるカラムが異なるタイミングで埋められるようなシナリオ向けに設計されています。一般的なユースケースとしては、断片化されたサブシステムからの IoT テレメトリ、ユーザープロファイルのエンリッチメント、ディメンションの遅延を伴う ETL パイプラインなどがあります。
同じソートキーを持つ行がマージされるとき、CoalescingMergeTree は行全体を置き換えるのではなく、各カラムごとに最新の非 NULL 値を保持します。この動作を意図どおりに機能させるには、キー以外のカラムを Nullable にする必要があります。ReplacingMergeTree と同様に、正しい統合結果を得るには FINAL を使用してください。
このエンジンは ClickHouse 25.6 から利用可能です。
詳細については、CoalescingMergeTree を参照してください。
CollapsingMergeTree
更新はコストが高いものの、挿入を活用して更新を実現できるという考え方に基づき、CollapsingMergeTree は Sign カラムを使用して、マージ時に行をどのように扱うかを ClickHouse に指示します。Sign カラムに -1 が挿入されると、その行は対応する +1 の行とペアになった際に折りたたまれて(削除され)ます。更新対象の行は、テーブル作成時の ORDER BY 句で使用されたソートキーに基づいて特定されます。
ReplacingMergeTree と異なり、CollapsingMergeTree ではソートキーの値を変更できます。これは、金融トランザクションやゲーム状態の追跡のような、取り消しセマンティクスを持つ可逆な操作に適しています。
上記の更新手法では、キャンセル用の行を挿入するために、アプリケーションがクライアント側で状態を保持しておく必要があります。これは ClickHouse の観点からは最も効率的ですが、大規模になると扱いが複雑になる可能性があります。また、正しい結果を得るには、クエリでも sign の乗算を伴う集約が必要になります。
詳しくは: CollapsingMergeTree
宣言的な更新
これらのメソッドは、MergeTree ファミリー エンジンを使用するテーブルで動作します。
| Method | Syntax | Best for | Trade-offs |
|---|---|---|---|
| Mutations | ALTER TABLE ... UPDATE | 物理削除が必要なコンプライアンス要件; 頻度の低いバルク更新 | I/O 負荷が高い; カラムを書き換える |
| Lightweight updates | UPDATE ... SET ... WHERE | 小規模な更新 (~0.1〜10% の行); パフォーマンスが求められる高頻度の更新 | SELECT のオーバーヘッドが増える; パッチ用パーツが上限にカウントされる |
| On-the-fly mutations | ALTER TABLE ... UPDATE と apply_mutations_on_fly=1 の併用 | 即時反映; 実行できるオペレーション数が限定的 | Keeper への依存; スケールは数十件程度のオペレーションまでに制限される |
ミューテーション
ミューテーション(ALTER TABLE ... UPDATE)は、WHERE 式に一致する行を含むすべてのパーツを書き換えます。これにより、ディスク上の物理データが確実に変更されます。
コンプライアンス上、物理データの確実な削除が求められる場合や、I/O のオーバーヘッドを許容できるまれな一括更新操作を行う場合に、mutation を使用します。
Mutation は I/O 負荷がかなり高く、WHERE 式に一致するすべてのパーツを書き換えます。この処理にはアトミック性はなく、mutated パーツが準備できしだい元のパーツと置き換えられます。そのため、mutation の実行中に開始された SELECT クエリは、すでに mutation 済みのパーツのデータと、まだ mutation されていないパーツのデータを両方参照します。進行状況は system.mutations テーブル経由で追跡できます。
Mutation は I/O 負荷が高く、クラスタの SELECT パフォーマンスに影響を与える可能性があるため、使用は最小限に抑えるべきです。Mutation のキュー投入が処理速度より速くなると、クエリ パフォーマンスは低下します。system.mutations を用いてキューを監視してください。
論理更新
論理更新では、従来のミューテーションのようにカラム全体を書き換えるのではなく、更新対象のカラムと行のみを含む特別なデータパーツである「patch parts」を使用します。
このアプローチは標準的な UPDATE 構文を使用し、マージを待たずに即座にパッチ用のパーツを作成します。更新された値は、パッチ適用を通じて SELECT クエリからすぐに参照できますが、ストレージ上で物理的に反映されるのは後続のマージ時のみです。これにより、論理更新は、テーブル全体のうち小さな割合(最大約 10% 程度)の行を予測可能なレイテンシで更新する用途に最適です。ベンチマークでは、ミューテーションより最大 23 倍高速になり得ることが示されています。
トレードオフとして、SELECT クエリはパッチを適用する際にオーバーヘッドが発生し、パッチ用パーツもパーツ数の上限にカウントされます。約 10% のしきい値を超えると、読み取り時のパッチ適用オーバーヘッドは更新割合に応じて増加し、大規模な更新では同期的なミューテーションの方が効率的になります。
詳細はこちら: Lightweight UPDATE
オンザフライのミューテーション
オンザフライのミューテーションは、バックグラウンド処理を待たずに行を更新し、その後の SELECT クエリが自動的に変更後の値を返すようにするためのメカニズムを提供します。これにより、通常のミューテーションにおけるアトミシティに関する制約が実質的に解消されます。
ミューテーションとそれに続く SELECT クエリの両方で、apply_mutations_on_fly = 1 の設定を有効にする必要があります。ミューテーション条件は ClickHouse Keeper に保存され、すべてメモリ上に保持され、クエリ実行時にオンザフライで適用されます。
ミューテーションは依然としてデータ更新に使用される点に注意してください。単に、その結果が即座にマテリアライズされないだけです。ミューテーション自体はバックグラウンドで非同期プロセスとして適用され続け、通常のミューテーションと同等の重いオーバーヘッドが発生します。また、この操作で利用できる式にも制限があります(詳細を参照)。
オンザフライミューテーションは、少数の操作に対してのみ使用すべきです。多くても数十件程度に抑えてください。Keeper は条件をメモリに保持するため、使い過ぎるとクラスタの安定性に影響します。Keeper への高負荷は、無関係なテーブルにも影響するセッションタイムアウトを引き起こす可能性があります。
詳細はこちら: On-the-fly mutations
比較のまとめ
次の表は、ベンチマーク に基づくクエリパフォーマンスのオーバーヘッドをまとめたものです。Mutations は、完了後はクエリが本来の速度で実行され、データが物理的に書き換えられるため、ベースラインとして扱います。
| Method | Query slowdown | Memory overhead | Notes |
|---|---|---|---|
| Mutations | ベースライン | ベースライン | 完了後は本来の速度で実行される; データは物理的に書き換えられる |
| On-the-fly mutations | 可変 | 可変 | 即時に反映される; 多数の更新が蓄積するとパフォーマンスが低下 |
| Lightweight updates | 7–18% (平均 ~12%) | +20–210% | クエリに対して最も高効率; テーブルの ≤10% を更新するケースに最適 |
ReplacingMergeTree + FINAL | 21–550% (平均 ~280%) | ベースラインの 20–200× | すべての行バージョンを読み取る必要があり、クエリオーバーヘッドが最も大きい |
CoalescingMergeTree + FINAL | ReplacingMergeTree と同程度 | ReplacingMergeTree と同程度 | カラム単位の coalescing により同程度のオーバーヘッドが発生 |
| CollapsingMergeTree | 集約内容に依存 | 集約内容に依存 | オーバーヘッドはクエリの複雑さに依存 |
参考資料
ClickHouse における更新機能が時間の経過とともにどのように進化してきたかを、ベンチマーク分析とあわせて詳しく知りたい場合は、次の記事を参照してください。