この記事は PostgreSQL 9.5 に基づいて記述している。
このページでは PostgreSQL のエクステンション(extension)を開発する人向けに、PostgreSQL のテーブルやインデックスを構成するブロックまたはページの内部構造について紹介する。
PostgreSQL の他の記事へのインデックスはここ。
更新履歴
(2017.03.01) 作成。
(2017.03.04) ヒープ操作、システムカタログ、Relation cache entry の説明を追加。
目次
1. はじめに
1.1 PostgreSQL データの構成要素
リレーショナルデータベースはその中心にテーブル(Table)の概念がある。 テーブルは複数の行(Row)を持ち、行の中にはあらかじめ定義された列(Column)が並んでいる。 逆にデータベース(Database)には複数のテーブルが格納されている。 データベース内のテーブルは一つのクエリーの中で同時に操作することができる。
PostgreSQL は 1 つのインスタンスで複数のデータベースを同時に動かすことができる。 これをデータベースクラスタ(Database Cluster)と呼んでいる。 データベースクラスタを運用するインスタンスは1つの TCP ポートでクライアントの接続を待ち受けするが、クライアントは最初にどのデータベースに接続するかを指定してコネクションを張る。 例えば下は JDBC 接続に与える URL で、同じインスタンスへ接続しても dbname によって接続するデータベースを選択できる。
jdbc:postgresql://hostname:5432/dbname
PostgreSQL の場合、一度特定のデータベースとコネクションを張ったら、同一のデータベースクラスタ内にあっても別のデータベースに接続し直すことはできない。 2つのデータベースに同時にアクセスしたければ、2本のコネクションを張る必要がある。
データベース毎に別インスタンスにしないのは、1台のサーバーで複数のユーザーがそれぞれ別のデータベースを運用できるようにしているためである。 データベースが異なれば同じ名前のテーブルを作成するのも自由である。 反面、一つのデータベースクラスタは原則として CPU、メモリ、ディスクのような資源を共有する。 またデータベース間では相互に干渉し合わないと言ったが、ユーザー管理はデータベースクラスタ内で共通となる。
レベル | 名称 | 別名 |
---|---|---|
1 | データベースクラスタ(Database Cluster) | |
2 | データベース(Database) | |
3 | テーブル(Table) | リレーション(Relation) |
4 | 行(Row) | レコード(Record)またはタプル(Tuple) |
5 | 列(Column) | 属性(Attribute) |
さらに PostgreSQL にはシステムカタログと呼ばれるシステムが提供する特殊なテーブルが存在する。 データベースクラスタ内に存在するデータベースは pg_database システムカタログ に 1 つの行として記載されている。 テーブルを管理するのは pg_class システムカタログ であり、各テーブルは pg_class システムカタログに対応する行がある。 実際にはシステムカタログは「データベース毎に個別に存在するもの」と「データベースクラスタに属しデータベース共通のもの」がある。 pg_database はデータベース間で共通で、pg_class はデータベース個別になる。

