インフラ・DevOps
PostgreSQLの内部構造を読む:データベースクラスタ、データベース、テーブル
Reading the internals of Postgres: Database cluster, databases, and tables (buraksen.dev)
要約
この記事は、PostgreSQLの内部構造、特にデータベースクラスタ、データベース、テーブルの論理的および物理的構造について詳しく解説しています。各オブジェクトがどのようにOidによって識別され、システムカタログに保存されるか、またデータがファイルシステム上でどのように配置されるかを具体例と共に説明しています。読者がPostgreSQLの動作原理をより深く理解するための手助けとなるでしょう。
全文翻訳
PostgreSQLの内部構造を読む:データベースクラスタ、データベース、テーブル
2026年6月28日
Postgresの内部構造を深く掘り下げていく中で、読んだ内容を内面化し、自分自身の責任感を保つために、メモを書き留めるのが良いだろうと思いました。Postgresに関する素晴らしい参考文献と彼の研究に感謝します、鈴木啓伸氏。ソースリンクはこちらです: https://www.interdb.jp/pg/index.html。
データベースクラスタの論理構造#
PostgreSQLの文脈において、データベースクラスタはデータベースサーバの集合ではありません(SQL標準ではカタログクラスタという用語を使用します)。これは、単一のPostgreSQLインスタンスによって管理されるデータベースのグループを意味します。辞書的な定義としてはおそらく正しいですが、通常「データベースクラスタ」と聞くと、単一のシステムとして機能する複数のノード/インスタンスのデータベースを想定するでしょう。データベースは、テーブル、インデックス、ビューなどのデータベースオブジェクトのコレクションです。PostgreSQLでは、データベースもデータベースオブジェクトであり、Oidという符号なし整数オブジェクト識別子で表されます。
sqlSELECT oid, datname FROM pg_database ORDER BY oid;
oiddatname
1template1
4template0
5postgres
(3 rows)
組み込みオブジェクト(このクエリにおけるデータベース)は、ハードコードされた低い値を持っています。他のユーザーが作成したテーブル/オブジェクトは、16384からOIDsを持ち始めます(OIDs 1-16383は予約されているか、initオブジェクトによって使用されます)。
データベースクラスタ · 一つのPostgreSQLインスタンス
template1
oid 1
template0
oid 4
postgres
oid 5
shop
oid 16384
pg_databaseはすべてのデータベースをリストします(クラスタごとに一つの共有カタログ)
'shop'内のオブジェクト
orders
table
oid 16386
orders_pkey
index
oid 16395
views, sequences, …
その他のオブジェクト
oid …
すべてのオブジェクトは独自のOidによって識別されます
これらのオブジェクトとその関連性は、PostgreSQLの通常のテーブルであるシステムカタログに保存されます。いくつか例を挙げます。
Table Description
pg_class テーブルおよびテーブルに類似する他のオブジェクト(ビュー、インデックス、TOASTテーブルなど)
pg_database 利用可能なデータベースに関する情報を格納します。
pg_index インデックス情報
...
カタログに関するいくつかのトリビア:pg_databaseはクラスタ内のすべてのデータベースで共有されます(クラスタごとに1つのテーブル)が、ほとんどのシステムカタログはデータベースごとに作成されます。pg_classはインデックスも格納しますが、pg_indexカタログも存在します。その理由は、pg_classが汎用的なリレーショナル情報のためであるからです。pg_indexや他の関連カタログは、独自のカスタマイズされたスキーマを持っています。これにより、関心の分離が促進され、将来pg_class2のようなテーブルを作成する必要がなくなります(非常に誇張されていますが、有名なmerchants2テーブル https://jimmyhmiller.com/ugliest-beautiful-codebase を思い出しました)。
上記のように、これらは通常のテーブルであり、クエリを実行できます(自己責任で!)。型、関数、演算子などの多くの組み込みオブジェクトはこれらのテーブルに保存され、ユーザー定義のオブジェクトも同様に追加されます。これらのOIDsは、カタログテーブルに新しい行が追加されると自動的に作成されます。たとえば、拡張機能(例:pgvector)を登録する際に、pgvectorは自動的に作成されたOidとともにpg_extensionテーブルに追加されます。この動作は、以前はユーザー定義テーブルにも当てはまりました。時系列の履歴は次のとおりです。
PG <= 8.0: すべてのテーブル行はOidとともに作成されます。
8.1 <= PG < 12: 自動Oid生成はオプトイン機能です。有効にするには、ユーザーはCREATE TABLE foo (...) WITH OIDS; でテーブルを作成するか、GUC default_with_oids を有効にする必要があります。
PG >= 12: この機能は完全に削除されました。
DBクラスタの物理構造#
Postgresクラスタはすべてのデータをデータディレクトリに保存します。そのパスはPGDATA環境変数によって設定されます。一般的なデフォルトの場所は/var/lib/pgsql/dataと/var/lib/postgresql/<version>/mainです。initdbは、このディレクトリの設定と作成を担当し、Postgresインストーラーによって自動化されます。`brew install postgresql@18`が呼び出されると、Postgres自体がインストールされた後、`postgresql@18.rb`はポストインストールメソッドで以下の行を実行します。
system bin/"initdb", "--locale=en_US.UTF-8", "-E", "UTF-8", postgresql_datadir unless pg_version_exists?
同様のロジックは、他のPostgresインストール方法(Windows用EDB、apt/debなど)でも実装されています。$PGDATA内部には多くのサブディレクトリがあります。
$PGDATA/
├── base/ # データベースごとに1つのサブディレクトリ
│ └── {OID}/ # ファイルとしてのテーブルとインデックス(relfilenode)
├── global/ # クラスタ全体のカタログ(例:pg_class)
├── pg_wal/ # WALセグメントファイル
├── pg_xact/ # トランザクションコミットステータス(clog)
├── pg_tblspc/ # 外部テーブルスペースへのシンボリックリンク
├── PG_VERSION # メジャーバージョン番号
├── postgresql.conf # メインサーバ設定
└── ... # 15以上のサブディレクトリ
完全なリストはhttps://www.postgresql.org/docs/current/storage-file-layout.htmlにあります。サブディレクトリの変更は非常にまれなようです。ここの表は、PG9とPG10でのいくつかの命名変更と新しい追加を示しています。Postgresのソースコードも確認し、`current_logfiles`サブディレクトリがPG10リリースで追加されたことを確認しました。これは19dc233で追加され、PG10以降に含まれています。
bashgit tag --contains 19dc233c32f | grep -E '^REL' | sort -V | head
REL_10_0 REL_10_1 REL_10_2 ...
データベースサブディレクトリレイアウト#
上記のとおり、各データベースはbaseディレクトリ内に独自のサブディレクトリを持ち、そのOid(/base/{OID})にちなんで名付けられます。テーブルとインデックスは、サイズが1GB未満であれば、データベースサブディレクトリ内の単一のファイルに格納されます。Oidと同様に、物理ファイルはrelfilenodeによって識別され、この情報はテーブルおよび/またはインデックスのpg_class行に格納されます。それでは、これらのレイアウトを少し探ってみましょう。私はMacを使用しています。`brew install postgresql@18`でPostgreSQL 18をインストールし、サービスとして実行しています。次に、`psql -d postgres`でターミナルに接続します。
sqlSHOW data_directory;
data_directory/opt/homebrew/var/postgresql@18
(1 row)
上記のとおり、データディレクトリのパスは`/opt/homebrew/var/postgresql@{VERSION}`です。それでは、`shop`データベースを作成して調べてみましょう。
sqlCREATE DATABASE shop; SELECT oid, datname FROM pg_database WHERE datname = 'shop';
CREATE DATABASE
oiddatname
16384shop
(1 row)
そのOidを持つ新しいディレクトリがディスクに作成されました。
bashls /opt/homebrew/var/postgresql@18/base/
1 16384 4 5
驚いたことに、shopディレクトリは空ではありません。その理由は、CREATE DATABASEコマンドが既存のデータベースをコピーしてデータベースを作成するためであり、template1がそのデフォルトのソースデータベースであるためです。テンプレートに関する追加の詳細は公式ドキュメントに記載されています。
bashls /opt/homebrew/var/postgresql@18/base/16384 | head
112 113 1247 1247_fsm 1247_vm 1249 1249_fsm 1249_vm 1255 1255_fsm
基本的なテーブルを作成します。
sql\c shop
CREATE TABLE orders (
id bigserial PRIMARY KEY,
customer_id bigint NOT NULL,
total_cents bigint NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
これで、このテーブルはそれ自体と主キーのインデックスに対してOidを持ちます。pg_classをクエリしてこれらを調べてみましょう。
sqlSELECT oid, relname, relfilenode, relkind FROM pg_class WHERE relname IN ('orders', 'orders_pkey');
oidrelnamerelfilenoderelkind
16386orders16386r
16395orders_pkey16395i
(2 rows)
ご覧のとおり、relfilenodes(物理的な場所の識別子)とOIDsは同一です。pg_relation_filepathは、パスを計算するための組み込み関数です。
sqlSELECT pg_relation_filepath('orders'), pg_relation_filepath('orders_pkey');
pg_relation_filepathpg_relation_filepath
base/16384/16386base/16384/16395
(1 row)
database oid
16384
relfilenode · pg_class
16386
base/16384/16386
base/16384/16386
ディスク上のヒープファイル
ファイルはOidではなくrelfilenodeによって命名されます(CREATE直後のみ一致します)
私たちのテーブルとインデックスファイルは`base/{database_oid}/{table|index_relfilenode}`に保存されています。
いくつかの行を挿入し、VACUUMをトリガーします。
sqlINSERT INTO orders (customer_id, total_cents) SELECT (random() * 1000)::bigint, (random() * 100000)::bigint FROM generate_series(1, 1000);
VACUUM orders;
INSERT 0 1000
VACUUM
VACUUMは主にストレージを再利用するために使用されます。その詳細はドキュメントで確認でき、今後の投稿で詳しく説明します。結果のファイルは次のとおりです。
bashls -l /opt/homebrew/var/postgresql@18/base/16384/{16386*,16395}
-rw-------@ 1 burak admin 65536 Jun 15 10:43 .../base/16384/16386 -rw-------@ 1 burak admin 24576 Ju