TL;DR
標準 SQL の UPDATE が ClickHouse に登場しました。軽量なパッチパート更新により、ミューテーションと比べて最大 1,000 倍高速 です。
-
第 1 回: 専用エンジンの活用
ReplacingMergeTree、CollapsingMergeTree、CoalescingMergeTree などの挿入ベースのエンジンを使用し、ClickHouse が低速な行レベル更新をどのように回避しているかを解説します。 -
第 2 回: 宣言型 SQL スタイルの UPDATE
パッチパートを用いて最小限のオーバーヘッドで標準の UPDATE 構文を ClickHouse に導入した仕組みを探ります。 -
本記事: ベンチマークと結果
実際の処理速度を確認します。宣言型 UPDATE を含む各手法のベンチマークを実施し、最大 1,000 倍の高速化 を確認しました。 -
番外編: ClickHouse と PostgreSQL の比較
同一のハードウェアとデータで、ClickHouse の新しい SQL UPDATE を PostgreSQL と直接対決させました。単一行の更新では同等、一括変更では最大 4,000 倍高速 という結果が得られました。
ClickHouse における高速な SQL UPDATE: ベンチマークとエスプレッソを飲んだリス
リスを思い浮かべてみてください。小さく、すばしっこく、隠し場所の間を駆け回り、新しい木の実を素早く落とし込んでいきます。
では、そのリスがエスプレッソを飲んだところを想像してください。信じられないほどの猛スピードで、まばたきする間もなく隠し場所の間を疾走します。
ClickHouse の軽量な更新 (Lightweight update) は、まさにその感覚です。データパート間を素早く駆け回り、瞬時に適用される極小のパッチを配置して、クエリの高速性を維持します。
要点: これらは軽量パッチパート機構で実装された完全な標準 SQL UPDATE です。今回のベンチマークでは、標準 SQL の一括 UPDATE が 100 秒ではなく 60 ms で完了し、従来のミューテーション比で 1,600 倍高速 でした。また、最悪のシナリオでのクエリ実行でも、完全に書き換えられたデータとほぼ同等の速度で動作します。
本記事は ClickHouse での高速な UPDATE に関する深掘り連載の第 3 回であり、全編ベンチマーク特集となっています。
(この高速化をどのように実現したかを知りたい場合は、第 2 回: SQL スタイルの UPDATE をご覧ください。)
ここでは、すべての UPDATE 手法を並べてベンチマークします:
- 従来のミューテーションとオンザフライミューテーション
- 軽量な更新 (パッチパート)
- ReplacingMergeTree による挿入
検証の焦点は以下の 2 点です:
-
更新速度と可視性 – 各手法で変更がどれだけ早く反映されるか
-
マージ前のクエリへの影響 – 実体化される前の更新がレイテンシにどう影響するか
オープンソースとして公開しているスクリプトとクエリにより、すべて完全に再現可能です。
まずはデータセットとベンチマーク環境の概要を説明し、その後に測定結果を見ていきます。
データセットとベンチマーク環境
データセット
第 1 回と第 2 回では、基本的な更新シナリオを説明するために小さな注文 (orders) テーブルを使用しました。
今回のベンチマークでも同じ「注文」のテーマを引き継ぎますが、顧客注文の明細をモデル化した TPC-H の lineitem テーブル を使ってスケールアップしています。これは、初回挿入後に数量、価格、割引率などが変更されるという、更新の代表的なシナリオです:
CREATE TABLE lineitem (
l_orderkey Int32,
l_partkey Int32,
l_suppkey Int32,
l_linenumber Int32,
l_quantity Decimal(15,2),
l_extendedprice Decimal(15,2),
l_discount Decimal(15,2),
l_tax Decimal(15,2),
l_returnflag String,
l_linestatus String,
l_shipdate Date,
l_commitdate Date,
l_receiptdate Date,
l_shipinstruct String,
l_shipmode String,
l_comment String)
ORDER BY (l_orderkey, l_linenumber);このテーブルのスケールファクター 100 バージョンを使用します。これには約 6 億行 (~1 億 5,000 万件の注文からなる約 6 億件の明細) が含まれ、圧縮後で約 30 GiB (非圧縮で約 60 GiB) を占めます。
ベンチマークでは、lineitem テーブルを単一のデータパートとして保存しています。圧縮後のサイズである約 30 GiB は、バックグラウンドマージによって小さなパートが 1 つにまとめられる ClickHouse の圧縮マージしきい値 (150 GiB) を十分に下回っています。これにより、テーブルは自然に単一の完全にマージされたパートとして存在し、現実的な更新シナリオを再現できます。
以下は、すべてのベンチマーク実行で使用した lineitem ベーステーブルの概要であり、単一パートのサイズを確認できます:
SELECT
formatReadableQuantity(sum(rows)) AS row_count,
formatReadableQuantity(count()) AS part_count,
formatReadableSize(sum(data_uncompressed_bytes)) AS size_uncomp,
formatReadableSize(sum(data_compressed_bytes)) AS size_comp
FROM system.parts
WHERE active AND database = 'default' AND `table` = 'lineitem_base_tbl_1part';┌─row_count──────┬─part_count─┬─size_uncomp─┬─size_comp─┐
│ 600.04 million │ 1.00 │ 57.85 GiB │ 26.69 GiB │
└────────────────┴────────────┴─────────────┴───────────┘ハードウェアと OS
Ubuntu 24.04 LTS を実行する AWS m6i.8xlarge EC2 インスタンス (32 コア、128 GB RAM) と gp3 EBS ボリューム (16,000 IOPS、最大スループット 1,000 MiB/s) を使用しました。
ClickHouse バージョン
すべてのテストは ClickHouse 25.7 で実施しました。
自身での検証の実行
GitHub 上のベンチマークスクリプト を使用して、すべての結果を再現できます。
リポジトリには以下が含まれています:
- 更新ステートメントセット (一括更新および単一行更新)
- ベンチマークを実行して所要時間を収集するスクリプト
- データパートのサイズとクエリへの影響を調査するためのヘルパー
環境とデータセットが整ったので、多くのパイプラインの根幹である一括更新から順に、各種更新手法の挙動を見ていきましょう。
一括 UPDATE
まずは、バッチパイプラインの根幹であり従来のミューテーションで最も一般的なワークロードである一括更新から始めます。
GitHub 上の SQL スクリプトにリンクされた 3 つの手法を用いて、10 種類の典型的な一括更新 をベンチマークしました:
-
ミューテーション – 影響を受けるデータパートの完全な書き換えをトリガーします。
-
軽量な更新 – 小さなパッチパートのみをディスクに書き込みます。
(標準の SQL UPDATE ステートメントを使用します。「軽量」という名称は効率的なパッチパート実装のみを指します。) -
オンザフライミューテーション – ミューテーションと同じ構文を使用しますが、更新式をメモリ内にのみ保持し、読み取り時に適用します。完全なカラムの書き換えは非同期に行われます。
注意:
-
ここでは宣言型の更新に焦点を当てています。ReplacingMergeTree のような専用エンジンは、1 行ずつの挿入による一括更新が実用的ではないため、複数行のベンチマークからは除外しています。(後ほど、これらのエンジン向けに単一行更新を取り上げます。)
-
DELETEについては個別にベンチマークを行っていません。DELETEは_row_existsを書き換える従来のミューテーションか、そのマスクの軽量パッチのいずれかであり、両方のメカニズムは上記ですでに検証されています。
(ここからグラフが続きます。結果だけを確認したい場合は、各セクションの「測定方法」ボックスをスキップしてください。)
測定方法 – 一括更新の実行時間 (クリックして展開)
1. lineitem テーブルが存在する場合は DROP します。
2. ベーステーブルから新しい lineitem テーブルを クローン します。
3. 更新を実行して実行時間を測定 します。
手法ごとに 3 回繰り返し、平均実行時間を算出します。
この手順を 10 個の一括更新すべて について繰り返します。
一括更新がクエリから参照可能になるまでの時間
10 個の一括 UPDATE それぞれについて、下のグラフは UPDATE を発行してからその変更がクエリから参照可能になるまでの時間 を測定し、3 つの手法すべてを比較しています。
また、クエリが変更を確認できるようになる前に影響を受けるデータパートの書き換えを完了しなければならない従来のミューテーションに対して、軽量な更新がどれだけ高速か (% 表示) も示しています。
グラフ内のその他の主要指標 (クリックして展開)