実際には、データベースやテーブルだけではなく、関数、操作、ロール、その他様々なものに対してシステムカタログがある。 詳細は 参考1を参照のこと。
1.2 Object Identifier (OID)
PostgreSQL はシステムカタログの一部と、WITH OIDS を指定して CREATE TABLE コマンドを実行して作成したテーブルには、その中の行に OID という識別子を付ける事ができる。 OID は 32 ビットの符号なし整数で、INSERT 時に行に対してユニークな値が割り振られる。 UPDATE では OID は変わらない。
OID 付きのテーブルの行には全て OID が付き、OID なしのテーブルの行には全て OID は付かない。混在することはない。
ユーザーが作成するテーブルには WITH OIDS を指定する意味はあまりない(デフォルトは WITHOUT OIDS)。 ただし重要なシステムカタログには OID が付いており、データベース、テーブル、ロール、関数、操作、データ型などにはそれぞれ個別の OID が割り振られる。 OID がそれらの情報を指すインデックスとして利用することができる。
OID は値域によって以下のように使い分けられている。
- 0 は使わない。0 は無効な OID 値を指す。
- FirstBootstrapObjectId(10000) 未満は固定で決まっている。例えば text 型は 25 番、now 関数は 1299 番のように組み込みデータ型や関数に固定値が付いている。システムカタログがデータベース毎に分かれていても、このような OID 値は共通である。
- FirstBootstrapObjectId(10000) 以上、FirstNormalObjectId(16384) 未満は initdb 中に作成したオブジェクトの OID 値として付く。
- FirstNormalObjectId(16384) 以上の値はデータベースの運用中に利用される。
テーブルのストレージレイアウトを考える場合、テーブルに対して OID が割り振られる点が重要である。 実際にテーブルの OID は以下の SQL を実行すれば確認できる。
CREATE TABLE T1 (C1 INT, C2 INT); SELECT 'T1'::regclass::int;
PostgreSQL は新しい OID を採番するためのカウンターをインスタンス内の共有メモリで管理している。 そのため同じデータベースクラスタ内では 16387 → 16388 → 16389 と連番が割り振られるように見える。 ただし OID はあくまでもテーブル内で一意な識別子なので、別のテーブルには同じ OID 番号が振られることがある。 実際には OID 付きテーブルは OID の一意性を管理インデックスで管理して、同じ OID を振らないようにしている。
1.3 データディレクトリ
PostgreSQL は OS のファイルシステムの特定のディレクトリ以下をデータベースクラスタ用のデータ格納位置とする。 データディレクトリと呼ぶ(18.2. ファイルの場所)。 pg_ctl や initdb で -D オプションで指定するディレクトリである。 データディレクトリの下には多数のディレクトリ・ファイルが作成され、それら全てがデータベースクラスタを構成している。
PostgreSQL のデータは全て OS の標準的なファイルシステムのみ介してアクセスしている。 そのため OS のディスクキャッシュと自身の共有メモリバッファ(shared_buffer)が同じデータを2重に持つなど非効率なところがある。 反面移植性は高い。
2. リレーション(Relation)
2.1 リレーションの概要
PostgreSQL のテーブルがどのようにディスクへ格納されるか考える前に、「リレーション」を再考する。 表1で述べたように一般的な RDBMS ではテーブル≒リレーションなのだが、PostgreSQLでは「リレーション」という言葉が2重の意味に使われる。
第一に PostgreSQL の「リレーション」はテーブルを格納するための「ファイル群」あるいは「仮想ファイル」を指す。 リレーションファイルである。 1つのテーブルは1つの「リレーション」に格納される。 「リレーション」はテーブルのサイズの 1GiB ごとに分割されて別々のファイルとして記録される。 複数のファイルに分かれていても PostgreSQL 側では 1 つの連続したファイルに見える。
これだけならテーブル≒リレーションなのだが、PostgreSQL 内にはディスクに格納する必要のあるデータ構造がテーブル以外にも存在し、これもリレーションに格納する。 インデックス、マテリアライズド・ビューなども全てリレーションで格納することになる。 インデックスを格納するのはインデックス・リレーションであり、マテリアライズド・ビューを格納するのはマテリアライズド・ビュー・リレーションである。 ではテーブルを格納するのは? これはリレーション・リレーションである。
リレーションの中でヒープタプルを格納することのできるものをヒープと呼ぶこともある。
名称 | マクロ | 識別子 | ヒープ | 説明 |
---|---|---|---|---|
Relation Relation | RELKIND_RELATION | 'r' | Yes | 通常のテーブル。CREATE TABLE で作成される。 |
Index Relation | RELKIND_INDEX | 'i' | Maybe No (インデックス種類に依存) |
インデックス。CREATE INDEX を使った場合やプライマリーキーや一意制約のあるテーブルを CREATE TABLE、ALTER TABLE した場合に作成される。 |
Sequence Relation | RELKIND_SEQUENCE | 'S' | Yes | シーケンス。CREATE SEQUENCE を使った場合や連番型を使った場合に作成される。 |
Toast Relation | RELKIND_TOASTVALUE | 't' | Yes | TOAST。通常のテーブルやマテリアライズド・ビューが大きな行のテーブル外格納を許す設定の場合、必要に応じて作成される(CREATE TABLE 時ではなく行がインサートされた時に)。ただしテーブル、マテリアライズド・ビューの1個に付き1個まで。 |
View Relation | RELKIND_VIEW | 'v' | No | ビュー。CREATE VIEW で作成されるビューのためのものだが、CREATE VIEW を実行しても実体を持つリレーションは作成されない。API によって明示的に作成することは可能。 |
Compositetype Relation | RELKIND_COMPOSITE_TYPE | 'c' | No | 複合型。CREATE TYPE で作成されるビューのためのものだが、実行しても実体を持つリレーションは作成されない。API によって明示的に作成することは可能。 |
Foreign Table Relation | RELKIND_FOREIGN_TABLE | 'f' | No | 外部テーブル。CREATE FOREIGN TABLE で作成される外部テーブルのためのものだが、CREATE FOREIGN TABLE を実行しても実体を持つリレーションは作成されない。API によって明示的に作成することは可能。 |
Materialized View Relation | RELKIND_MATVIEW | 'm' | Yes | マテリアライズド・ビュー。CREATE MATERIALIZED VIEW で作成される。 |
通常テーブル、マテリアライズド・ビューのリレーションは普通の意味での「リレーション」だが他は異なる。
- インデックス・リレーションはインデックス 1 個づつに作成されるが、その中には B-tree などのデータを格納するので普通の意味でのリレーションになっていない。
- シーケンス・リレーションはシーケンス番号を生成するための 64 ビットカウンターとキャッシュの記録だけを行う。
リレーションの使い方は色々だが、次節で述べるブロック構造には必ず従うことになる。
実体を持たないビュー、複合型、外部テーブルのリレーションも API を直接叩けば作成することができる。
2.2 データディレクトリ中のリレーションの実体
リレーションの実体はデータディレクトリ中に格納されている。 データディレクトリの直下に base というディレクトリがあり、数字のディレクトリと pgsql_tmp ディレクトリがある。 数字のディレクトリは「データベース」の OID(pg_database システムカタログの該当行の OID)となる。 template1 データベースが OID が固定で 1 であり、 initdb 時の割り振りで template0 データベースが 13236 に、postgres データベースが 13241 に決まる。 一方、16384 は CREATE DATABASE で新規に作成したユーザーのデータベースの OID になる。 何番が割り振られるかは運用によって変わる。
base/ 以外に global/ もあるが、これはデータベースクラスタ間で共通のシステムカタログ等を格納するために使われる。
- data directory name/
- base/
- 1/
- 13236/
- 13241/
- 16384/
- pgsql_tmp/
- global/
- base/
各データベースに属するリレーションはこのディレクトリの下に作成される。 例えば 16384/ の下は、数字のファイルと _fsm や _vm のようなサフィックスのついたファイルが並ぶ。
- 13092
- 13092_fsm
- 13092_vm
数値はリレーションの OID を示している。 数値の後の _fsm や _vm のようなサフィックスは、リレーションのフォーク(fork)と呼ぶ。 本体となるサフィックスのないメインリレーションと周辺フォークが集まって、一つのリレーションを構成している。
正確にはデータディレクトリ中に格納されるのはリレーションファイルノードである。 2.5 節 で説明する。
フォークの種類と意味は以下の通りである。
マクロ | 番号 | Suffix | 意味 |
---|---|---|---|
MAIN_FORKNUM | 0 | なし | リレーションの本体。 |
FSM_FORKNUM | 1 | _fsm | Free Space Map。メインリレーションの中の空きブロックを管理する。 PostgreSQL 文書の63.3. 空き領域マップも参照のこと。 |
VISIBILITYMAP_FORKNUM | 2 | _vm | Visibility Map。VACUUM の中で行う検査で、メインリレーションの中のブロックのうち、ブロック内の削除されたタプルの残骸がなく、全てのタプルの可視性が決定されている場合場合に 1 が立つ。ブロック内で INSERT/UPDATE/DELETE が行われると、その操作の中で 0 に戻る。 IndexOnlyScan は visibility map に 1 が立っているブロックはインデックスの情報だけで処理を進める。Visibility map が 0 の場合は、インデックスで合致すると判定されたタプルはヒープを検査してすでに削除されたか確認してから処理を進める。 PostgreSQL 文書の63.4. 可視性マップも参照のこと。 |
INIT_FORKNUM | 3 | _init | これは UNLOGGED テーブルに対するインデックスでのみ使用する特殊なフォークである。空のテーブルに対するインデックス・リレーションの初期状態を記録するために使う。 UNLOGGED テーブルは異常終了した場合、そのヒープは PostgreSQL が再起動した時に 0 ブロックに切り詰められる。これは TRUNCATE を適用したのと同様である。この時、UNLOGGED テーブルに対するインデックスも空テーブルに合わせて初期化が必要になる。ただしインデックス・リレーションは初期状態であっても制御用のブロック(メタページ)があるのため、0 ブロックに切り詰めるだけでは動作しない。 だが PostgreSQL が異常終了した後の再起動シーケンスでは REINDEX 相当の処理を走行することはできない。そこで UNLOGGED テーブルに対する CREATE INDEX 時に予め初期状態をこの INIT フォークに記録し、INIT フォークの内容をメインフォークに単純コピーすることで済ませる。 PostgreSQL 文書の63.5. 初期化フォークも参照のこと。 |
リレーション内のファイルは各フォークごとに 1GiB 単位とするセグメントで分割される。 2 番目以降の 1GiB に対しては .1、.2 のようにセグメント番号が付いた別ファイルとなる。
- 13092
- 13092.1
- 13092.2
- 13092.3
- ...
- 13092_fsm
- 13092_fsm.1
- 13092_fsm.2
- ...
- 13092_vm
- 13092_vm.1
- 13092_vm.2
- ...
PostgreSQL文書の63.1. データベースファイルのレイアウトに詳細が説明されている。
2.3 リレーションの構造
リレーションはその内部を 8 KiB のブロックによって区切られている。 作成直後のリレーションは空(0 ブロック)だが、最大 0xFFFFFFFE (4,294,967,294) ブロックまで増える。 そのため 1 つのリレーションは最大は 0x1FFFFFFFC000 (35,184,372,072,448) バイトとなる。 これは約 32 TiB で、メインフォークは 32,767 個のファイルに分割されることになる。 ただし複数のファイルに分かれても、一つのリレーションは連続したブロック番号が割り振られる。
2.4 API を使った操作
リレーションを作成したり破棄したりする API を表4 に示す。
API | 戻り値の型 | 説明 |
---|---|---|
heap_create_with_catalog(const char *relname, 省略) | Oid | リレーションを作成し、システムカタログに登録する。 登録された pg_class システムカタログ上の OID が戻り値となる。 |
heap_create(const char *relname, 省略) | Relation | 指定された OID のリレーションを作成する。
Relation 型へのポインタが戻り値として返る。
heap_create_with_catalog() は内部で heap_create() を呼び出している。
|
RelationBuildLocalRelation(const char *relname, 省略) | Buffer | リレーションの作成の中間段階として Relation 型のオブジェクトを作成して戻り値として返す。
heap_create() は内部で RelationBuildLocalRelation() を呼び出している。
|
RelationCreateStorage(RelFileNode node, char relpersistence) | void | ディスク上に物理的なリレーションファイルを作成する。
heap_create() は内部で RelationCreateStorage() を呼び出している。
|
RelationDropStorage(Relation rel) | void | relで指定された物理的なリレーションファイルの削除をスケジューリングする。 この意味は2.5 節で説明する。 |
RelationTruncate(Relation rel, BlockNumber nblocks) | void | relで指定された物理的なリレーションファイルを、先頭から nblocks 個のブロックを残して以降のブロックを削除する。
nblocks が 0 ならリレーションファイルは空になる。
VACUUM によってリレーションの後半部分に空のブロックができた場合に使用される。
この操作は API を呼び出すと直ちに実行され、トランザクションロールバックしても元には戻らない。
そのため TRUNCATE TABLE コマンドでテーブルを空行にする場合は、テーブルが作成直後など他のトランザクションの影響がない場合はこの RelationTruncate() が呼ばれるが、それ以外は2.5節で述べる機構を用いて処理する。
|
通常、エクステンションの内部でプログラム的にテーブルを作成する場合は サーバプログラミングインタフェース を使えばよいので、API を直接叩いてリレーションを作成するのは特殊なシチュエーションに限られる。 例えば任意のリレーション種類のリレーションを作成するには以下のようにやる。
Oid
create_view_relation(const char *relname, Oid relnamespace, Oid reltablespace, char relpersistence, char relkind, Oid parentclassid, Oid parentobjectid)
{
Oid ownerid;
RangeVar *newrv;
Relation pg_class;
Oid new_oid;
int natts;
TupleDesc new_tupdesc;
Relation new_rel;
Relation pg_attr;
CatalogIndexState indstate;
ownerid = GetUserId();
/* 新しく作成するリレーションの RangeVar を設定する */
newrv = makeRangeVar(get_namespace_name(relnamespace), relname, -1 /* location */);
new_oid = RangeVarGetRelid(newrv, AccessShareLock, true /* missing_ok */);
/* 新しく作成するリレーションが既に登録済みでないこと */
Assert(!OidIsValid(new_oid));
/* 新しいリレーションを登録するために pg_class システムカタログを開く */
pg_class = heap_open(RelationRelationId, RowExclusiveLock);
/* 新しいリレーションファイルノードを作成する */
new_oid = GetNewRelFileNode(reltablespace, pg_class, relpersistence);
get_user_default_acl(ACL_OBJECT_RELATION, ownerid, relnamespace);
/*
* 新しいリレーションの定義となるタプルディスクリプターを作成する。
* ここでは foo、bar、baz の 3 つの integer 型の列を持つことにする。
*
* @see 「PostgreSQL の基本データ型とタプルの扱い」の5. Tuple Descriptorを参照。
*/
natts = 3;
new_tupdesc = CreateTemplateTupleDesc(natts, false);
TupleDescInitEntry(new_tupdesc, (AttrNumber) 1, "foo", INT4OID, -1, 0);
TupleDescInitEntry(new_tupdesc, (AttrNumber) 2, "bar", INT4OID, -1, 0);
TupleDescInitEntry(new_tupdesc, (AttrNumber) 3, "baz", INT4OID, -1, 0);
/* relcache entry を作成する */
new_rel = RelationBuildLocalRelation(relname,
relnamespace,
new_tupdesc,
new_oid, /* relid */
new_oid, /* relfilenode */
reltablespace,
false, /* shared_relation */
false, /* mapped_relation */
relpersistence,
relkind);
/* ストレージマネージャーをオープンする */
RelationOpenSmgr(new_rel);
/* 物理的なリレーションファイルを作成する */
RelationCreateStorage(new_rel->rd_node, relpersistence);
/* 作成中のリレーションの情報を埋める */
new_rel->rd_rel->relpages = 0;
new_rel->rd_rel->reltuples = 0;
new_rel->rd_rel->relallvisible = 0;
new_rel->rd_rel->relfrozenxid = RecentXmin;
new_rel->rd_rel->relminmxid = GetOldestMultiXactId();
new_rel->rd_rel->relowner = ownerid;
new_rel->rd_rel->reltype = InvalidOid;
new_rel->rd_rel->reloftype = InvalidOid;
new_rel->rd_att->relhasrules = false;
new_rel->rd_att->tdtypeid = InvalidOid;
/* pg_class システムカタログに登録 */
InsertPgClassTuple(pg_class, new_rel, new_oid, (Datum) 0 /* relacl */, (Datum) 0 /* reloptions */);
/*
* 新たに作成したリレーションの列情報を pg_attribute システムカタログに登録する。
* 登録するのはダミーで作成した foo、bar、baz になる。
*/
pg_attr = heap_open(AttributeRelationId, RowExclusiveLock);
/* InsertPgAttributeTuple()
に渡すために pg_attribute システムカタログのインデックス情報を取り出す */
indstate = CatalogOpenIndexes(pg_attr);
for (i = 0; i < natts; i++)
{
Form_pg_attribute attr;
attr = new_tupdesc->attrs[i];
attr->attrelid = new_oid;
attr->attstattarget = -1;
attr->attcacheoff = -1;
attr->attstorage = 'p'; /* 'p': Value must be stored plain always */
InsertPgAttributeTuple(pg_attr, attr, indstate);
}
CatalogCloseIndexes(indstate);
heap_close(pg_attr, RowExclusiveLock);
/*
* 新たに作成したリレーションに親となるオブジェクトとの依存関係を設定すると、
* 親オブジェクトが削除された時に作成したリレーションやその属性情報が自動的に削除されるようになる。
*/
if (OidIsValid(parentclassid) && OidIsValid(parentobjectid))
{
ObjectAddress depender;
ObjectAddress referenced;
referenced.classId = parentclassid;
referenced.objectId = parentobjectid;
referenced.objectSubId = 0;
depender.classId = RelationRelationId;
depender.objectId = new_oid;
depender.objectSubId = 0;
recordDependencyOn(&depender, &referenced, DEPENDENCY_INTERNAL);
}
/*
* 新規に作成したリレーションはロックを解放せずに、トランザクションの終了時に自動解放されるのを待つ。
*
* @see PostgreSQL のトランザクション & MVCC & スナップショットの仕組み」 の 6. データ定義言語(DDL)のトランザクションを参照。
*/
heap_close(new_rel, NoLock);
return new_oid;
}
作成したリレーションをオープン・クローズする API を表5 に示す。
API | 戻り値の型 | 説明 |
---|---|---|
relation_open(Oid relationId, LOCKMOD lockmode) | Relation | relationId で指定されたリレーションを lockmode のロックモードで開く。 relationId が存在しない場合には、ERROR を送出する。 オープンしたリレーションの relcache entry を戻り値として返す。 |
try_relation_open(Oid relationId, LOCKMOD lockmode) | Relation | relationId で指定されたリレーションを lockmode のロックモードで開く。 relationId が存在した場合にはオープンしたリレーションの relcache entry を戻り値として返す。 relationId が存在しない場合には NULL が返り ERROR は送出されない。 |
relation_openrv(const RangeVar *relation, LOCKMOD lockmode) | Relation | relationId で指定されたリレーションを lockmode のロックモードで開く。 relationId が存在しない場合には、ERROR を送出する。 オープンしたリレーションの relcache entry を戻り値として返す。 |
relation_openrv_extended(const RangeVar *relation, LOCKMOD lockmode, bool missing_ok) |
Relation |
missing_ok が false の場合は relation_openrv() と同じ動作をする。
missing_ok が true の場合は、relationId が存在しない場合には戻り値として NULL が返り ERROR は送出されない。
|
relation_close(Relation relation, LOCKMOD lockmode) | void | relation でオープンされているリレーションを lockmode のロックモードで閉じる。 |
heap_open(Oid relationId, LOCKMOD lockmode) | Relation | relation_open() と同等だがインデックス・リレーションと複合型・リレーションをオープンしようとすると ERROR を送出するというチェックが付いている。
他に heap_openrv() と heap_openrv_extended() もある。
|
heap_close(Relation relation, LOCKMOD lockmode) | void | relation_close() と同等の関数である。
|
オープンしたいリレーションの OID を指定する場合には、以下のように API を操作する。
Relation relation; relation = relation_open(oid, AccessShareLock); /* この中で relation を操作する */ relation_close(relation, AccessShareLock);
オープンしたいリレーションの名前とスキーマ(名前空間)から指定する場合には、以下のように API を操作する。
RangeVar ragevar; Relation relation; rangevar = makeRangeVar(get_namespace_name(relnamespace), relname, -1 /* location */); relation = relation_openrv(rangevar, AccessShareLock); /* この中で relation を操作する */ relation_close(relation, AccessShareLock); /* rangevar のメモリは自動的には解放されないので注意 */
2.4.1 Relation Cache Entry
リレーションを扱う API が返す Relation 型は RelationData 型へのポインタである。 これは Relation Cache Entry と呼ばれる。
PostgreSQL はテーブルの構造を pg_class システムカタログで管理しているが、テーブルの列は pg_attribute システムカタログで管理している。 それ以外にもリレーション種類によっては様々なシステムカタログを参照することになる。 RelationData 構造体のインスタンスはそのようなテーブルやリレーションを操作するために必要な様々なデータをまとめて読み込んで保管しているメモリ上のデータ構造である。
さらに同一のセッション中で同じテーブルに対する RelationData は何度も必要となるので、システムカタログの読み直しを防ぐために、RelationData 構造のインスタンスはキャッシュしておく。 キャッシュはリレーション ID をキーとしたハッシュテーブルである。 これが relation cache entry である。 Relation cache entry はプロセス毎に独立している。
2.5 リレーションファイルの削除と再作成
リレーションは CREATE TABLE などのタイミングで RelationCreateStorage()
が呼ばれることにより、データディレクトリ中に実体リレーションファイルとして作成される。
しかし DROP TABLE のタイミングで RelationDropStorage()
が呼ばれた場合、リレーションの実体リレーションファイルは削除のスケジューリングがされるだけで、すぐには削除を受けない。
これは PostgreSQL のトランザクション制御が、データ定義言語(DDL)もトランザクションを実現しているため、DROP TABLE コマンドがロールバック(ROLLBACK)により取り消される場合もあるからだ(PostgreSQL のトランザクション & MVCC & スナップショットの仕組みの6. データ定義言語(DDL)のトランザクションを参照)。 そのためリレーションファイルは遅延して削除するようになっている。
RelationDropStorage()
は削除するリレーションファイルの情報を pendingDeletes というリストに繋げる。
トランザクションの完了時などに smgrDoPendingDeletes()
が呼び出され、pendingDeletes につながったもののうち生存中の全てのトランザクションから見て削除済みだと判定できる時に実際にリレーションファイルが削除される。
また TRUNCATE TALBE コマンドなどでテーブルの全行が削除される場合、高速化のために既存のリレーションファイルをいったん削除して再作成する。 この際も TRUNCATE TABLE 操作自体がロールバック可能である必要があるので、元のリレーションファイルは直ちに削除されるのではなく pendingDeletes の仕組みで遅延削除される。 新しい状態は新しい OID をつけた空のリレーションファイルが作成されるが、これは pg_class システムカタログの relfilenode によって管理される。 CREATE TABLE 直後は pg_class システムカタログの行の OID がリレーションファイルの OID だが、TRUNCATE TABLE コマンドなどのリレーションファイルを再作成する操作を行うと relfilenode が更新される。
CREATE TABLE T1 (C1 INT); INSERT INTO T1 (C1) VALUES (1), (2), (3); -- 作成直後のテーブルは pg_class の oid と relfilenode が一致している。 SELECT oid, relname, relfilenode FROM pg_class WHERE oid = 'T1'::regclass; oid | relname | relfilenode -------+---------+------------- 24815 | t1 | 24815 (1 row) BEGIN; TRUNCATE TABLE T1; -- TRUNCATE するとリレーションファイルが再作成され、新しい OID が振られる。relfilenode が書き換わる。 SELECT oid, relname, relfilenode FROM pg_class WHERE oid = 'T1'::regclass; TRUNCATE TABLE oid | relname | relfilenode -------+---------+------------- 24815 | t1 | 24818 (1 row) ROLLBACK; -- ロールバックすると前のリレーションファイルに戻される。 SELECT oid, relname, relfilenode FROM pg_class WHERE oid = 'T1'::regclass; oid | relname | relfilenode -------+---------+------------- 24815 | t1 | 24815 (1 row)
TRUNCATE TABLE 以外にインデックスの再作成を伴う処理(REINDEX や ATLER TABLE)などでもリレーションファイルは再作成される。
初期のリレーションファイルノードの ID はリレーションの OID だが、再作成されたリレーションファイルノードの ID も OID ではある。 ただし pg_class システムカタログ上にはエントリは作成されない。 再作成されたリレーションファイルノードの ID は、pg_class システムカタログの OID の一意性を管理するインデックスの中にエントリを直に書いているように見える。
3. ブロック(Block)とページ(Page)
リレーションは 8 KiB のブロックに分割されるが、PostgreSQL をこれを共有メモリバッファ(Shared Memory Buffers)にロードして読み書きする。 共有メモリバッファは他の RDBMS ではよく DB バッファと呼ばれるメモリ領域である。
共有メモリバッファは PostgreSQL インスタンス(=データベースクラスタ)毎に確保されるバッファで、postgresql.conf の中の shared_buffers オプションでそのサイズが指定される。 PostgreSQL インスタンスの起動時に確保されて、以降増減はできない。
共有メモリバッファはデータベースクラスタ内のデータベースが共有し、ブロック単位でマッピングする。 そのため共有メモリバッファも 8 KiB を単位に分かれている。 ブロックを読み込む 8 KiB のメモリ領域を ページ(Page) と呼ぶ。 あるいは バッファ(Buffer) と呼ばれることもある。
一応、ストレージ上のリレーションのデータ切片が「ブロック」で、メモリ上のデータ切片が「ページ」「バッファ」と呼び分けるが、8 KiB は同一のデータ構造を持っているのでブロックとページ(バッファ)は論理的に同一である。
3.1 ブロック(ページ)の構造
8 KiB のブロック(ページ)のレイアウトは PostgreSQL文書の63.6. データベースページのレイアウト で指定されている。
ページの先頭位置にページヘッダーがある。 PageHeaderData 構造体がその構造を持っていて、表6 のレイアウトのフィールドを持っている。 データサイズは 24 バイトで、SizeOfPageHeaderData マクロで取得できる。
フィールド | 型 | バイト長 | 説明 |
---|---|---|---|
pd_lsn | PageXLogRecPtr | 8 バイト |
いわゆるLSN(Log Sequence Number)を記録するフィールド。
|
pd_checksum | uint16 | 2 バイト | このページのチェックサムを記録する。 |
pd_flags | uint16 | 2 バイト | フラグビット。 |
pd_lower | LocationIndex | 2 バイト | 空き領域の始まりに対するオフセット。 |
pd_upper | LocationIndex | 2 バイト | 空き領域の終わりに対するオフセット。 |
pd_special | LocationIndex | 2 バイト | 特別な空間の始まりに対するオフセット。 |
pd_pagesize_version | uint16 | 2 バイト | ページサイズおよびレイアウトのバージョン番号の情報。 |
pd_prune_xid | TransactionId | 4 バイト |
ページ上でもっとも古い切り詰められていないXMAX。存在しなければゼロ。
このフィールドは VACUUM に寄らないページ単位の削除タプルの回収のために heap_page_prune() を呼び出すタイミングを測るために記録される。
|
リレーションの種類によらずページヘッダーは共通である。

