مرکزی مواد پر جائیں
友田 陽大
PostgreSQL internals & performance
PostgreSQL
パフォーマンス
アーキテクチャ設計

PostgreSQL 本番パフォーマンスチューニング総論(v18対応):計測→インデックス→実行計画→メモリ→VACUUM を正しい順で速くする

PostgreSQL を本番で速くするための体系的ガイド。pg_stat_statements での計測起点、EXPLAIN による実行計画の読み方、shared_buffers/work_mem などメモリ設定の意味、MVCC/VACUUM とインデックス、接続プーリング、PostgreSQL 18 の非同期I/O・B-treeスキップスキャンまでを、公式ドキュメント(v18)に忠実な実コードで解説します。

Published
Last updated
Updated
Reading time
16 min read
Author
友田 陽大
شیئر کریں

「PostgreSQL が遅い」という相談の9割は、正しい順番で計測していないことに原因があります。shared_buffers をいきなり巨大化したり、当てずっぽうでインデックスを増やしたり——それは多くの場合、効かないどころか書き込みを遅くし、プランナーを混乱させ、メモリを食い潰します

この記事は、PostgreSQL を本番で速くするための全体地図です。「どこから手を付け、何を計測し、どの順で最適化するか」という方法論を軸に、インデックス・実行計画・メモリ・VACUUM・接続プーリング・パーティショニングを正しい優先順位で並べ直します。各テーマの深掘りは個別記事に譲り、本稿は**「全体を貫く設計判断」**に集中します。

この記事のルール:設定項目の既定値・意味・SQL構文・PostgreSQL 18 の新機能は、すべて PostgreSQL 18 公式ドキュメント(2026年6月時点) に基づきます。マネージドサービス(RDS/Aurora・Cloud SQL・Supabase 等)の既定値はエンジンのマイナーやサービス側の上書きで本家と異なることがあります。本番に適用する前に、必ずお使いのインスタンスで SHOW <param>; と実測で確認してください。数値はワークロードで変わり得ます。


1. 大原則:推測するな、計測せよ

パフォーマンスチューニングの鉄則は「Don't speculate, measure.」です。PostgreSQL のプランナーはコストベースで動き、その判断は統計情報設定値から導かれます。人間の直感は、たいてい外れます。

最適化に着手する前に、必ずこの2点を押さえます。

  1. どのクエリが、どれだけ全体の負荷を占めているか(→ pg_stat_statements
  2. その遅いクエリは、なぜ遅いのか(→ EXPLAIN (ANALYZE, BUFFERS)

そして、以下の効く順番で手を入れます。上ほど効果が大きく、コストが低い。

優先度レイヤー主な武器深掘り記事
1クエリ・インデックス設計適切な索引、N+1除去、SARGable な述語インデックス設計
2実行計画の理解EXPLAIN ANALYZE、統計の鮮度EXPLAIN とスロークエリ改善
3メモリ設定work_mem / shared_buffers / effective_cache_size本記事 §4
4MVCC・VACUUMautovacuum、肥大化(bloat)対策、HOT更新MVCC と VACUUM
5接続管理接続プーリング(PgBouncer 等)本記事 §6
6物理設計宣言的パーティショニングパーティショニング

やってはいけない最適化:①計測前に設定値をいじる、②EXPLAIN ANALYZE を見ずにインデックスを足す、③本番で SET enable_seqscan = off を常用する(プランナーの判断を壊す対症療法)、④全カラムにインデックスを張る(書き込みが遅くなり、肥大化する)。


2. 計測の起点:pg_stat_statements

pg_stat_statements は、実行されたクエリを正規化(リテラルを $1 に置換)して集計する拡張です。「SELECT * FROM orders WHERE id = 42」と「= 99」を同一クエリとして束ね、呼び出し回数・合計実行時間・平均時間・返却行数を記録します。本番の「重いクエリ Top N」はここから出します。

shared_preload_libraries に登録(サーバー再起動が必要)し、対象DBで有効化します。

-- postgresql.conf: shared_preload_libraries = 'pg_stat_statements'
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

累計実行時間が長い=全体への影響が大きい」クエリから潰すのが費用対効果の鉄則です。1回0.5秒のクエリより、1回5msでも100万回呼ばれるクエリの方が、システム全体では重い。

-- 全体負荷への寄与が大きい順(時間は ms)
SELECT
  substring(query, 1, 80)              AS query,
  calls,
  round(total_exec_time::numeric, 1)   AS total_ms,
  round(mean_exec_time::numeric, 2)    AS mean_ms,
  rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;

この total_exec_time DESC のリストが、あなたの最適化の To-Do リスト です。上位5本を改善するだけで、体感は劇的に変わります。

本番では、しきい値を超えた遅いクエリの実行計画そのものをログに残す auto_explain も併用します(詳細はEXPLAIN の記事)。


3. プランナーは統計情報で動く(だから ANALYZE が命)

PostgreSQL のプランナーは、テーブルを実際にスキャンする前に「この WHERE は何行に絞れるか(選択率)」を統計情報から見積もり、最も安いプランを選びます。統計が古いと、見積もりが外れ、間違ったプラン(本来インデックスを使うべき所でシーケンシャルスキャン等)を選びます。

統計には2種類あります(公式 planner-stats)。

  • テーブル単位pg_class):reltuples(行数)、relpages(ブロック数)。VACUUM / ANALYZE / CREATE INDEX 等で更新され、常に多少古い
  • 列単位pg_statisticpg_stats ビュー):most_common_vals(高頻度値)、histogram_bounds(ヒストグラム)、n_distinctnull_fracANALYZE / VACUUM ANALYZE で更新され、常に近似値

