ALGORITHM VISUALIZER

Nested Loop Join(ネステッドループ結合)

外側テーブルの各行に対して内側テーブルの全行を走査し、結合条件を評価する最も基本的なJOINアルゴリズム O(N×M)

コントロール
速度: 5
ステップ: 0 / 0
統計
外側テーブル行数
0
内側テーブル行数
0
総比較回数
0
マッチ数
0
結果
まだマッチなし
操作ログ
ステップを進めるとログが表示されます
NESTED LOOP JOINREADY
「次へ」を押してJOIN処理を開始
PRESS NEXT TO BEGIN
解説

📌
Nested Loop Joinとは

Nested Loop Join(ネステッドループ結合)は、リレーショナルデータベースにおける最も基本的なJOINアルゴリズムです。SQLの SELECT * FROM users JOIN departments ON users.dept_id = departments.id のようなJOIN文がデータベース内部でどのように実行されるかを理解するための基礎となるアルゴリズムです。

日常的な例で考えてみましょう。教室で出席確認をする場面を想像してください。名簿の1人目「田中」に対して、教室にいる全員に「あなたは田中さんですか?」と順番に聞いていきます。全員に聞き終わったら、名簿の2人目「鈴木」に対して、再び教室にいる全員に「あなたは鈴木さんですか?」と聞いていく。これをすべての名前について繰り返す。これがまさにNested Loop Joinの動作です。

技術的には、外側テーブル(駆動表)の各行に対して、内側テーブル(内部表)の全行を走査する二重ループ構造です。計算量は O(N × M) で、外側テーブルがN行、内側テーブルがM行のとき、最大N×M回の比較が発生します。このデモでは5行×3行 = 15回の比較を行います。

PostgreSQLで EXPLAIN を使うと、実行計画に Nested Loop と表示されることがあります。これは、オプティマイザがこのアルゴリズムを選択したことを意味します。特に小さいテーブル同士のJOINや、内側テーブルにインデックスがある場合に選ばれやすいアルゴリズムです。

Nested Loop Joinが使われるSQL例
  • SELECT u.name, d.dept_name FROM users u JOIN departments d ON u.dept_id = d.id
  • SELECT o.order_id, c.name FROM orders o JOIN customers c ON o.customer_id = c.id WHERE o.date > '2024-01-01'
  • SELECT e.name, m.name AS manager FROM employees e JOIN employees m ON e.manager_id = m.id

🔍
このツールの値は何を表している?

上のビジュアライザでは、2つのテーブルを使ってNested Loop Joinの動作を可視化しています。外側テーブル = users(社員テーブル)には5名の社員データが、内側テーブル = departments(部署テーブル)には3つの部署データが入っています。

usersテーブルの dept_id カラムが外部キーで、departmentsテーブルの id カラムに対応しています。例えば dept_id=1 は「営業部」、dept_id=2 は「開発部」、dept_id=3 は「人事部」に対応します。

JOINの結合条件は users.dept_id = departments.id です。この条件が一致する行の組み合わせが結果として出力されます。紫色のハイライトは現在比較中の行を、緑色は既にマッチが見つかった行を示しています。

users田中 dept_id=1鈴木 dept_id=2佐藤 dept_id=1高橋 dept_id=3departmentsid=1 営業部id=2 開発部id=3 人事部dept_id = id

⚙️
Nested Loop Joinの仕組み

1. 二重ループの構造

Nested Loop Joinは名前のとおり、ネスト(入れ子)されたループで構成されます。外側ループが1回進むたびに、内側ループが最初から最後まで全行走査します。

外側ループ: for each row in users内側ループ: for each row in departmentsif (users.dept_id == departments.id) → 結果に追加計算量: 5行 × 3行 = 15回の比較

2. 比較の流れ(全15ステップ)

各ステップで外側テーブルの1行と内側テーブルの1行を比較します。外側のポインタが固定されている間に内側のポインタが全行を走査する様子を確認してください。

