モダンなWebアプリケーションのためのPostgresベストプラクティス
多くの開発者がPostgreSQLを選ぶ理由は、無料で、信頼性が高く、実戦で証明されているからです。しかし、選択するのは簡単な部分です。本当の課題は、それをうまく使うこと、つまり変化する要件に耐えられるスキーマを設計し、データが増えても高速なクエリを書き、アプリケーションのパフォーマンスを密かに低下させないような接続管理を行うことです。
この記事では、本番環境で最も重要なWebアプリケーション向けのPostgreSQLベストプラクティスを解説します。
重要なポイント
- フィルタリング、ソート、結合に使用するフィールドには型付きカラムと制約を使用し、JSONBは進化する、または非構造化データのために予約する。
- インデックスは意図的に選択する — 等価性と範囲クエリにはB-tree、JSONBと全文検索にはGIN、スコープ付きフィルタリングには部分インデックスを使用する。
- 特にサーバーレス環境では、接続枯渇を防ぐためにPgBouncerのようなコネクションプーラーをトランザクションモードで使用する。
- スキーママイグレーションは追加的かつ非ロッキングにし、本番環境で実行する前に必ずマイグレーションツールが生成するものを検証する。
pg_stat_statementsを有効にして、本番インシデントになる前に遅いクエリを特定する。
スキーマ設計:構造化から始め、柔軟性を保つ
優れたPostgreSQLスキーマ設計は、確実に重要なものと後で重要になるかもしれないものを明確に分離することから始まります。
フィルタリング、ソート、結合に使用するフィールド(ユーザーID、タイムスタンプ、ステータスフラグ、外部キーなど)には、制約付きの適切な型付きカラムを使用します。それ以外のすべてについては、JSONBカラムを使用することで、リレーショナル整合性を放棄することなくドキュメントストアの柔軟性が得られます。
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
metadata JSONB DEFAULT '{}'::jsonb
);
このハイブリッドアプローチは、利用可能な最も実用的なモダンPostgreSQL機能の1つです。重要な部分ではスキーマの強制が得られ、要件がまだ進化している部分では柔軟性が得られます。JSONBフィールドがWHERE句に定期的に現れ始めたら、それを実際のカラムに昇格させ、制約を追加します。このマイグレーションは簡単で、実行する価値があります。
避けるべきこと:最初からすべてをJSONBに格納すること。NOT NULL制約を強制したり外部キーを追加したりする必要が出てくるまでは柔軟に感じられますが、その時点でスキーマの半分を書き直すことになります。
実際に役立つPostgreSQLインデックス戦略
インデックスは、ほとんどのPostgreSQLパフォーマンスのヒントが集約される場所です。デフォルトは開始時には問題ありませんが、本番ワークロードには意図的な選択が必要です。
B-treeインデックスは、ほとんどの等価性クエリと範囲クエリを処理します。最近のPostgreSQLバージョン(18+)では、プランナーは複数カラムのB-treeインデックスに対してスキップスキャンを実行できるため、カラムの順序は以前ほど重要ではありません。ただし、最も選択性の高いカラムを先頭に配置することは、依然としてより安全なデフォルトです。
GINインデックスは、JSONBカラムと全文検索に適した選択肢です:
CREATE INDEX idx_user_metadata ON users USING GIN (metadata);
これにより、大きなテーブルでもWHERE metadata @> '{"plan": "premium"}'::jsonbのようなクエリが高速になります。
部分インデックスは十分に活用されておらず、アクティブまたは最近のレコードをフィルタリングするための最良のツールであることが多いです:
CREATE INDEX idx_active_users ON users (email) WHERE deleted_at IS NULL;
すべてのカラムにインデックスを作成することは避けてください。各インデックスは書き込みオーバーヘッドを追加し、メモリを消費します。クエリが実際にフィルタリングするものにインデックスを作成し、EXPLAIN ANALYZEで検証し、使用されていないインデックスを削除してください。
Discover how at OpenReplay.com.
Webワークロード向けの接続管理
PostgreSQLの接続モデルはステートフルで、確立するのに比較的コストがかかります。各接続はサーバー上で専用のプロセスを生成します。大規模では、Webアプリからの数百の短命な接続がパフォーマンスを顕著に低下させます。
解決策は、アプリとデータベースの間にコネクションプーラーを配置することです。PgBouncerが標準的な選択肢です。トランザクションモードでは、多数のアプリ接続をはるかに少数の実際のデータベース接続上で多重化します。
これは特にサーバーレス環境(Lambda、Vercel、Cloudflare Workers)で重要です。各関数呼び出しが新しい接続を開こうとする可能性があります。プーリングがないと、中程度のトラフィックでPostgreSQLのmax_connections制限に達します。詳細については、接続設定に関するPostgreSQLドキュメントを参照してください。
本番環境での安全なマイグレーション
スキーマ変更は問題が発生する場所です。最も安全なパターンは、最初に変更を追加的にすることです:デフォルト値を持つ新しいカラムを追加し、データをバックフィルし、その後のデプロイメントで古いカラムを削除します。
負荷下でテーブルをロックするALTER TABLE操作は避けてください。たとえば、デフォルト値なしでNOT NULLカラムを追加すると、古いPostgreSQLバージョンではテーブル全体が書き換えられます。PostgreSQL 11以降では、非揮発性のデフォルト値を持つカラムの追加はテーブルの書き換えを必要としませんが、本番環境で実行する前にマイグレーションツールが実際に生成するものを確認する価値があります。
可能な限りトランザクション内でマイグレーションを実行し、デプロイ前にロールバックパスをテストしてください。
クエリパフォーマンス:まず遅いクエリを見つける
パフォーマンスの問題を推測しないでください。pg_stat_statementsを有効にして、ワークロード全体のクエリ実行統計を追跡します:
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
次に、最も遅い、または最も呼び出されているクエリを見つけるためにクエリを実行します:
SELECT query, calls, mean_exec_time, total_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
ターゲットが見つかったら、EXPLAIN (ANALYZE, BUFFERS)を使用して、プランナーが何をしているか、インデックスが役立つかどうかを理解します。
まとめ
PostgreSQLは、最初からスキーマ設計、インデックス作成、接続管理を真剣に考える開発者に報いてくれます。これらは高度なトピックではなく、予期しない事態なしに本番環境でWebアプリを実行するための基本です。重要な部分では型付きカラムと制約から始め、柔軟性が本当に必要な場合にのみJSONBを追加し、推測ではなく実際のクエリパターンに基づいてインデックスを作成してください。それをコネクションプーラーと規律あるマイグレーション戦略と組み合わせれば、本番アプリケーションを妨げる最も一般的な落とし穴を回避できます。
よくある質問
定期的にフィルタリング、ソート、結合を行う場合は、JSONBフィールドを独自のカラムに昇格させます。専用カラムを使用すると、NOT NULL制約、外部キー、標準のB-treeインデックスを追加でき、これらすべてがネストされたJSONBパスのクエリと比較してデータ整合性とクエリパフォーマンスを向上させます。
トランザクションモードは、各トランザクション後に接続をプールに返すため、スループットを最大化するため、ほとんどのWebアプリにとって適切なデフォルトです。アドバイザリロック、LISTEN/NOTIFY、セッション設定の変更、または複数のトランザクションにわたって永続化する必要がある一時テーブルなどのセッションレベル機能にアプリケーションが依存している場合にのみ、セッションモードを使用してください。
PostgreSQL 11以降では、非揮発性のデフォルト値を持つカラムの追加はメタデータのみの操作であり、テーブルを書き換えません。ただし、古いバージョンでデフォルト値なしでNOT NULLカラムを追加すると、テーブル全体がロックされて書き換えられます。本番環境に対して実行する前に、必ずマイグレーションツールが生成するSQLを確認してください。
固定の数はありません。クエリが実際にフィルタリング、ソート、結合するカラムにのみインデックスを作成してください。追加のインデックスごとに書き込みオーバーヘッドとメモリ使用量が増加します。pg_stat_user_indexesを使用して未使用のインデックスを特定し、削除してください。最小限から始め、EXPLAIN ANALYZEで監視し、観察されたクエリパターンに基づいてインデックスを追加してください。
Gain control over your UX
See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay. — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.