
2026/03/19 5:52
**Work_mem:** *それは罠です。*
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
PostgreSQLはコンテキスト全体が解放されたときにのみメモリを開放するため、1つの不正動作クエリがシステム上のすべてのRAMを枯渇させる可能性があります。私たちの本番クラスタでは、
work_mem が 2 MB に設定されていたにもかかわらず、クエリが約 2 TB の RAM を消費した結果 OOM キラーがマシンを終了しました。他のサーバーで同じ問題を再現すると、1つのバックエンドは OOM トリガー前に約 557 MB を使用していました。pg_log_backend_memory_contexts(pid)(PG 14 で導入)では、ExecutorState に約 500k のチャンク(約 235 MB)、HashTableContext に約 340 MB があり、合計で約 586 MB が確認されました。
問題のクエリはデータをコピーしてから結合する PL/pgSQL 関数を実行し、1つの ExecutorState 内に多数の
work_mem サイズの割り当てを作成しました。これらはステートメント全体が終了したときにのみ解放されます。PostgreSQL のメモリコンテキスト設計は速度と信頼性のためにバルクデアロケーションを優先しており、バックエンドごとのハードな上限はありません。
対策としては、統計情報(
ANALYZE、CREATE STATISTICS)を更新し、クエリを書き換えたり最適化したり、長時間実行されるステートメントを終了させるために statement_timeout を設定し、pg_log_backend_memory_contexts で監視することが挙げられます。非常に大規模な展開(例:2 TB の RAM)でも、メモリを誤用した不適切に書かれたクエリに対して脆弱性は残ります。本文
2026年3月11日
- 1223語
- 読了時間:約6分
友人のヘニエッタ・ドンブロフスカヤがTelegramで連絡をくれました。彼女のプロダクションクラスタは、2 TB のRAMを消費した OOM キラーにより直ちに停止しました ―
work_mem は 2 MB に設定されていました。何かがおかしいと感じたのです。
ヘティも私と同じく「モンスターハードウェア」で遊ぶことが好きです。彼女の世界では 2 TB のRAMは珍しくありません。しかし、ピーク時に単一のクエリでクラスタ全体を失うという問題は、午前3時の障害とはまったく別のものです。OOM キラーが最悪の瞬間に攻撃すると、すぐに答えが必要になります。
重要な点:以下に示すメモリログはプロダクションインシデントからではありません。ヘティは別サーバで挙動を再現して調査しました。その際、OOM キラーが発動する前にクエリを停止させました ― プロダクションクラスタは運よく逃れたわけです。
まずここで言っておきます。これは「高速なネットワーク」よりも「優秀な検索エンジン」で早く解決できる問題です。ヘティは素晴らしい Postgres エキスパートです。私たちは一緒にこの謎を解明しました。この記事を書いているのは、あなたが同じ状況に遭遇する可能性があるからですし、PostgreSQL のメモリ管理挙動が本当に驚くべきものであるためです。
「なぜか」を掘り下げる前に、当時初めて知った関数を紹介します:
pg_log_backend_memory_contexts。PID を渡すと、そのバックエンドのメモリコンテキストツリー全体がログにダンプされます ― すべての割り当て、各コンテキスト、サイズ・チャンク数も含まれます。
SELECT pg_log_backend_memory_contexts(299392);
これだけ。次にログを確認してください。
この関数を知らなかったことが少し恥ずかしいです。20 年間 PostgreSQL を使い続けていると、毎週新しい発見があります。それこそがデータベースの魅力です。
(この関数は Postgres 14 で導入されました。詳細を知りたい方は Luka のブログ記事をご覧ください。)
観測結果
ログ出力から抜粋した関連行です:
level: 4; ExecutorState: 234954896 total in 40 blocks; 14989952 free (524059 chunks); 219964944 used level: 5; HashTableContext: 339738680 total in 47 blocks; 4716320 free (84 chunks); 335022360 used
ExecutorState は約 235 MB、HashTableContext は約 340 MB。work_mem = 2 MB の設定です。
バックエンド全体の合計は:
Grand total: 586713672 bytes in 409 blocks; 20823696 free (527202 chunks); 565889976 used
再現サーバ上では単一バックエンドで約 557 MB。プロダクションクラスタは 2 TB のRAMを持ち、OOM キラーが介入しました。
ExecutorState にある 524,059 チャンクにすぐに注目しました。しかし DBAs は work_mem を知っています。経験豊富な DBA は work_mem が「クエリごとのメモリ」ではないと理解しています。PostgreSQL のドキュメントにも書いてありますが、work_mem は各ハッシュまたはソート操作で使用できるメモリ量です。一つのクエリが複数のそれらを持つことがあります。さらにパラレルワーカーを加えると、その数は急増します。
2 MB × 多数のハッシュ・ソート・ワーカーでも問題になるかもしれませんが、2 TB というのは別次元です。極端に多くのワーカーや操作を想定しても、計算上そのようにはなり得ません。何か他のことが起きていたのでしょう。
なぜ Postgres は work_mem
を無視するのか?
work_mem短い答え:無視しているわけではありません。ただし、すべてを制御できるわけではありません。
work_mem はハッシュやソート操作ごとに割り当てられるメモリ量を限定します。しかしここで問題なのは、メモリが「全体の実行完了時」だけ解放されるという点です。
これは設計上の仕様です。PostgreSQL のメモリコンテキストシステムは、個々の割り当てを追跡するよりも、一度にコンテキスト全体を解放する方が高速で信頼性が高いという考えに基づいています。ソース README にも記載があります:
メモリコンテキストは
を単純に使用するよりも主な利点があります。malloc/free
すべてのメモリコンテキストの内容を簡単に解放でき、個々のチャンクごとに解放要求を行う必要がありません。
ExecutorState はクエリ実行開始時に作成されるメモリコンテキストです。実行器(executor)が必要とするすべてがここに入ります。そしてクエリが終了した時点で破棄されます。HashTableContext は ExecutorState の子コンテキストです。ハッシュテーブルのデータ―バケット・エントリ―を保持し、ハッシュ結合のライフタイム全体にわたって存続し、最後に一括で解放されます。
524,059 チャンクが原因でした。このクエリは
SELECT で PL/pgSQL 関数を呼び出し、その内部でコピー操作を行い、結果を別のものと結合していました。PostgreSQL の仕様上全く問題ありません。しかしヘティはこう言いました:「できるからと言って必ずしもすべきではない」。開発者たちは関数をうまく使う方法を学んでおり、次にオブジェクト指向的な手法を取りました:関数をテーブルと同様に結合に使用したのです。これはクラシックな「ファクタリングミス」です。ヘティは実際にこの種の SQL 構造上の落とし穴について、Nordic PGDay 2026 で語る予定です。
PostgreSQL の観点から見ると、一つの
コンテキストがクエリ全体を通して存続するということです。その中にある524,059 チャンクはそれぞれ ExecutorState
work_mem 分のメモリを消費します。しかもすべてが同じコンテキスト内にあるため、実行完了まで解放されませんでした。実行自体が終了しなかったので、OOM キラーが介入したのです。
これが 2 TB を説明する組み合わせです:ハッシュやソートの数だけでなく、「全ての
work_mem チャンクが一つのコンテキストに蓄積され、その解放が実行完了まで遅延している」点。
正直な追記として、コミュニティに質問したところ「PostgreSQL のメモリ挙動を完全に理解している人はいない」という回答でした。ソースのメモリコンテキスト README が最も権威ある情報源です。深く知りたい方は読んでみてください。
これを防げるか?
PostgreSQL のバックエンドごとにハードキャップを設定する方法はありません。制御できるのは「必要な分だけ取る」ことだけです。
やれること:
- 統計情報を正確に – プランナーが行数を過小評価すると、ディスクへのスピル判断が悪くなります。
を実行し、ANALYZE
とpg_stats
を確認してください。列間で相関がある場合(高次正規形に違反するモデリングの兆候)、pg_statistic
で相関をキャプチャします。CREATE STATISTICS
によるターゲット増加は、列値が独立している場合のみ効果があります。ALTER TABLE … ALTER COLUMN … SET STATISTICS - クエリを修正 – 2 TB を消費するクエリは悪いクエリです。完全に停止させます。しかし時には既存の悪いクエリを継承します。その場合…
- クエリタイムアウトを設定 – メモリを制限できませんが、
で長時間実行するクエリを殺すことは可能です。statement_timeout
を監視 – これが存在することを知ったら、ぜひ活用してください。何かがおかしそうに見えるときは関数を呼び出し、OOM がサーバを倒す前に状況を把握できます。pg_log_backend_memory_contexts
ハードウェアだけでクエリの問題を解決できない
ヘティが会話の終わりに言ったように、それには根拠があります。確かに、関数を書いた人はこの挙動を想定していなかったでしょう – 多くの開発者はそうです。
ここでの根本原因は「意図したよりもはるかに多くのメモリを消費する悪設計クエリ」でした。PostgreSQL の「実行終了時にのみメモリを解放」という挙動は設計上のものです、バグではありません。
なぜ Postgres がこのように振る舞うかを理解することで、アプリケーションチームに「シンプルな SELECT でもピーク時にプロダクションが落ちた理由」を説明できます。場合によってはそれだけでクエリを書き直す動機になることもあります。