実務上の指針はシンプルです。

-- 統計の収集精度(既定 100)。偏った分布の列だけ列単位で引き上げる
ALTER TABLE orders ALTER COLUMN status SET STATISTICS 1000;
ANALYZE orders;

相関する複数列(例:citypostal_code)は、プランナーが「各条件は独立」と仮定するため、組み合わせ条件で行数を桁違いに過小評価します。これは拡張統計で矯正します。

-- 「都市が決まれば郵便番号もほぼ決まる」関数従属を学習させる
CREATE STATISTICS orders_city_zip (dependencies, ndistinct)
  ON city, postal_code FROM orders;
ANALYZE orders;

「インデックスを張ったのに使われない」の典型原因は統計の陳腐化です。大量 INSERT/更新の直後は必ず ANALYZE。autovacuum 任せにせず、バッチ処理の最後に明示実行するのが安全です。


4. メモリ設定:意味を理解して触る

メモリ設定は「大きくすれば速い」ではありません。それぞれが守る対象が違うので、意味を理解して触ります。以下はすべて公式 runtime-config-resource / runtime-config-query の既定値です。

4.1 shared_buffers — PostgreSQL 自身のキャッシュ

PostgreSQL がデータページをキャッシュする共有メモリ。既定 128MB。公式の指針は「RAM が 1GB 以上なら 25% から始める」「40% を超えても良くなることは稀」。理由は、PostgreSQL は OS のページキャッシュにも強く依存しており、shared_buffers を上げすぎると OS キャッシュと二重持ちになり、かえって効率が落ちるためです。

# 例:RAM 16GB のサーバー
shared_buffers = 4GB              # RAM の約25%。サーバー起動時のみ反映
effective_cache_size = 12GB       # 後述。OS含めキャッシュ可能と見込む量

4.2 work_mem — 最大の地雷(クエリ単位ではない)

ソート(ORDER BY / DISTINCT / マージ結合)やハッシュ(ハッシュ結合・ハッシュ集約)が一時ファイルに書き出す前に使えるメモリ既定 4MB

ここが最大の落とし穴です。公式の表現を引くと——

複雑なクエリは複数のソート・ハッシュ操作を同時に実行することがあり、各操作がそれぞれ work_mem まで使える。さらに複数セッションが同時に実行し得る。したがって総メモリ使用量は work_mem の何倍にもなり得る。

つまり実効上限は work_mem × クエリ内の同時ソート/ハッシュ・ノード数 × 同時セッション数。安易に work_mem = 1GB などにすると、ピーク時に OOM でサーバーが落ちます。さらにハッシュ系は hash_mem_multiplier(既定 2.0)が掛かるため、実際は work_mem × 2.0 まで使えます。

-- グローバルは控えめ(例 16MB〜64MB)。重い集計クエリだけセッション/トランザクションで上げる
SET LOCAL work_mem = '256MB';   -- このトランザクション内だけ。BEGIN〜COMMIT で囲む
SELECT customer_id, sum(amount) FROM orders GROUP BY customer_id;

EXPLAIN ANALYZESort Method: external merge Disk: 12345kB が出たら、そのソートが work_mem を溢れてディスクに退避した証拠=work_mem 不足のサインです。

4.3 maintenance_work_mem — 保守作業用

