---
title: "ClickHouseの列指向ストレージで高速なUPDATEを実現した方法 – パート2: SQLスタイルのUPDATE"
date: "2026-09-25T07:21:22.778Z"
author: "Tom Schreiber"
category: "Engineering"
excerpt: "ClickHouseで高速かつ宣言的なUPDATEが可能になりました。パッチパートにより、最小限のI/O、クエリへの即時反映、高い並列性を実現しています。本記事では、それを支える仕組みを詳しく解説します。"
---

# ClickHouseの列指向ストレージで高速なUPDATEを実現した方法 – パート2: SQLスタイルのUPDATE

> **TL;DR**<br/><br/>
ClickHouse の列指向ストレージに向けて、SQL スタイルの UPDATE を根本から再設計しました。本記事では、重量級のミューテーションから、スケールするパッチパートを活用した軽量な更新に至るまでの歩みを詳しく解説します。ベンチマーク結果はパート 3 で紹介します。

<div style="margin-bottom: 0.5em;">
  <strong>本記事は、ClickHouse における高速な UPDATE に関するシリーズ記事の一部です。</strong>
</div>
<ul style="margin-top: 0.2em;">
  <li>
    <strong><a href="https://clickhouse.com/ja/blog/updates-in-clickhouse-1-purpose-built-engines-jp">パート 1: 専用エンジン</a></strong><br/>
    ReplacingMergeTree、CollapsingMergeTree、CoalescingMergeTree などの挿入ベースのエンジンを使用し、ClickHouse が低速な行レベルの更新をいかに回避しているかを解説します。
  </li>
  <li>
    <strong>本パート: 宣言的な SQL スタイルの UPDATE</strong><br/>
    パッチパートを活用して最小限のオーバーヘッドで標準的な UPDATE 構文を ClickHouse に導入した方法を探ります。
  </li>
  <li>
    <strong><a href="https://clickhouse.com/ja/blog/updates-in-clickhouse-3-benchmarks-jp">パート 3: ベンチマーク</a></strong><br/>
    実際の処理速度をご紹介します。宣言的 UPDATE を含むあらゆるアプローチをベンチマーク測定し、<strong>最大 1,000 倍の高速化</strong>を達成しました。
  </li>
    <li>
    <strong><a href="https://clickhouse.com/ja/blog/update-performance-clickhouse-vs-postgresql-jp">番外編: ClickHouse と PostgreSQL の比較</a></strong><br/>
    ClickHouse の新しい SQL UPDATE を、同一のハードウェアとデータ上で PostgreSQL と直接対決させました。ポイント更新では同等、バルク変更では<strong>最大 4,000 倍高速</strong>という結果が得られています。
  </li>
</ul>


## 不可能とされていた更新機能

**列指向ストレージでは、更新を高速に行うことはできないとされてきました。**

長年、分析向けに構築されたシステムは、読み取り速度のために更新性能を犠牲にしてきました。行レベルの変更は、スキャンに最適化された高スループットなアーキテクチャとは相容れないものと考えられていたためです。

ClickHouse も例外ではありませんでしたが、*更新の本質を捉え直す*ことで、その常識を打ち破りました。

