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 msSort 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 msSort Method: quicksort Memory: 31820kB。ディスクを使わなくなった。
PostgreSQL 18.6 / 2026-08-22 採取 / 並列・JIT は OFF
変わったのは Sort Method の 1 行だけです。external merge Disk: 16656kB がquicksort 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 msHash ノードの Buckets / Batches / Memory Usage。
PostgreSQL 18.6 / 2026-08-22 採取 / 並列・JIT は OFF
Hash ノードに付く 3 つの数字がそれです。
Buckets— ハッシュ表のバケット数Batches— 1 なら一発で収まった。2 以上ならメモリに収まらず、その回数に分けて処理したMemory Usage— 実際に使ったメモリ
Batches: 1 でない計画を見たら、そこがあふれています。(originally 1) のような表記が付くこともあり、 これは途中で見積りが外れて分割し直したという意味です。
一時ファイルの量を確かめる
あふれた量は Buffers: 行の temp read / temp written にも出ます。EXPLAIN (ANALYZE, BUFFERS) で見られます(PostgreSQL 18 ではANALYZE を付けると既定で出ます)。
ここが 0 でなければ、そのクエリはディスクに書いています。
直す方向は 2 つある
- 入ってくる行を減らす。そもそも 50 万行を並べ替える必要があるのかを見ます。 手前のノードで絞れるなら、そちらの方が効きます
- 作業メモリを増やす。ただし同時に走るソートやハッシュの数だけ確保されうるので、 サーバ全体のメモリ設計の話になります。 このセクションでは「この行が何を意味するか」までを扱い、 設定値の決め方には踏み込みません
1 位を直したら順位表を作り直す。この Sort が 1 位になったのは、 その前に 1 位だった内側のインデックス参照を直したからです。 ボトルネックは1 つ潰すと次が出てきます(遅いノードの見つけ方)。
よくある疑問
関連トピック
もっと学びたい方へ(おすすめ書籍)
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 コース受講)