-
オンザフライミューテーション — クエリされたデータに即座に適用できるように、更新式がメモリに保持されます。
-
従来のミューテーション — 最も低速なパスです。クエリが新しいデータを参照できるようになる前に、すべての変更がディスク上のカラム全体を書き換えます。
-
軽量な更新 — クエリ実行時に瞬時に組み込まれる小さな「パッチパート」を書き込むことで、最大 1,700 倍高速 です。
これらのグラフの元となった JSON ファイルについては、生のベンチマーク結果 を参照してください。
各手法の間で処理速度に違いが生じる理由は、後の詳細解説で詳しく説明します。
高速な更新は素晴らしいものですが、それがクエリを損なわないことが前提です。各手法がクエリパフォーマンスにどのような影響を与えるかを見ていきましょう。
更新後の最悪条件下でのクエリ時間 — 一括更新
完全に実体化された後は、使用した更新手法に関係なくクエリパフォーマンスは同一になります。
バックグラウンド処理が完了すると、テーブルのデータパートは完全に更新され、クエリは UPDATE 手法に関係なくベースラインの速度で動作します:
-
ミューテーション – データパートは即座に書き換えられます。
-
オンザフライミューテーション – バックグラウンドでのカラム書き換えによって、メモリ内の更新式が最終的に実体化されます。
-
軽量な更新 – パッチパートは通常のバックグラウンドマージによってメインのデータパートにマージされます。
そのため、パフォーマンスを最も控えめに見積もる指標として更新後の最悪条件下でのクエリ時間を測定します。これは、ディスク上で変更が完全に実体化される前に、更新されたばかりの行にヒットするクエリの実行時間として定義されます。
次に、従来のミューテーションがすべての更新をディスクに書き終えた後に同じデータにヒットするクエリのベースライン時間と比較して、これがどれだけ遅いか (速度低下率のパーセンテージ) を示します。
測定方法 – 一括更新後の最悪条件下でのクエリ時間 (クリックして展開)
• 10 個の更新と 1 対 1 で対になる 10 個の分析クエリ を使用します。
• 各更新後に対になるクエリを 3 回 実行 し、平均実行時間とメモリ使用量 を測定します。
• 各クエリは少なくとも更新された行とカラムを読み取りますが、現実的な分析を想定してより広範なデータをスキャンするものも多くあります。
• 最悪条件のレイテンシを捉えるため、ベンチマークでは バックグラウンドマージを無効化 しています:
• 軽量な更新 は Patch-on-read (暗黙的な FINAL) に依存します。
次のグラフは、完全に実体化された更新のベースラインと比較した、各タイプの一括 UPDATE 後における 10 個のクエリ 全部の更新後の最悪条件下でのクエリ時間を示しています:
グラフ内のその他の主要指標 (クリックして展開)