[パート 1](https://clickhouse.com/ja/blog/updates-in-clickhouse-1-purpose-built-engines-jp) では、更新を挿入へと変換するという、ClickHouse が採用した根本的に異なるモデルを紹介しました。ReplacingMergeTree や CollapsingMergeTree などの専用エンジンにより、超高速な取り込み速度を維持しながら、後からバックグラウンドで非同期にマージによって更新を解決できるようにしました。

しかし、誰もがマージのセマンティクスを意識して設計したいわけではありません。多くのユーザーは、シンプルに次のように書きたいだけです。
<pre>
<code type='click-ui' language='sql'>
UPDATE orders
SET discount = 0.2
WHERE quantity >= 40;
</code>
</pre>

そこで私たちは、ClickHouse 本来の高速性を損なうことなく、これを実現することを目指しました。

**本記事では、その実現に至るプロセスを解説します。**

ClickHouse における SQL スタイルの更新の進化を順に追っていきます。

1. シンプルながら処理負荷の大きい、従来型の**ミューテーション**。

2. ミューテーションの完了を待たずに済む、一歩進んだ **On-the-fly 更新**。

3. そして高頻度のワークロード向けに専用設計された、スケーラブルで列指向ネイティブな更新メカニズムである**パッチパート**を活用した、高速で宣言的な SQL 更新。

> パッチパートによる更新が実際にどれほど高速なのか気になる方は、ぜひご覧ください。<br/>[パート 3](https://clickhouse.com/ja/blog/updates-in-clickhouse-3-benchmarks-jp) でベンチマークを実施したところ、最大 1,000 倍、場合によっては 1,600 倍以上の高速化を確認できました。

まずは、ClickHouse が長年サポートしてきた、従来のミューテーションベースの UPDATE から見ていきましょう。


## ステージ 1: 従来のミューテーションとカラムの書き換え

2018年以降、ClickHouse は [ALTER TABLE ... UPDATE](https://clickhouse.com/docs/ja/reference/statements/alter/update) ステートメントによる SQL スタイルの更新をサポートしてきました。

ミューテーション経由で UPDATE がどのように機能するかを説明するため、[パート 1 の例](https://clickhouse.com/ja/blog/updates-in-clickhouse-1-purpose-built-engines-jp)と同じ orders テーブルを使用します。
<pre>
<code type='click-ui' language='sql'>
CREATE TABLE orders (
    order_id   Int32,
    item_id    String,
    quantity   UInt32,
    price      Decimal(10,2),
    discount   Decimal(5,2)
)
ENGINE = MergeTree
ORDER BY (order_id, item_id);
</code>
</pre>

最初の挿入から始めて、シンプルな UPDATE が内部でどのようにミューテーションをトリガーするかを順を追って見ていきましょう。


### 初回挿入

まず、同じ注文の2商品を含むパートから始めます。
<pre>
<code type='click-ui' language='sql'>
INSERT INTO orders VALUES
    (1001, 'kbd',  10, 45.00, 0.00),
    (1001, 'mouse', 6, 25.00, 0.00);
</code>
</pre>

これにより、`all_1_1_0` という[名前](https://clickhouse.com/ja/blog/updates-in-clickhouse-1-purpose-built-engines-jp)のデータパートが作成されます。

![Blog-updates Part 2.001.png](https://cms.clickhouse-dev.com:1337/uploads/Blog_updates_Part_2_001_8013fe2a98.png)

### UPDATE によるミューテーションのトリガー

mouse の商品の数量と割引率を更新します。
<pre>
<code type='click-ui' language='sql'>
ALTER TABLE orders
UPDATE quantity = 60, discount = 0.20
WHERE order_id = 1001 AND item_id = 'mouse';
</code>
</pre>

> `ALTER TABLE ... UPDATE` 構文が[標準 SQL の UPDATE](https://www.w3schools.com/sql/sql_update.asp) と意図的に異なっているのは、内部で何が起きているかを反映しているためです。ClickHouse では行をインプレースで変更するのではなく、データパートを書き換える(「ミューテーションする」)処理が行われます。

**この UPDATE により、ClickHouse は内部で[ミューテーション](https://clickhouse.com/docs/ja/reference/statements/alter#mutations)を実行します。** これにより、次の3つの内部ステップがトリガーされます。



1. 更新用に**新しいブロック番号**(例: `2`)が[割り当てられ](https://clickhouse.com/ja/blog/updates-in-clickhouse-1-purpose-built-engines-jp)、ClickHouse はこれを使用して書き換えが必要なパートを追跡します。

2. ディスク上に `all_1_1_0_2` という名前の**新しいミューテーション済みパート**が作成されます(末尾の 2 は**ミューテーションバージョン**です)。

3. ミューテーションは、元のパート `all_1_1_0` のように、ブロック番号が 2 未満のパートにのみ適用されます。

以下の図は変更内容を示しています。更新されたカラム(`quantity`、`discount`)は**完全に書き直され**、変更されていないカラム(`order_id`、`item_id`、`price`)は**[ハードリンク](https://en.wikipedia.org/wiki/Hard_link)**されます。これらのカラムのデータはコピーされず、新しいパートはディスク上のハードリンクを介して基盤となる同一ファイルを単に再利用します。

![Blog-updates Part 2.002.png](https://cms.clickhouse-dev.com:1337/uploads/Blog_updates_Part_2_002_e61e7d0c11.png)

ミューテーションが完了すると、ミューテーション済みパート `all_1_1_0_2` が元の `all_1_1_0` を置き換え、元のパートは削除されます。これにより元の[フォルダ](https://clickhouse.com/ja/blog/updates-in-clickhouse-1-purpose-built-engines-jp)とファイルエントリは削除されますが、新しいパート `all_1_1_0_2` がハードリンクしているカラムファイルはディスク上に安全に保持されます。参照するパートが完全になくなるまでデータが削除されないことがハードリンクによって保証されるためです。

### まとめとトレードオフ

従来のミューテーションモデルは信頼性が高いものの、トレードオフも存在します。



* **重量級の更新**: UPDATE ごとに対象カラム全体が書き直されるため、大規模環境ではコストが高くなる可能性があります。
* **可視化の遅延**: バックグラウンドのミューテーションが完了するまで、クエリ結果に変更が反映されません。

* **マージへの依存**: ミューテーションは、実行前に先行するマージやミューテーションの完了を待つ必要があります。(*ここでは詳細を割愛しますが、これは実環境において予想外の制約となることがあります。*)

> [デフォルト](https://clickhouse.com/docs/ja/reference/settings/session-settings)では、`ALTER TABLE … UPDATE` ステートメントは非同期で実行されるため、ClickHouse は複数の更新を単一のミューテーションにまとめることができます。これにより、書き換えコストを償却できます。

On-the-fly ミューテーションに進む前に、このモデルに基づいて構築されたもう1つの過渡的な最適化である**軽量削除(Lightweight DELETE)**を見てみましょう。


## ステージ 1.5: 軽量 DELETE(ミューテーションでありながら高速化を実現)

On-the-fly ミューテーションが登場する前、ClickHouse は従来のミューテーションモデルのもとで DELETE を高速化するためのよりシンプルな最適化を導入しました。

行を即座に削除する代わりに、DELETE は特別なマスクカラム `_row_exists = 0` を設定する ALTER TABLE ... UPDATE に書き換えられます。これにより**軽量ミューテーション**がトリガーされ、`_row_exists` カラムのみが(再)書き込み*され、**他のすべてのカラムはハードリンクされる**ため、不要な I/O を回避できます。

***パートに対する**初回の削除ミューテーション**である場合、`_row_exists` カラムはミューテーション済みパート内に**新規作成**されます。パートがマージされる前の**2回目以降の削除ミューテーション**である場合は、`_row_exists` が**再書き込み**されます。*

次の図は、元のパートと[軽量 DELETE](https://clickhouse.com/docs/ja/concepts/features/operations/delete/lightweight-delete) によるミューテーション結果を示しています。以下で、プロセスの各ステップを順に説明します。

![Blog-updates Part 2.003.png](https://cms.clickhouse-dev.com:1337/uploads/Blog_updates_Part_2_003_96fff00c29.png)
*(わかりやすくするため、図では元のパートに _row_exists を記載していますが、前述のとおり、このカラムはまだ存在しておらず、初回の削除ミューテーションによって追加されます。)*


</br>1. **初回挿入**: ステージ 1 と同じ orders の小さな例を再利用します。2行が挿入され、元のパート `all_1_1_0` が作成されます。
<pre>
<code type='click-ui' language='sql'>
INSERT INTO orders VALUES
    (1001, 'kbd',  10, 45.00, 0.00),
    (1001, 'mouse', 6, 25.00, 0.00);
</code>
</pre>

2. **DELETE の発行**: mouse の商品が削除されます。
<pre>
<code type='click-ui' language='sql'>
DELETE FROM orders WHERE order_id = 1001 AND item_id = 'mouse';
</code>
</pre>

内部的には、ClickHouse は `_row_exists = 0` に更新します。
<pre>
<code type='click-ui' language='sql'>
ALTER TABLE orders
UPDATE _row_exists = 0
WHERE order_id = 1001 AND item_id = 'mouse';
</code>
</pre>

3. **ミューテーションの作成**: 新しいパート `all_1_1_0_2` が作成され、`_row_exists` のみが書き直されます。

4. **クエリの挙動**: `_row_exists = 0` の行は結果から除外されます。

5. **クリーンアップ**: 次回の通常のバックグラウンドマージ中に行が完全に削除されます。

### まとめとトレードオフ

このアプローチにより、中核となるミューテーションモデルを変更することなく DELETE が大幅に高速化されましたが、依然としてバックグラウンドでの書き換えは必要でした。

次の段階では、バックグラウンドでの書き換えそのものは解消されなかったものの、ミューテーションの実行完了を待たずに更新を即座に可視化できるようになりました。


## ステージ 2: 即時可視化を実現する On-the-fly 更新

従来のミューテーションは重量級です。データカラム全体を書き直すため、特に大規模なデータセットでは完了までに時間がかかります。UPDATE を発行してから結果が確認できるようになるまでのレイテンシを短縮するため、ClickHouse はパートが書き直される前であっても更新を即座に可視化する最適化である [On-the-fly ミューテーション](https://clickhouse.com/docs/ja/concepts/features/operations/update/overview)を導入しました。

> これは、パッチパートへの道筋における自然な第一歩でした。書き換えを回避するものではありませんでしたが、更新が即座に行われたように体感できるようになりました。

以下の図はそのメカニズムを示しています。UPDATE はメモリに保持されて読み取り時に適用され、実際のミューテーションはバックグラウンドで非同期に実行されます。

![Blog-updates Part 2.004.png](https://cms.clickhouse-dev.com:1337/uploads/Blog_updates_Part_2_004_e4bdb4f086.png)

1. **挿入**: 先ほどと同様に、2行が orders テーブルに ① 挿入され、② 初期パート `all_1_1_0` が作成されます。

2. **UPDATE の発行**: ③ mouse の行を更新します。ClickHouse は更新式をメモリに保持します。

3. **SELECT の発行**: ④ クエリは元のデータパートを読み取りますが、ClickHouse はメモリ内で ⑤ オンザフライで更新を適用します。

4. **結果**: 更新された行は、⑥ クエリ結果に即座に反映されます。

5. **ミューテーションの実行**: バックグラウンドで、ClickHouse はパートを `all_1_1_0_2` として ⑦ 書き直し、古いパートを破棄します。


### まとめとトレードオフ

On-the-fly ミューテーションによって応答性は向上しますが、バックグラウンドでの書き換え自体はなくなりません。また、更新が多数蓄積されると SELECT が低速になる可能性があり、[サブクエリや非決定論的関数のサポートも限定的](https://clickhouse.com/docs/ja/concepts/features/operations/update/overview#support-for-subqueries-and-non-deterministic-functions)です。

これは実用的な過渡期のソリューションであり、より優れた機能を構築するための猶予を生み出す、手軽で迅速な最適化でした。本当のブレークスルーはその先にありました。頻繁な更新を大規模環境でも効率的に処理できるように設計された、根本的に異なるメカニズムである**パッチパート**です。

## ステージ 3: パッチパート – ClickHouse 流の更新

初期のアプローチには限界があったため、より優れた仕組みを一から構築しました。


### なぜ従来のミューテーションでは不十分だったのか

On-the-fly 更新のような最適化を行っても、コアモデルには限界がありました。



* 変更された行がごく少数であってもカラム全体が書き直されるため、大規模環境ではリソースの無駄が生じる点。

* 先行するマージやミューテーションの完了待ちによってブロックされ、レイテンシが増大し予測困難になる点。


そこで、その役割から名付けられた新しいメカニズムである**パッチパート**を一から構築しました。
**マージ時にソースパートにパッチを適用し、変更されたデータのみを反映します。**


### 実績ある機能の上に築かれたモデル

パッチパートは、[専用エンジン](https://clickhouse.com/ja/blog/updates-in-clickhouse-1-purpose-built-engines-jp)で実証済みの**高速な挿入**と**バックグラウンドマージ**というアイデアを取り入れ、柔軟な SQL スタイルの更新向けに完全にカプセル化して一般化したものです。



1. **高速な挿入**: ClickHouse は[高スループット](https://clickhouse.com/ja/blog/updates-in-clickhouse-1-purpose-built-engines-jp)で挿入を処理します(例えばある本番環境では、[ClickHouse は毎秒 10 億行以上を取り込んでいます](https://clickhouse.com/ja/blog/how-tesla-built-quadrillion-scale-observability-platform-on-clickhouse-jp))。この特性を利用して、更新や削除を軽量な挿入としてモデル化します。

2. **バックグラウンドマージ**: MergeTree はすでにデータのスキャンと書き換えを行っています。このプロセス中についでに更新の適用や行の削除を行っても、オーバーヘッドは[ほぼゼロ](https://clickhouse.com/ja/blog/updates-in-clickhouse-1-purpose-built-engines-jp)です。

これがどのように動作するか、シンプルな例で確認してみましょう。(*直感的に理解できるようまずは小さな例から始め、内部構造については次のセクションで詳しく解説します。*)


## パッチパートの仕組み

パッチパートがどのように動作し、なぜこれほど効率的なのかを理解していきましょう。

前述の小さな orders テーブルを再利用して、シンプルな更新の流れを追ってみます。

<pre>
<code type='click-ui' language='sql'>
CREATE TABLE orders (
    order_id   Int32,
    item_id    String,
    quantity   UInt32,
    price      Decimal(10,2),
    discount   Decimal(5,2)
)
ENGINE = MergeTree
ORDER BY (order_id, item_id);
</code>
</pre>

> **パッチパートによる UPDATE は 25.7 の時点ではまだ実験的機能です**<br/>ClickHouse 25.7 で試すには、手動で機能を有効化する必要があります。<br/><br/>`SET allow_experimental_lightweight_update = 1;`<br/>` ALTER TABLE orders MODIFY SETTING enable_block_number_column = 1, enable_block_offset_column = 1;`<br/><br/>*(25.8 でベータ版になる予定です)*

先ほどと同じ初期注文データ(kbd、mouse の2商品)も再利用します。これらの行は `all_1_1_0` という名前のデータパートに挿入されます(後述の図を参照)。
<pre>
<code type='click-ui' language='sql'>
INSERT INTO orders VALUES
    (1001, 'kbd',  10, 45.00, 0.00),
    (1001, 'mouse', 6, 25.00, 0.00);
</code>
</pre>

その後、mouse の数量が 60 個に増やされ、20% のまとめ買い割引が適用されるようになったとします。行を更新します。
<pre>
<code type='click-ui' language='sql'>
UPDATE orders
SET quantity = 60, discount = 0.20
WHERE order_id = 1001 AND item_id = 'mouse';
</code>
</pre>

この UPDATE により、パッチパートを活用した**軽量更新(Lightweight UPDATE)**がトリガーされます。

> [パッチパートベースの更新には、標準の SQL UPDATE 構文を使用します](https://www.w3schools.com/sql/sql_update.asp)。この新機能は、ClickHouse では[軽量更新](https://clickhouse.com/docs/ja/reference/statements/update)と呼ばれています。従来のミューテーションとは異なり、より行レベルの更新に近い挙動となり、小規模で頻繁な変更も効率的かつ高いパフォーマンスで処理できます。

この時点で、更新された値は**すでにクエリから参照可能**であり、バックグラウンド処理の完了を待つ必要はありません。この仕組みについては[後ほど](/ja/blog/updates-in-clickhouse-2-sql-style-updates-jp)説明します。

### パッチパートは置き換えではなく差分

従来のミューテーションとは異なり、ClickHouse はカラムやパート全体を書き直すことはしません。その代わり、以下のみを含む新しいコンパクトな**パッチパート**を作成します。



* **変更されたカラム値**(quantity = 60、discount = 0.20)

* ソースパート内の**元の行を特定するためのメタデータ**

> パッチパートは「diff」のようなものだと考えてください。「この行の、このカラムだけを更新する」と指示する小さな差分です。そのため、ClickHouse ではこれらを**軽量更新**と呼んでいます。極めて**コンパクトで効率的**です。


### 図解: パッチパートによる行の更新手順

上記の更新中に何が起きているかを視覚的に確認してみましょう。

*(基本概念に焦点を当てるため、実際の実装を少し簡略化しています。完全な詳細は次のセクションで解説します。)*

![Blog-updates Part 2.005.png](https://cms.clickhouse-dev.com:1337/uploads/Blog_updates_Part_2_005_1842c7cbff.png)

**① 元のデータパート**: <br/>キーボードとマウスの両方の注文を含み、テーブルのソートキー `(order_id, item_id)` で明示的にソートされています。各行には、パート内での位置を示す *[仮想システムカラム](https://clickhouse.com/docs/ja/reference/engines/table-engines/mergetree-family/mergetree#virtual-columns)* `_part_offset` も存在するため、パートは `_part_offset` によっても暗黙的にソートされています。


**② パッチパート**: <br/>UPDATE によって作成されます。ソースパート `all_1_1_0` の2行目に対する変更された値(`quantity`、`discount`)と、メタデータシステムカラム(`_part` = `all_1_1_0`、`_part_offset` = `1`)のみが含まれます。


**③ マージ結果**: <br/>バックグラウンドマージ中に、ClickHouse は元のパートとパッチパートをマージし、一致する行を更新された値で置き換えます。

### コンパクトなデータサイズ

パッチパートは、書き込まれるデータ量を最小限に抑えるよう設計されています。

* **更新された値**のみが書き込まれます。

* **変更されていないカラム**(例: `order_id`、`item_id`、`price`)は完全にスキップされ、パッチパートには一切含まれません。
*(ReplacingMergeTree のような専用エンジンの場合、更新には変更されていない値も含めた行全体の再挿入が必要です。)*


### 優れた処理効率

パッチパートは**[バックグラウンドですでに実行されているマージ](https://clickhouse.com/ja/blog/updates-in-clickhouse-1-purpose-built-engines-jp)に相乗り**します。ClickHouse が常時実行しているプロセスに組み込まれるため、オーバーヘッドはほぼゼロです。



* **すべてのパートが自然に整列している**: 元のパートはテーブルのソートキー(および暗黙的に `_part_offset`)でソートされており、パッチは `(_part, _part_offset)` でソートされています。

* **そのため、マージで行をシームレスに整列可能**: 追加のインデックス作成、再ソート、書き換えを行うことなく、`_part_offset` を使用して行を揃えられます。

* **更新がスムーズに収まる**: 1回の効率的なマージパスで適用されます。エンジンは、一時バッファやランダムアクセスを使用せず、*パートの単一の線形スキャンでパートのデータをインターリーブ(挟み込み)*するだけです。

*(これは専用エンジンの仕組みと似ていますが、パッチパートはソートキーではなく `_part_offset` に依存します。)*

パッチパートが単一の更新に対して原理的にどう機能するかに着目するため、意図的に内容を少し簡略化して説明しました。

それでは全体像を広げ、これがどのようにスケールするかを見ていきましょう。


## 大規模環境におけるパッチパート: 追跡、ターゲット特定、マージ

ClickHouse が**本番環境に耐えうるスケーラブルな更新**をどのようにサポートしているかを理解するために、パッチパートを支える内部システムカラムとメタデータ構造を詳しく見ていきます。

> 実環境の更新では、これらの内部構造を利用して複数パートにまたがる行を効率的にターゲットし、大規模環境での**ノンブロッキング**な更新をサポートしています。

詳細を見ていきましょう。

数量が 40 以上のすべての注文の全商品に 20% の割引を適用したいとします。
<pre>
<code type='click-ui' language='sql'>
UPDATE orders
SET discount = 0.2
WHERE quantity >= 40;
</code>
</pre>

このような SQL ステートメントは、**複数のパートにまたがる多数の行**に影響を与える可能性があります。しかし、その方法についてユーザーが心配する必要はありません。期待どおりに動作します。

> **宣言的更新の重要性**<br/>更新行ごとに行を再挿入する必要がある ReplacingMergeTree などの[専用エンジン](https://clickhouse.com/ja/blog/updates-in-clickhouse-1-purpose-built-engines-jp)とは異なり、宣言的な SQL 更新では内部メカニズムが自動で処理されます。やりたい意図を記述するだけで十分です。

パッチパートが**複数のパート**にまたがる行をいかに効率的にターゲットするかを示すため、以下の図ではより多くの挿入と上記の更新を含む拡張例を使用しています。

> **注記:** 理解しやすくするため、*この例で使用されていない*すべてのシステムカラムと構造は**薄い色**で表示しています。<br/><br/>• **オレンジ色**は使用されているシステムカラムを示します。<br/>• **青色**は使用されているインデックスを示します。<br/>• **薄いグレー**は未使用の構造を示します。<br/><br/>これにより、内部で何が存在しているかの全体像を把握しつつ、重要な部分に焦点を当てることができます。

![Blog-updates Part 2.006.png](https://cms.clickhouse-dev.com:1337/uploads/Blog_updates_Part_2_006_326f03e9ec.png)

**① 初回の注文**: 2商品を含む基本の例。

**② 別の注文**: 新しい `order_id` で、別途挿入。

**③ 拡張された注文**: 初回の注文にさらに2商品を追加。

これらは通常の挿入であり、時間の経過とともに**3つのデータパート**が作成されます。

**④ パッチパート**: UPDATE によって作成されます。新しいカラム値(2行分の discount = 0.2)と、変更対象の正確な行をターゲットするための十分なメタデータのみが含まれます。

**⑤ マージされたデータパート**: 古い行とパッチの値を1つの出力パートに結合します。

**クリーンアップ:** マージ後、元のパート ① 〜 ③(そして最終的には ④ も)破棄されます。マージ結果である ⑤ のみが残ります。

それでは、上の図に描かれているシステムカラムと内部データ構造を順に見ていきましょう。


### 元の行に含まれるシステムカラム

パッチパートのメカニズムを実現するため、元のデータパート内のすべての行は**3つのシステムカラム**を保持しています。

| システムカラム                          | 説明と目的                                                            |
|------------------------------------------|----------------------------------------------------------------------|
| `_part_offset`                           | パート内での行の序数位置。マージ中に行を整列させ、パッチの更新を効率的に適用するために使用されます。                                   |
| `_block_number`, `_block_offset`         | 挿入時の[ブロック番号](/ja/blog/updates-in-clickhouse-1-purpose-built-engines-jp)と、そのブロック内での行のオフセット。マージ後に `_part_offset` が無効になった際、行を特定するために使用されます。(*この例では使用されません*)     |

*(これらのシステムカラムはデフォルトで[仮想カラム](https://clickhouse.com/docs/ja/reference/engines/table-engines/mergetree-family/mergetree#virtual-columns)です。先ほどの簡略化した例と同様に、ここではわかりやすくするために実体化されたものとして図示していますが、実際にはマージ後にのみ物理的に保存されます。)*

### パッチパート内の正確なターゲティングメタデータ

各パッチパートの行には、更新対象の行を見つけるための**必要最小限のメタデータ**が含まれています。

| システムカラム                              | 目的                                                                                                      |
|----------------------------------------------|--------------------------------------------------------------------------------------------------------------|
| `_part`, `_part_offset`                       | 元の行の特定                                                                                   |
| `_block_number`, `_block_offset`             | マージをまたいだ追跡のサポート(*ここでは未使用*)                                                           |
| `_data_version`                               | 更新バージョンの追跡: 適用済みパッチのスキップや、他のパッチパートとのマージに使用(*後述*) |

残りは以下のみです。

| `<更新されたカラム>`         | 古い値を置き換える新しい値(例: `discount = 0.2`) |
|----------------------------|----------------------------------------------------------|


### パッチパートのインデックス

パッチパートが作成されると、ClickHouse はそれをいつ、どこに適用すべきかを把握する必要があります。そのために、ClickHouse は(パッチパートごとに)順方向と逆方向の2つの軽量インデックスを構築します。

| `インデックス名`           | 目的                                                                                           |
|------------------------|---------------------------------------------------------------------------------------------------|
| `ソースパートインデックス`    | 影響を受ける各ソースパートを最小/最大のデータバージョンにマッピング。*このパッチをこのパートに適用すべきか？* の判定に使用。 |
| `逆インデックス`        | パッチのデータバージョンを、それを必要とするすべてのパートとブロックにマッピング。マージ後でもパッチの対象を見つけるために使用。 |

これらのインデックスにより、バックグラウンドでデータパートがマージされた場合でも、**効率的なターゲティング**が可能になります。



### インデックスによる高速なソースパートのマッチング

ClickHouse は、パッチパートの `ソースパートインデックス` を使用して、更新がどのソースパートに適用されるかをすばやく判断します。今回の例では、`all_1_1_0` と `all_3_3_0` のみが一致し、更新は `all_2_2_0` には触れません。


### システムカラム主導のソートによる高速なパッチマージ

前述の通り、**[パッチパートはすでにバックグラウンドで実行されているマージに相乗りします](/ja/blog/updates-in-clickhouse-2-sql-style-updates-jp)**。すべてのパートが(暗黙的または明示的に)_part_offset でソートされているため、ClickHouse は**単一の効率的なマージパス**でパッチを適用できます。


### マージ中の行アイデンティティの保持

マージされたパート(⑤)では、各行の新しい位置を反映するように `_part_offset` の値が**再作成**されます。しかし、`_block_number` と `_block_offset` は元のパートから**変更されずにそのままコピー**されます。

> この詳細は、対象となるパートの**マージと同時に実行される**更新をサポートする上で**極めて重要**です。

これがどのように機能するかを、次のセクションで見ていきましょう。


## 更新はマージを待たない

ClickHouse の更新は**ノンブロッキング**です。マージの完了を待つことはありません。代わりに、各更新は UPDATE の開始時に存在するパートの[スナップショット](https://clickhouse.com/blog/clickhouse-release-25-06#single-snapshot-for-select)に対して実行されます。

> 従来のミューテーションでは、実行前に先行するマージやミューテーションの完了を待つ必要がありました。

ほとんどの場合、そのスナップショットは後でパッチパートが適用される時点でも有効です。しかし、パッチが適用される前にそれらのパートが**マージによって消滅**した場合、ClickHouse は自動的に別のマッチング戦略へとフォールバックします。

そのフォールバックがどのように動作するかを見てみましょう。


### パッチが適用される前にパートがマージされると何が起きるか？

次の図は、前のセクションと同じ拡張された例を基にしていますが、パッチが適用される**前**にパートがマージされた場合に何が起こるかを示しています。

> 前の図では、`_block_number` と `_block_offset` を薄く表示していました。今回は、使用されない `_part`、`_part_offset`、およびパッチの `ソースパートインデックス` を薄いグレーで表示しています。

![Blog-updates Part 2.007.png](https://cms.clickhouse-dev.com:1337/uploads/Blog_updates_Part_2_007_4ed2800064.png)

**このシナリオでは何が変わるのか？**

**①〜③**: 前述と同様に**同じ挿入**と ⑤ の UPDATE が実行され、これらのステップは変更されません。


**④**: しかし今回は、パッチが適用される前に**バックグラウンドマージ**が元のパートを1つのパートにまとめます。その後、マージの通常動作としてソースパート(①〜③)は削除されます。

**⑤**: パッチパートによって参照されていた元の `_part` と `_part_offset` の値は無効になります。そのため、ClickHouse はマージ中も保持されていた `_block_number` と `_block_offset` へとフォールバックします。

**⑥〜⑧**: **ClickHouse は保持されていたそれらの値に基づいてハッシュ結合を使用しパッチを適用**し、パッチが適用されたマージ済みデータパートを生成します。


このフォールバックのパッチ適用パスがどのように動作するか、詳しく見てみましょう。


### 逆インデックスによるソースパートのマッチング

逆インデックスは、このパッチが**データバージョン 4** に適用され、パート `all_1_1_0` と `all_3_3_0` を対象としていることを示しています。これらは(その[名前](https://clickhouse.com/ja/blog/updates-in-clickhouse-1-purpose-built-engines-jp)に基づくと)**ブロック番号 1 と 3** を含んでいました。

マージされたパート `all_1_3_1` は(その名前に基づくと)**ブロック番号 1 から 3** にまたがっているため、有効な一致となります。また、そのデータバージョンは `1`(最小のブロック番号から推測)であり、パッチのバージョン `4` よりも小さいため、パッチを適用できます。

この逆マッピングにより、マージによって元のパート名が無効になった後でも、ClickHouse はデータをマッチングしてパッチを適用できます。


### ブロックベースのシステムカラムの結合によるパッチ適用

ソースパートはすでに存在せず、元の値がマージ後の出力内の対応する行と一致しなくなっているため、マージでパッチを効率的に適用するために `_part` と `_part_offset` を使用することはできなくなっています。

代わりに、ClickHouse は**ハッシュ結合ベースのアルゴリズム**を使用してパッチを適用します。パッチパートを `(_block_number, _block_offset)` をキーとするハッシュテーブルとしてメモリに読み込み、同じキーを使用してマージされたパートと結合して行を更新します。

このフォールバックは、前のセクションで説明した高速パスよりも低速で、メモリを多く消費します。パッチが完全にメモリに収まる必要がありますが、将来的にはメモリ制約のない `full merge join` をサポートする可能性があります(現時点では未実装です)。

幸いなことに、通常はソースデータパートが高速なパッチ適用に十分な時間存続するため、実際にこのフォールバックが発生することは稀です。

ClickHouse がマージ中であってもどのように UPDATE を処理するかを見てきたところで、次は DELETE がいかに同様の効率化を遂げたかについて説明します。


## 軽量な DELETE がさらに軽量に

[ステージ 1.5](/ja/blog/updates-in-clickhouse-2-sql-style-updates-jp) では、**軽量な DELETE** によってすでに改善が得られていました。ALTER UPDATE 経由で `_row_exists` 削除マスクのみを書き換えることで、行全体の書き換えを回避していました。

しかし、さらに軽量化できます。

[`lightweight_delete_mode`](https://clickhouse.com/docs/ja/reference/settings/session-settings) = `lightweight_update` の場合、DELETE は ALTER でさえなくなります。ClickHouse は削除された行に対して単に `_row_exists = 0` を設定するパッチパートを作成します。その後、次のバックグラウンドマージ中に行が破棄されます。

以下の図は、ステージ 1.5 と同じ例を再利用したものです。元の注文からマウスの商品を削除します。(*わかりやすくするため、_block_number、_block_offset などの未使用のシステムカラムは省略しています。*)

![Blog-updates Part 2.008.png](https://cms.clickhouse-dev.com:1337/uploads/Blog_updates_Part_2_008_1825514794.png)

① 初期のパートには2行が含まれています。

② パッチパートが、削除された行(マウス)に対して `_row_exists = 0` を設定します。

③ 次のマージ中に、その行は消滅します。

ClickHouse はパッチを瞬時に適用します。マージが行われるまで、クエリは単に `_row_exists = 0` の行を無視します。これが**読み取り時パッチ適用(patch-on-read)によるクエリ実行**の核心です。


## 読み取り時パッチ適用の仕組み

ClickHouse は更新された結果を返す前に、パッチパートが実体化されるのを待ちません。その代わりに、次のように処理します。

> パッチパートは、[暗黙的な FINAL のように](https://clickhouse.com/ja/blog/updates-in-clickhouse-1-purpose-built-engines-jp)、クエリ実行中にメモリ上で自動的かつオンザフライで適用されます。

**この読み取り時パッチ適用のメカニズムは、パフォーマンスへの影響を最小限に抑えるように設計されています。**

通常、クエリのために選択されたデータは([インデックス分析](https://clickhouse.com/docs/ja/concepts/core-concepts/primary-indexes)の後)、複数のデータパート内のいくつかの**データ範囲**(連続した行ブロック)に配置されています。これらの範囲は、[クエリエンジン](https://clickhouse.com/docs/ja/concepts/core-concepts/academic-overview)によって ① 個別の**並列ストリームステージ**(データストリーム)へと動的に分散され、データをフィルタリング、集計、ソート、制限して最終結果を生成する ② **[並列処理レーン](https://clickhouse.com/docs/ja/concepts/core-concepts/query-parallelism#distributing-work-across-processing-lanes)** によって処理されます。

![Blog-updates Part 2.009.png](https://cms.clickhouse-dev.com:1337/uploads/Blog_updates_Part_2_009_bfefb2e8c3.png)

まだマージされていないパッチパートは、各データストリームのデータ範囲ごとに ③ マッチングされ、**個別に適用**されます。これにより、並列性を損なうことなく更新が正しく適用されます。

*(必要に応じて、`ALTER TABLE … APPLY PATCHES` を実行して、蓄積された未マージの変更をすべて完全に実体化することもできますが、これは任意です。)*

パッチパートを完全に理解するには、そのライフサイクル、つまり ClickHouse がバックグラウンドでどのようにマージ、重複排除、クリーンアップを行うかも見る必要があります。


## パッチパートは時間の経過とともにどうなるか？

パッチパートは特殊に見えるかもしれませんが、内部的には ClickHouse の通常のパートと変わりません。つまり、以下の性質を持ちます。



* `_data_version` を[バージョンカラム](https://clickhouse.com/docs/ja/reference/engines/table-engines/mergetree-family/replacingmergetree#replacingmergetree-parameters)として使用し、*[ReplacingMergeTree](/ja/blog/updates-in-clickhouse-1-purpose-built-engines-jp)* アルゴリズムによって**他のパッチパートとマージ**されます。これにより、各パッチパートには更新された各行の最新バージョンのみが保持されます。

* 変更が影響を受けるすべてのデータパートに完全に実体化されるか、別のパッチパートとマージされると、**自動的にクリーンアップ**されます。バックグラウンドのクリーンアップスレッドがこれを安全に処理します。

* **[TOO_MANY_PARTS の閾値にカウント](https://clickhouse.com/docs/ja/reference/settings/merge-tree-settings)** されます。これは**パーティションごと**に適用されます。この影響を緩和するため、パッチパートは**更新されたカラムのセットに基づいて別々のパーティション**に保存されます。そのため、`SET x = …`、`SET y = …`、`SET x = …, y = …` のように異なるカラムに影響を与える複数の UPDATE 文を実行した場合、それぞれ独自のパート数を持つ別々のパッチパーティションが作成されます。


この設計により、パッチパートの高速性と効率性が維持され、MergeTree のメカニズムと深く統合されています。

ここまでは、パッチパートが単独でどのように振る舞うかに焦点を当ててきました。しかし、複数の更新が同時に届いた場合はどうなるでしょうか？ClickHouse が同時実行される更新をどのように安全に調整するかを見てみましょう。


## 同時実行される更新の調整

**ClickHouse はデフォルトで更新を並列実行します。** 2つの更新が同じカラムに触れる場合、自動的に正しい順序で実行されます。何も設定する必要はなく、そのまま動作します。

いくつかの設定でこの動作を微調整できます。



* **[`update_parallel_mode`](https://clickhouse.com/docs/ja/reference/settings/session-settings)**:

    * `auto`(デフォルト): 依存関係のある更新(例: `UPDATE a=3 WHERE b=2` と `UPDATE b=2 WHERE a=1`)を直列化します。その他は並列に実行します。

    * `sync`: すべての更新を1つずつ順番に実行します。

    * `async`: 調整を行わずにすべての更新を実行します。

* **[`update_sequential_consistency`](https://clickhouse.com/docs/ja/reference/settings/session-settings)**(デフォルトは無効): パフォーマンスへの影響と引き換えに、各更新が最新の可視状態を参照できるようにします。

しかし、ほとんどのワークロードでは、デフォルトの設定が高速で安全であり、適切に処理されます。

これらすべての調整は水面下で行われるため、外側からは更新が通常の SQL のように感じられます。ただそれだけで機能します。

この記事の最後に、一歩引いて全体像を振り返ってみましょう。


## エンジン固有の工夫から使い慣れた SQL へ

パッチパートは、列指向ストレージのルールを破るのではなく、それを受け入れることで、効率的な SQL スタイルの UPDATE を ClickHouse にもたらします。

私たちは ClickHouse を高速にしている要素をそのまま活用しました。<br/>
**挿入は高速である。マージは継続的である。パートはイミュータブルでソートされている。**

そして**挿入が極めて高速**であるため、更新を挿入へと変換しました。<br/>
ClickHouse は水面下でコンパクトな**パッチパート**を挿入し、マージの際にそれらを効率的に適用します。

**マージはすでに実行されているため、より多くの処理を行わせるようにしました。実質的な追加負荷をかけることなく。**<br/>
エンジンはすでにバックグラウンドでデータパートをマージしているため、最小限のオーバーヘッドで更新を適用できるようになりました。

**パッチパートはクエリのパフォーマンスにほとんど影響を与えることなく、瞬時に組み込まれます。**<br/>
更新はすぐに反映され、未マージのパッチは並列性を損なわない方法で適用されます。

これが、現在 ClickHouse の*軽量な更新*を支えている中核のメカニズムです。

これらは ReplacingMergeTree のようなエンジンと同じ原則に基づきながらも、完全に汎用的な形で構築されており、柔軟で標準的な SQL 構文の背後にカプセル化されています。
<pre>
<code type='click-ui' language='sql'>
UPDATE orders
SET discount = 0.2
WHERE quantity >= 40;
</code>
</pre>

その結果、更新は瞬時に反映され、クエリは高速なまま維持され、何もブロックされません。これこそが ClickHouse 流の UPDATE であり、すべて使い慣れた SQL で実現されています。

**[パート 3](https://clickhouse.com/ja/blog/updates-in-clickhouse-3-benchmarks-jp) では、これをテストします。**<br/>
ミューテーション、オンザフライミューテーション、軽量なパッチパートによる更新など、あらゆる更新手法のベンチマークを行い、ClickHouse の標準 SQL UPDATE がどれほど高速になるかを示します。<br/>
**ネタバレ: 単に速いだけではありません。最大で 1,000 倍、場合によってはそれ以上に高速です。**