ClickHouseの更新
更新の処理に関しては、分析データベースとトランザクションデータベースでは、基盤となる設計思想と対象ユースケースが異なるため、更新の扱い方にも違いがあります。 ClickHouseは、読み取り負荷の高い分析処理と高スループットの追記専用操作向けに最適化されたカラム指向データベースです。 実運用では、削除や更新を追記操作に変換し、それらを非同期または読み取り時に処理できるようにするため、テーブル構造を見直すことがよくあります。これにより、高スループットのデータインジェストにおけるClickHouseの強みを生かせます。 ClickHouseは、更新と削除のための堅牢な操作もサポートしています。
このガイドでは、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 [table] SET ... WHERE | ほとんどのシナリオで、特に application やワークフローの一部として頻繁に小規模な UPDATE (テーブルの約 10% まで) を実行する場合に使用します。たとえば、あるユーザーが自分のイベント履歴を削除したいものの、イベントが多数のユーザーを含むマルチテナントテーブル全体に分散している場合です。このアプローチでは、列全体を書き換えることなく、即時可視化のためのパッチ用パーツを作成します。SELECT クエリにオーバーヘッドを追加しますが、レイテンシは予測可能です。 |
| Update mutation | ALTER TABLE [table] UPDATE | より大規模なデータ管理を行う際、特に更新内容がテーブルのパーティション分割に沿っている場合に使用します。たとえば、月単位でパーティション分割されたテーブルで、ある月に含まれるすべての行のカラムを更新する必要がある場合です。 |
専用テーブルエンジンを利用した更新
ReplacingMergeTree
ReplacingMergeTree は、バックグラウンドマージ中に同じソートキーを持つ行を重複排除し、最新バージョンのみを保持します。
このエンジンは、更新が安定したキーによって識別される、個々の行に対する高頻度の更新に最適です。 ベンチマークでは、単一行の更新において、ミューテーションより最大 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 |
|---|---|---|---|
| ミューテーション | ALTER TABLE ... UPDATE | 頻度の低いバルク更新。特に、更新内容がテーブルのパーティション分割に対応している場合に適しています。 | I/O 負荷が高い; カラムを書き換える |
| 論理更新 | UPDATE ... SET ... WHERE | 小規模な更新 (~0.1〜10% の行); パフォーマンスが求められる高頻度の更新 | SELECT のオーバーヘッドが増える; パッチ用パーツが上限にカウントされる |
ミューテーション
ミューテーション (ALTER TABLE ... UPDATE) は、WHERE 式に一致する行を含むすべてのパーツを書き換えます。
ミューテーションは I/O 負荷がかなり高く、WHERE 式に一致するすべてのパーツを書き換えます。
この処理にはアトミック性はありません。
ミューテーション済みのパーツが準備できしだい元のパーツと置き換えられるため、ミューテーションの実行中に開始された SELECT クエリは、すでに ミューテーション済みのパーツのデータと、まだ ミューテーションされていないパーツのデータを両方参照します。
進行状況は system.mutations テーブル経由で追跡できます。
ミューテーションは I/O 負荷が高く、クラスタの SELECT パフォーマンスに影響を与える可能性があるため、使用は最小限に抑えるべきです。ミューテーションのキュー投入が処理速度より速くなると、クエリ パフォーマンスは低下します。system.mutations を用いてキューを監視してください。
オンザフライのミューテーション
ALTER TABLE ... UPDATE によるミューテーションでは、変更後の値がクエリ結果に反映されるまで、バックグラウンドプロセスでミューテーションが適用されるのを待つ必要がある場合があります。
ClickHouse では、"オンザフライのミューテーション" によってこの動作を変更できます。
オンザフライのミューテーションが有効な場合、更新された行は即座に更新済みとしてマークされ、以降の SELECT クエリでは変更後の値が自動的に返されます。
オンザフライのミューテーションは、クエリレベルの設定 apply_mutations_on_fly を有効にすることで、MergeTree ファミリーのテーブルで使用できます。
例
テーブルを作成し、いくつかのミューテーションを実行してみましょう。
SELECT クエリで更新結果をチェックしてみましょう。
新しいテーブルに対してクエリを実行した時点では、行の値はまだ更新されていないことに注意してください。
次に、オンザフライのミューテーションを有効にするとどうなるかを見てみましょう。
これで、ミューテーションが適用されるのを待たなくても、SELECT クエリは直ちに正しい結果を返します。
パフォーマンスへの影響
オンザフライのミューテーションが有効化されている場合、ミューテーションはすぐにはマテリアライズされず、SELECT クエリの実行時にのみ適用されます。ただし、ミューテーション自体は引き続きバックグラウンドで非同期にマテリアライズされており、これは負荷の高い処理である点に注意してください。
送信されるミューテーション数が、一定期間にバックグラウンドで処理されるミューテーション数を継続的に上回ると、適用待ちの未マテリアライズなミューテーションのキューは増え続けます。その結果、最終的に SELECT クエリのパフォーマンスが低下します。
未マテリアライズなミューテーションの無制限な増加を防ぐため、設定 apply_mutations_on_fly は、number_of_mutations_to_throw や number_of_mutations_to_delay などの他の MergeTree レベルの設定とあわせて有効にすることを推奨します。
サブクエリおよび非決定的関数のサポート
オンザフライのミューテーションでは、サブクエリと非決定的関数のサポートは限定的です。サポートされるのは、結果のサイズが適切な範囲内にあるスカラーサブクエリのみです (このサイズは設定 mutations_max_literal_size_to_replace で制御されます) 。また、定数の非決定的関数のみがサポートされます (例: 関数 now()) 。
これらの動作は、次の設定によって制御されます。
| Setting | 説明 | デフォルト |
|---|---|---|
mutations_execute_nondeterministic_on_initiator | true の場合、非決定的関数はイニシエーター レプリカで実行され、UPDATE および DELETE クエリ内でリテラルに置き換えられます。 | false |
mutations_execute_subqueries_on_initiator | true の場合、スカラーサブクエリはイニシエーター レプリカで実行され、UPDATE および DELETE クエリ内でリテラルに置き換えられます。 | false |
mutations_max_literal_size_to_replace | UPDATE および DELETE クエリ内で置き換える、シリアライズされたリテラルの最大サイズ (バイト) 。 | 16384 (16 KiB) |
論理更新
論理更新では、従来のミューテーションのようにカラム全体を書き換えるのではなく、更新対象のカラムと行のみを含む特別なデータパーツである「patch parts」を使用します。
このアプローチは標準的な UPDATE 構文を使用し、マージを待たずに即座にパッチ用のパーツを作成します。更新された値は、パッチ適用を通じて SELECT クエリからすぐに参照できますが、ストレージ上で物理的に反映されるのは後続のマージ時のみです。これにより、論理更新は、テーブル全体のうち小さな割合(最大約 10% 程度)の行を予測可能なレイテンシで更新する用途に最適です。ベンチマークでは、ミューテーションより最大 23 倍高速になり得ることが示されています。
トレードオフとして、SELECT クエリはパッチを適用する際にオーバーヘッドが発生し、パッチ用パーツもパーツ数の上限にカウントされます。約 10% のしきい値を超えると、読み取り時のパッチ適用オーバーヘッドは更新割合に応じて増加し、大規模な更新では同期的なミューテーションの方が効率的になります。
詳細はこちら: Lightweight UPDATE
オンザフライのミューテーション
オンザフライのミューテーションは、バックグラウンド処理を待たずに行を更新し、その後の SELECT クエリが自動的に変更後の値を返すようにするためのメカニズムを提供します。これにより、通常のミューテーションにおけるアトミシティに関する制約が実質的に解消されます。
ミューテーションとそれに続く SELECT クエリの両方で、apply_mutations_on_fly = 1 の設定を有効にする必要があります。ミューテーション条件は ClickHouse Keeper に保存され、すべてメモリ上に保持され、クエリ実行時にオンザフライで適用されます。
ミューテーションは依然としてデータ更新に使用される点に注意してください。単に、その結果が即座にマテリアライズされないだけです。ミューテーション自体はバックグラウンドで非同期プロセスとして適用され続け、通常のミューテーションと同等の重いオーバーヘッドが発生します。また、この操作で利用できる式にも制限があります (詳細を参照) 。
オンザフライのミューテーションは、少数の操作に対してのみ使用すべきです。多くても数十件程度に抑えてください。Keeper は条件をメモリに保持するため、使い過ぎるとクラスタの安定性に影響します。Keeper への高負荷は、無関係なテーブルにも影響するセッションタイムアウトを引き起こす可能性があります。
比較のまとめ
次の表は、ベンチマーク に基づくクエリパフォーマンスのオーバーヘッドをまとめたものです。ミューテーションは、ミューテーション完了後にクエリがフルスピードで実行され、データが物理的に書き換えられるため、ベースラインとして扱います。
| Method | Query slowdown | Memory overhead | Notes |
|---|---|---|---|
| ミューテーション | ベースライン | ベースライン | 完了後はフルスピードで実行される; データは物理的に書き換えられる |
| On-the-fly ミューテーション | 可変 | 可変 | 即時に可視化される; 多数の更新が蓄積するとパフォーマンスが低下する |
| 論理更新 | 7–18% (平均 ~12%) | +20–210% | クエリに対して最も効率的; テーブルの ≤10% を更新する場合に最適 |
ReplacingMergeTree + FINAL | 21–550% (平均 ~280%) | ベースラインの 20–200× | すべての行バージョンを読み取る必要があり、クエリオーバーヘッドが最も大きい |
CoalescingMergeTree + FINAL | ReplacingMergeTree と同程度 | ReplacingMergeTree と同程度 | カラムレベルの統合により同程度のオーバーヘッドが追加される |
| CollapsingMergeTree | 集約に依存 | 集約に依存 | オーバーヘッドはクエリの複雑さに依存する |
参考資料
ClickHouse における更新機能が時間の経過とともにどのように進化してきたかを、ベンチマーク分析とあわせて詳しく知りたい場合は、次の記事を参照してください。