ClickHouse における JOIN の使い方
ClickHouse は標準 SQL の JOIN を完全にサポートしており、効率的なデータ分析が可能です。 このガイドでは、正規化された IMDB データセット(relational dataset repository 由来)を用いた Venn 図とサンプルクエリを通じて、よく利用される代表的な JOIN の種類とその使い方を解説します。
テストデータとリソース
テーブルの作成とロード手順はこちらにあります。 テーブルをローカルで作成・ロードしたくないユーザー向けに、このデータセットは playground からも利用できます。
以下のサンプルデータセット内の 4 つのテーブルを使用します。

これら 4 つのテーブルのデータは、1 つ以上のジャンルを持つことができる映画を表しています。 映画の役は俳優によって演じられます。
上の図の矢印は外部キーと主キーの関係を表しています。例えば、genres テーブルのある行の movie_id カラムには、movies テーブルの行の id の値が格納されています。
映画と俳優の間には多対多の関係があります。
この多対多の関係は、roles テーブルを使用して 2 つの一対多の関係に正規化されています。
roles テーブルの各行には、movies テーブルと actors テーブルの id カラムの値が格納されています。
ClickHouse でサポートされている結合の種類
ClickHouse は次の結合の種類をサポートしています:
次のセクションでは、上記それぞれの JOIN の種類に対するサンプルクエリを示します。
INNER JOIN
INNER JOIN は、結合キーでマッチする各行の組み合わせごとに、左側のテーブルの行のカラム値と右側のテーブルの行のカラム値を結合して返します。
ある行が複数回マッチする場合は、そのすべてのマッチが返されます(つまり、結合キーが一致する行に対して デカルト積 が生成されます)。

このクエリは、movies テーブルと genres テーブルを結合することで、各映画のジャンル(1つまたは複数)を取得します。
INNER キーワードは省略できます。
INNER JOIN の挙動は、次のいずれかの他の結合タイプを使用することで拡張または変更できます。
(LEFT / RIGHT / FULL) OUTER JOIN
LEFT OUTER JOIN は INNER JOIN と同様に動作しますが、左テーブル側で結合条件に一致しない行については、右テーブルのカラムに対して ClickHouse がデフォルト値を返します。
RIGHT OUTER JOIN クエリも同様で、右テーブル側で結合条件に一致しない行の値とともに、左テーブルのカラムに対するデフォルト値を返します。
FULL OUTER JOIN クエリは LEFT OUTER JOIN と RIGHT OUTER JOIN を組み合わせたもので、左テーブルおよび右テーブルの結合条件に一致しない行の値と、それぞれ右および左テーブルのカラムに対するデフォルト値を返します。

ClickHouse は、デフォルト値の代わりに NULL を返すように設定できます(ただし、パフォーマンス上の理由から、あまり推奨されません)。
次のクエリは、ジャンルを持たないすべての映画を検索します。genres テーブルに一致する行を持たない movies テーブルのすべての行を取得し、その結果として、クエリ実行時に movie_id カラムにはデフォルト値 0 が設定されます。
OUTER キーワードは省略可能です。
CROSS JOIN
CROSS JOIN は、結合キーを考慮せずに 2 つのテーブルの完全な直積を生成します。
左側のテーブルの各行は、右側のテーブルの各行と組み合わされます。

したがって、次のクエリでは、movies テーブルの各行が genres テーブルの各行と組み合わされます。
前の例のクエリ単体ではあまり意味がありませんでしたが、WHERE 句を追加して一致する行を関連付けることで、各映画のジャンルを特定するための INNER JOIN の動作を再現できます。
CROSS JOIN の代替構文として、FROM 句で複数のテーブルをカンマ区切りで指定する方法があります。
ClickHouse は、クエリの WHERE 句に結合条件となる式がある場合、CROSS JOIN を INNER JOIN に書き換えます。
この動作は、例のクエリに対して EXPLAIN SYNTAX を使うことで確認できます(EXPLAIN SYNTAX は、クエリが実行される前に書き換えられる、構文レベルで最適化されたクエリのバージョンを返します):
構文上最適化された CROSS JOIN クエリ版では、INNER JOIN 句に ALL キーワードが明示的に追加されています。これは、INNER JOIN に書き換えた場合でも CROSS JOIN のデカルト積のセマンティクスを維持するためであり、INNER JOIN ではデカルト積が無効化される場合があるためです。
そして、上で述べたように、RIGHT OUTER JOIN では OUTER キーワードを省略でき、さらにオプションの ALL キーワードを追加できるので、ALL RIGHT JOIN と書いても問題なく動作します。
(LEFT / RIGHT) SEMI JOIN
LEFT SEMI JOIN クエリは、右テーブルに少なくとも 1 つの結合キーのマッチがある左テーブルの各行について、そのカラム値を返します。
最初に見つかったマッチだけが返されます(デカルト積は発生しません)。
RIGHT SEMI JOIN クエリも同様で、左テーブルに少なくとも 1 つのマッチがある右テーブルのすべての行について値を返しますが、やはり最初に見つかったマッチだけが返されます。

