結合の種類(Nested Loop / Hash Join / Merge Join)
Nested Loop は外側の 1 行ごとに内側を引く方式、Hash Join は片側でハッシュ表を作ってもう片側を突き合わせる方式、Merge Join は両側をキー順に並べて突き合わせる方式である。オプティマイザは行数の見積りとインデックスの有無からどれかを選ぶ。
3 つのやり方がある
2 つのテーブルを結合するとき、データベースには 3 通りのやり方があります。どれを選ぶかは、行数の見積りとインデックスの有無で決まります。
Nested Loop — 外側 1 行ごとに内側を引く
矢印の本数 = 内側を引いた回数 = loops。1 回で何行返るかは別の話
loops。外側が 3 行なら 3 回、25 万行なら 25 万回。外側の見積りが外れると、この本数がまるごと外れるので事故になる。3 本とも同じ入口に入るのが Nested Loop の特徴で、 内側を毎回引き直している(Merge Join のように前へ進むのではない)。 内側にインデックスがあって、外側が少ないときだけ安い。 図は仕組みの模式で、実際の回数は計画の loops に出る。 Nested Loop (cost=0.85..28.26 rows=4 width=17) (actual time=0.027..0.036 rows=4.00 loops=1)
-> Index Scan using members_pkey on members m (cost=0.42..8.44 rows=1 width=17) (actual time=0.015..0.015 rows=1.00 loops=1)
Index Cond: (id = 42)
Index Searches: 1
-> Index Scan using orders_s_member_idx on orders_s o (cost=0.43..19.78 rows=4 width=8) (actual time=0.011..0.018 rows=4.00 loops=1)
Index Cond: (member_id = 42)
Index Searches: 1
Planning Time: 0.239 ms
Execution Time: 0.076 ms外側が 1 行なので、内側を 1 回引くだけで済む。
PostgreSQL 18.6 / 2026-08-22 採取 / 並列・JIT は OFF
外側から 1 行取り、その値で内側を引く。これを外側の行数だけ繰り返します。
- 向いているとき — 外側が少なく、内側にインデックスがある
- 向かないとき — 外側が多い。回数がそのまま効いてくる
内側のノードに loops= が付くのが目印です。 この計画では外側が 1 行なので loops=1、つまり 1 回引いて終わりです。
Hash Join — 片側でハッシュ表を作る
内側は 1 回だけ読む。外側も 1 回だけ流す(各 1 パス)
Batches が 2 以上になる。 Hash Join (cost=4534.20..40595.24 rows=40310 width=17) (actual time=6.428..138.374 rows=40000.00 loops=1)
Hash Cond: (o.member_id = m.id)
-> Seq Scan on orders_s o (cost=0.00..30811.00 rows=2000000 width=8) (actual time=0.047..51.442 rows=2000000.00 loops=1)
-> Hash (cost=4407.95..4407.95 rows=10100 width=17) (actual time=6.344..6.345 rows=10000.00 loops=1)
Buckets: 16384 Batches: 1 Memory Usage: 636kB
-> Bitmap Heap Scan on members m (cost=114.70..4407.95 rows=10100 width=17) (actual time=0.821..5.602 rows=10000.00 loops=1)
Recheck Cond: (city = 'city-7'::text)
Heap Blocks: exact=4167
-> Bitmap Index Scan on members_city_idx (cost=0.00..112.17 rows=10100 width=0) (actual time=0.484..0.484 rows=10000.00 loops=1)
Index Cond: (city = 'city-7'::text)
Index Searches: 1
Planning Time: 0.456 ms
Execution Time: 139.601 ms内側でハッシュ表を作り、外側を流しながら突き合わせる。
PostgreSQL 18.6 / 2026-08-22 採取 / 並列・JIT は OFF
先に内側を全部読んでハッシュ表を作り、 そのあと外側を 1 行ずつ流して突き合わせます。
- 向いているとき — 両側とも行数が多い。等値結合である
- 向かないとき — ハッシュ表がメモリに収まらない
Hash というノードが子に現れ、その中にBuckets / Batches / Memory Usage が出ます。Batches が 2 以上なら、メモリに収まらず分割しているという意味です (ソートとメモリ)。
Hash Join は内側を全部読み終わるまで 1 行も返せません。だから開始コストが大きくなります。
Merge Join — 両側を並べて突き合わせる
両側とも 1 回ずつ、前から順に読むだけ
Merge Join (cost=2.48..122529.37 rows=1995534 width=8) (actual time=0.027..380.488 rows=2000000.00 loops=1)
Merge Cond: (m.id = o.member_id)
-> Index Only Scan using members_pkey on members m (cost=0.42..12996.42 rows=500000 width=4) (actual time=0.015..26.957 rows=500000.00 loops=1)
Heap Fetches: 0
Index Searches: 1
-> Index Scan using orders_s_member_idx on orders_s o (cost=0.43..83328.21 rows=2000000 width=8) (actual time=0.010..222.412 rows=2000000.00 loops=1)
Index Searches: 1
Planning Time: 0.398 ms
Execution Time: 418.211 ms両側がキー順に並んでいるので、前から順に突き合わせるだけで済む。
PostgreSQL 18.6 / 2026-08-22 採取 / 並列・JIT は OFF
両側を結合キーの順に並べて、前から順に突き合わせます。 トランプの神経衰弱ではなく、ソート済みの 2 つの束を合わせるイメージです。
- 向いているとき — 両側がすでに並んでいる (インデックスを順に辿ればソート済みで取れる)
- 向かないとき — 並べ直しが必要で、その費用が高い
この計画では両側とも Index Only Scan / Index Scan で並んだ状態で取れているので、並べ直しの費用がかかっていません。Merge Cond: が目印です。
選ばれ方は「行数の見積り」でほぼ決まる
3 つのうちどれを選ぶかは、外側が何行返ると見積もったかでほぼ決まります。
- 外側が少ないと見積もった → Nested Loop(内側を数回引くだけなら安い)
- 外側が多いと見積もった → Hash Join(まとめて突き合わせた方が安い)
- 両側がすでに並んでいる → Merge Join
つまり見積りが外れると、選択そのものが間違います。
Nested Loop が事故るとき
Limit (cost=196523.68..196523.70 rows=10 width=30) (actual time=2160.299..2160.379 rows=10.00 loops=1)
-> Sort (cost=196523.68..196526.29 rows=1046 width=30) (actual time=2160.293..2160.320 rows=10.00 loops=1)
Sort Key: (sum((i.price * i.qty))) DESC
Sort Method: top-N heapsort Memory: 26kB
-> GroupAggregate (cost=196477.54..196501.07 rows=1046 width=30) (actual time=2090.929..2158.921 rows=20000.00 loops=1)
Group Key: c.name
-> Sort (cost=196477.54..196480.15 rows=1046 width=22) (actual time=2090.883..2131.767 rows=500000.00 loops=1)
Sort Key: c.name
Sort Method: external merge Disk: 16656kB
-> Nested Loop (cost=597.43..196425.08 rows=1046 width=22) (actual time=3.118..1561.364 rows=500000.00 loops=1)
-> Hash Join (cost=597.00..57141.20 rows=523 width=18) (actual time=3.087..177.650 rows=250000.00 loops=1)
Hash Cond: (o.customer_code = c.code)
-> Seq Scan on orders o (cost=0.00..56537.00 rows=526 width=11) (actual time=0.129..127.002 rows=250000.00 loops=1)
Filter: ((ordered_at >= '2026-07-01'::date) AND (status = 'shipped'::text) AND (channel = 'web'::text) AND (payment = 'card'::text))
Rows Removed by Filter: 1750000
-> Hash (cost=347.00..347.00 rows=20000 width=21) (actual time=2.844..2.847 rows=20000.00 loops=1)
Buckets: 32768 Batches: 1 Memory Usage: 1309kB
-> Seq Scan on customers c (cost=0.00..347.00 rows=20000 width=21) (actual time=0.053..1.144 rows=20000.00 loops=1)
-> Index Scan using order_items_order_id_idx on order_items i (cost=0.43..266.09 rows=23 width=12) (actual time=0.005..0.005 rows=2.00 loops=250000)
Index Cond: (order_id = o.id)
Filter: (qty = 3)
Rows Removed by Filter: 4
Index Searches: 250000
Planning Time: 3.419 ms
Execution Time: 2162.557 ms外側の見積りが 526 行、実測は 250,000 行。内側が 25 万回まわっている。
PostgreSQL 18.6 / 2026-08-22 採取 / 並列・JIT は OFF
この計画では、外側の Seq Scan on orders が526 行と見積もられて、実際は 250,000 行返っています(475 倍)。
プランナは「526 行なら内側を 526 回引くだけ」と考えて Nested Loop を選びました。 実際は loops=250000 です。1 回あたり 0.005 ミリ秒でも、25 万回まわれば 1.25 秒になります。
Nested Loop 自体が悪いのではありません。外側の見積りが外れたことが原因で、そこを直せば選択も変わります。 この計画を最後まで解いているのが遅いノードの見つけ方です。
内側に loops が付いていたら、必ず掛け算してから読む。表示されているのは 1 回あたりの平均なので、 そのまま読むと内側がいちばん軽いノードに見えます(EXPLAIN ANALYZE の見方)。
よくある疑問
関連トピック
もっと学びたい方へ(おすすめ書籍)
PostgreSQLの内部構造・ストレージ・インデックス機構を丁寧に解説。設計と運用計画の鉄則が学べる。
「なぜこの書き方が速いのか」を実行計画から説明する一冊。条件分岐・集約・結合・更新のそれぞれで、良い書き方と悪い書き方を対比しながら読める。
テーブル設計と正規化、パフォーマンス考慮のインデックス設計まで実務レベルで学べる定番書。第2版ではクラウド対応も強化。
ER 図をどう「使える設計」に落とすか、実務の判断まで踏み込んだ入門書。エンティティの切り出しから多対多の扱いまで具体例が豊富。
ドリル 256 問を実際に打ちながら進める SQL の入門書。付属のブラウザ環境で演習できるので、SELECT から結合・集約までを環境構築で止まらずに通せる。
SQLの本質的な使い方と、インデックスが効くクエリの書き方を学べる。ウィンドウ関数など現代SQLも網羅。
実務でやりがちなSQL・DB設計のアンチパターンとその回避策を体系的に学べる。
IPAデータベーススペシャリスト試験の総合対策書。インデックス関連は本サイトと合わせて学ぶと理解が深まる。
リレーショナルモデルの理論から、インデックス設計を含む実務で使えるSQLまで解説。
本セクションはAmazonアソシエイトのリンクを含みます。
もっと深くDBを学びたい方へ。
たいてっくが、SQL・データベース設計・パフォーマンスチューニング・IPAデータベーススペシャリスト対策まで、1対1で学習をサポートします。まずは無料相談から。
「教え方も上手で、お人柄も良いメンターです。DB周りの知識はもちろん、何より、しっかり教えてあげようという姿勢がとてもありがたかったです。データベース、SQLの学習を考えている方にはおススメです。」
— H 様(DB・SQL コース受講)「体系的に知識を教えてくださり、実際の業務でも大変役立っております。特に短い時間で効率よく知識の習得や、練習をできているのは期待以上でした。」
— M 様(DB・SQL コース受講)「大変充実したコンテンツでわかりやすいご説明をありがとうございました。基本的な質問にも丁寧にご説明いただき、また業務のご相談にも乗って頂き大変有意義な時間でした。」
— K 様(DB・SQL コース受講)