発展原因を指せるようになる

結合の種類(Nested Loop / Hash Join / Merge Join)

定義

Nested Loop は外側の 1 行ごとに内側を引く方式、Hash Join は片側でハッシュ表を作ってもう片側を突き合わせる方式、Merge Join は両側をキー順に並べて突き合わせる方式である。オプティマイザは行数の見積りとインデックスの有無からどれかを選ぶ。

3 つのやり方がある

2 つのテーブルを結合するとき、データベースには 3 通りのやり方があります。どれを選ぶかは、行数の見積りとインデックスの有無で決まります。

Nested Loop — 外側 1 行ごとに内側を引く

Nested Loop — 外側 1 行につき、内側を 1 回引き直す
外側(1 行ずつ取り出す)A-1A-2A-31 回目: A-1 で検索2 回目: A-2 で検索3 回目: A-3 で検索内側 — 毎回ここを引き直すインデックス内側テーブル1 回で返る行0 行のことも複数行のことも内側は毎回はじめから検索し直す。前回の続きからにはならない

矢印の本数 = 内側を引いた回数 = 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 — 片側でハッシュ表を作る

Hash Join — 先に片側でハッシュ表を作り、もう片側を流す
1. 内側を全部読むB-1B-2B-3B-4ハッシュ表を作るハッシュ表bucket0 → B-1bucket1 → B-2bucket2 → B-3bucket3 → B-42. 外側を 1 行ずつ流すA-1A-2A-3内側は 1 回読むだけ。外側の行数が増えても読み直さないただし表ができるまで 1 行も返せない(開始が遅い)

内側は 1 回だけ読む。外側も 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 — 両側を並べて突き合わせる

Merge Join — 両側を前から突き合わせて、一緒に進む
左(キー順に並んでいる)102030右(キー順に並んでいる)10203040一致一致一致2 つの位置が前へ進むだけ。戻らないし、表も作らない

両側とも 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 orders526 行と見積もられて、実際は 250,000 行返っています(475 倍)。

プランナは「526 行なら内側を 526 回引くだけ」と考えて Nested Loop を選びました。 実際は loops=250000 です。1 回あたり 0.005 ミリ秒でも、25 万回まわれば 1.25 秒になります。

Nested Loop 自体が悪いのではありません。外側の見積りが外れたことが原因で、そこを直せば選択も変わります。 この計画を最後まで解いているのが遅いノードの見つけ方です。

内側に loops が付いていたら、必ず掛け算してから読む。表示されているのは 1 回あたりの平均なので、 そのまま読むと内側がいちばん軽いノードに見えます(EXPLAIN ANALYZE の見方)。

よくある疑問

Q.Nested Loop が出ていたら遅いのですか?
A.外側が少なければ Nested Loop がいちばん速い形です。問題になるのは、外側の行数を少なく見積もりすぎて選ばれたときで、そのとき内側が想定の何百倍もまわります。
Q.どの結合方式が選ばれるかは指定できますか?
A.設定で無効化することはできますが、実務でそれをやるのは最後の手段です。まず見積りが合っているかを確認します。方式の選択はほぼ行数の見積りで決まるので、見積りが直れば選択も直ります。
Q.Merge Join はあまり見かけません
A.両側がすでにキー順に並んでいるときに向く方式なので、条件が揃わないと選ばれません。両側にインデックスがあり、結合キーの順で結果が欲しいようなクエリで出てきます。

関連トピック

もっと学びたい方へ(おすすめ書籍)

[改訂3版]内部構造から学ぶPostgreSQL
勝俣智成 ほか

PostgreSQLの内部構造・ストレージ・インデックス機構を丁寧に解説。設計と運用計画の鉄則が学べる。

Amazon で見る →
SQL実践入門 ── 高速でわかりやすいクエリの書き方
ミック

「なぜこの書き方が速いのか」を実行計画から説明する一冊。条件分岐・集約・結合・更新のそれぞれで、良い書き方と悪い書き方を対比しながら読める。

Amazon で見る →
おすすめ
達人に学ぶDB設計徹底指南書 第2版
ミック

テーブル設計と正規化、パフォーマンス考慮のインデックス設計まで実務レベルで学べる定番書。第2版ではクラウド対応も強化。

Amazon で見る →
おすすめ
楽々ERDレッスン (CodeZine BOOKS)
羽生章洋

ER 図をどう「使える設計」に落とすか、実務の判断まで踏み込んだ入門書。エンティティの切り出しから多対多の扱いまで具体例が豊富。

Amazon で見る →
おすすめ
スッキリわかるSQL入門 第4版 ドリル256問付き!
中山清喬/飯田理恵子

ドリル 256 問を実際に打ちながら進める SQL の入門書。付属のブラウザ環境で演習できるので、SELECT から結合・集約までを環境構築で止まらずに通せる。

Amazon で見る →
達人に学ぶSQL徹底指南書 第2版
ミック

SQLの本質的な使い方と、インデックスが効くクエリの書き方を学べる。ウィンドウ関数など現代SQLも網羅。

Amazon で見る →
SQLアンチパターン 第2版
Bill Karwin

実務でやりがちなSQL・DB設計のアンチパターンとその回避策を体系的に学べる。

Amazon で見る →
情報処理教科書 データベーススペシャリスト 2025年版
三好康之

IPAデータベーススペシャリスト試験の総合対策書。インデックス関連は本サイトと合わせて学ぶと理解が深まる。

Amazon で見る →
理論から学ぶデータベース実践入門
奥野幹也

リレーショナルモデルの理論から、インデックス設計を含む実務で使えるSQLまで解説。

Amazon で見る →

本セクションはAmazonアソシエイトのリンクを含みます。

オンライン個別指導

もっと深くDBを学びたい方へ。

たいてっくが、SQL・データベース設計・パフォーマンスチューニング・IPAデータベーススペシャリスト対策まで、1対1で学習をサポートします。まずは無料相談から。

無料相談を予約する →
受講者の声 (DB・SQL コース)
menta レビュー原文を見る →
  • 教え方も上手で、お人柄も良いメンターです。DB周りの知識はもちろん、何より、しっかり教えてあげようという姿勢がとてもありがたかったです。データベース、SQLの学習を考えている方にはおススメです。
    H 様DB・SQL コース受講
  • 体系的に知識を教えてくださり、実際の業務でも大変役立っております。特に短い時間で効率よく知識の習得や、練習をできているのは期待以上でした。
    M 様DB・SQL コース受講
  • 大変充実したコンテンツでわかりやすいご説明をありがとうございました。基本的な質問にも丁寧にご説明いただき、また業務のご相談にも乗って頂き大変有意義な時間でした。
    K 様DB・SQL コース受講