VACUUMCREATE INDEXALTER TABLE ADD FOREIGN KEY 等が使うメモリ。既定 64MB。同時に1セッション1つしか走らない前提なので、公式も「work_mem より大幅に大きく設定して安全」としています。インデックス再作成やバルクロードの前に一時的に上げると効果絶大です。

SET maintenance_work_mem = '1GB';   -- 索引作成・VACUUM を高速化

4.4 effective_cache_size と random_page_cost — プランナーの世界観

この2つはメモリを確保しません。プランナーに「世界の見え方」を教えるパラメータです。

  • effective_cache_size(既定 4GB):1クエリが利用できそうなディスクキャッシュの総量(shared_buffers+OSキャッシュ)の見積もり。大きいほどインデックススキャンが有利に評価されます。RAM の 50〜75% を目安に。
  • random_page_cost(既定 4.0):ランダムなページ読み取りの相対コスト(seq_page_cost は 1.0)。HDD前提の既定値で、SSD/NVMe では実態に合いません。SSD なら 1.1 程度に下げると、インデックススキャンが正当に選ばれやすくなります。
effective_cache_size = 12GB       # RAM 16GB の 75%
random_page_cost = 1.1            # SSD/NVMe の現実に合わせる(HDD既定は 4.0)

これは「効くのに見落とされがち」な設定です。 クラウドのマネージドDBはほぼ全て SSD ですが、random_page_cost が 4.0 のままだと、プランナーは「インデックスは高い」と誤解してシーケンシャルスキャンを選びがちです。


5. PostgreSQL 18 の新機能:何が速くなったか

PostgreSQL 18(2025年9月リリース、本稿時点の最新は 18.4)は、I/O 性能を大きく押し上げました。チューニングの前提が変わるので押さえておきます。

5.1 非同期I/O(AIO)— 最大3倍

公式リリースアナウンス曰く、PG18 は非同期I/Oサブシステムを導入し、I/O要求を逐次待たずに並行発行できるようになりました。対象はシーケンシャルスキャン・bitmap heap scan・VACUUMで、ベンチマークでは最大3倍の性能向上が示されています。

これを制御するのが新パラメータ io_method(サーバー起動時のみ)。値は3つ。

内容
worker既定。専用ワーカープロセスで非同期I/Oを実行(io_workers 既定 3)
io_uringLinux の io_uring を使用(--with-liburing ビルドが必要)
sync旧来どおり同期I/O(無効化)

あわせて effective_io_concurrencymaintenance_io_concurrency の既定値が 1 → 16 に引き上げられました(公式コメント:「現代のハードウェアをより正確に反映する」)。

5.2 B-tree スキップスキャン

複合インデックスの先頭列に等値条件がなくても、後続列の条件でインデックスを使えるケースが増えました。公式リリースノートを引くと——

複数列のB-treeインデックスを、先頭や前方の列に制約がない(あるいは非等値の制約しかない)場合でも、後続の列に有用な制約があるときに利用できるようにする。

これは「複合インデックスの列順を間違えると全く使われない」という従来の鉄則を部分的に緩和します(ただし先頭列のカーディナリティが低いほど効く)。詳細はインデックス設計の記事へ。

5.3 EXPLAIN ANALYZE が BUFFERS を既定で表示

地味ですが効きます。PG18 から EXPLAIN ANALYZEBUFFERS を自動で含むようになりました(公式:「Buffers information is automatically included when ANALYZE is used.」)。バッファヒット/読み込みが標準で見えるので、「キャッシュに乗っているか/ディスクを叩いているか」の診断が一段楽になります。

-- PG18 では BUFFERS を明示しなくても shared hit/read が出る
EXPLAIN (ANALYZE) SELECT * FROM orders WHERE customer_id = 42;
-- 抑制したいときだけ: EXPLAIN (ANALYZE, BUFFERS OFF) ...

その他、uuidv7()(時刻順ソート可能なUUID=インデックス局所性が良い)、仮想生成列が既定化、複合主キーに非btreeのユニークインデックスを使える、など実務に効く改善が入っています。


6. 接続管理:スレッドではなくプロセス、だからプーリング必須

PostgreSQL は接続ごとにOSプロセスを1つ生成します(スレッドではない)。1接続あたり数MB〜のメモリを消費し、work_mem の同時消費もここに乗ります。アプリのコネクションを無制限に張ると、max_connections を上げる→メモリ枯渇、という典型的な事故になります。

