---
title: "ClickHouseがJOINを高速化した方法"
date: "2026-11-04T06:57:48.191Z"
author: "Tom Schreiber"
category: "Engineering"
excerpt: "2年間にわたるJOINエンジニアリングへの集中的な取り組みにより、ClickHouseはJOIN負荷の高いTPC-H SF100ワークロードで26倍高速になりました。並列ハッシュ結合、ランタイムフィルタ、遅延カラムレプリケーション、よりスマートなJOINプランニングによる改善の詳細を解説します。"
---

# ClickHouseがJOINを高速化した方法

> **TL;DR**<br/>
ClickHouse はこの 2 年間で、JOIN を多用する分析ワークロードにおいて **26 倍高速化**しました。本記事では、JOIN を第一級の強みへと引き上げたエンジニアリングの取り組みを解説します。

 
<br/>

## JOIN に注力した 2 年間のエンジニアリング

ClickHouse は、高速な分析クエリ、高い圧縮率、大規模環境におけるリアルタイム性能で知られています。

この 2 年間、エンジニアリングにおける大きな注力分野の 1 つが、JOIN を多用する SQL クエリに対しても同じ性能プロファイルをもたらすことでした。

[ClickHouse 24.5 リリースウェビナー](https://www.youtube.com/live/dURnKjLuZLg?si=1Bx618RgGfAwN4iP&t=2216)において、ClickHouse の生みの親である Alexey Milovidov は、その方向性を次のように明確に述べました。

> 「今後は、すべての ClickHouse リリースで JOIN の改善を目にすることになるでしょう。」

以下のチャートは、それが実際にどのように進んだかを示しています。

![Blog-JOINS-improvements.001.png](https://clickhouse.com/uploads/Blog_JOINS_improvements_001_be4d4145a8.png)

1 年目は基盤を固めました。より高速なパラレルハッシュ結合、よりスマートなプランニング、積極的なフィルタープッシュダウン、そしてローカルな結合順序の並べ替え(join reordering)です。

> 25.4 までに、同一の [TPC-H](https://clickhouse.com/docs/ja/get-started/sample-datasets/tpch) SF100 による JOIN 負荷の高いワークロードは、22.4 と比較してすでに **4.4 倍高速**になっていました。

2 年目はさらに大きく前進しました。25.4 から 26.4 の間に、オプティマイザと実行エンジンの新たな改善が一連となって導入され、デフォルト設定のままで同一ワークロードがさらに 6 倍高速化しました。

> 22.4 と比較して、現在の ClickHouse は全体として TPC-H SF100 において **26 倍高速**です。

本記事では、その実現に至った過程を解説します。[連動記事](/ja/blog/tpc-h-clickhouse-cloud-vs-snowflake-databricks-bigquery-redshift-jp)では、それによって何が可能になったかを紹介しています。**ClickHouse Cloud は 1 セント未満のコストで TPC-H を実行可能**になり、SF100 において Snowflake、Databricks、BigQuery、Redshift と直接競合できるようになりました。


## 1 年目: 基盤の構築

1 年前、サンフランシスコで開催された[当社初のユーザーカンファレンス「Open House」](https://clickhouse.com/blog/highlights-from-open-house-our-first-user-conference)において、ClickHouse の JOIN エンジニアリングリードである Robert Schulze が、JOIN 性能に関する最初の 1 年間の主要な成果を発表しました。

<iframe width="768" height="432" src="https://www.youtube.com/embed/gd3OyQzB_Fc?si=AQBytxlDmL8EjmoS" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

<br/>

その最初の 1 年は、基盤の構築に費やされました。ClickHouse は 24.12 でパラレルハッシュ結合をデフォルトにし、2 テーブル間の結合に対するローカルな自動結合順序変更を追加したほか、低レベルの実行処理の改善を継続的にリリースしました。

これらの変更のいくつかは、連続するリリースを通じて導入されました。


* **24.7:** [パラレルハッシュ結合を高速化するためのハッシュテーブル割り当ての改善](https://clickhouse.com/blog/clickhouse-release-24-07#faster-parallel-hash-join)
* **24.12:** [パラレルハッシュ結合](https://clickhouse.com/blog/clickhouse-fully-supports-joins-hash-joins-part2#parallel-hash-join)をデフォルトの戦略にし、[ローカルな自動結合順序変更](https://clickhouse.com/blog/clickhouse-release-24-12#automatic-join-reordering)を導入
* **25.1:** [ハッシュ結合のプローブフェーズを高速化](https://clickhouse.com/ja/blog/clickhouse-release-25-01-jp)
* **25.2:** [ハッシュ結合のビルドフェーズにおけるスレッド競合を解消](https://clickhouse.com/blog/clickhouse-release-25-02#faster-parallel-hash-join)

これらの改善により、同一の TPC-H SF100 による JOIN 負荷の高いワークロードは、22.4 と比較して **25.4 までに 4.4 倍高速化**しました。

しかし、1 年目は基盤に過ぎませんでした。2 年目こそ、デフォルト設定のままで JOIN が劇的に向上した時期です。

## 2 年目: デフォルトで競争力のある JOIN の実現

今年の Open House で、Robert は JOIN 性能の物語の第 2 章を[再び登壇して語りました](/uploads/image_cc8a4cd7f3.png)。


1 年目で基盤を築き、2 年目でデフォルト設定における JOIN の競争力を高めました。

目標はシンプルでした。ユーザーがクエリを書き直したり、手動で結合順序を調整したり、どの内部最適化を有効にすべきか知る必要がないようにすることです。ClickHouse 側で JOIN を多用する SQL をより多く認識し、より優れたプランを自動的に選択し、実行中の不要な処理を回避すべきです。

以下のチャートは、25.4 から 26.4 の間にデフォルトで有効になった 4 つの主な改善点を示しています。これらにより、同一の TPC-H SF100 ワークロードがさらに 6 倍高速化しました。

![Blog-JOINS-improvements.002.png](https://clickhouse.com/uploads/Blog_JOINS_improvements_002_0f8d32bb46.png)

本記事の残りの部分では、これら 4 つの改善点について順を追って説明します。

① **JOIN における相関サブクエリ(Correlated subqueries)** — より多くの実環境の SQL を直接サポート。

② **遅延カラム複製(Lazy column replication)** — 結合によって生成される重複値のコピーを回避。

③ **ランタイムフィルター(Runtime filters)** — コストの高いハッシュテーブル参照の前に、プローブ側の行をスキップ。

④ **統計情報に基づく結合順序変更(Statistics-based join reordering)** — より優れた結合プランを自動的に選択。

すべての例では、比較を容易にするため、同一のハードウェアである AWS EC2 m6i.8xlarge インスタンス(32 vCPU、128 GiB RAM)上で [TPC-H SF100](https://clickhouse.com/docs/ja/get-started/sample-datasets/tpch) を使用しています。


### ① JOIN における相関サブクエリ

2 年前の課題は、一部の結合処理が期待よりも遅かったことだけではありません。重要な結合クエリの一部がまったく実行できなかったのです。


#### なぜこれが重要だったのか

VLDB 2024 で発表された[最初の研究論文](https://www.vldb.org/pvldb/vol17/p3731-schulze.pdf)の TPC-H ベンチマークセクションでは、Q2、Q4、Q13、Q17、そして Q20〜Q22 の 7 つのクエリを除外せざるを得ませんでした。当時、ClickHouse が相関サブクエリを完全にはサポートしていなかったためです。


#### 相関関係を持つ TPC-H クエリ

TPC-H Q4 は良い例です。外部クエリのテーブルである `orders` を参照する、`lineitem` に対する内部クエリを含んでいます。

![Blog-JOINS-improvements.001.png](https://clickhouse.com/uploads/Blog_JOINS_improvements_001_d351241bde.png)

これが**相関サブクエリ**と呼ばれる所以です。内部クエリは外部クエリの値に依存しているため、単独では解釈できません。


#### 1 行ずつの実行から集合指向のプランへ

相関サブクエリは直感的に記述できるため、よく使われます。また、エージェント型システムが生成するクエリをはじめ、自動生成された SQL でも目にする機会が増えています。しかし、データベースエンジンにとって、これを効率的に実行するのは困難です。

単純なプランは、外部クエリの各行に対して内部クエリを 1 回ずつ評価することです。これは単純ですが、性能面では壊滅的な結果を招くことがよくあります。これらのクエリを高速化するには、オプティマイザが非相関化(decorrelate)を行わなければなりません。つまり、通常は結合、集約、セミ結合、またはアンチ結合を使用して、同等の集合指向プランへとクエリを書き換える必要があります。

その書き換えでは、重複行、集約関数、NULL に対する SQL セマンティクスを維持しなければならず、見た目よりもはるかに困難です。


#### 結果

> 相関サブクエリは ClickHouse において第一級の SQL になりました。従来は手動での書き換えが必要だったクエリ(除外されていた TPC-H クエリを含む)が、直接実行できるようになりました。

<br/>

### ② JOIN における遅延カラム複製

遅延カラム複製は、JOIN によって同じ値が論理的に何度も繰り返される際の CPU とメモリの使用量を削減します。


#### 課題: 論理的な重複が物理的なコピーになる

JOIN の結果では、1 つの入力行から多数の出力行が生成されることがあります。たとえば TPC-H では、各顧客が複数の注文を持つことができます。`orders` と `customer` を結合すると、一致する注文ごとに顧客カラムが論理的に繰り返されます。

<pre>
<code type='click-ui' language='sql' show_line_numbers='false'>
SELECT
    o.o_orderkey,
    o.o_orderdate,
    c.c_name,
    c.c_address,
    c.c_comment
FROM orders AS o
INNER JOIN customer AS c
    ON o.o_custkey = c.c_custkey;
</code>
</pre>

論理的には、結果は次のようになります。

![Blog-JOINS-improvements.004.png](https://clickhouse.com/uploads/Blog_JOINS_improvements_004_3a121d9e14.png)

概念上、これは正しい結果です。一致する各注文の横に同じ顧客の値が表示されます。

負荷が大きいのは物理的な複製です。ClickHouse が結合されたすべての行に c_address や c_comment などの幅の広い値をコピーすると、JOIN 処理は同じデータの複製を繰り返すために CPU サイクルとメモリ帯域幅を消費します。そして、集約などの後続のオペレーターがそれらの行の大半を破棄する場合、そのコピー処理の多くは無駄になります。


#### 遅延カラム複製による先行コピーの回避

結合中に重複する値を物理的に複製する代わりに、ClickHouse は元の値を小さな辞書データ構造に 1 回だけ保持し、結合されたカラムをそれらを参照するコンパクトなインデックス値として表現します。

![Blog-JOINS-improvements.005.png](https://clickhouse.com/uploads/Blog_JOINS_improvements_005_5694c466bf.png)

後続のクエリステップで完全に実体化されたカラムが必要になった場合でも、ClickHouse はそれを生成できます。しかし、多くの分析処理はコンパクトな表現のまま直接処理できるため、重複する値を物理的にコピーする必要はまったくありません。

これは、文字列などの幅の広いカラムを複製する JOIN において特に有効です。


#### ベンチマーク: 物理的な複製の回避

ベンチマークでは、`orders` に対する自己結合を使用して多数の重複した `o_comment` 値を生成し、それを直後に `cityHash64` で消費します。

まず、遅延複製を無効にした状態で、以下の結合例を実行しました。

<pre>
<code type='click-ui' language='sql' show_line_numbers='false'>
SELECT sum(cityHash64(t1.o_comment))
FROM orders AS t1
INNER JOIN orders AS t2
    ON t1.o_custkey = t2.o_custkey
SETTINGS
    enable_lazy_columns_replication = 0,
    allow_special_serialization_kinds_in_output_formats = 0;
</code>
</pre>

3 回の実行のうち最も速かった結果:

<pre><code type='click-ui' language='text' show_line_numbers='false'>
1 row in set. Elapsed: 5.419 sec. Processed 300.00 million rows, 8.89 GB (55.36 million rows/s., 1.64 GB/s.)
Peak memory usage: 5.27 GiB.
</code></pre>


次に、遅延カラム複製を有効にして同じクエリを実行しました。

これは遅延複製にとって理想的なケースです。`o_comment` は関数の入力としてのみ必要とされるため、ClickHouse は重複する文字列カラムを物理的に実体化することなく、コンパクトな表現に対して直接 `cityHash64` を評価できます。

<pre>
<code type='click-ui' language='sql' show_line_numbers='false'>
SELECT sum(cityHash64(t1.o_comment))
FROM orders AS t1
INNER JOIN orders AS t2
    ON t1.o_custkey = t2.o_custkey
SETTINGS
    enable_lazy_columns_replication = 1,
    allow_special_serialization_kinds_in_output_formats = 1;
</code>
</pre>

3 回の実行のうち最も速かった結果:

<pre><code type='click-ui' language='text' show_line_numbers='false'>
1 row in set. Elapsed: 2.847 sec. Processed 300.00 million rows, 8.89 GB (105.36 million rows/s., 3.12 GB/s.)
Peak memory usage: 5.22 GiB.
</code></pre>

#### 結果

> 結果: 遅延カラム複製により、この結合は **1.9 倍高速化**し、実行時間は 5.419 秒から 2.847 秒へと短縮されました。また、ピークメモリ使用量も 5.27 GiB から 5.22 GiB へとわずかに減少しました。

この高速化は、重複する文字列を物理的にコピーするのではなく、複製されたカラムのコンパクトな表現に対して `cityHash64` を直接評価したことによるものです。


### ③ JOIN におけるランタイムフィルター

ランタイムフィルターは、ハッシュ結合におけるプローブ側の無駄な処理を削減します。

これは、ClickHouse の[フルソートマージ結合アルゴリズム](https://clickhouse.com/blog/clickhouse-fully-supports-joins-full-sort-partial-merge-part3#full-sorting-merge-join)ですでに使用されていた手法を一般化したものです。そこでは、結合自体が実行される前に、結合対象のテーブル同士をお互いの[結合キーでフィルタリング](https://clickhouse.com/blog/clickhouse-fully-supports-joins-full-sort-partial-merge-part3#filtering-tables-by-each-others-join-key-values-before-joining)できます。ClickHouse は、デフォルトのパラレルハッシュ結合アルゴリズムにも同様のアイデアを導入しました。

おさらいとして、[パラレルハッシュ結合アルゴリズム](https://clickhouse.com/blog/clickhouse-fully-supports-joins-hash-joins-part2#parallel-hash-join)は、入力データを分割して複数のハッシュテーブルを並列に構築することでビルドフェーズを高速化する、[ハッシュ結合](https://clickhouse.com/blog/clickhouse-fully-supports-joins-hash-joins-part2#hash-join)のバリエーションです。

以下の例では、ClickHouse が 2 つのハッシュテーブルを並列に構築するように [max_threads](https://clickhouse.com/docs/ja/reference/settings/session-settings) = 2 を設定しています。実際には、max_threads は通常、利用可能な CPU コア数から導出されます。

図は、orders と customer を結合する TPC-H クエリに対する、ClickHouse ではクエリパイプラインと呼ばれる物理クエリプランを示しています。

![Blog-JOINS-improvements.006.png](https://clickhouse.com/uploads/Blog_JOINS_improvements_006_d7c0b8e7ae.png)

① `customer` に対する述語がプッシュダウンされ、結合の前に適用されます。

② ビルドフェーズ中、`max_threads = 2` であるため、結合の右側であるフィルタリングされた `customer` の行が 2 つのバケットに分割されます。各バケットは、1 つのインメモリハッシュテーブルの構築に使用されます。

③ プローブフェーズ中、結合の左側である `orders` の行が並列にストリーミングされ、ルックアップのために対応するハッシュテーブルへとルーティングされます。


#### 課題: プローブ側の無駄な処理

ステップ ② において、ビルドフェーズ中にハッシュテーブルへ挿入されるのは顧客キーのサブセットのみである点に注目してください。しかしステップ ③ では、ClickHouse は依然としてすべての orders 行を処理します。顧客フィルターによって除外された顧客の orders については、ルックアップが成功することは決してありません。それらの行は、結合処理によって不一致として破棄される前に、CPU とメモリ帯域幅を消費し続けます。


#### アイデア: ルックアップの前にプローブ行をフィルタリングする

ランタイムフィルターは、まさにこの無駄な処理に対処するものです。

ClickHouse は、フィルタリングされた顧客行から(DRAM 内に)ハッシュテーブルを構築する際、ビルド側に実際に到達した結合キーからコンパクトなフィルター(ブルームフィルターまたは最小値/最大値)も作成します。この例では、c_nation = 'DE' を通過した顧客キーのみが含まれます。

そのフィルターは、ハッシュテーブルのルックアップを行う前に orders 側に適用されます。orders 行が、フィルタリングされたビルド側に存在しない o_custkey を持っている場合、ClickHouse はそれを結合にルーティングすることなく早期に破棄できます。

ランタイムフィルターはハッシュテーブル自体よりもはるかに小さいため、通常は L1 や L2 キャッシュなど、CPU の近くに留めておくことができます。


#### ランタイムフィルターがクエリパイプラインにどのように組み込まれるか


更新されたパイプラインは次のようになります。

![Blog-JOINS-improvements.007.png](https://clickhouse.com/uploads/Blog_JOINS_improvements_007_1cf7d0baf0.png)

更新されたパイプラインでは、2 つのステップが追加されています。

② フィルタリングされた customer 行を読み取る際、ClickHouse はそれらの結合キーからランタイムフィルターを構築します。

④ ハッシュテーブルをプローブする前に、ClickHouse はそのランタイムフィルターを orders 行に適用します。

⑤ ランタイムフィルターを通過した行のみが、ハッシュテーブルのルックアップへと進みます。

パラレルハッシュ結合の構造は維持されますが、コストの高いルックアップの前に多くのプローブ側の行が除外されます。

これによりパラレルハッシュ結合の構造は変わらないまま、一致する可能性のないプローブ側の行が排除されます。選択性の高い結合では、これによって CPU の処理量とメモリ帯域幅を大幅に削減できます。


#### クエリプラン上での現れ方

EXPLAIN plan を使用して、論理クエリプランでこれを確認できます。

まず、ランタイム事前フィルタリングを無効にした場合です。

<pre>
<code type='click-ui' language='sql' show_line_numbers='false'>
EXPLAIN plan
SELECT *
FROM orders, customer
WHERE o_custkey = c_custkey
SETTINGS enable_join_runtime_filters = 0;
</code>
</pre>

<pre><code type='click-ui' language='text' show_line_numbers='false'>
...                             
Join                                                       
...
ReadFromMergeTree (default.orders)                   
ReadFromMergeTree (default.customer) 
</code></pre>

プランの残りは省略し、中核となる仕組みに焦点を当てます。

出力を**下から上**に向かって読むと、ClickHouse が `orders` と `customer` の 2 つのテーブルからデータを読み取り、結合を実行する計画であることがわかります。

次に、同じ結合について、今度は**ランタイム事前フィルタリングを有効にした**論理クエリプランを確認してみましょう。

<pre>
<code type='click-ui' language='sql' show_line_numbers='false'>
EXPLAIN plan
SELECT *
FROM orders, customer
WHERE o_custkey = c_custkey
SETTINGS enable_join_runtime_filters = 1;
</code>
</pre>

プランの該当部分は次のようになります。


<pre><code type='click-ui' language='text' show_line_numbers='false'>
...
Join                                                                                                                                                                                             
...                                                                                                                                                                         
Prewhere filter column: __filterContains(_runtime_filter_14211390369232515712_0, __table1.o_custkey)                                                                          
...
BuildRuntimeFilter (Build runtime join filter on __table2.c_custkey (_runtime_filter_14211390369232515712_0))
...
</code></pre>

プランを**下から上**に向かって読むと、ClickHouse がまず ① **右側**の(`customer`)テーブルの結合キー値から**ランタイムフィルター**を構築していることがわかります。

このランタイムフィルターは、次に ② **左側**の(`orders`)テーブルで **[PREWHERE](https://clickhouse.com/docs/ja/concepts/features/performance/prewhere) フィルター**として適用され、結合が実行される**前**に関係のない行をスキップできるようにします。


#### ベンチマーク: 行数の削減と省メモリ化

効果を測定するために、クエリを少し拡張し、orders、customer、nation を結合してフランスの顧客の平均注文総額を計算します。

まず、**ランタイム事前フィルタリングを無効**にして開始します。

<pre>
<code type='click-ui' language='sql' show_line_numbers='false'>
SELECT avg(o_totalprice)
FROM orders, customer, nation
WHERE (c_custkey = o_custkey) AND (c_nationkey = n_nationkey) AND (n_name = 'FRANCE')
SETTINGS enable_join_runtime_filters = 0;
</code>
</pre>

<pre><code type='click-ui' language='text' show_line_numbers='false'>
┌──avg(o_totalprice)─┐
│ 151149.41468432106 │
└────────────────────┘

1 row in set. Elapsed: 1.005 sec. Processed 165.00 million rows, 1.92 GB (164.25 million rows/s., 1.91 GB/s.)
Peak memory usage: 1.24 GiB.

</code></pre>

次に、今度は**ランタイム事前フィルタリングを有効**にして同じクエリを再度実行します。

<pre>
<code type='click-ui' language='sql' show_line_numbers='false'>
SELECT avg(o_totalprice)
FROM orders, customer, nation
WHERE (c_custkey = o_custkey) AND (c_nationkey = n_nationkey) AND (n_name = 'FRANCE')
SETTINGS enable_join_runtime_filters = 1;
</code>
</pre>

<pre><code type='click-ui' language='text' show_line_numbers='false'>
┌──avg(o_totalprice)─┐
│ 151149.41468432106 │
└────────────────────┘

1 row in set. Elapsed: 0.471 sec. Processed 165.00 million rows, 1.92 GB (350.64 million rows/s., 4.08 GB/s.)
Peak memory usage: 185.18 MiB.
</code></pre>

#### 結果

> ランタイム事前フィルタリングにより、このクエリは **2.1 倍高速化**し、実行時間は 1.005 秒から 0.471 秒へと短縮され、ピークメモリも 1.24 GiB から 185 MiB に削減されました。

早い段階で行をフィルタリングすることで、ClickHouse は一致を生成しないプローブ側の処理を回避し、CPU の負荷とメモリ使用量の両方を削減します。


### ④ 統計情報に基づく結合順序変更

ClickHouse は、数十のテーブルにまたがる複雑な結合グラフと[主要なすべての結合タイプ](https://clickhouse.com/ja/blog/clickhouse-fully-supports-joins-part1-jp)で結合順序を並べ替えることができるようになりました。

SQL の結合は[結合則(結合法則)](https://en.wikipedia.org/wiki/Associative_property)を満たすため、これが重要になります。同じ論理クエリであっても、多くの場合、さまざまな結合順序を使用して実行できます。結果は同一ですが、実行時間は劇的に異なる可能性があります。


#### 課題: 結合順序の重要性

下の図は、同じ結合クエリに対する 3 つの異なる結合順序と、その結果として得られるハッシュ結合ツリーを示しています。クエリ結果は 3 つのケースすべてで同じです。

![Blog-JOINS-improvements.008.png](https://clickhouse.com/uploads/Blog_JOINS_improvements_008_6eab572778.png)

クエリが結合するテーブルが多くなるほど、結合順序はより重要になります。

前のセクションで説明したように、ハッシュ結合では右側を使用してインメモリハッシュテーブルを構築し、左側がそれをプローブします。小さい方の入力をビルド側に配置する方が、通常はるかに効率的です。

場合によっては、**適切な結合順序と不適切な結合順序の間で、実行時間に何桁もの差が生じることさえあります**。


#### 適切な結合順序の探索

探索空間が極めて急速に拡大するため、適切な結合順序を見つけるのは困難です。12 個のテーブルを結合する場合、可能な結合順序はすでに **28 兆通り**に達します。

そのため、ClickHouse は結合順序オプティマイザを使用します。大まかには、候補となる結合ツリーを列挙し、そのコストを見積もり、優れたツリーを選択します。

![Blog-JOINS-improvements.009.png](https://clickhouse.com/uploads/Blog_JOINS_improvements_009_b14a1bc01e.png)

#### カラム統計情報によるオプティマイザの有効化

オプティマイザには、カーディナリティの推定値(フィルタや結合述語を適用した後に、各中間結合結果に何行含まれると予想されるか)が必要です。

これらの推定値は[カラム統計情報](https://clickhouse.com/docs/ja/reference/engines/table-engines/mergetree-family/mergetree#available-types-of-column-statistics)から得られます。26.4 以降、ClickHouse はすべてのテーブルに対してこれらの統計情報を自動的に生成するため、デフォルト設定のままで統計情報に基づく結合順序変更が有効に機能します。

小規模な結合グラフの場合、ClickHouse は全探索動的計画法(DPSize)を使用して最適な順序を見つけることができます。より大規模な結合グラフの場合は、はるかに高速ですが最適解の発見が保証されない貪欲探索を使用します。

![Blog-JOINS-improvements.010.png](https://clickhouse.com/uploads/Blog_JOINS_improvements_010_3fb770935a.png)

#### ベンチマーク: 1 時間から 2.7 秒へ

その効果を測定するために、同じ 6 テーブルの TPC-H クエリを 2 回実行しました。

1. 統計情報なし: [デフォルトの DDL](https://clickhouse.com/docs/ja/get-started/sample-datasets/tpch#data-generation-and-import) で作成されたテーブル。

2. 統計情報あり: [カラム統計情報を有効にする拡張 DDL](https://pastila.nl/?01031010/7475d7ba575a41d9fd86eaaff97cb201#SGYcmaof4DoQzewNH3tBKA==) で作成されたテーブル。

どちらの実行でも、同じクエリと同じ結合順序オプティマイザ設定を使用しました。

<pre>
<code type='click-ui' language='sql' show_line_numbers='false'>
SELECT
  n_name,
  sum(l_extendedprice * (1 - l_discount)) AS revenue
FROM
  customer, 
  orders, 
  lineitem, 
  supplier, 
  nation, 
  region
WHERE
c_custkey = o_custkey
AND l_orderkey = o_orderkey
AND l_suppkey = s_suppkey
AND c_nationkey = s_nationkey
AND s_nationkey = n_nationkey
AND n_regionkey = r_regionkey
AND r_name = 'ASIA'
AND o_orderdate >= DATE '1994-01-01'
AND o_orderdate < DATE '1994-01-01' + INTERVAL '1' year
GROUP BY
  n_name
ORDER BY
  revenue DESC;
</code>
</pre>

まず、**統計情報なし**のテーブルでクエリを実行しました。

<pre><code type='click-ui' language='text' show_line_numbers='false'>
USE tpch_no_stats;
SET query_plan_optimize_join_order_limit = 10;
SET allow_statistics_optimize = 1;
<TEST-QUERY>
</code></pre>

<pre><code type='click-ui' language='text' show_line_numbers='false'>
┌─n_name────┬─────────revenue─┐
│ VIETNAM   │  5310749966.867 │
│ INDIA     │ 5296094837.7503 │
│ JAPAN     │ 5282184528.8254 │
│ CHINA     │ 5270934901.5602 │
│ INDONESIA │ 5270340980.4608 │
└───────────┴─────────────────┘

5 rows in set. Elapsed: 3903.678 sec. Processed 766.04 million rows, 16.03 GB (196.23 thousand rows/s., 4.11 MB/s.)
Peak memory usage: 99.12 GiB.
</code></pre>

<p>1 時間以上かかりました！ &#x1F40C; しかも 99 GiB のメインメモリを使用しました。</p>


次に、**統計情報あり**のテーブルで同じクエリを再実行しました。

<pre><code type='click-ui' language='text' show_line_numbers='false'>
USE tpch_stats;
SET query_plan_optimize_join_order_limit = 10;
SET allow_statistics_optimize = 1;
<TEST-QUERY>
</code></pre>

<pre><code type='click-ui' language='text' show_line_numbers='false'>
┌─n_name────┬─────────revenue─┐
│ VIETNAM   │  5310749966.867 │
│ INDIA     │ 5296094837.7503 │
│ JAPAN     │ 5282184528.8254 │
│ CHINA     │ 5270934901.5602 │
│ INDONESIA │ 5270340980.4608 │
└───────────┴─────────────────┘

5 rows in set. Elapsed: 2.702 sec. Processed 638.85 million rows, 14.76 GB (236.44 million rows/s., 5.46 GB/s.)
Peak memory usage: 3.94 GiB.
</code></pre>

#### 結果

> 統計情報に基づく結合順序変更により、6 テーブルの TPC-H クエリは 3903.7 秒から 2.7 秒へと短縮され(約 **1,450 倍高速化**)、ピークメモリも 99.1 GiB から 3.9 GiB に減少しました。

<br/>

## これによって実現したこと

2 年前、私たちは結合を ClickHouse の第一級の強みにすることを決めました。

最初の 1 年で基盤を築きました。並列ハッシュ結合の高速化、ビルド/プローブ側のより賢い選択、プランニングの改善、より積極的なフィルタのプッシュダウン、そしてローカルな結合順序変更です。25.4 までに、同じ TPC-H SF100 の結合多用ワークロードは 22.4 と比較してすでに **4.4 倍高速化**していました。

2 年目には、結合はさらに劇的な進化を遂げました。25.4 から 26.4 の間で、オプティマイザと実行エンジンのさらなる改善の波により、同じワークロードがデフォルト設定のままで**さらに 6 倍高速化**しました。

上記で取り上げた 4 つの改善は、それぞれ結合の課題の異なる側面に対処したものです。

**① 相関サブクエリ**により、SQL 互換性における大きなギャップが解消されました。ClickHouse は、以前は手動での書き換えが必要だったり、完全に除外せざるを得なかった TPC-H クエリを含め、`SELECT`、`WHERE`、`HAVING` にわたる相関サブクエリを実行できるようになりました。

**② Lazy Column Replication** により、結合によって生成される重複した値の物理コピーが回避されます。TPC-H の例では、結合が **1.9 倍高速化**しました。

**③ ランタイムフィルタ**は、行がハッシュテーブルのルックアップに到達する前にフィルタリングすることで、プローブ側の無駄な処理を削減します。ベンチマークでは、ランタイム事前フィルタリングによりクエリが **2.1 倍高速化**し、ピークメモリが**約 7 分の 1** に削減されました。

**④ 統計情報に基づく結合順序変更**により、ClickHouse はより優れた物理結合プランを自動的に選択できるようになります。6 テーブルの TPC-H クエリでは、実行時間が **3903.7 秒から 2.7 秒**へと短縮され(約 **1,450 倍高速化**)、ピークメモリは **99.1 GiB から 3.9 GiB** に減少しました。

これらを総合すると、結合は「ClickHouse でも一応できること」から、**競争力を持って、自動的に、しかもデフォルト設定で**実行できるものへと変化しました。

これによって[関連ベンチマーク](/ja/blog/tpc-h-clickhouse-cloud-vs-snowflake-databricks-bigquery-redshift-jp)の実施が可能になりました。ClickHouse Cloud は、結合を多用する TPC-H ワークロード全体を、Snowflake、Databricks、BigQuery、Redshift と比較して高速かつコスト効率良く、**1 セント未満**で実行できるようになりました。

![Blog-JOINS-results.005.png](https://clickhouse.com/uploads/Blog_JOINS_results_005_f2a4c0e257.png)

## 次の展開

 
ClickHouse は結合において大きな進歩を遂げてきました。

2 年前、TPC-H SF100 ワークロードの状況は大きく異なっていました。結合に特化したエンジニアリングの最初の 1 年を経て、すでに **4.4 倍高速化**していました。2 年目を経た現在、デフォルト設定のもとで直近の 1 年間だけでさらに **6 倍の高速化**を果たし、**22.4 と比較して 26 倍高速**になっています。

これが今回の記事でお伝えした内容です。結合を ClickHouse の第一級の強みにするための最初の 2 年間の歩みです。

しかし、私たちはまだ始まったばかりです。TPC-H SF1000 以上のより大規模なワークロードに向けた次のステップの 1 つとして、[分散結合](/ja/blog/multi-stage-distributed-query-execution-clickhouse-cloud-jp)を掲げ、すでに 3 年目の取り組みを進めています。

1 年後にまた進捗をご報告します。

> 「結合パフォーマンスの最適化はいつやめるのか？ 決してやめません！」 – Alexey Milovidov(ClickHouse 開発者)、[ClickHouse 25.10 リリースウェビナーにて](https://www.youtube.com/live/cV2hiOCzDG4?si=hEjfRABUz6ZIfp-w&t=1866)