#外側(users)内側(departments)比較結果
1田中(dept_id=1)営業部(id=1)1==1マッチ
2田中(dept_id=1)開発部(id=2)1!=2-
3田中(dept_id=1)人事部(id=3)1!=3-
4鈴木(dept_id=2)営業部(id=1)2!=1-
5鈴木(dept_id=2)開発部(id=2)2==2マッチ
6鈴木(dept_id=2)人事部(id=3)2!=3-
7佐藤(dept_id=1)営業部(id=1)1==1マッチ
8佐藤(dept_id=1)開発部(id=2)1!=2-
9佐藤(dept_id=1)人事部(id=3)1!=3-
10高橋(dept_id=3)営業部(id=1)3!=1-
11高橋(dept_id=3)開発部(id=2)3!=2-
12高橋(dept_id=3)人事部(id=3)3==3マッチ
13伊藤(dept_id=2)営業部(id=1)2!=1-
14伊藤(dept_id=2)開発部(id=2)2==2マッチ
15伊藤(dept_id=2)人事部(id=3)2!=3-

3. マッチ判定の仕組み

各比較ステップで、外側テーブルの結合カラム(dept_id)と内側テーブルの結合カラム(id)が等しいかを判定します。一致すれば両方の行を結合して結果行として出力します。

田中 dept_id=1== ?営業部 id=11 == 1 → マッチ!

📊
特徴

1
実装が最もシンプル
二重のforループだけで実装できるため、データベースエンジンの基本的なJOIN実装として必ず存在します。コードが単純なため、バグが入りにくくメンテナンスしやすいという利点があります。
2
メモリ使用量が最小
一度に外側テーブルの1行と内側テーブルの1行しかメモリ上に保持する必要がありません。Hash JoinやSort-Merge Joinがテーブル全体をメモリに載せる必要があるのに対し、極めて省メモリで動作します。
3
小規模テーブルで高速
数十行程度の小さなテーブル同士のJOINでは、Hash Joinのハッシュテーブル構築コストやSort-Merge Joinのソートコストが不要なため、最も高速に動作します。
4
インデックスとの相性が良い
内側テーブルにインデックスがある場合、全行走査がインデックスルックアップに置き換わり、計算量がO(N × log M)に改善されます。これがIndex Nested Loop Joinと呼ばれるバリエーションです。
5
大規模テーブルでは非効率
両テーブルが大きい場合(例:100万行×100万行)、比較回数が爆発的に増加します。このような場合はHash JoinやSort-Merge Joinが圧倒的に効率的です。

🎯
ユースケース

マスタテーブルとの結合
社員テーブルと部署マスタ(数十行)のJOINなど、片方が小規模なテーブルの場合。マスタテーブルのid列にはインデックスがあるため、Index NLJとして高速に動作します。
OLTP(トランザクション処理)
主キーや外部キーで少数の行を結合する処理。例:注文詳細画面で注文IDから商品名を引く。インデックスがあれば1注文あたりの結合コストはO(log M)程度で済みます。
NOT INやEXISTSの実装
SQLのNOT IN (サブクエリ) やEXISTS句は、内部的にNested Loop(またはAnti/Semi Join変種)として実行されることが多いです。外側テーブルの各行に対してサブクエリの結果を確認する構造です。
クロスジョイン(直積)
CROSS JOINは結合条件なしの全組み合わせ生成です。NLJの内側ループでフィルタなしに全行を出力するだけなので、最も自然に実装できます。テスト用データ生成などに使われます。

📖
用語解説