- 新しいページを生成した直後はページ内の全バイトは 0 に初期化されている。
この状態は
PageIsNew(page)
で確認できる。 - ページに対する最初の操作を行った時に PageHeaderData を設定する。 この際にページ内の空き領域の先頭となるブロック内のオフセット(バイト)を pd_lowerに設定する。 またページ内の空き領域の最後となるブロック内オフセットを pd_upper に設定する。
- ページヘッダー以外はリレーション種類によって異なる。
- 通常のテーブル(リレーション・リレーション)のページは、ページ内の pd_special が示すオフセットよりも低位のアドレスを アイテム(Item) と ヒープタプル で埋める。 ヒープタプルの形式はPostgreSQL の基本データ型とタプルの扱いの4. Heap Tuple と Minimal Tupleに詳細を述べている。
- ページ内にヒープタプルを挿入する場合、pd_upper が指す空き領域の最上位にヒープタプルを配置し、pd_lower が指す空き領域の最低位にアイテムを配置する。 pd_upper と pd_lower は更新される。
- ページ内の pd_special が示すオフセット以上のアドレスは特別な領域で、通常テーブルのアイテム・ヒープタプル管理とは異なる方法で管理される。 これはインデックス・リレーションにおいて使われる。
3.2 バッファとブロック-ページのマッピング
リレーション内のバッファは共有メモリバッファにマッピングされるが、これを模式的にあらわしたのが fig-3 になる。 HDD 内にあるリレーションは 8 KiB のブロック毎に共有メモリバッファにマッピングにされる。
共有メモリバッファは PostgreSQL インスタンス内の複数のプロセスで共通である。 同じリレーション内のブロックは常に共有メモリバッファ内の 1 つのバッファにマッピングされ、1つのブロックが複数のバッファにマッピングされることはない。 共有メモリバッファに空きバッファがない場合には、置換アルゴリズムによりあまり参照されていないバッファが選択され、そのバッファが変更を受けている場合にはリレーションのブロックに 8 KiB の内容が書き込まれた後に、別のブロックの読み込みのために明け渡すことになる。