-
従来のミューテーション (ベースライン) → 同期ミューテーションがディスク上の影響を受けるデータパートを完全に書き換えた後のクエリ実行時間。
-
軽量な更新 → デフォルトでオーバーヘッドは最小限 (平均 15%)、ファストマージモード ではわずか 8〜21% の速度低下にとどまります。
-
軽量な更新のまれなフォールバック → パッチ作成と並行してソースパートがマージされた場合、低速な JOIN モード が発生することがあります (39〜121% のオーバーヘッド)。
-
オンザフライミューテーション → 即座に可視化されますが、最大の速度低下 を引き起こします (12〜427%、平均 149%)。
メモリへの影響 → オンザフライは ~0.7 MiB から ~302 MiB へ急増することがありますが、軽量な更新は非常に控えめな状態を維持します。
これらの速度低下とメモリ変化の背景にある仕組みについては、後ほど詳細解説のセクションで探ります。
一括更新はバッチパイプラインの根幹ですが、多くの実際のアプリケーションでは頻繁な単一行の更新に依存しています。そこでの比較を見てみましょう。
単一行の更新
ReplacingMergeTree や CoalescingMergeTree に適したワークロードを表現するため、単一行の更新もベンチマークしました。
-
先ほどの 10 個の更新 はそれぞれ、(数千行ではなく) 1 行を対象にするよう変更されています。 SQL ファイルを見る →
-
ReplacingMergeTree では更新ごとに新しい行を挿入し、更新されないカラムには既存の値を反復して入れます。 Replacing による挿入 →
- (CoalescingMergeTree を使えば更新されないカラムの反復は回避できますが、更新されるすべてのカラムが Nullable である必要があります。)
注意: ここでは、第 1 回 で取り上げた更新専用エンジンの代表として ReplacingMergeTree のみをベンチマークしました。CoalescingMergeTree や CollapsingMergeTree でも性能は同等になります。
ReplacingMergeTree の UPDATE パフォーマンス を他の手法と比較するため、同じ 10 個の単一行更新 をミューテーション、オンザフライミューテーション、および軽量な更新 (パッチパートで実装された標準 SQL の UPDATE 構文) としても記述しました。 SQL ファイルを見る →
測定方法 – 単一行更新の実行時間 (クリックして展開)
単一行 (ポイント) 更新には同じ手順を使用しました: テーブルを DROP して クローン し、更新を 適用 して実行時間を 3 回 測定 して平均を算出します。これを 手法ごとに 10 回の更新 で繰り返しました。
単一行 (ポイント) 更新がクエリから参照可能になるまでの時間
一括更新と同様に、10 個の単一行 (ポイント) UPDATE それぞれについて、次のグラフは 4 つの手法すべてで UPDATE を発行してからその変更がクエリから参照可能になるまでの経過時間 を示しています。
また、影響を受けるすべてのデータパートが書き換えられた後にのみ可視化される従来のミューテーションのベースラインと比較して、軽量な更新と Replacing による挿入がどれだけ高速か (% 表示) も示しています。
グラフ内のその他の主要指標 (クリックして展開)

