PostgreSQLアーキテクチャ入門

PostgreSQLは30年以上活発に開発されてきたオープンソースのオブジェクトリレーショナルデータベース管理システムです。バックエンド開発者にとって、PostgreSQLの仕組みを理解することは、より高速なクエリの作成、より良いスキーマの設計、パフォーマンス問題の効果的なトラブルシューティングに役立ちます。

コアアーキテクチャは複数のレイヤーが協調して動作する構成になっています。最上位では、クライアントアプリケーションがPostgreSQLワイヤープロトコルを使用してネットワークソケット経由で接続します。接続はまずpostmasterプロセスによって受信され、クライアント接続ごとに新しいバックエンドプロセスがフォークされます。この接続ごとのプロセスモデルは強力な分離を提供しますが、同時実行性の高いアプリケーションでは接続プーリング(PgBouncerなど)を使用する必要があります。

クエリがバックエンドプロセスに到達すると、クエリプロセッサを通過します: パーサーが構文をチェックし、アナライザー/リライタールールを適用して権限を検証し、プランナーが最適な実行プランを生成し、エクスペキュータがそのプランをストレージ層に対して実行します。ストレージ層は、行が読み書きされるヒープファイル構造を使用し、別のインデックスファイル(通常はB-tree)がインデックス化された列の高速検索を提供します。

正規化されたリレーショナルスキーマの設計

リレーショナル設計とは、冗長性を最小限に抑え、データの整合性を保護するためにテーブルを整理する手法です。基礎となる概念は正規化であり、データを関連するテーブルに分割する方法を導くルール群(正規形)です。

最も一般的に適用される正規形は第1正規形(1NF)、第2正規形(2NF)、第3正規形(3NF)です。第1正規形では、各カラムがアトミックな値を保持することが求められます — 単一のセル内に配列やカンマ区切りのリストを含めることはできません。第2正規形は1NFに基づき、すべての非キー列が主キー全体に依存することを要求します。これは主に複合キーの場合に重要です。第3正規形はさらに進み、非キー列が他の非キー列に依存してはいけません。

ブログ記事とその著者、タグを保存する必要がある実用的なシナリオを考えてみましょう。非正規化されたアプローチでは、すべてを1つのテーブルに格納しますが、これにより更新異常が発生します — 著者がメールアドレスを変更した場合、その著者が書いたすべての行を更新しなければなりません。正規化された設計では、これを外部キーを持つ別のテーブルに分割します。

sql
CREATE TABLE authors (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL
);
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
body TEXT,
author_id INTEGER NOT NULL REFERENCES authors(id),
published_at TIMESTAMPTZ
);
CREATE TABLE tags (
id SERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL
);
CREATE TABLE post_tags (
post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (post_id, tag_id)
);

post_tagsテーブルは、記事とタグの多対多関係を解決する結合テーブルです。SERIALが自動増分整数を提供し、REFERENCESが外部キー制約を定義し、ON DELETE CASCADEが親が削除されたときに結合行を自動的に削除することに注目してください。

データ型と制約

PostgreSQLは基本的なINTEGERやVARCHARを超える豊富な型システムを提供しています。バックエンド開発者にとって、特に便利な型には、タイムゾーン認識付きのタイムスタンプ用のTIMESTAMPTZ、インデックス機能を備えた半構造化データ保存用のJSONB、グローバル一意識別子用のUUID、真偽フラグ用のBOOLEANなどがあります。

制約はアプリケーションコードのみに依存するのではなく、データベースレベルでデータ整合性を強制します。一般的な制約には、NOT NULL、UNIQUE、CHECK、FOREIGN KEY、PRIMARY KEYがあります。制約を使用することで、バグのあるアプリケーションが無効な値を挿入しようとしても、データが有効な状態を維持できます。

sql
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(200) NOT NULL,
price NUMERIC(10,2) CHECK (price >= 0),
stock INTEGER DEFAULT 0 CHECK (stock >= 0),
metadata JSONB DEFAULT '{}'
);

NUMERIC(10,2)は正確な10進数値を保存し、浮動小数点エラーが許容されない金額に最適です。CHECK制約により、priceとstockが負になることは決してありません。metadataに対するDEFAULT句により、新しい行はNULLではなく空のJSONオブジェクトを自動的に取得します。

クライアント接続からパーサー、アナライザー、プランナー、エクスペキュータ、ストレージ層へと至るクエリ処理パイプラインを示すフローチャート

よくある設計ミス

よくある間違いの1つは、長さ制限なしでVARCHARを使用することで、予期せず巨大な行につながる可能性があります。もう1つは、列が本当に無制限の長さを必要とするかどうかを考慮せず、あらゆる場所でTEXTを選択することです — 制約を明示することでバグをより早期に発見できます。開発者は外部キーカラムのインデックスを忘れがちで、これにより大規模なテーブルでのJOIN操作とCASCADE削除が遅くなります。

過度な正規化は反対の落とし穴です: データを小さすぎるテーブルに分割しすぎると、クエリが過剰なJOINを必要とする可能性があります。読み込み中心のワークロードでは、戦略的な非正規化やマテリアライズドビューが正しいトレードオフかもしれません。目標は、特定のアクセスパターンに合わせたバランスを見つけることです。

まとめ

PostgreSQLのマルチプロセスアーキテクチャとパーサープランナーエクスペキュータパイプラインは、予測可能なパフォーマンス特性を提供します。1NF、2NF、3NFによる正規化されたリレーショナル設計は冗長性を減らし更新異常を防ぎ、外部キーと結合テーブルが複雑な関係を処理します。PostgreSQLの豊富な型システムと制約サポートにより、スキーマレベルでデータ整合性を強制し、エラーがアプリケーションコードに到達する前に検出できます。次のレッスンでは、スキーマ設計からこれらの構造に対する効率的なクエリの作成へと進みます。

レッスンのチェックポイント

1. 新しいクライアント接続を受け入れ、それぞれに対してバックエンドプロセスを生成するのは、どのPostgreSQLプロセスが担当していますか?

2. 第3正規形(3NF)は、1NFと2NFに加えて何を要求しますか?

3. ブログスキーマの例で、postsテーブルにtagsカラムを設けるのではなく、なぜpost_tags結合テーブルが使用されるのですか?

4. 正確な精度が求められる金額を保存するのに最も適したPostgreSQLデータ型はどれですか?

5. 外部キー制約にON DELETE CASCADEが定義されていると、何が起こりますか?

6. SQLクエリはPostgreSQLのクエリプロセッサ内でどの段階を通過しますか?

7. レッスンで警告している、よくあるスキーマ設計のミスとは何ですか?