PostgreSQL パフォーマンスチューニング完全ガイド
2026年にPostgreSQLを選ぶ理由
PostgreSQLは2026年、世界で最も人気のあるオープンソースリレーショナルデータベースとして確固たる地位を築いています。強力な拡張性、豊富なデータ型、優れたSQL標準準拠性により、スタートアップから大企業までのデータベースの第一選択となっています。
PostgreSQLのコア利点
| 次元 | PostgreSQL | MySQL | SQL Server | Oracle |
|---|---|---|---|---|
| 拡張性 | 極めて強い(拡張フレームワーク) | 中程度 | 中程度 | 強い |
| データ型 | 極めて豊富(JSONB、GIS、配列) | 中程度 | 豊富 | 豊富 |
| SQL標準準拠 | 標準に最も近い | 中程度 | 高い | 高い |
| オープンソース | 完全オープン(PostgreSQL License) | GPL | 部分的オープン | クローズド |
| 同時実行制御 | MVCC(優秀) | MVCC | MVCC | MVCC |
| パーティショニング | ネイティブ宣言型パーティショニング | ネイティブ | ネイティブ | ネイティブ |
2026年のPostgreSQL主要進展
- PostgreSQL 18:パラレルクエリのさらなる強化、論理レプリケーションパフォーマンスの大幅向上
- インクリメンタルソート最適化:複数カラムソートシナリオで3-5倍のパフォーマンス向上
- JSONパス式の強化:より強力なJSONBクエリとインデックスサポート
- 非同期I/Oサポート:基盤I/Oアーキテクチャの改善、高同時実行スループットの大幅向上
- 論理レプリケーション改善:DDLレプリケーションサポート、データ同期アーキテクチャの大幅簡素化
💡 SQLフォーマッターツールを使ってPostgreSQLクエリを整形し、実行計画の分析を容易にしましょう。
EXPLAIN ANALYZE 深掘り解析
EXPLAIN ANALYZEはPostgreSQLパフォーマンスチューニングの最も重要なツールです。クエリ計画を表示するだけでなく、クエリを実行して実際のタイミング統計を返します。
基本的な使い方
-- 実行計画を表示(実行なし)
EXPLAIN SELECT o.order_id, c.customer_name
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE o.order_date >= '2026-01-01'
AND o.status = 'shipped';
-- クエリを実行して実際の統計を返す
EXPLAIN ANALYZE SELECT o.order_id, c.customer_name
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE o.order_date >= '2026-01-01'
AND o.status = 'shipped';
-- 詳細出力を表示(バッファ統計付き)
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders WHERE status = 'shipped';
スキャンタイプの解説
シーケンシャルスキャン(Seq Scan)
EXPLAIN ANALYZE SELECT * FROM orders WHERE amount > 1000;
-- Seq Scan on orders (cost=0.00..15432.00 rows=100000 width=88)
-- Filter: (amount > 1000)
- 適用シナリオ:テーブルデータ量が小さい、クエリが大部分の行を返す、適切なインデックスがない
- 最適化方向:返される行数が総行数の5%未満の場合、インデックスの追加を検討
インデックススキャン(Index Scan)
EXPLAIN ANALYZE SELECT * FROM orders WHERE order_id = 12345;
-- Index Scan using orders_pkey on orders (cost=0.42..8.44 rows=1 width=88)
-- Index Cond: (order_id = 12345)
- 適用シナリオ:等価クエリ、高選択性条件、少数行の返却
- 最適化方向:インデックスカラムの順序がクエリ条件と一致することを確認
ビットマップスキャン(Bitmap Scan)
EXPLAIN ANALYZE SELECT * FROM orders WHERE status = 'shipped';
-- Bitmap Heap Scan on orders (cost=523.12..8234.56 rows=15000 width=88)
-- Recheck Cond: (status = 'shipped')
-- -> Bitmap Index Scan on idx_orders_status (cost=0.00..519.37 rows=15000)
- 適用シナリオ:中程度の行数を返す(テーブルの5%-15%)、複数条件の組み合わせクエリ
- 最適化方向:Bitmap Index Scan + Bitmap Heap Scanの組み合わせ、中程度の選択性に適している
ジョインタイプの解説
ネステッドループジョイン(Nested Loop)
EXPLAIN ANALYZE
SELECT * FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
WHERE o.customer_id = 100;
-- Nested Loop (cost=0.85..234.56 rows=50 width=200)
-- -> Index Scan using idx_orders_customer on orders o
-- -> Index Scan using idx_items_order on order_items oi
- 適用シナリオ:小テーブル駆動大テーブル、内テーブルにインデックスがある、少数行の返却
- 最適化方向:内テーブルのジョインカラムにインデックスがあることを確認
ハッシュジョイン(Hash Join)
EXPLAIN ANALYZE
SELECT * FROM orders o
JOIN customers c ON o.customer_id = c.customer_id;
-- Hash Join (cost=345.67..12345.89 rows=100000 width=200)
-- Hash Cond: (o.customer_id = c.customer_id)
-- -> Seq Scan on orders o
-- -> Hash
-- -> Seq Scan on customers c
- 適用シナリオ:大テーブル同士のジョイン、等価ジョイン、ソート不要
- 最適化方向:Build側(ハッシュテーブル)がwork_memに収まることを確認
マージジョイン(Merge Join)
EXPLAIN ANALYZE
SELECT * FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
ORDER BY o.customer_id;
-- Merge Join (cost=0.86..15678.90 rows=100000 width=200)
-- Merge Cond: (o.customer_id = c.customer_id)
-- -> Index Scan using idx_orders_customer on orders o
-- -> Index Scan using customers_pkey on customers c
- 適用シナリオ:両側がソート済み、等価ジョイン、大データ量
- 最適化方向:両側のジョインカラムにインデックス(ソート済み)があることを確認
EXPLAIN出力のキーフィールド
| フィールド | 意味 | 注目点 |
|---|---|---|
| cost | 推定コスト(起動..合計コスト) | 合計コストが高い場合は最適化が必要 |
| rows | 推定返却行数 | 実際の行数との差が大きい場合はANALYZEが必要 |
| width | 平均行幅(バイト) | 広すぎる場合はSELECTカラムの削減を検討 |
| actual time | 実際の所要時間(ミリ秒) | 遅いノードに注目 |
| actual rows | 実際の返却行数 | 推定との差が大きい場合は統計を更新 |
| loops | 実行回数 | ネステッドループでは loops × time = 合計時間 |
| Buffers | バッファヒット統計 | shared hitが高い=キャッシュフレンドリー |
💡 JSONフォーマッターツールを使って
FORMAT JSON出力のEXPLAIN結果を解析しましょう。
インデックスタイプと選定実践
PostgreSQLは豊富なインデックスタイプを提供しています。適切なインデックスタイプの選択がパフォーマンスチューニングの鍵です。
B-treeインデックス(デフォルト)
-- B-treeインデックスの作成
CREATE INDEX idx_orders_date ON orders (order_date);
-- 複合B-treeインデックス
CREATE INDEX idx_orders_status_date ON orders (status, order_date);
-- 適用シナリオ:等価クエリ、範囲クエリ、ソート、LIKE 'prefix%'
-- 最も一般的なインデックスタイプ、90%以上のシナリオをカバー
GINインデックス(転置インデックス)
-- JSONBフィールドインデックス
CREATE INDEX idx_products_attrs ON products USING GIN (attributes);
-- 配列フィールドインデックス
CREATE INDEX idx_articles_tags ON articles USING GIN (tags);
-- 全文検索インデックス
CREATE INDEX idx_docs_content ON documents USING GIN (to_tsvector('english', content));
-- クエリ例
SELECT * FROM products WHERE attributes @> '{"color": "red"}';
SELECT * FROM articles WHERE tags @> ARRAY['postgresql', 'performance'];
SELECT * FROM documents WHERE to_tsvector('english', content) @@ to_tsquery('postgresql & tuning');
- 適用シナリオ:JSONB、配列、全文検索
- 注意:GINインデックスは構築が遅いがクエリは速い;
fastupdate = onで書き込みを高速化
GiSTインデックス(汎用検索ツリー)
-- 地理空間インデックス(PostGIS)
CREATE INDEX idx_stores_location ON stores USING GiST (location);
-- 範囲型インデックス
CREATE INDEX idx_events_period ON events USING GiST (time_range);
-- クエリ例
SELECT * FROM stores WHERE ST_DWithin(location, ST_Point(116.4, 39.9), 5000);
SELECT * FROM events WHERE time_range && '[2026-01-01, 2026-06-30)'::daterange;
- 適用シナリオ:地理空間、範囲型、ツリー構造
- 注意:GiSTはカスタム演算子クラスをサポート
BRINインデックス(ブロック範囲インデックス)
-- 時系列データインデックス
CREATE INDEX idx_logs_created ON logs USING BRIN (created_at);
-- カスタムブロックサイズ
CREATE INDEX idx_logs_created ON logs USING BRIN (created_at) WITH (pages_per_range = 32);
-- クエリ例
SELECT * FROM logs WHERE created_at >= '2026-06-01' ORDER BY created_at DESC LIMIT 100;
- 適用シナリオ:大テーブルで自然にソートされたカラム(時系列データ、オートインクリメントID)
- 利点:インデックスサイズが極めて小さい(通常B-treeの1/100)
- 注意:物理的に順序付けられたデータにのみ適している;選択性クエリではB-treeに劣る
Hashインデックス
-- Hashインデックスの作成
CREATE INDEX idx_users_email_hash ON users USING HASH (email);
-- 適用シナリオ:純粋な等価クエリ
-- 注意:PostgreSQL 10+でHashインデックスはWALをサポートし、安全に使用可能
-- ただし通常B-treeで十分;Hashインデックスの使用場面は限定的
インデックスタイプ選定デシジョンテーブル
| シナリオ | 推奨インデックス | 理由 |
|---|---|---|
| 等価/範囲クエリ | B-tree | 最も汎用的、安定したパフォーマンス |
| JSONB包含クエリ | GIN | ネイティブ@>演算子サポート |
| 全文検索 | GIN | tsvectorによる効率的検索 |
| 地理空間クエリ | GiST | PostGIS標準の組み合わせ |
| 範囲重複クエリ | GiST | ネイティブ&&演算子サポート |
| 時系列範囲クエリ | BRIN | 極小インデックス、物理的に順序付けられたデータに適している |
| 純粋な等価クエリ(超大テーブル) | Hash | 等価クエリでB-treeよりわずかに高速 |
部分インデックスと式インデックス
部分インデックス(Partial Index)
-- 未完了の注文のみインデックス(インデックスサイズを削減)
CREATE INDEX idx_orders_pending ON orders (customer_id, order_date)
WHERE status = 'pending';
-- アクティブユーザーのみインデックス
CREATE INDEX idx_users_active ON users (last_login_at)
WHERE is_active = true;
-- クエリは自動的に部分インデックスを使用
SELECT * FROM orders WHERE status = 'pending' AND customer_id = 100;
- 利点:インデックスサイズが大幅に削減、書き込みオーバーヘッドが低下
- 注意:クエリ条件がWHERE句を満たす必要がある
式インデックス
-- 大文字小文字を区別しないクエリ
CREATE INDEX idx_users_email_lower ON users (LOWER(email));
-- 日付切り詰めクエリ
CREATE INDEX idx_orders_date_trunc ON orders (DATE(order_date));
-- JSONB特定キーインデックス
CREATE INDEX idx_products_color ON products ((attributes->>'color'));
-- クエリ例
SELECT * FROM users WHERE LOWER(email) = 'admin@example.com';
SELECT * FROM orders WHERE DATE(order_date) = '2026-06-11';
SELECT * FROM products WHERE attributes->>'color' = 'red';
- 利点:クエリ時の式計算を回避、インデックスを直接使用
- 注意:クエリ条件がインデックス式と完全に一致する必要がある
複合インデックスのカラム順序の原則
-- 順序:等価条件カラム → 範囲条件カラム → ソートカラム
CREATE INDEX idx_orders_optimal ON orders (status, customer_id, order_date DESC);
-- カバリングインデックス:すべてのクエリカラムを含め、テーブルルックアップを回避
CREATE INDEX idx_orders_covering ON orders (status, order_date)
INCLUDE (customer_id, amount);
クエリ最適化パターン
SELECT * の回避
-- アンチパターン
SELECT * FROM orders WHERE customer_id = 100;
-- 最適化:必要なカラムのみクエリ
SELECT order_id, order_date, amount
FROM orders WHERE customer_id = 100;
ページネーション最適化
-- アンチパターン:OFFSETは大きなオフセットでパフォーマンスが極めて悪い
SELECT * FROM orders ORDER BY order_id OFFSET 100000 LIMIT 20;
-- 最適化1:キーセットページネーション(カーソルページネーション)
SELECT * FROM orders WHERE order_id > 100000 ORDER BY order_id LIMIT 20;
-- 最適化2:カバリングインデックス + JOIN
SELECT o.* FROM orders o
JOIN (SELECT order_id FROM orders ORDER BY order_id OFFSET 100000 LIMIT 20) t
ON o.order_id = t.order_id;
サブクエリ最適化
-- アンチパターン:相関サブクエリ
SELECT * FROM orders o
WHERE EXISTS (SELECT 1 FROM order_items oi WHERE oi.order_id = o.order_id AND oi.price > 100);
-- 最適化:JOINに書き換え
SELECT DISTINCT o.* FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
WHERE oi.price > 100;
CTE最適化
-- PostgreSQL 12+:CTEはデフォルトでインライン最適化可能
WITH recent_orders AS (
SELECT * FROM orders WHERE order_date >= '2026-01-01'
)
SELECT ro.*, c.customer_name
FROM recent_orders ro
JOIN customers c ON ro.customer_id = c.customer_id;
-- CTEをマテリアライズする必要がある場合は明示的に指定
WITH MATERIALIZED expensive_calc AS (
SELECT customer_id, SUM(amount) AS total
FROM orders GROUP BY customer_id
)
SELECT * FROM expensive_calc WHERE total > 10000;
バッチ操作最適化
-- バッチ挿入(単一INSERTで複数値)
INSERT INTO orders (customer_id, amount, status)
VALUES
(1, 100.00, 'pending'),
(2, 200.00, 'shipped'),
(3, 150.00, 'pending');
-- CASEを使用したバッチ更新
UPDATE orders SET status = CASE
WHEN order_id = 1 THEN 'shipped'
WHEN order_id = 2 THEN 'delivered'
WHEN order_id = 3 THEN 'cancelled'
ELSE status
END
WHERE order_id IN (1, 2, 3);
-- COPYによる高速インポート
COPY orders FROM '/data/orders.csv' WITH (FORMAT csv, HEADER true);
コネクションプール設定(PgBouncer)
PgBouncerはPostgreSQLで最も人気のあるコネクションプールで、接続数過多によるパフォーマンス問題を効果的に解決します。
なぜコネクションプールが必要か?
各PostgreSQL接続はプロセスをforkし、約10MBのメモリを消費します。1000接続 = 10GBのメモリオーバーヘッド、さらにプロセススイッチングコストも高いです。
PgBouncerのインストールと設定
# PgBouncerのインストール
sudo apt install pgbouncer
# PgBouncerの起動
sudo systemctl start pgbouncer
sudo systemctl enable pgbouncer
; /etc/pgbouncer/pgbouncer.ini
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 10000
default_pool_size = 50
min_pool_size = 10
reserve_pool_size = 10
reserve_pool_timeout = 3
server_idle_timeout = 300
server_lifetime = 3600
log_connections = 0
log_disconnections = 0
stats_period = 60
コネクションプールモード
| モード | 特徴 | 適用シナリオ |
|---|---|---|
| session | セッションレベルの再利用、接続をクライアントにバインド | セッション変数、一時テーブルが必要 |
| transaction | トランザクションレベルの再利用、トランザクション終了後に接続を返却 | ほとんどのWebアプリ(推奨) |
| statement | ステートメントレベルの再利用、各ステートメント後に返却 | 短いクエリ、トランザクションなし |
接続数の計画
-- 現在の接続数を確認
SELECT count(*) FROM pg_stat_activity;
-- ステータス別にグループ化
SELECT state, count(*) FROM pg_stat_activity GROUP BY state ORDER BY count DESC;
-- PostgreSQLの最大接続数を確認
SHOW max_connections;
-- 推奨計算式
-- PgBouncer default_pool_size = CPUコア数 × 2 + ディスク数
-- PostgreSQL max_connections = 全PgBouncer pool_sizeの合計 + 予約分
コアパラメータチューニング
shared_buffers
-- 共有バッファサイズ、PostgreSQLの最も重要なパラメータ
-- 推奨値:システム総メモリの25%、ただし8GBを超えない(Linux)
ALTER SYSTEM SET shared_buffers = '4GB';
work_mem
-- ソートとハッシュ操作のメモリ上限
-- 推奨値:総メモリ / (max_connections × 3)、通常16MB-256MB
ALTER SYSTEM SET work_mem = '64MB';
-- work_memによるディスクソートを確認
SELECT query, calls, temp_blks_written
FROM pg_stat_statements
WHERE temp_blks_written > 0
ORDER BY temp_blks_written DESC LIMIT 10;
effective_cache_size
-- クエリオプティマイザのシステムキャッシュ推定値(実際にはメモリを割り当てない)
-- 推奨値:システム総メモリの50%-75%
ALTER SYSTEM SET effective_cache_size = '12GB';
WAL関連パラメータ
-- WAL書き込み方式:open_datasync(Linuxデフォルト)、fdatasync、fsyncなど
ALTER SYSTEM SET wal_sync_method = 'open_datasync';
-- WALバッファサイズ
ALTER SYSTEM SET wal_buffers = '64MB';
-- チェックポイント間隔
ALTER SYSTEM SET checkpoint_timeout = '10min';
ALTER SYSTEM SET max_wal_size = '2GB';
ALTER SYSTEM SET min_wal_size = '512MB';
-- 整合性保証(本番環境では必ずON)
ALTER SYSTEM SET fsync = 'on';
ALTER SYSTEM SET synchronous_commit = 'on';
主要パラメータクイックリファレンス
| パラメータ | 推奨値(16GB RAM) | 説明 |
|---|---|---|
| shared_buffers | 4GB | 共有バッファ |
| work_mem | 64MB | ソート/ハッシュメモリ |
| effective_cache_size | 12GB | オプティマイザキャッシュ推定 |
| maintenance_work_mem | 1GB | メンテナンス操作メモリ |
| wal_buffers | 64MB | WALバッファ |
| max_wal_size | 2GB | WAL最大サイズ |
| checkpoint_timeout | 10min | チェックポイント間隔 |
| random_page_cost | 1.1(SSD) | ランダムページコスト |
| effective_io_concurrency | 200(SSD) | 同時I/O数 |
| max_connections | 100-200 | 最大接続数 |
# pgtuneを使用して推奨設定を自動生成
pgtune -i postgresql.conf -M 16GB -T web -c 200
パーティショニング戦略
PostgreSQL 10+はネイティブ宣言型パーティショニングをサポートし、大テーブル処理のコアアプローチです。
レンジパーティショニング(Range)
-- 月別に注文テーブルをパーティショニング
CREATE TABLE orders (
order_id BIGINT,
customer_id INT,
order_date DATE,
amount DECIMAL(10,2),
status TEXT
) PARTITION BY RANGE (order_date);
CREATE TABLE orders_2026_q1 PARTITION OF orders
FOR VALUES FROM ('2026-01-01') TO ('2026-04-01');
CREATE TABLE orders_2026_q2 PARTITION OF orders
FOR VALUES FROM ('2026-04-01') TO ('2026-07-01');
CREATE TABLE orders_2026_q3 PARTITION OF orders
FOR VALUES FROM ('2026-07-01') TO ('2026-10-01');
CREATE TABLE orders_2026_q4 PARTITION OF orders
FOR VALUES FROM ('2026-10-01') TO ('2027-01-01');
CREATE TABLE orders_default PARTITION OF orders DEFAULT;
リストパーティショニング(List)
-- 地域別パーティショニング
CREATE TABLE customers (
customer_id BIGINT,
name TEXT,
region TEXT,
created_at TIMESTAMP
) PARTITION BY LIST (region);
CREATE TABLE customers_north PARTITION OF customers
FOR VALUES IN ('Beijing', 'Tianjin', 'Hebei');
CREATE TABLE customers_south PARTITION OF customers
FOR VALUES IN ('Guangdong', 'Fujian', 'Hainan');
CREATE TABLE customers_default PARTITION OF customers DEFAULT;
ハッシュパーティショニング(Hash)
-- ハッシュパーティショニング(均等分散)
CREATE TABLE access_logs (
log_id BIGINT,
user_id INT,
path TEXT,
created_at TIMESTAMP
) PARTITION BY HASH (user_id);
CREATE TABLE access_logs_p0 PARTITION OF access_logs
FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE access_logs_p1 PARTITION OF access_logs
FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE access_logs_p2 PARTITION OF access_logs
FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE access_logs_p3 PARTITION OF access_logs
FOR VALUES WITH (MODULUS 4, REMAINDER 3);
パーティションメンテナンス
-- 将来のパーティションを自動作成(pg_partman拡張を使用)
CREATE EXTENSION pg_partman;
SELECT partman.create_parent(
p_parent_table := 'public.orders',
p_control := 'order_date',
p_type := 'range',
p_interval := '1 month',
p_premake := 6
);
-- パーティションプルーニングの検証
EXPLAIN SELECT * FROM orders WHERE order_date = '2026-06-11';
-- orders_2026_q2パーティションのみをスキャンすべき
-- 古いパーティションの削除(アーカイブ後)
DROP TABLE orders_2024_q1;
パラレルクエリ
PostgreSQL 9.6+でパラレルクエリが導入され、バージョン18で大幅に強化されました。
パラレルクエリの有効化
-- パラレルクエリを有効にする
ALTER SYSTEM SET max_parallel_workers_per_gather = 4;
ALTER SYSTEM SET max_parallel_workers = 8;
ALTER SYSTEM SET parallel_tuple_cost = 0.01;
ALTER SYSTEM SET parallel_setup_cost = 100;
-- パラレルをトリガーする最小テーブルサイズ(デフォルト8MB)
ALTER SYSTEM SET min_parallel_table_scan_size = '8MB';
-- テーブルサイズに応じてパラレルワーカー数を増加
ALTER SYSTEM SET min_parallel_index_scan_size = '512kB';
パラレルクエリの検証
EXPLAIN ANALYZE SELECT COUNT(*), SUM(amount)
FROM orders WHERE order_date >= '2026-01-01';
-- Gather Merge
-- -> Sort
-- -> Partial Aggregate
-- -> Parallel Seq Scan on orders
パラレルクエリの適用シナリオ
| シナリオ | パラレル効果 | 備考 |
|---|---|---|
| 大テーブルフルスキャン | 非常に良い | Parallel Seq Scan |
| 大テーブル集約 | 非常に良い | Parallel Aggregation |
| 大テーブルJOIN | 良い | Parallel Hash Join |
| インデックススキャン | 普通 | Parallel Bitmap Scan |
| 小テーブルクエリ | 効果なし | min_parallel_table_scan_size未満 |
| CTE(マテリアライズド) | 非サポート | MATERIALIZED CTEはパラレル化されない |
VACUUMとautovacuumチューニング
PostgreSQLのMVCCメカニズムはデッドタプルを生成し、VACUUMによって領域を回収する必要があります。
手動VACUUM
-- 通常VACUUM(ディスク領域を回収せず、再利用可能としてマークのみ)
VACUUM orders;
-- VACUUM FULL(ディスク領域を回収、テーブルをロック!)
VACUUM FULL orders;
-- パラレルVACUUM(PostgreSQL 12+、テーブルロックなし)
VACUUM (PARALLEL 4) orders;
-- 統計情報の分析
ANALYZE orders;
-- VACUUM + ANALYZEを同時に実行
VACUUM ANALYZE orders;
autovacuumチューニング
-- autovacuumを有効にする(デフォルトで有効)
ALTER SYSTEM SET autovacuum = 'on';
-- autovacuumワーカー数
ALTER SYSTEM SET autovacuum_max_workers = 4;
-- トリガー閾値(デッドタプル数がこの値を超えるとトリガー)
-- 計算式:autovacuum_vacuum_scale_factor × テーブル行数 + autovacuum_vacuum_threshold
ALTER SYSTEM SET autovacuum_vacuum_scale_factor = 0.1;
ALTER SYSTEM SET autovacuum_vacuum_threshold = 50;
-- 分析トリガー閾値
ALTER SYSTEM SET autovacuum_analyze_scale_factor = 0.05;
ALTER SYSTEM SET autovacuum_analyze_threshold = 50;
-- autovacuum1回あたりのコスト制限
ALTER SYSTEM SET autovacuum_vacuum_cost_limit = 2000;
ALTER SYSTEM SET autovacuum_vacuum_cost_delay = '2ms';
テーブルごとのautovacuumチューニング
-- 頻繁に更新されるテーブルのトリガー閾値を下げる
ALTER TABLE orders SET (
autovacuum_vacuum_scale_factor = 0.01,
autovacuum_analyze_scale_factor = 0.01,
autovacuum_vacuum_cost_limit = 5000
);
-- テーブルのVACUUM統計を確認
SELECT relname, n_live_tup, n_dead_tup,
last_vacuum, last_autovacuum,
last_analyze, last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC LIMIT 10;
VACUUM進捗のモニタリング
-- PostgreSQL 12+ VACUUM進捗を確認
SELECT pid, datname, relid::regclass, phase,
heap_blks_total, heap_blks_scanned, heap_blks_vacuumed,
index_vacuum_count, num_dead_item_ids
FROM pg_stat_progress_vacuum;
pg_stat_statementsによるスロークエリ分析
pg_stat_statementsはPostgreSQLの最も強力なスロークエリ分析ツールです。
インストールと有効化
-- 拡張をロード
CREATE EXTENSION pg_stat_statements;
-- postgresql.confを変更
-- shared_preload_libraries = 'pg_stat_statements'
-- pg_stat_statements.track = all
-- pg_stat_statements.max = 10000
Top N スロークエリ
-- 合計時間でソート
SELECT query, calls, total_exec_time, mean_exec_time,
min_exec_time, max_exec_time, rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC LIMIT 10;
-- 平均時間でソート
SELECT query, calls, mean_exec_time, total_exec_time
FROM pg_stat_statements
WHERE calls > 10
ORDER BY mean_exec_time DESC LIMIT 10;
-- ディスクI/Oでソート
SELECT query, calls, shared_blks_read, shared_blks_hit,
ROUND(shared_blks_hit::numeric / NULLIF(shared_blks_hit + shared_blks_read, 0), 4) AS cache_hit_ratio
FROM pg_stat_statements
WHERE shared_blks_read > 0
ORDER BY shared_blks_read DESC LIMIT 10;
-- 統計をリセット
SELECT pg_stat_statements_reset();
キャッシュヒット率分析
-- 全体キャッシュヒット率(> 99%であるべき)
SELECT
sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) AS cache_hit_ratio
FROM pg_statio_user_tables;
-- テーブル別キャッシュヒット率
SELECT relname,
heap_blks_hit, heap_blks_read,
ROUND(heap_blks_hit::numeric / NULLIF(heap_blks_hit + heap_blks_read, 0), 4) AS cache_hit_ratio
FROM pg_statio_user_tables
WHERE heap_blks_read > 0
ORDER BY heap_blks_read DESC;
一般的なパフォーマンスアンチパターン
アンチパターン1:過剰インデックス
-- 問題:各インデックスは書き込みオーバーヘッドを増加させる
-- 未使用インデックスを確認
SELECT schemaname, relname, indexrelname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;
-- 未使用インデックスを削除
DROP INDEX CONCURRENTLY idx_orders_unused;
アンチパターン2:大規模トランザクション
-- 問題:長時間トランザクションはVACUUMによるデッドタプル回収を妨げ、テーブル膨張を引き起こす
-- 長時間トランザクションを確認
SELECT pid, now() - xact_start AS duration, query, state
FROM pg_stat_activity
WHERE state IN ('idle in transaction', 'active')
AND xact_start IS NOT NULL
ORDER BY duration DESC;
-- トランザクションタイムアウトを設定(PostgreSQL 17+)
ALTER SYSTEM SET idle_in_transaction_session_timeout = '60000';
アンチパターン3:型の不一致
-- 問題:暗黙的な型変換によりインデックスが使用できない
-- アンチパターン
SELECT * FROM orders WHERE order_id = '12345'; -- VARCHARとBIGINTの不一致
-- 最適化:型を一致させる
SELECT * FROM orders WHERE order_id = 12345;
アンチパターン4:OR条件の乱用
-- アンチパターン
SELECT * FROM orders WHERE customer_id = 100 OR amount > 5000;
-- 最適化:UNION ALLを使用
SELECT * FROM orders WHERE customer_id = 100
UNION ALL
SELECT * FROM orders WHERE amount > 5000 AND customer_id != 100;
アンチパターン5:関数によるインデックス無効化
-- アンチパターン:インデックスカラムに関数を使用
SELECT * FROM orders WHERE DATE(created_at) = '2026-06-11';
-- 最適化:範囲クエリを使用
SELECT * FROM orders WHERE created_at >= '2026-06-11' AND created_at < '2026-06-12';
-- または式インデックスを使用
CREATE INDEX idx_orders_created_date ON orders (DATE(created_at));
Prometheus + Grafana モニタリング
postgres_exporterのデプロイ
# postgres_exporterのインストール
wget https://github.com/prometheus-community/postgres_exporter/releases/download/v0.15.0/postgres_exporter-0.15.0.linux-amd64.tar.gz
tar xzf postgres_exporter-0.15.0.linux-amd64.tar.gz
# データソースの設定
export DATA_SOURCE_NAME="postgresql://monitor:password@localhost:5432/mydb?sslmode=disable"
# 起動
./postgres_exporter --web.listen-address=:9187
Prometheus設定
scrape_configs:
- job_name: 'postgresql'
static_configs:
- targets: ['localhost:9187']
metric_relabel_configs:
- source_labels: [__name__]
regex: 'pg_.*'
action: keep
主要モニタリングメトリクス
| メトリクス | 意味 | アラート閾値 |
|---|---|---|
| pg_stat_activity_count | 現在の接続数 | > max_connections × 80% |
| pg_stat_database_deadlocks | デッドロック数 | > 0 |
| pg_stat_user_tables_n_dead_tup | デッドタプル数 | > 総行数 × 10% |
| pg_stat_statements_mean_exec_time | 平均クエリ時間 | P99 > 500ms |
| pg_replication_lag | レプリケーション遅延 | > 30s |
| pg_database_size_bytes | データベースサイズ | > ディスク85% |
アラートルール例
groups:
- name: postgresql_alerts
rules:
- alert: PostgreSQLHighConnections
expr: pg_stat_activity_count > 160
for: 5m
labels:
severity: warning
annotations:
summary: "PostgreSQL接続数が高すぎます"
description: "現在の接続数が160を超え、5分間継続"
- alert: PostgreSQLReplicationLag
expr: pg_replication_lag > 30
for: 2m
labels:
severity: critical
annotations:
summary: "PostgreSQLレプリケーション遅延が高すぎます"
description: "レプリケーション遅延が30秒を超過"
- alert: PostgreSQLDeadTuplesHigh
expr: pg_stat_user_tables_n_dead_tup / pg_stat_user_tables_n_live_tup > 0.1
for: 10m
labels:
severity: warning
annotations:
summary: "PostgreSQLデッドタプル比率が高すぎます"
description: "デッドタプルが10%を超過、VACUUMが必要"
💡 ハッシュ暗号化ツールを使ってモニタリングアラートwebhookの署名キーを生成しましょう。
FAQ
Q1:shared_buffersはどのくらいに設定すべきですか?
システム総メモリの**25%**に設定することを推奨しますが、8GBを超えないようにしてください(Linux)。PostgreSQLはOSのページキャッシュに依存しているため、shared_buffersが大きすぎるとOSキャッシュと競合します。16GB RAMのサーバーの場合、shared_buffers = 4GBに設定します。
Q2:B-treeの代わりにBRINインデックスを使うべき時は?
テーブルが非常に大きく(1億行超)、カラム値が物理ストレージ順序と相関している場合(タイムスタンプ、オートインクリメントIDなど)、BRINインデックスのサイズはB-treeの1/100で、クエリパフォーマンスは同等です。典型的なユースケース:ログテーブルの時間カラム、時系列データの収集タイムスタンプ。
Q3:パーティショニングが必要かどうかの判断方法は?
単一テーブルが1000万行を超え、クエリが通常部分的なデータのみを対象とする場合(例:時間範囲クエリ)、パーティショニングを検討してください。パーティションプルーニングによりスキャンデータ量を大幅に削減できます。
Q4:VACUUM FULLはいつ使うべきですか?
VACUUM FULLはテーブルをロックし全体を再構築するため、すべての断片化した領域を回収します。テーブル膨張が深刻(デッドタプル > 50%)で、通常のVACUUMで回収できない場合にのみ使用してください。オンライン代替としてpg_repack拡張の使用を推奨します。
Q5:PgBouncerのtransactionモードの制限は?
transactionモードでは、トランザクション終了後に接続が回収されるため、一時テーブル(トランザクション間)、SETによるセッションレベルパラメータ(トランザクション間)、LISTEN/NOTIFY(トランザクション間)、プリペアドステートメント(トランザクション間)は使用できません。これらの機能が必要な場合はsessionモードを使用してください。
Q6:スロークエリを素早く特定する方法は?
pg_stat_statements拡張を有効にするpg_stat_statementsをtotal_exec_timeでソートしてクエリ- スロークエリに
EXPLAIN ANALYZEを使用して実行計画を分析 - Seq Scan、高コストノードがないか確認
- 適切にインデックスを追加またはクエリを書き換え
Q7:random_page_costはいくつに設定すべきですか?
- SSD/NVMe:1.1に設定(シーケンシャルI/Oコストに近い)
- HDD:デフォルトの4.0を維持
- このパラメータはオプティマイザがインデックススキャンとシーケンシャルスキャンのどちらを選択するかに直接影響します
Q8:PostgreSQLの接続数上限の決め方は?
各接続は約10MBのメモリを消費します。推奨計算式:max_connections = 利用可能メモリ(GB) × 100、ただし実際の接続数はPgBouncerプーリングで管理すべきです。PostgreSQLのmax_connectionsは100-200に設定し、外部接続はPgBouncerで管理します。
まとめ
PostgreSQLは2026年も最も強力なオープンソースリレーショナルデータベースであり続けています。パフォーマンスチューニングには、クエリ分析、インデックス設計、パラメータ設定、アーキテクチャ最適化の4つの次元から体系的にアプローチする必要があります。重要なポイントの振り返り:
- EXPLAIN ANALYZEはパフォーマンスチューニングの出発点;各スキャンとジョインタイプの適用シナリオを理解する
- インデックス選定が重要:B-treeは汎用、GINはJSONB/配列、BRINは時系列データに適している
- パラメータチューニングの4つのコア:shared_buffers、work_mem、effective_cache_size、WAL設定
- パーティショニング戦略は大テーブル管理の基盤:レンジパーティショニングが最も一般的、ハッシュパーティショニングで均等分散
- VACUUMチューニングでテーブル膨張を防止:autovacuumを必ず有効にし適切に設定
- モニタリングシステムが保障:pg_stat_statements + postgres_exporter + Grafanaの3点セット
ブラウザローカルツールを無料で試す →