対策は接続プーリングです。アプリ↔DB の間にプーラー(PgBouncer 等)を挟み、物理接続を少数で使い回す。サーバーレス(Lambda 等)や多数の Web ワーカーでは必須級です。

[多数のアプリプロセス] ──▶ [PgBouncer transaction mode] ──▶ [少数の物理接続] ──▶ PostgreSQL
                                  ↑ 物理接続を共有し、max_connections を低く保つ

アプリ側でも、サーバーレスでは「リクエストごとに接続を作らない」「プーラー前提でプール上限を絞る」のが鉄則です。

// node-postgres: アプリ内プールは小さく、外側で PgBouncer に集約する設計
import { Pool } from "pg";

export const pool = new Pool({
  connectionString: process.env.DATABASE_URL, // 秘密情報は環境変数(コードに埋めない)
  max: 10,                    // 1インスタンスあたりの上限。台数×max が物理接続を超えないこと
  idleTimeoutMillis: 30_000,
  connectionTimeoutMillis: 5_000,
});

// クエリは必ずパラメータ化(文字列連結は SQL インジェクションの温床)
export async function findOrder(id: string) {
  const { rows } = await pool.query(
    "SELECT id, customer_id, amount FROM orders WHERE id = $1",
    [id],
  );
  return rows[0] ?? null;
}

セキュリティの最重要点:ユーザー入力は**必ずパラメータ化クエリ($1)**で渡す。文字列連結で SQL を組み立てた瞬間、SQL インジェクションの扉が開きます。ORM(Drizzle/Prisma 等)や pg のパラメータ化は、この境界を構造的に守るための仕組みです。


7. 書き込みを速くする:バルクロードとHOT更新

読み取りだけでなく、投入と更新にもセオリーがあります。

7.1 大量投入は INSERT ではなく COPY

公式 populate の指針:大量データは INSERT ではなく COPY を使う(1コマンドで全行を流し、パース・プラン・コミットのオーバーヘッドを償却)。さらに、

  • インデックスと外部キーは投入後に作る(投入中の索引メンテを避ける)
  • maintenance_work_memmax_wal_size を一時的に上げる
  • 投入後に必ず ANALYZE(さもないとプランナーが空テーブルの統計で誤動作)
-- 初期ロードの定石
ALTER TABLE big_table SET (autovacuum_enabled = false);  -- ロード中の自動VACUUMを止める
-- (索引を落としてから)COPY で投入
COPY big_table FROM '/path/data.csv' WITH (FORMAT csv, HEADER true);
-- 索引・FKを作成し直す → 統計を更新
CREATE INDEX ...;
ANALYZE big_table;
ALTER TABLE big_table SET (autovacuum_enabled = true);

7.2 更新は「肥大化」を生む(HOT更新で抑える)

PostgreSQL の UPDATE は、MVCC のため古い行を即座に消さず、新しい行バージョンを追記します(MVCC の記事で詳述)。これが**テーブル肥大化(bloat)**の原因です。

緩和策が HOT 更新。「更新がインデックス対象列を変えず」「同一ページに空きがある」場合、新しいインデックスエントリを作らずに済み、肥大化を抑えます。更新が多いテーブルは fillfactor を下げて同一ページの空きを確保すると、HOT 更新が成立しやすくなります。

-- 更新の多いテーブルは fillfactor を下げて HOT 更新を促す(既定100)
ALTER TABLE sessions SET (fillfactor = 85);

8. 緊急時のチェックリスト(本番が重いとき)

「いま遅い」を切り分ける順番です。上から確認してください。

  1. 詰まっているクエリは? pg_stat_activitystate = 'active' かつ wait_event を確認。長時間 idle in transaction のセッションは VACUUM を妨げ、ロックを握り続ける元凶。
  2. ロック待ちは? pg_lockspg_stat_activity と結合し、ブロック元のPIDを特定。
  3. 重いクエリの計画は? 該当クエリを EXPLAIN (ANALYZE, BUFFERS)Seq Scan + 大きい Rows Removed by Filterexternal merge Disk、巨大な loops を探す。
  4. 統計は新鮮か? 直近に大量更新があったテーブルを ANALYZE
  5. 肥大化していないか? pg_stat_user_tablesn_dead_tup と autovacuum の最終実行時刻を確認。
-- いま実行中で重いセッション(長時間 idle in transaction は要注意)
SELECT pid, state, wait_event_type, wait_event,
       now() - xact_start AS xact_age, substring(query,1,60) AS query
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY xact_start;