-
オンザフライミューテーション → 更新式がメモリに保持されるだけなので即座に可視化され (~0.03 秒)、完全な書き換えは後からバックグラウンドで行われます。
-
ReplacingMergeTree による挿入 → 新しい行のみを書き込むため未加工の速度としては最速 (0.03〜0.04 秒) で、完全なミューテーションと比較して最大 4,700 倍高速 です。行の検索は一切行われませんが、その後のクエリではバックグラウンドマージが完了するまで複数の行バージョンを解決する必要があります。
-
軽量な更新 → ほぼ同等の速さ (0.04〜0.07 秒) で、完全なミューテーションと比較して最大 2,400 倍高速 であり、極小のパッチパートを書き込みます。行のルックアップが発生するためわずかに遅くなりますが、クエリ実行時の効率ははるかに高くなります。
-
ミューテーション → たった 1 行であっても影響を受けるすべてのカラムを書き換えるため、最も低速です (90〜170 秒)。
一括更新の結果と同様に、こうした傾向の理由は後ほど詳細解説で説明します。
繰り返しになりますが、更新速度は全体の一面にすぎません。単一行の更新はクエリにどう影響するでしょうか。
更新後の最悪条件下でのクエリ時間 — 単一行の更新
単一行のベンチマークでは、複数行のテストと同じ 10 個の分析クエリ を再利用しています。
このセクションでは、一括更新のテストと同様に、ディスク上で変更が完全に実体化される前に更新されたばかりの行にヒットするクエリの所要時間である更新後の最悪条件下でのクエリ時間をベンチマークし、従来のミューテーションが更新を完全に実体化させた後の同じクエリの実行時間 (ベースライン) と比較した速度低下率 (%) を報告します。
測定方法 – ポイント更新後の最悪条件下でのクエリ時間 (クリックして展開)
• 各クエリはその更新と 1 対 1 で対応しており、3 回の実行にわたる平均実行時間とメモリを 記録 します。
• 各クエリは少なくとも更新された行とカラムを読み取りますが、現実的な分析を想定してより広範なデータをスキャンするものも多くあります。
• 最悪条件のレイテンシを捉えるため、ベンチマークでは バックグラウンドマージを無効化 しています:
• 軽量な更新 は Patch-on-read (暗黙的な FINAL) に依存します。
• ReplacingMergeTree クエリは 明示的な FINAL を付けて実行されます。
次のグラフは、完全に実体化された更新のベースラインと比較した、各タイプの一括 UPDATE 後における 10 個の分析クエリ 全部の更新後の最悪条件下でのクエリ時間を示しています:
グラフ内のその他の主要指標 (クリックして展開)