このクエリは、2023 年に映画に出演したすべての俳優・女優を抽出します。
通常の(INNER)結合では、2023 年に複数の役を持っている場合、同じ俳優・女優が複数回表示される点に注意してください。
(LEFT / RIGHT) ANTI JOIN
LEFT ANTI JOIN は、左側のテーブルで一致しない行のすべてのカラム値を返します。
同様に、RIGHT ANTI JOIN は、右側のテーブルで一致しない行のすべてのカラム値を返します。

前の外部結合の例クエリは、データセット内でジャンルを持たない映画を見つけるために ANTI JOIN を使用する形で書き換えることもできます。
(LEFT / RIGHT / INNER) ANY JOIN
LEFT ANY JOIN は LEFT OUTER JOIN と LEFT SEMI JOIN を組み合わせたものであり、ClickHouse は左テーブルの各行に対して、右テーブルで一致する行が存在する場合はその行のカラム値と結合し、一致する行が存在しない場合は右テーブルのデフォルトのカラム値と結合したカラム値を返します。
左テーブルの 1 行に対して右テーブル側に複数の一致がある場合、ClickHouse は最初に見つかった一致からの結合後のカラム値のみを返し(カルテジアン積は無効化されます)、それ以外は返しません。
同様に、RIGHT ANY JOIN は RIGHT OUTER JOIN と RIGHT SEMI JOIN を組み合わせたものです。
また、INNER ANY JOIN はカルテジアン積を無効化した INNER JOIN です。

次の例では、2 つの一時テーブル(left_table と right_table)を使用した抽象的な例を用いて、LEFT ANY JOIN を示します。これらのテーブルは values テーブル関数 を使って構築されています。
こちらは、RIGHT ANY JOIN を使用した同じクエリです:
次は INNER ANY JOIN を使用したクエリです。
ASOF JOIN
ASOF JOIN は、非完全一致のマッチングを行える機能を提供します。
左側のテーブルの行に右側のテーブルで完全一致する行が存在しない場合、右側のテーブルから「最も近い」行が代わりにマッチとして使用されます。
これは特に時系列分析で有用で、クエリの複雑さを大幅に削減できます。

次の例では、株式市場データの時系列分析を行います。
quotes テーブルには、1 日の特定の時刻における銘柄の気配値が格納されています。
この例のデータでは、価格は 10 秒ごとに更新されます。
trades テーブルには銘柄の取引が一覧として格納されており、特定の時刻に特定の出来高でその銘柄が買われたことを表します:

各取引の正確なコストを計算するには、その取引に最も近い時刻の気配値と対応付ける必要があります。
これは ASOF JOIN を使うと簡潔に表現できます。ON 句で完全一致の条件を指定し、AND 句で最も近いマッチ条件を指定します。つまり、特定のシンボル(完全一致)に対して、そのシンボルの取引が発生した時刻と同じかそれ以前の quotes テーブルの行のうち、「最も近い」時刻を持つ行(非完全一致)を探します。
ASOF JOIN では ON 句が必須であり、AND 句の非厳密な一致条件に加えて、厳密な一致条件を指定します。
まとめ
このガイドでは、ClickHouseがすべての標準的な SQL の JOIN 型に加え、分析クエリを強化するための専用の JOIN もサポートしていることを説明します。 JOIN の詳細については、JOIN 文のドキュメントを参照してください。