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

Sort Method と work_mem(メモリに載らないと何が起きるか)

定義

Sort Method は並べ替えの方式を示し、quicksort はメモリ内で完結したこと、external merge は work_mem に収まらず一時ファイルに書き出したことを意味する。Memory: と Disk: のどちらが表示されるかで見分ける。

Sort Method の 1 行で、メモリに載ったかが分かる

Sort ノードには Sort Method: という行が付きます。ここを見るだけで、並べ替えがメモリで完結したかどうかが分かります。

表示意味ディスクを使ったか
quicksortメモリ内で全部並べ替えた使っていない
top-N heapsort上位 N 件だけ保持しながら流した(ORDER BY + LIMIT使っていない
external mergeメモリに収まらず、一時ファイルに書き出して並べ替えた使った

後ろに付く Memory:Disk: が決定的です。Disk: と書いてあれば一時ファイルに落ちています。

同じクエリで、1 行だけが変わる

遅いノードの見つけ方で 内側を直したあとの計画です。このときの 1 位は Sort でした。

 Limit  (cost=60395.21..60395.23 rows=10 width=30) (actual time=934.953..934.974 rows=10.00 loops=1)
   ->  Sort  (cost=60395.21..60397.87 rows=1064 width=30) (actual time=934.949..934.956 rows=10.00 loops=1)
         Sort Key: (sum((i.price * i.qty))) DESC
         Sort Method: top-N heapsort  Memory: 26kB
         ->  GroupAggregate  (cost=60348.28..60372.22 rows=1064 width=30) (actual time=878.864..933.942 rows=20000.00 loops=1)
               Group Key: c.name
               ->  Sort  (cost=60348.28..60350.94 rows=1064 width=22) (actual time=878.847..910.373 rows=500000.00 loops=1)
                     Sort Key: c.name
                     Sort Method: external merge  Disk: 16656kB
                     ->  Nested Loop  (cost=597.43..60294.78 rows=1064 width=22) (actual time=8.830..404.174 rows=500000.00 loops=1)
                           ->  Hash Join  (cost=597.00..57141.20 rows=523 width=18) (actual time=8.802..109.528 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.086..69.011 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=8.554..8.554 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.048..1.306 rows=20000.00 loops=1)
                           ->  Index Only Scan using order_items_covering on order_items i  (cost=0.43..5.80 rows=23 width=12) (actual time=0.001..0.001 rows=2.00 loops=250000)
                                 Index Cond: (order_id = o.id)
                                 Filter: (qty = 3)
                                 Rows Removed by Filter: 4
                                 Heap Fetches: 0
                                 Index Searches: 250000
 Planning Time: 1.754 ms
 Execution Time: 935.912 ms

Sort Method: external merge Disk: 16656kB。一時ファイルに 16MB 書いている。
PostgreSQL 18.6 / 2026-08-22 採取 / 並列・JIT は OFF

external merge Disk: 16656kB と出ています。 50 万行を並べ替えるのに、作業メモリ(既定 4MB)では足りず16MB ぶんを一時ファイルに書いたということです。

この作業メモリの大きさを決めているのが work_mem(公式ドキュメント)です。増やして、同じクエリをもう一度実行します。

SET work_mem = '128MB';
 Limit  (cost=60395.21..60395.23 rows=10 width=30) (actual time=834.246..834.271 rows=10.00 loops=1)
   ->  Sort  (cost=60395.21..60397.87 rows=1064 width=30) (actual time=834.242..834.250 rows=10.00 loops=1)
         Sort Key: (sum((i.price * i.qty))) DESC
         Sort Method: top-N heapsort  Memory: 26kB
         ->  GroupAggregate  (cost=60348.28..60372.22 rows=1064 width=30) (actual time=795.088..832.844 rows=20000.00 loops=1)
               Group Key: c.name
               ->  Sort  (cost=60348.28..60350.94 rows=1064 width=22) (actual time=795.064..807.410 rows=500000.00 loops=1)
                     Sort Key: c.name
                     Sort Method: quicksort  Memory: 31820kB
                     ->  Nested Loop  (cost=597.43..60294.78 rows=1064 width=22) (actual time=5.537..390.310 rows=500000.00 loops=1)
                           ->  Hash Join  (cost=597.00..57141.20 rows=523 width=18) (actual time=5.513..113.784 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.202..74.525 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=5.180..5.183 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.098..2.167 rows=20000.00 loops=1)
                           ->  Index Only Scan using order_items_covering on order_items i  (cost=0.43..5.80 rows=23 width=12) (actual time=0.001..0.001 rows=2.00 loops=250000)
                                 Index Cond: (order_id = o.id)
                                 Filter: (qty = 3)
                                 Rows Removed by Filter: 4
                                 Heap Fetches: 0
                                 Index Searches: 250000
 Planning Time: 1.372 ms
 Execution Time: 836.672 ms

Sort Method: quicksort Memory: 31820kB。ディスクを使わなくなった。
PostgreSQL 18.6 / 2026-08-22 採取 / 並列・JIT は OFF

変わったのは Sort Method の 1 行だけです。external merge Disk: 16656kBquicksort Memory: 31820kB になりました。

数字が 2 倍近く増えていることに注意してください。Disk: の 16MB は一時ファイルとして書き出した量、Memory: の 31MB はメモリ上で使った量で、同じものを測っているわけではありません。ディスクへは行のデータだけを詰めて書くのに対して、 メモリ側は並べ替えるための配列とアロケータの管理領域を一緒に数えています (1 行あたり 30 バイト前後)。1 行 34 バイトが 65 バイトに増えているのはこの差です。

Hash 側にも同じ現象がある

あふれるのはソートだけではありません。Hash Join のハッシュ表も、 メモリに収まらなければ分割されます。

 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

Hash ノードの Buckets / Batches / Memory Usage。
PostgreSQL 18.6 / 2026-08-22 採取 / 並列・JIT は OFF

Hash ノードに付く 3 つの数字がそれです。

  • Buckets — ハッシュ表のバケット数
  • Batches1 なら一発で収まった。2 以上ならメモリに収まらず、その回数に分けて処理した
  • Memory Usage — 実際に使ったメモリ

Batches: 1 でない計画を見たら、そこがあふれています。(originally 1) のような表記が付くこともあり、 これは途中で見積りが外れて分割し直したという意味です。

一時ファイルの量を確かめる

あふれた量は Buffers: 行の temp read / temp written にも出ます。EXPLAIN (ANALYZE, BUFFERS) で見られます(PostgreSQL 18 ではANALYZE を付けると既定で出ます)。

ここが 0 でなければ、そのクエリはディスクに書いています。

直す方向は 2 つある

  1. 入ってくる行を減らす。そもそも 50 万行を並べ替える必要があるのかを見ます。 手前のノードで絞れるなら、そちらの方が効きます
  2. 作業メモリを増やす。ただし同時に走るソートやハッシュの数だけ確保されうるので、 サーバ全体のメモリ設計の話になります。 このセクションでは「この行が何を意味するか」までを扱い、 設定値の決め方には踏み込みません

1 位を直したら順位表を作り直す。この Sort が 1 位になったのは、 その前に 1 位だった内側のインデックス参照を直したからです。 ボトルネックは1 つ潰すと次が出てきます遅いノードの見つけ方)。

よくある疑問

Q.work_mem はどれくらいにすればいいですか?
A.このページでは扱いません。work_mem は同時に走るソートやハッシュの数だけ確保されうるので、サーバ全体のメモリ設計の話になります。ここで扱うのは「出力のこの行が何を意味するか」までです。
Q.external merge が出ていたら必ず直すべきですか?
A.一時ファイルが出ること自体は異常ではありません。まず、その Sort が本当に必要かを見ます。ソートに流れる行数を減らせるなら、そちらの方が効きます。
Q.top-N heapsort とは何ですか?
A.ORDER BY と LIMIT が組み合わさったときに使われる方式です。全部並べ替えるのではなく、上位 N 件だけを保持しながら流すので、メモリ使用量が小さくて済みます。

関連トピック

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

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

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

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 で見る →
SQL実践入門 ── 高速でわかりやすいクエリの書き方
ミック

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

Amazon で見る →

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

オンライン個別指導

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

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

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