いくつか注意事項を述べる。
- PostgreSQL インスタンスはマルチプロセス構成をとり、共有メモリバッファはそのプロセス間で共有されるメモリ領域である。 ただし共有メモリバッファのプロセスメモリアドレス空間内のマッピング位置はプロセスによって異なるかもしれない。 グローバル変数 BufferBlocks からオフセットアドレスでアクセスする。
- 共有メモリバッファは 1 から始まるバッファ番号(?)で管理されている。
バッファ番号は Buffer 型(実際は
typedef int Buffer
) で保持する。 - 各バッファがどのリレーションのブロックをマップしているか、参照カウンタ、各種フラグなどの情報が BufferDesc 構造体に記録されている。
- リレーションのブロックが何番目のブロックに記録されるかは、ハッシュテーブルによって管理されている。 このハッシュテーブルのハッシュ関数は、リレーションの属するデータベースID、リレーションの属するテーブルスペースのID、リレーションファイルノードID、フォーク種類、ブロック番号から生成される。
PostgreSQL には通常テーブル以外に CREATE TEMPORARY TABLE で作成する一時テーブルがある。 一時テーブルはトランザクションの終わりまたはセッションの終わりで自動的に削除されるテーブルである。 PostgreSQL では 1 つのセッションは 1 つのプロセスで処理されているので、一時テーブルはプロセス内で閉じた寿命を持っているといえる。
この性質を使い、一時テーブルを構成リレーション内のブロックを読み込む際には共有メモリバッファを使わず、プロセス内のローカルバッファ(Local Buffer) に保存される。 通常のバッファを記録した Buffer 型変数は 1 以上の値を取るが、ローカルバッファは負数をとる。
3.3 ブロックを操作する上での概念
リレーション上のブロックを読み書きする操作を考えると、以下の2つの概念が登場する。
- ピン(pin)
- リレーション上のブロックは共有メモリバッファ上に読み込まれるが、操作中はこの対応関係をピンしておく必要がある。 PostgreSQL のインスタンス中では別のセッションや背景で動作しているプロセスによって別のブロックが共有メモリバッファに読み込まれ、知らないうちに置換が起きて追い出されてしまうかもしれないからである。 バッファをピンすることによって、勝手な置換が発生しなくなる。
- ロック(lock)またはコンテンツロック(content lock)
- リレーション上のブロックは共有メモリバッファ上にピンされている間、複数のプロセスはブロックにアクセスが可能である。 しかし自分がブロック内のデータを書き換える場合には、他のプロセスにブロックを読み書きされて欲しくない。 そこでブロックに対してロックをかけることになる。
順番としては、ブロックとバッファの関係をピンしてから、バッファをロックすることになる。
具体的な API の紹介をする前にコンテンツロックの変則的な部分を紹介する。 一般的にこのようなロックは read-write ロックとなる。 つまりブロックを読む場合は read ロックをとるが read ロックは複数のプロセスが同時に獲得してもいいが、書き込む場合は write ロックをとり write ロックは他のプロセスと排他的に獲得することになる、という奴である。 PostgreSQL のブロックのコンテンツロックはこれに近いが Shared、Exclusive、Cleanup の 3 つのモードがある。
- Shared
- Read ロック相当。複数のプロセスが同時に shared ロックを獲得することができる。 Excluisve/cleanup のロックがすでに獲得されている場合は、shared ロックはスピンまたはブロックされる。
- Exclusive
- Write ロック相当。同時に 1つのプロセスだけが excluisve ロックは獲得できる。 Read/excluisve/cleanup のロックがすでに獲得されている場合は、exclusive ロックはスピンまたはブロックされる。
- Cleanup
- Exclusive ロックの強化版と考えられる。 獲得の条件は exclusive ロックと同じだが、加えて対象となるバッファを他のプロセスがピンしていないことが条件である。 他のプロセスがピンしている場合は、ピンを外すまでスピンまたはブロックされる。 正確にいうとコンテキストロックのモードではない。
この 3 つのコンテンツロックは以下のように使用される。
No. | 操作 | 必要なコンテンツロック | 説明 |
---|---|---|---|
#1 | ページスキャン | shared or excluisve | ページ内のタプルをスキャンしている場合は shared または exclusive のコンテンツロックが必要。 また各タプルのヘッダー部分を見てコミットステータスを確認する場合にも同じロックが必要。 |
#2 | 可視性が確定したタプルのデータを読む | nothing |
heap_beginscan() 、heap_getnext() 、heap_endscan() を使ってヒープをスキャンする場合、条件が合えば page at a time という動作をする。
heap_getnext() はタプルを1個づつ返すが、新しいブロックに遷移した時にそのブロックをピンして shared でロックした後にページ内の全てのタプルの可視性を判定して、生きているタプルの位置を配列に記録する(この動作は #1 にあたる)。その後で shared ロックは解放する。
この後の同一ページに複数回の heap_getnext() が呼び出された場合は、配列の中からページ内のヒープタプルの位置を読み込み shared でロックせずに読んでしまう。
|
#3 | ページの内容の変更 | exclusive | ページ内にタプルを追加する場合、または既に存在するタプルの xmin/xmax フィールドを変更する場合には exlucisve ロックが必要である。 |
#4 | タプルヘッダーへのヒントビットの打刻 | shared or excluisve |
ページスキャン時に各タプルのヘッダー部分を見てコミットステータスを確認した場合、「PostgreSQL の基本データ型とタプルの扱い」の4.1.1 t_infomask、t_infomask2の中で t_infomask で紹介したフラグに HEAP_XMIN_COMMITTED、HEAP_XMIN_INVALID、HEAP_XMAX_COMMITTED、HEAP_XMAX_INVLIAD を立てることがある。
これはヒントビットである。
t_xmin と t_xmax は CLOG を照会することで、コミット済み(Committed)かアボート(Abort)か進行中(In Progress)かを判定する(詳細は「PostgreSQL のトランザクション & MVCC & スナップショットの仕組み」の3.1 トランザクションと可視性(Visibility))。
コミット済みあるいはアボートと判明した場合、次回同じコミットステータス確認をしても同じ結果となるので、先述のヒントビットを立てることで次回以降を省略するのである。 ここからが特異な点だが、 タプルヘッダーのヒントビットを打刻するということは、ページの内容を更新するということである。 しかしこれを shared のコンテキストロックで行っても構わない。 ただしタプルの xmin を凍結するために HEAP_XMIN_INVALID と HEAP_XMIN_COMMITTED を同時に立てる場合には shared ロックではなく exclusive ロックが必要となる。 |
#5 | ページからタプルの削除または再配置 | cleanup 状態 |
VACUUM 操作またはページ単位に heap_page_prune() が呼ばれる場合、ページ内のタプルが削除されたり、削除されたタプルを詰めて再配置が行われる。
この場合は cleannup のコンテンツロックが必要となる。要するに #2 でページ内をピンなしで読めるという最適化を導入してしまったため、exclusive ロックを獲得しただけではまたページ内を読んでいるプロセスがいる可能性があるので、ページ内のタプルを削除したり再配置する場合には #2 を行っているプロセスに不整合が見える。 そこでバッファをピンしているのは一人だけという cleanup 状態にする必要ができたのだ。 ちなみに #3 でページの内容を更新する場合、ページ内の既存のタプルを削除したり位置を変えたりしないので #2 と共存できる。 |
3.4 API のリスト
実際の API を紹介する。
API | 戻り値の型 | 説明 |
---|---|---|
ReadBuffer(Relation reln, BlockNumber blockNum) | Buffer |
reln で指定されたリレーションのメインフォークの blockNum 目のブロックを読み込みピンする。
この API では既に存在しているブロックを読むことしかできない。ReadBufferExtended() の簡易版で ReadBufferExtended(reln, MAIN_FORKNUM, blockNum, RBM_NORMAL, NULL) を指定したのに等しい。
コード中の出現率は高い。
|
ReadBufferExtended(Relation reln, ForkNumber forkNum, BlockNumber blockNum, ReadBufferMode mode, BufferAccessStrategy strategy) |
Buffer |
reln で指定された forkNum フォークの blockNum 目のブロックを読み込みピンする。 ただし幾つかの特殊機能がある。
|
ReleaseBuffer(Buffer buffer) | void | ReadBuffer() などで読み込んだブロックからピンを外す。バッファも解放する。 |
LockBuffer(Buffer buffer, int mode) | void | ReadBuffer() などで読み込んだブロックにロックをかける。
ロックは mode により BUFFER_LOCK_SHARE、BUFFER_LOCK_EXCLUSIVE、BUFFER_LOCK_UNLOCK がある。
|
UnlockReleaseBuffer(Buffer buffer) | void | LockBuffer(buffer, BUFFER_LOCK_UNLOCK) をした後に ReleaseBuffer(buffer) を実行する。
便利な組み合わせ API である。
|
ConditionalLockBuffer(Buffer buffer) | bool | バッファに BUFFER_LOCK_EXCLUSIVE ロックをとるが、LockBuffer() はロックを獲得できるまでスピンするかブロックするが、この API はトライロック的にロック獲得に失敗下場合は false を返して直帰する。true で帰って来た場合はロックが獲得できている。
|
ReleaseAndReadBuffer(Buffer buffer, Relation relation, BlockNumber blockNum) |
Buffer | ReleaseBuffer() をした後に ReadBuffer() を実行する。これも組み合わせ API である。 |
LockBufferForCleanup(Buffer buffer) | void | Cleanup ロックを獲得する。解放する場合は LockBuffer(buffer, BUFFER_LOCK_UNLOCK) でいい。 |
ConditionalLockBufferForCleanup(Buffer buffer) | bool | ConditionalLockBuffer() の Cleanup ロック版である。 |
すでに操作対象となるリレーションの OID が relid なら、そのブロックを読む操作は一般的に以下のように書ける。
Relation rel; Buffer buffer; Page page; /* リレーションをオープンする。ここではロックモードは AccessShareLock とする。 */ rel = relation_open(relid, AccessShareLock); /* blockNumber のブロックをバッファに読み込みピンする。 */ buffer = ReadBfufer(rel, blockNumber); /* バッファを読み込みのために BUFFER_LOCK_SHARE でロックする。 */ LockBuffer(buffer, BUFFER_LOCK_SHARE); /* バッファの実体となるメモリアドレスを取得する。 * page はページの先頭を指すポインタでここから 8KiB をアクセス可能する。 */ page = BufferGetPage(buffer); /* アンロックのピンの解除を同時に実行する。 */ UnlockReleaseBuffer(buffer); /* リレーションをクローズする。 */ relation_close(rel, AccessShareLock);
BufferGetpage()
はブロックがバッファにピンされている後なら使えるので、ロックを獲得する前に実行してもかまわない。
リレーションのブロックを変更する操作は以下のように書ける。
Relation rel;
Buffer buffer;
Page page;
/*
* リレーションをオープンする。
* 書き込みのために行うので RowExclusiveLock 以上のレベルのロックを指定する。
* ここでは RowExclusiveLock する。
*/
rel = relation_open(relid, RowExclusiveLock);
/* blockNumber のブロックをバッファに読み込みピンする。 */
buffer = ReadBfufer(rel, blockNumber);
/* バッファを書き込みのために BUFFER_LOCK_EXCLUSIVE でロックする。 */
LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE);
page = BufferGetPage(buffer);
/* ここでページを変更するような操作を行う。 */
/* ここからクリティカルセクションである。 */
START_CRIT_SECTION();
/* ページを変更した場合、ストレージに書き出されるように dirty マークを付ける。 */
MarkBufferDirty(buffer);
/* rel が UNLOGGED でない場合、変更内容に応じた WAL レコードを出力する。 */
if (RelationNeedsWAL(rel))
{
/* WAL レコードを出力する。書き方はリレーション種類や操作の内容によって異なる。 */
XLogRecPtr recptr;
/* XLogInsert()
でログを書き込む */
recptr = XLogInsert(RM_HEAP_ID, XLOG_HEAP_XXX);
/* WAL ログの最新位置を LSN としてページに記録する。 */
PageSetLSN(page, recptr);
}
END_CRIT_SECTION();
/* アンロックのピンの解除を同時に実行する。 */
UnlockReleaseBuffer(buffer);
/* リレーションをクローズする。 */
relation_close(rel, RowExclusiveLock);
- 明示的な書き込みに操作は BUFFER_LOCK_EXCLUSIVE のコンテントロックを獲得してからそれを解放するまでに行う必要がある。
- 書込み操作に対する WAL レコードの書き込み(
XLogInsert
) も BUFFER_LOCK_EXCLUSIVE のコンテントロックの間にある必要がある。 - クリティカルセクションは
START_CRIT_SECTION()
とEND_CRIT_SECTION()
で囲んだ区間である。 PostgreSQL は Java のエラー(java.lang.Error)のようなエラー送出の仕組みを持ち、TRY-CATCH を使ってスタックの上位層で捕捉することができるが、クリティカルセクション中でエラー送出はプロセスアボート相当に扱い異常終了とする。 WAL の書込み操作中にエラー送出すると復元できないので、クリティカルセクションに入れる。 MarkBufferDirty()
はクリティカルセクションの内側でも外側でも構わないが、PostgreSQL のソースコードの内の出現率ではクリティカルセクションの中に入れる書き方の方が多い。relation_open()
とrelation_close()
で指定するロックモードは一致させるのが普通だが、relation_close()
側に NoLock を指定してリレーションをクローズするが、ロックは解除しないという手法もある。この場合、ロックはトランザクションの終わりに解除される。特殊なパターンですでに同じリレーションがロック付きでオープンされていることが確定している場合は、relation_open()
も NoLock でオープンすることはできる。
PostgreSQL のインターフェイスは複数のブロックを同時にピン・ロックすることができるが、PostgreSQL やエクステンションのソースコードを見る限り1ブロックごとにピン→ロック→アンロック→ピン解除を行っており、複数のバッファを同時に扱うのはどうしてもその必要がある場合に限られている。 (システムカタログでない)通常のテーブルの操作中に、そのように複数バッファ操作が起きることはない。
ただし ReadBuffer()
や LockBuffer()
のような操作は、共有メモリ上のデータ構造にアクセスしてロック・マネージャーを介したりするので結構重い処理である。
ページ内の操作自体よりもピン・ロックに時間が掛ることもある。
このあたりが従来のディスクベース DB とインメモリ DB の違いであろう。
BUFFER_LOCK_EXCLUSIVE コンテンツロックを獲得が必要なページに対する明示的な書き込みとは 表7 の中で #3 にあたる。 #5 も明示的だが、これは Cleanup ロックまで必要となる。 #3 の明示的な操作には WAL レコード生成が伴う。
一方、#4 のヒントビットの打刻はページの内容を物理的には変更しているが論理的には変更しているとされない。
そのため BUFFER_LOCK_SHARE コンテンツロックを獲得するだけ許可され、MarkBufferDirty()
も実行されず、ヒントビットの打刻に対する WAL レコードの生成も行われない。
もしヒントビット打刻だけが行われたバッファが、共有メモリバッファから追い出される場合には、ディスクには保存されずに破棄される。
そのため次回ディスクから共有メモリバッファに載せられることがあれば、ヒントビットは打刻前と同じ状態となる。
同様にヒントビットの打刻は、WAL レコードが生成されないので、物理ストリーミング・レプリケーションもスタンバイ側へ転送されない。 マスターサーバーとスタンバイサーバーでヒントビット部分は一致しない可能性がある。
4. ヒープの操作
リレーションのうちヒープは、ヒープタプルを単位としてスキャンと INSERT/UPDATE/DELETE 操作を行う API が用意されている。 これらの API は 3章 で述べたようなブロックのピンやバッファのロックを隠蔽してくれる。
4.1 ヒープスキャン
リレーションのうちヒープをアクセスする場合、heap_beginscan()
、heap_getnext()
、heap_endscan()
などの API が提供される。
これらの API は HeapScanDesc というハンドル(HeapScanDescData 構造体へのポインタ)を使う。
4.1.1 ヒープスキャンの基本
ヒープのスキャンの基本は以下のように行う。
Relation relation; TupleDesc tupDesc; HeapScanDesc scan; HeapTuple tuple; Snapshot snapshot; /* * ヒープにアクセスする際のスナップショットを用意する。 * 別の方法で確保したスナップショットでも構わない。 */ snapshot = RegisterSnapshot(GetActiveSnapshot()); relation = heap_open(relid, AccessShareLock); tupDesc = RelationGetDescr(relation); /* ヒープ内の全てのタプルをスキャンする場合 */ scan = heap_beginscan(relation, snapshot, 0, NULL); while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) { /* ここで tuple にアクセスする */ int i; for (i = 0 ; i < tupDesc->natts ; i++) /* natts にテーブルの最大列数が入っている */ { Datum datum; bool isnull; /* 列の属性に attisdropped が設定されているものは読み飛ばす。 */ if (tupDesc->attrs[i]->attisdropped) continue; /* i 番目の属性の datum と isnull を取得。datum は */ datum = heap_getattr(tuple, i + 1, tupDesc, &isnull); /* 2 番目の引数は AttrNumber で 1-origin */ /* ここで datum と isnull を使う */ } } heap_endscan(scan); heap_close(relation, AccessShareLock); UnregisterSnapshot(snapshot);
- PostgreSQL は同コマンドによって列が削除された場合、ヒープの中から列のデータは削除せず、pg_attribute システムカタログの attisdropped を true にして「隠された列」にしてそのまま放置する。 プログラム側で attisdropped の設定された列を読み落とす必要がある。
heap_getattr()
が返すのは、基本型かヒープタプルである tuple の内部を指すポインタである。 「PostgreSQL の基本データ型とタプルの扱い」の6.1 Heap Tuple モードを参照のこと。heap_beginscan()
が返す scan の中の rs_pageatatime メンバー変数が true の場合には page-at-a-time モードである。表7 で述べた #2 の操作を実施する。heap_getnext()
が呼ばれる前に scan->rs_pageatatime = false と設定すると、page-at-a-time は解除され、この時は 1 タプル毎にページロックと BUFFER_LOCK_SHARE のロックを行う。つまり #1 の操作となる。- HeapScanDesc を使う場合、HeapScanDescData 構造体の Buffer 型のメンバー変数 rs_cbuf がブロックをピンしている。
4.1.2 スキャン条件を指定したヒープスキャン
HeapScanDesc を使った API は、ScanKey を用いることによりスキャン条件を設定することができる。 スキャン条件はヒープ内の1つの列と定数値に対して小なり、以下、等しい、以上、大なりの5パターンを指定することができる。 スキャン条件は複数の条件を指定することも可能である。
スキャン条件は ScanKeyData 型の配列をローカル変数として定義し、そこに ScanKeyInit()
でデータを埋めることで実現する。
ScanKeyData key[N]; relation = heap_open(relid, AccessShareLock); /* スキャン条件 */ ScanKeyInit(&key[0], 1, BTEqualStrategyNumber, F_INT4EQ, Int32GetDatum(123)); ... ScanKeyInit(&key[N-1], N-1, BTEqualStrategyNumber, F_TEXTEQ, PointerGetDatum(cstring_to_text("abc")); scan = heap_beginscan(relation, snapshot, N, key); while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) { /* snip */ } heap_endscan(scan); heap_close(relation, AccessShareLock);
スキャン条件は ScanKeyInit()
で作るが、これは以下のように指定する。
void ScanKeyInit(ScanKey entry, AttrNumber attributeNumber, StrategyNumber strategy, RegProcedure procedure, Datum argument);
- entry
- 出力を格納する ScanKeyData 型へのポインタとなる。
- attributeNumber
- ヒープ内の列は 1-origin の列番号で指定する。 システム列を指定する場合は -1 〜 -7 の負数を指定するが 表のマクロを用いる。
- strategy
- B-tree インデックスのストラテジのうち1つを指定する。
BTEqualStrategyNumber (等しい)を指定することが多い。
表9: B-tree インデックスのストラテジ マクロ 番号 演算 BTLessStrategyNumber 1 小なり BTLessEqualStrategyNumber 2 以下 BTEqualStrategyNumber 3 等しい BTGreaterEqualStrategyNumber 4 以上 BTGreaterStrategyNumber 5 大なり - procedure
- スキャン条件の判定に使う関数。
列のデータ型が foo 型の場合、以下の SQL を実行してシステムカタログを検索し、amopstrategy が strategy と一致する oprcode を使うとよい。
複数行が出力された場合は最適なものを選ぶ必要がある。
SELECT amoplefttype, amopstrategy, amoprighttype, (SELECT oprcode FROM pg_operator WHERE oid = amopopr) FROM pg_amop WHERE amopmethod = (SELECT oid FROM pg_am WHERE amname = 'btree') AND amoplefttype = 'foo'::regtype ORDER BY amopstrategy;
- argument
- スキャン条件の比較対象となる定数を Datum 型に丸めて指定する。
スキャン条件を指定した場合、ヒープにインデックスが張ってありそれが利用可能な場合にはインデックスが利用され高速になる。 対応するインデックスがない場合でもスキャン条件によるフィルタリングは有効だが、速度が高速になるわけではない。
ScanKeyInit()
のスキャン条件が strategy と procedure の 2 つも指定するのは冗長だが、内部的にはヒープに B-tree インデックスが張ってある場合、strategy を使ってインデックスを利用し、インデックスがない場合は procedure を使ってスキャン条件というふうに使い分けている(と思われる)。
4.1.3 システムカタログのスキャン
システムカタログを使う場合は、HeapScanDesc の替わりに SysScanDesc を使う。
API も systable_beginscan()
、systable_getnext()
と少し替わる。
pg_class システムカタログから relOid という OID のタプルを探す場合は、以下のようなプログラムになる。
Relation relation; ScanKeyData skey; SysScanDesc sscan; HeapTuple tuple; /* pg_class システムカタログの OID は RelationRelationId となる。 */ relation = heap_open(RelationRelationId, AccessShareLock); /* * システム列の oid が relOid と一致することをスキャン条件とする。 * oid は ObjectIdAttributeNumber で指定する。 */ ScanKeyInit(&skey, ObjectIdAttributeNumber, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(relOid)); /* * ヒープ内の全てのタプルをスキャンする場合 * システムカタログに貼られているインデックスを使う場合(通常は使う)は、インデックスにも固定 OID が付いている。 */ sscan = systable_beginscan(relation, ClassOidIndexId, true /* indexOK */, snapshot, 1, &skey); while ((tuple = systable_getnext(sscan)) != NULL) { /* snip */ } systable_endscan(scan); heap_close(relation, AccessShareLock);
指定するしすてむカタログの OID マクロや、システムカタログに張られているインデックスは 参考1 を参照のこと。
4.2 ヒープタプルの INSERT/UPDATE/DELETE
ヒープにヒープタプルを挿入するには simple_heap_insert()
を使う。
ただしヒープの列数が N で各列のデータ型判明している前提でハードコーディングしている。
Relation relation;
Oid oid;
TupleDesc tupDesc;
HeapTuple tuple;
Datum values[N];
bool isnull[N];
relation = heap_open(relid, RowExclusiveLock);
tupDesc = RelationGetDescr(relation);
/* ヒープタプルの値を設定 */
values[0 ] = Int32GetDatum(123);
...
values[N - 1] = PointerGetDatum(cstring_to_text("abc");
isnull[0 ] = false;
...
isnull[N - 1] = false;
/* ヒープタプルを合成 */
tuple = heap_form_tuple(tupDesc, values, isnull);
/* ヒープタプルを挿入 */
oid = simple_heap_insert(relation, tuple);
#if 0
/* システムカタログの場合は CatalogUpdateIndexes()
を呼び出してインデックスを更新する。 */
CatalogUpdateIndexes(relation, tuple);
#endif
heap_close(relation, RowExclusiveLock);
ヒープからヒープタプルを削除するには simple_heap_delete()
を使う。
削除にはタプルの位置をブロック番号とアイテム番号を組み合わせた TID で指定する。
relation = heap_open(relid, RowExclusiveLock); scan = heap_beginscan(relation, snapshot, 0, NULL); while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) { if (condition) { simple_heap_delete(relation, &tuple->t_self); } } heap_endscan(scan); heap_close(relation, RowExclusiveLock);
ヒープからヒープタプルを更新するには simple_heap_update()
を使う。
このために元のヒープタプルをベースに内容を更新する列を指定して heap_modify_tuple()
を使い新しいヒープタプルを合成する。
HeapTuple tuple, newtuple;
relation = heap_open(relid, RowExclusiveLock);
scan = heap_beginscan(relation, snapshot, 0, NULL);
while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
if (condition)
{
Datum values[N] = {0};
bool isnull[N] = {0};
bool replaces[N] = {0};
/* ヒープタプルのうち更新を行う列の replaces[] を true にする。 */
values[i] = Int32GetDatum(123);
isnull[i] = false;
replaces[i] = true;
/* 更新後のヒープタプルを生成 */
newtuple = heap_modify_tuple(tuple, RelationGetDescr(relation), values, isnull, replaces);
simple_heap_update(relation, &tuple->t_self, newtuple);
#if 0
/* システムカタログの場合は CatalogUpdateIndexes()
を呼び出してインデックスを更新する。 */
CatalogUpdateIndexes(relation, newtuple);
#endif
}
}
heap_endscan(scan);
heap_close(relation, RowExclusiveLock);
参考1: システムカタログ
システムカタログの情報を載せる。 より詳細な情報は PostgreSQL 文書の 49. システムカタログ に載っている。 システムカタログに貼られているインデックスの情報は PostgreSQL ソースコードの include/catalog/indexing.h を参考にしている。
- OID マクロの欄は
heap_open()
に指定するシステムカタログの OID 値を与えるマクロ。 - インデックスの欄は、そのシステムカタログに固定で張られているインデックスを列挙している。 一つのシステムカタログに複数のインデックスが張られていることもある。 インデックスが ProcedureNameArgsNspIndexId (proname, proargtypes, pronamespace) のように記述されている場合、ProcedureNameArgsNspIndexId はインデックスの OID である。 続く proname, proargtypes, pronamespace はインデックスの張られている列の名前で、複数列の場合は登場順にソートされている。
カタログ名 | 用途 | OID マクロ | インデックス |
---|---|---|---|
pg_aggregate | 集約関数 | AggregateRelationId |
|
pg_am | インデックスアクセスメソッド | AccessMethodRelationId |
|
pg_amop | アクセスメソッド演算子 | AccessMethodOperatorRelationId |
|
pg_amproc | アクセスメソッドサポートプロシージャ | AccessMethodProcedureRelationId |
|
pg_attrdef | 列デフォルト値 | AttrDefaultRelationId |
|
pg_attribute | テーブル列(属性) | AttributeRelationId |
|
pg_authid | 認証識別子(ロール) | AuthIdRelationId |
|
pg_auth_members | 認証識別子メンバーシップ関係 | AuthMemRelationId |
|
pg_cast | キャスト(データ型変換) | CastRelationId |
|
pg_class | リレーション | RelationRelationId |
|
pg_collation | 照合順序 | CollationRelationId |
|
pg_constraint | 検査制約、一意性制約、主キー制約、外部キー制約 | ConstraintRelationId |
|
pg_conversion | エンコード方式変換情報 | ConversionRelationId |
|
pg_database | データベース | DatabaseRelationId |
|
pg_db_role_setting | ロール毎およびデータベース毎の設定 | DbRoleSettingRelationId |
|
pg_default_acl | オブジェクト種類のデフォルト権限 | DefaultAclRelationId |
|
pg_depend | データベースオブジェクト間の依存関係 | DependRelationId |
|
pg_description | データベースオブジェクトの説明やコメント | DescriptionRelationId |
|
pg_enum | 列挙型のラベルや値の定義 | EnumRelationId |
|
pg_event_trigger | イベントトリガ | EventTriggerRelationId |
|
pg_extension | 拡張(エクステンション) | ExtensionRelationId |
|
pg_foreign_data_wrapper | 外部データラッパの設定 | ForeignDataWrapperRelationId |
|
pg_foreign_server | 外部サーバーの設定 | ForeignServerRelationId |
|
pg_foreign_table | 外部テーブルの設定 | ForeignTableRelationId |
|
pg_index | インデックス | IndexRelationId |
|
pg_inherits | テーブル継承階層 | InheritsRelationId |
|
pg_language | 手続き言語 | LanguageRelationId |
|
pg_largeobject | ラージオブジェクト用のデータページ | LargeObjectRelationId |
|
pg_largeobject_metadata | ラージオブジェクトのメタデータ | LargeObjectMetadataRelationId |
|
pg_namespace | 名前空間(スキーマ) | NamespaceRelationId |
|
pg_opclass | アクセスメソッド用の演算子クラス | OperatorClassRelationId |
|
pg_operator | 演算子 | OperatorRelationId |
|
pg_opfamily | アクセスメソッド用の演算子族 | OperatorFamilyRelationId |
|
pg_pltemplate | 手続き言語のためのテンプレートデータ | PLTemplateRelationId |
|
pg_policy | 行単位セキュリティポリシー | PolicyRelationId |
|
pg_proc | 関数とプロシージャ | ProcedureRelationId |
|
pg_range | 範囲型 | RangeRelationId |
|
pg_replication_origin | 登録されたレプリケーション起点 | ReplicationOriginRelationId |
|
pg_rewrite | 問い合わせ書き換えルール | RewriteRelationId |
|
pg_seclabel | データベースオブジェクトのセキュリティラベル | SecLabelRelationId |
|
pg_shdepend | 共有オブジェクトの依存関係 | SharedDependRelationId |
|
pg_shdescription | 共有オブジェクトに対するコメント | SharedDescriptionRelationId |
|
pg_shseclable | 共有データベースオブジェクトのセキュリティラベル | SharedSecLabelRelationId |
|
pg_statistic | プランナ統計情報 | StatisticRelationId |
|
pg_tablespace | テーブルスペース | TableSpaceRelationId |
|
pg_transform | 変換(データ型を手続き言語間の変換ルール) | TransformRelationId |
|
pg_trigger | トリガー | TriggerRelationId |
|
pg_ts_config | 全文検索設定 | TSConfigRelationId |
|
pg_ts_config_map | 全文検索設定のトークン写像 | TSConfigMapRelationId |
|
pg_ts_dict | 全文検索設定辞書 | TSDictionaryRelationId |
|
pg_ts_parser | 全文検索パーサ | TSParserRelationId |
|
pg_ts_template | 全文検索テンプレート | TSTemplateRelationId |
|
pg_type | データ型 | TypeRelationId |
|
pg_user_mapping | 外部サーバへのユーザーのマッピング | UserMappingRelationId |
|