外側テーブル(Outer Table / 駆動表)
二重ループの外側で走査されるテーブルです。「駆動表(Driving Table)」とも呼ばれ、このテーブルの行数がループの外側の反復回数を決定します。オプティマイザは通常、行数の少ないテーブルを外側に選択して総比較回数を最小化します。PostgreSQLのEXPLAINでは上位に表示されるテーブルが外側です。
内側テーブル(Inner Table / 内部表)
二重ループの内側で繰り返し走査されるテーブルです。外側テーブルの各行に対して全行がスキャンされます。インデックスが存在する場合は全行スキャンの代わりにインデックスルックアップが使われ、大幅に高速化されます。
結合条件(Join Condition / Join Predicate)
2つのテーブルのどの行を結合するかを決めるWHERE句またはON句の条件です。例:users.dept_id = departments.id。この条件が満たされた行の組み合わせだけが結果に含まれます。結合条件がない場合は全組み合わせ(クロスジョイン)になります。
等価結合(Equi-Join)
結合条件が等号(=)による比較のJOINです。Nested Loop Joinはどんな結合条件(不等号、BETWEEN、LIKEなど)にも対応できますが、等価結合の場合はHash JoinやSort-Merge Joinも選択肢になります。等価結合は最も一般的なJOIN形態です。
駆動表(Driving Table)
JOINの実行計画において最初にアクセスされるテーブルのことです。Nested Loop Joinでは外側テーブルが駆動表です。オプティマイザはテーブルの統計情報(行数、カーディナリティ)を基に、最もコストの低い順序を自動的に選択します。STRAIGHT_JOIN(MySQL)やLEADING(Oracle)ヒントで手動指定も可能です。

📋
Nested Loop Joinの手順

1
外側テーブルの最初の行を取得
usersテーブルの1行目「田中(dept_id=1)」を取得します。これが外側ループのカレント行になります。
2
内側テーブルを先頭から走査開始
departmentsテーブルの1行目「営業部(id=1)」を取得し、結合条件 dept_id=1 == id=1 を評価します。一致するので結果に「田中 + 営業部」を追加します。
3
内側テーブルの残りの行を走査
departmentsテーブルの2行目「開発部(id=2)」→ dept_id=1 != id=2 で不一致。3行目「人事部(id=3)」→ dept_id=1 != id=3 で不一致。内側テーブルの走査が完了しました(3回比較)。
4
外側テーブルの次の行に進む
usersテーブルの2行目「鈴木(dept_id=2)」に移動します。内側テーブルのポインタを先頭にリセットし、再び全行を走査します。
5
全組み合わせの比較を継続
鈴木×営業部(2!=1)→不一致、鈴木×開発部(2==2)→マッチ、鈴木×人事部(2!=3)→不一致。同様に佐藤、高橋、伊藤についても内側テーブル全行と比較します。
6
全行の走査完了、結果を返却
外側テーブル5行×内側テーブル3行=15回の比較が完了。5件のマッチが見つかりました:田中+営業部、鈴木+開発部、佐藤+営業部、高橋+人事部、伊藤+開発部。これがJOINの結果セットです。

⚖️
JOINアルゴリズム比較

Nested Loop JoinBlock NLJIndex NLJHash JoinSort-Merge Join
計算量O(N×M)O(N×M/B)O(N×logM)O(N+M)O(NlogN+MlogM)
メモリ最小中(バッファ)最小大(ハッシュ表)大(ソート領域)
インデックス不要不要必須(内側)不要不要(あると有利)
結合条件任意任意等価/範囲等価のみ等価/範囲
小規模テーブル最適良好最適構築コスト大ソートコスト大
大規模テーブル非効率改善インデックス次第最適良好
ソート済み出力なしなしなしなしあり

Block Nested Loop Join(Block NLJ)は、外側テーブルの行を1行ずつではなくブロック単位でバッファに読み込み、内側テーブルの各行をバッファ内の全行と一度に比較するバリエーションです。ディスクI/Oの回数を大幅に削減できるため、通常のNLJよりも効率的です。MySQLでは join_buffer_size パラメータでバッファサイズを調整できます。

Index Nested Loop Join(Index NLJ)は、内側テーブルの結合カラムにインデックスがある場合に使われます。内側テーブルの全行走査がインデックスルックアップに置き換わるため、計算量がO(N × log M)に改善されます。実際のデータベースでは最もよく使われるNLJの形態です。

Hash JoinSort-Merge Joinは大規模テーブルのJOINに適したアルゴリズムです。Hash Joinは内側テーブルのハッシュテーブルを構築してO(1)ルックアップを実現し、Sort-Merge Joinは両テーブルをソートしてマージすることで効率的に結合します。

関連コンテンツ