-
従来のミューテーション (ベースライン) → 同期ミューテーションがディスク上の影響を受けるデータパートを完全に書き換えた後のクエリ実行時間。
-
軽量な更新 → クエリに対して最も効率的で、速度低下は 7〜18% (平均 ~12%)、メモリ増加は +20%〜210% です。グラフの見やすさを保つため、ファストマージモード のみを表示しています。
-
オンザフライミューテーション → 即座に可視化され、ReplacingMergeTree + FINAL よりも高速な場合が多いですが、メモリ内に多くの更新がたまると急激に遅くなります (ここでは図示していません)。
-
ReplacingMergeTree + FINAL → 最も負荷の高いクエリとなり、クエリがすべての行バージョンを読み取る必要があるため、速度低下は 21〜550% (平均 ~280%)、メモリはベースラインの 20 倍〜200 倍になります。
結果は極めて顕著です。では、ClickHouse がなぜこのような振る舞いをするのか、内部の仕組みを覗いてみましょう。
詳細解説: 結果がこのようになる理由
第 1 回と第 2 回では、従来のミューテーション、軽量な更新、ReplacingMergeTree などの専用エンジンといった、各更新手法が内部でどのように動作するかをすでに解説しました。
ここではさらに一歩踏み込み、これらの手法を並べて比較することで、ベンチマークのグラフがなぜこのような形になったのか、そして ClickHouse の内部で実際に何が起きているのかを明らかにします。
軽量な更新が挿入のように感じられる理由
その名称にかかわらず、これらは通常の SQL UPDATE ステートメント です。ClickHouse はカラム全体を書き換える代わりにパッチパートを書き込むことで、それらを最適化しています。
軽量な更新は、カラム全体を書き換える代わりに極小のパッチパートを書き込みます。
クエリ実行時には、メモリ上でこれらのパッチを重ね合わせるだけ (patch-on-read) であるため、小さな変更の適用は新しい行を挿入するのとほぼ同等の速度に感じられます。
具体的には、軽量な更新が書き出すパッチパートには以下のみが含まれます:
- 更新された値
- ごく少量の**対象指定メタデータ**: _part、_part_offset、_block_number、_block_offset、および _data_version
このメタデータは更新が必要な行を正確に指し示すため、ClickHouse は関係のないデータをスキャンしたり書き換えたりすることを回避できます。
更新のパフォーマンスは、変更された値と対象行のメタデータのみを書き込む INSERT INTO … SELECT とおおよそ同等です:
-- 軽量な更新ステートメント:
UPDATE lineitem
SET l_discount = 0.045, l_tax = 0.11, ...
WHERE l_commitdate = '1996-02-01' AND l_quantity = 1;≈
-- おおよそ同等の処理:
INSERT INTO patch
SELECT 0.045 as l_discount, 0.11 as l_tax, ...,
_part, _part_offset, _block_number, _block_offset
FROM lineitem
WHERE l_commitdate = '1996-02-01' AND l_quantity = 1;軽量な更新が小さな挿入と同じくらい高速に感じられるのは、無関係なデータのスキャンや書き換えを行わないためです。
そのメカニズムは意図的に ClickHouse の高速かつ効率的な挿入の仕組みに基づいているため、挿入を実行できるのと同じくらいの頻度で軽量な更新を実行できます。
軽量な更新を使用すべき場合 (および使用すべきでない場合)
軽量な更新は、頻繁で小さな変更 (テーブルの ≈ 10% 以下) 向けに設計されています。
更新後の最悪条件でのクエリ速度低下をほとんど発生させず、更新をほぼ瞬時に反映させたい場合に使用してください。
軽量な更新は、一度にテーブルの数パーセントを更新するような、小さく頻繁な修正で威力を発揮します。
しかし、大規模な更新を行うと巨大なパッチパートが作成され、以下のような問題が生じます:
-
マージされるまですべてのクエリ実行時にメモリ上で適用 (patch-on-read) しなければならない。
-
実体化されるまでの間、CPU と RAM のオーバーヘッドが増大する。
-
最終的にはソースパートへマージされることになる。
大規模な変更の場合、次回の定期バックグラウンドマージでパッチが実体化されるまでの間、クエリが大幅に遅くなる可能性があります。 その場合は、従来のミューテーションを実行し、1 回だけ長く待って (更新によってパートを書き換え)、その後にベースラインのクエリパフォーマンスを享受する方が適していることがよくあります。
軽量な更新は付箋のようなものだと考えてください。素早く正確な編集には最適です。 しかし、壁全体を張り替えるのであれば、新しい壁紙を貼る (ミューテーション) 方が高速で綺麗に仕上がります。
このトレードオフを視覚化するために、2 つの概略図を用意しました。
図 1 – 更新レイテンシ
図 1 は、変更される行の割合が増加するにつれて更新レイテンシがどのように変化するかを示しています。