9. 設計判断のまとめ(トレードオフ表)

施策効く対象コスト/リスク判断基準
インデックス追加読み取り書き込み低下・肥大化・容量pg_stat_statements 上位の検索条件にだけ張る
work_mem 引き上げソート/ハッシュOOM(同時実行で乗算)グローバルは控えめ、重い集計のみ SET LOCAL
random_page_cost 低下プラン選択誤って seq scan を捨てるSSD なら 1.1 前後(HDDは 4.0 維持)
パーティショニング超大規模表設計・運用の複雑化表サイズが物理メモリを超えるのが目安
接続プーリング同時接続多運用コンポーネント増サーバーレス/多ワーカーでは必須
非durable設定投入速度OSクラッシュ時のデータ損失初期ロード等の再生成可能な処理のみ

非durable設定の注意(公式 non-durability):synchronous_commit = off は「OSクラッシュ時にトランザクションを失うリスクはあるが、データ破損はしない」。fsync = off は破損リスクあり。いつでも作り直せるデータ(初期ロード、再生成可能な集計)に限って使うこと。


10. まとめ:速さは「順番」で決まる

  • 計測が先pg_stat_statements で重いクエリを特定し、EXPLAIN (ANALYZE, BUFFERS) で原因を見てから手を打つ。
  • 効く順は、①インデックス・クエリ設計 → ②統計(ANALYZE)→ ③メモリ → ④VACUUM → ⑤接続 → ⑥物理設計。上ほど安く効く。
  • work_mem は per-operation。総使用量は乗算で膨らむ。グローバルは控えめに、必要な所だけ SET LOCAL
  • SSD なら random_page_cost を下げるeffective_cache_size は実態に合わせて大きく。
  • PG18 は I/O が速くなった(非同期I/O、BUFFERS既定ON、スキップスキャン)。前提が変わったので最新で測り直す。

このシリーズでは、本記事で並べた各レイヤーを個別に深掘りします。まずは EXPLAIN の読み方インデックス設計 から。PostgreSQL は「正しく計測し、正しい順で触れば」、専用DBを増やさずとも本番の要求に十分応えます。


参考(PostgreSQL 18 公式ドキュメント)

عمومی سوالات

PostgreSQL のチューニングは何から始めるべきですか?
推測ではなく計測からです。pg_stat_statements で重いクエリを特定し、EXPLAIN (ANALYZE, BUFFERS) で実行計画を確認してから手を打ちます。ボトルネックを見ずに設定だけ触るのは逆効果になりがちです。
shared_buffers と work_mem の目安は?
shared_buffers は RAM の約25%が初期目安で、40%超はむしろ逆効果になりやすいです。work_mem はクエリ単位ではなくプランの「ノード単位」で消費され、総使用量は work_mem × 同時ソート/ハッシュ数 × 同時セッション数になり得るため、控えめに設定します。
効くチューニングの順番は?
①インデックス・クエリ設計 → ②統計情報(ANALYZE)→ ③メモリ設定 → ④VACUUM/autovacuum → ⑤接続プーリング → ⑥パーティショニング、の順です。上流ほど効果が大きく、順番を守ることが速さに直結します。

حوالہ جات

友田

友田 陽大

وزیر (METI) ایوارڈ یافتہ پروڈکٹ کے ڈویلپر۔ TypeScript + Python + AWS کے ساتھ میں SaaS، صنعتی ڈیجیٹل تبدیلی اور پروڈکشن کے قابل جنریٹو اے آئی (RAG) تنہا ابتدا سے انتہا تک فراہم کرتا ہوں — تقاضوں کی تعیین سے انفراسٹرکچر اور آپریشن تک۔

Got a challenge?

From design to implementation and operations — solo × generative AI

Implementation like this article's, end to end from requirements to production. Start with a free 30-minute technical consult and tell me about your situation.

پروجیکٹ کی بنیاد پر کام اور تکنیکی مشاورت دونوں کے لیے دستیاب ہوں۔ 30 منٹ کی مفت مشاورت سے آغاز کریں۔

最短ルート:カレンダーから直接予約

相談内容が固まっている方は、フォーム送信よりその場で日程を確定する方がスムーズです。下記から空き時間をお選びください。

  • 30分のオンライン無料相談
  • Google Meet / Zoom / Microsoft Teams
  • NDA 商談前締結可・無理な営業はいたしません
無料相談の空き枠を予約する

یہ بھی پڑھنے کے قابل