-
ミューテーション (灰色の線) は影響を受けるすべてのカラムを書き換えるため、レイテンシは一定のままです。
-
軽量な更新 (黄色の線) はパッチパートのみを書き込むため、極小の変更では最も高速ですが、更新がテーブルのより広い範囲に及ぶにつれてミューテーションのレイテンシに徐々に近づいていきます。
図 2 – 一括更新後のクエリ実行時間
図 2 は、一括 (複数行) 更新の後、更新が完全に実体化される前に代表的なクエリがどれだけ遅くなるかを推定したものです。

-
同期ミューテーションでは、クエリが実行される前にデータパートが書き換えられるため、実行時間は一定のままです。
-
patch-on-read では、ClickHouse が実行中にパッチパートを重ね合わせます。パッチが大きくなるにつれて、クエリは比例して遅くなります。
そのため、小規模で頻繁な一括変更 (テーブルの最大約 10% まで) には軽量な更新を使用してください: patch-on-read はパッチパートが大きくなるにつれてクエリの低下をもたらしますが、バックグラウンドマージが完了すればベースラインのパフォーマンスに戻ります。それ以上の規模の変更には、従来の (同期) ミューテーションを使い続けてください。
patch-on-read のモード: 高速モードと結合フォールバック
今回のデータセットにおいて、パッチパートが**高速マージモードの patch-on-read** (元のソースパートがまだ存在している状態) で適用された場合、速度低下は 8% 〜 21% の範囲にとどまり、平均は 15.3% であったことを前述しました。
第 2 回で説明したように、次回のバックグラウンドマージによってパッチが最終的に実体化されるときも、そのマージが発生する前にクエリが実行されて patch-on-read 経由でメモリ上に適用されるときも、ほとんどの場合はパッチのソースパートがまだ存在しています。
ごくまれに、無害なタイミングの重複が発生することがあります。パッチパートは UPDATE 開始時点のソースパートのスナップショットに対して作成されますが、パッチの書き込みと並行してそれらのパートがマージされた場合、そのパッチは (高速マージモードの patch-on-read において) アクティブでなくなったソースパートを対象にすることになります。
その場合、ClickHouse は自動的に、より低速な結合モードの patch-on-read にフォールバックします。前述のとおり、このモードのオーバーヘッドはより高く、今回のベンチマークでは 39% 〜 121%、平均 68% でした。このモードは、パッチとマージ済みデータの間でインメモリ結合を実行します。
測定方法 – より低速な結合モードの patch-on-read (クリックして展開)
このシナリオを明示的に測定するために、 apply_patches_on_merge を 無効 にした状態で、 テーブルの単一データパートの自己マージを強制 しました。 マージによってパートが新しい名前で書き換えられたため、パッチの ソースインデックス が無効化され、クエリ実行時により低速な結合モードへのフォールバックがトリガーされました。
ReplacingMergeTree の挿入が生の速度で優れている理由
ReplacingMergeTree は更新を新しい行の挿入としてモデル化し、ディスクには新しい行のみを書き込みます。
-
古い行を見つけるための検索 (ルックアップ) が不要であり、これが実質的に最速の更新方法となっています。
-
今回のベンチマークでは、更新は通常 0.03 〜 0.04 秒で完了し、従来のミューテーションと比べて最大 4,700 倍高速でした。
軽量な更新もほぼ同等に高速
単一行の (ポイント) UPDATE では、軽量な更新はディスクにごくわずかなパッチパート (多くの場合、数十バイト程度) のみを書き込みます。
-
まず、ClickHouse は元のパート内で更新対象の行を特定し、そのターゲットメタデータ (_part、_part_offset、_block_number、_block_offset、_data_version) を生成します。
-
その後、更新された値とその小さなメタデータパッチのみを書き込みます。
この追加の検索処理により、軽量な更新は ReplacingMergeTree の挿入よりもわずかに遅くなりますが、patch-on-read はどの行を重ね合わせるべきかを正確に把握しているため、クエリ実行時の効率は大幅に向上します。
次のセクションでは、この挙動を ReplacingMergeTree の仕組みと比較します。
FINAL が patch-on-read よりも遅い理由
FINAL クエリは最新の行を見つけ出す必要がありますが、patch-on-read クエリはそれらの行がどこにあるかをすでに知っています。
違いを示す 2 つの簡易図
図 1 – FINAL: ClickHouse はどのパートに最新の行があるかを判別できないため、パートを読み込み、メモリ上でその場でマージし、クエリエンジンがデータをさらに処理する前にソートキーごとに最新の行のみを保持します。

ここでは詳細には触れませんが、FINAL のマージ処理は近年大幅に最適化されています。また、FINAL 処理中に必要な作業量を最小限に抑えるためのベストプラクティスも存在します。
図 2 – patch-on-read: パッチパートによって ClickHouse はどの行が変更されたかを正確に把握できるため、メモリ上でその場でそれらの行のみを重ね合わせます。広範囲に及ぶマージは必要ありません。

パッチパートは _part、_part_offset、_block_number、_block_offset を介してターゲット行を正確に参照するため、極めてピンポイントで的を絞った重ね合わせが可能になります。
クイックサマリー表
| FINAL (ReplacingMergeTree) | patch-on-read (軽量な更新) |
|---|---|
| どのパートに最新バージョンの行が含まれているかを把握していない。 | どの行が更新され、どこに保存されているか (パッチパート) を正確に把握している。 |
| すべてのパートにわたってすべての候補範囲をマージし、ソートキーと挿入時間に基づいて最新バージョンを解決しなければならない。 | パッチパートが _part、_part_offset、_block_number、_block_offset を介して更新対象の行を直接参照する。 |
| 行バージョンの解決が広範囲かつ探索的であり、負荷の高い処理を必要とする。 | マージがピンポイントかつ的を絞ったものであり、把握済みの更新行のみを重ね合わせる。 |
| 歴史的に通常の読み取りよりも大幅に遅かった。現在は大幅に最適化されているものの、依然として patch-on-read より重い。 | 非常に効率的で軽量、かつ予測可能。 |
仕組みが明確になったところで、すべてをまとめる簡単な比較の振り返りを示します。
UPDATE 手法の概要
まとめる前に、各 UPDATE 手法の一覧比較を示します:
-
従来のミューテーション — 最も遅い経路。クエリが新しいデータを参照できるようになる前に、ディスク上のカラム全体を書き換える。
-
オンザフライミューテーション — 即時。更新式をメモリに保持するため、クエリは遅くなる。
-
ReplacingMergeTree の挿入 — 書き込みは高速だが、FINAL によるクエリのオーバーヘッドが増加する。
-
軽量な更新 — INSERT とほぼ同じくらい高速でありながら、クエリへの影響を低く抑えられる。
チートシート: 各更新手法の強みと、それに伴うコストは以下のとおりです。
| 手法 | 最適な用途 | 最悪条件でのクエリへの影響 | 注意点 |
|---|---|---|---|
| ミューテーション | 大規模で頻度の低い一括変更 | ベースライン | 適用に時間がかかる |
| オンザフライミューテーション | 即座の可視性が必要なアドホックの大規模変更 | ↑ 12 〜 427 % | 後からパートを書き換える処理は残る。 多く重なるとクエリが遅くなる |
| ReplacingMT + FINAL | 高頻度な単一行の更新 | ↑ 21 〜 550 % | FINAL 句が必要 |
| 軽量な更新 (パッチパート) | 高頻度な小規模更新 (テーブルの 10% 以下) | ↑ 7 〜 21 % | ソースがマージによって消失した場合、より低速な結合モードになる |
仕組みが明確になり、横並びの結果が得られたところで、これが実際のワークロードにとって何を意味するのかをまとめます。
まとめ: 1,000 倍高速な ClickHouse の UPDATE
ClickHouse は標準 SQL の UPDATE に対応しました。これはパッチパートを用いた軽量な更新として実装されており、列指向ストアのパフォーマンスにおける長年の課題を解決するアプローチです。
今回のベンチマーク結果:
-
一括 SQL UPDATE は従来のミューテーションより最大 1,700 倍高速
-
単一行の SQL UPDATE は最大 2,400 倍高速
-
これらすべてをわずかな (最悪条件下の) クエリオーバーヘッドのみで実現
これが意味すること:
-
使い慣れた SQL 構文を使用した、クエリを遅くさせない高頻度な小規模更新
-
リアルタイムおよびストリーミングワークロード向けの即座に可視化される変更
-
バッチ更新とインタラクティブな更新をシームレスに混在させられるスケーラブルなパイプライン
ClickHouse は、分析ワークロードにおいて優れた性能を維持しながら、従来は行指向ストア専用とされていた更新パターンも処理できるようになりました。
すべてのベンチマークスクリプトとクエリは GitHub で公開されています。
これで、ClickHouse 列指向ストア向けの高速な UPDATE 構築に関する 3 回シリーズは完結となります。
過去の記事をまだご覧になっていない方は、第 1 回: 専用に設計されたエンジンと第 2 回: SQL スタイルの UPDATEをぜひご確認ください。



