PostgreSQL のテーブルとブロックのデータ構造

作成日:2017.03.01
修正日:2017.03.04

この記事は 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: PostgreSQL データベースを構成する概念
レベル 名称 別名
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 はデータベース個別になる。

fig-1: データベースクラスタの構成
データベースクラスタの構成

実際には、データベースやテーブルだけではなく、関数、操作、ロール、その他様々なものに対してシステムカタログがある。 詳細は 参考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 は値域によって以下のように使い分けられている。

テーブルのストレージレイアウトを考える場合、テーブルに対して 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 内にはディスクに格納する必要のあるデータ構造がテーブル以外にも存在し、これもリレーションに格納する。 インデックス、マテリアライズド・ビューなども全てリレーションで格納することになる。 インデックスを格納するのはインデックス・リレーションであり、マテリアライズド・ビューを格納するのはマテリアライズド・ビュー・リレーションである。 ではテーブルを格納するのは? これはリレーション・リレーションである。

リレーションの中でヒープタプルを格納することのできるものをヒープと呼ぶこともある。

表2: リレーションの種類
名称マクロ識別子ヒープ説明
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 で作成される。

通常テーブル、マテリアライズド・ビューのリレーションは普通の意味での「リレーション」だが他は異なる。

リレーションの使い方は色々だが、次節で述べるブロック構造には必ず従うことになる。

実体を持たないビュー、複合型、外部テーブルのリレーションも API を直接叩けば作成することができる。

2.2 データディレクトリ中のリレーションの実体

リレーションの実体はデータディレクトリ中に格納されている。 データディレクトリの直下に base というディレクトリがあり、数字のディレクトリと pgsql_tmp ディレクトリがある。 数字のディレクトリは「データベース」の OID(pg_database システムカタログの該当行の OID)となる。 template1 データベースが OID が固定で 1 であり、 initdb 時の割り振りで template0 データベースが 13236 に、postgres データベースが 13241 に決まる。 一方、16384 は CREATE DATABASE で新規に作成したユーザーのデータベースの OID になる。 何番が割り振られるかは運用によって変わる。

base/ 以外に global/ もあるが、これはデータベースクラスタ間で共通のシステムカタログ等を格納するために使われる。

各データベースに属するリレーションはこのディレクトリの下に作成される。 例えば 16384/ の下は、数字のファイルと _fsm や _vm のようなサフィックスのついたファイルが並ぶ。

数値はリレーションの OID を示している。 数値の後の _fsm や _vm のようなサフィックスは、リレーションのフォーク(fork)と呼ぶ。 本体となるサフィックスのないメインリレーションと周辺フォークが集まって、一つのリレーションを構成している。

正確にはデータディレクトリ中に格納されるのはリレーションファイルノードである。 2.5 節 で説明する。

フォークの種類と意味は以下の通りである。

表3: リレーションのフォーク種類
マクロ番号Suffix意味
MAIN_FORKNUM0なしリレーションの本体。
FSM_FORKNUM1_fsmFree Space Map。メインリレーションの中の空きブロックを管理する。

PostgreSQL 文書の63.3. 空き領域マップも参照のこと。
VISIBILITYMAP_FORKNUM2_vmVisibility Map。VACUUM の中で行う検査で、メインリレーションの中のブロックのうち、ブロック内の削除されたタプルの残骸がなく、全てのタプルの可視性が決定されている場合場合に 1 が立つ。ブロック内で INSERT/UPDATE/DELETE が行われると、その操作の中で 0 に戻る。

IndexOnlyScan は visibility map に 1 が立っているブロックはインデックスの情報だけで処理を進める。Visibility map が 0 の場合は、インデックスで合致すると判定されたタプルはヒープを検査してすでに削除されたか確認してから処理を進める。

PostgreSQL 文書の63.4. 可視性マップも参照のこと。
INIT_FORKNUM3_initこれは UNLOGGED テーブルに対するインデックスでのみ使用する特殊なフォークである。空のテーブルに対するインデックス・リレーションの初期状態を記録するために使う。

UNLOGGED テーブルは異常終了した場合、そのヒープは PostgreSQL が再起動した時に 0 ブロックに切り詰められる。これは TRUNCATE を適用したのと同様である。この時、UNLOGGED テーブルに対するインデックスも空テーブルに合わせて初期化が必要になる。ただしインデックス・リレーションは初期状態であっても制御用のブロック(メタページ)があるのため、0 ブロックに切り詰めるだけでは動作しない。

だが PostgreSQL が異常終了した後の再起動シーケンスでは REINDEX 相当の処理を走行することはできない。そこで UNLOGGED テーブルに対する CREATE INDEX 時に予め初期状態をこの INIT フォークに記録し、INIT フォークの内容をメインフォークに単純コピーすることで済ませる。

PostgreSQL 文書の63.5. 初期化フォークも参照のこと。

リレーション内のファイルは各フォークごとに 1GiB 単位とするセグメントで分割される。 2 番目以降の 1GiB に対しては .1、.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 に示す。

表4: リレーションを作成・破棄する API
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 に示す。

表5: リレーションをオープン・クローズする API
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 マクロで取得できる。

表6: PageHeaderData のレイアウト
フィールド バイト長 説明
pd_lsn PageXLogRecPtr 8 バイト いわゆるLSN(Log Sequence Number)を記録するフィールド。

  • このページに対する更新内容を XLogInsert(rmid, info, rdata) を使い WAL レコードとして書き出した後に、PageSetLSN(page, lsn) を呼び出して記録する。
  • このフィールドはチェックポインティング処理で参照される。
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() を呼び出すタイミングを測るために記録される。

  • pd_prune_xid は、ページ内のタプルを DELETE した場合、ページ内に存在するタプルを UPDATE した場合、ページ内に INSERT したもののアボートされたタプルを発見した場合などに、その操作のトランザクション ID を PageSetPrunable(page, xid) で使って書き込まれる。 ただし PageSetPrunable(page, xid) は引数として xid がすでに記録されていた pd_prune_xid よりも古い場合に上書きする。
  • ページ単位の削除タプルの回収はヒープスキャン(heap_getnext())などで、heap_page_prune_opt() が呼ばれると、pd_prune_xid が有効値に設定されており、かつページ内の空き領域が fill factor 指定よりも少なくなっている場合に heap_page_prune() が呼ばれて開始される。 回収後は pd_prune_xid は 0 に戻される。

リレーションの種類によらずページヘッダーは共通である。

fig-2: ブロック(ページ)のレイアウト
ブロック(ページ)のレイアウト

3.2 バッファとブロック-ページのマッピング

リレーション内のバッファは共有メモリバッファにマッピングされるが、これを模式的にあらわしたのが fig-3 になる。 HDD 内にあるリレーションは 8 KiB のブロック毎に共有メモリバッファにマッピングにされる。

共有メモリバッファは PostgreSQL インスタンス内の複数のプロセスで共通である。 同じリレーション内のブロックは常に共有メモリバッファ内の 1 つのバッファにマッピングされ、1つのブロックが複数のバッファにマッピングされることはない。 共有メモリバッファに空きバッファがない場合には、置換アルゴリズムによりあまり参照されていないバッファが選択され、そのバッファが変更を受けている場合にはリレーションのブロックに 8 KiB の内容が書き込まれた後に、別のブロックの読み込みのために明け渡すことになる。

fig-3: 共有メモリバッファとブロックのマッピング
共有メモリバッファとブロックのマッピング

いくつか注意事項を述べる。

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 つのコンテンツロックは以下のように使用される。

表7: 操作とコンテンツロックの関係
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_COMMITTEDHEAP_XMIN_INVALIDHEAP_XMAX_COMMITTEDHEAP_XMAX_INVLIAD を立てることがある。 これはヒントビットである。 t_xmin と t_xmax は CLOG を照会することで、コミット済み(Committed)かアボート(Abort)か進行中(In Progress)かを判定する(詳細は「PostgreSQL のトランザクション & MVCC & スナップショットの仕組み」の3.1 トランザクションと可視性(Visibility))。 コミット済みあるいはアボートと判明した場合、次回同じコミットステータス確認をしても同じ結果となるので、先述のヒントビットを立てることで次回以降を省略するのである。

ここからが特異な点だが、 タプルヘッダーのヒントビットを打刻するということは、ページの内容を更新するということである。 しかしこれを shared のコンテキストロックで行っても構わない。

ただしタプルの xmin を凍結するために HEAP_XMIN_INVALIDHEAP_XMIN_COMMITTED を同時に立てる場合には shared ロックではなく exclusive ロックが必要となる。
#5 ページからタプルの削除または再配置 cleanup 状態 VACUUM 操作またはページ単位に heap_page_prune() が呼ばれる場合、ページ内のタプルが削除されたり、削除されたタプルを詰めて再配置が行われる。 この場合は cleannup のコンテンツロックが必要となる。

要するに #2 でページ内をピンなしで読めるという最適化を導入してしまったため、exclusive ロックを獲得しただけではまたページ内を読んでいるプロセスがいる可能性があるので、ページ内のタプルを削除したり再配置する場合には #2 を行っているプロセスに不整合が見える。 そこでバッファをピンしているのは一人だけという cleanup 状態にする必要ができたのだ。

ちなみに #3 でページの内容を更新する場合、ページ内の既存のタプルを削除したり位置を変えたりしないので #2 と共存できる。

3.4 API のリスト

実際の API を紹介する。

表8: ブロック/ページ(バッファ)を操作する 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 目のブロックを読み込みピンする。
ただし幾つかの特殊機能がある。
  • blockNum が P_NEW(-1) ならリレーションに新しいブロックを 1 つ追加し、それを読み込む。
  • modeRBM_NORMAL ならピンするだけだが、RBM_ZERO_AND_LOCKRBM_ZERO_AND_CLEANUP_LOCKRBM_ZERO_ON_ERROR なら個別にロックも行う。
ReleaseBuffer(Buffer buffer) void ReadBuffer() などで読み込んだブロックからピンを外す。バッファも解放する。
LockBuffer(Buffer buffer, int mode) void ReadBuffer() などで読み込んだブロックにロックをかける。 ロックは mode により BUFFER_LOCK_SHAREBUFFER_LOCK_EXCLUSIVEBUFFER_LOCK_UNLOCK がある。
  • modeBUFFER_LOCK_SHAREBUFFER_LOCK_EXCLUSIVE を指定するとバッファにロックがかかる。 他のプロセスがロックを握っている間はスピンロックによる排他、またはブロック待機を受ける。
  • modeBUFFER_LOCK_UNLOCK を指定するとアンロックできる。
  • バッファは 3 つのロックのいずれかの状態を持ち、ロックを再帰的にかけるという概念はない。 BUFFER_LOCK_SHAREBUFFER_LOCK_EXCLUSIVE を行き来することもできる。
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);

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);

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 インデックスのストラテジ
マクロ番号演算
BTLessStrategyNumber1小なり
BTLessEqualStrategyNumber2以下
BTEqualStrategyNumber3等しい
BTGreaterEqualStrategyNumber4以上
BTGreaterStrategyNumber5大なり
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() のスキャン条件が strategyprocedure の 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 を参考にしている。

表10: システムカタログのマクロ
カタログ名 用途 OID マクロ インデックス
pg_aggregate 集約関数 AggregateRelationId
  • AggregateFnoidIndexId (aggfnoid)
pg_am インデックスアクセスメソッド AccessMethodRelationId
  • AmOidIndexId (oid)
  • AmNameIndexId (amname)
pg_amop アクセスメソッド演算子 AccessMethodOperatorRelationId
  • AccessMethodOperatorOidIndexId (oid)
  • AccessMethodStrategyIndexId (amopfamily, amoplefttype, amoprighttype, amopstrategy)
  • AccessMethodOperatorIndexId (amopopr, amoppurpose, amopfamily)
pg_amproc アクセスメソッドサポートプロシージャ AccessMethodProcedureRelationId
  • AccessMethodProcedureOidIndexId (oid)
  • AccessMethodProcedureIndexId (amprocfamily, amproclefttype, amprocrighttype, amprocnum)
pg_attrdef 列デフォルト値 AttrDefaultRelationId
  • AttrDefaultOidIndexId (oid)
  • AttrDefaultIndexId (adrelid, adnum)
pg_attribute テーブル列(属性) AttributeRelationId
  • AttributeRelidNameIndexId (attrelid, attname)
  • AttributeRelidNumIndexId (attrelid, attnum)
pg_authid 認証識別子(ロール) AuthIdRelationId
  • AuthIdOidIndexId (oid)
  • AuthIdRolnameIndexId (rolname)
pg_auth_members 認証識別子メンバーシップ関係 AuthMemRelationId
  • AuthMemRoleMemIndexId (roleid, member)
  • AuthMemMemRoleIndexId (member, roleid)
pg_cast キャスト(データ型変換) CastRelationId
  • CastOidIndexId (oid)
  • CastSourceTargetIndexId (castsource, casttarget)
pg_class リレーション RelationRelationId
  • ClassOidIndexId (oid)
  • ClassNameNspIndexId (relname, relnamespace)
  • ClassTblspcRelfilenodeIndexId (reltablespace, relfilenode)
pg_collation 照合順序 CollationRelationId
  • CollationOidIndexId (oid)
  • CollationNameEncNspIndexId (collname, collencoding, collnamespace)
pg_constraint 検査制約、一意性制約、主キー制約、外部キー制約 ConstraintRelationId
  • ConstraintOidIndexId (oid)
  • ConstraintNameNspIndexId (conname, conamespace)
  • ConstraintRelidIndexId (conrelid)
  • ConstraintTypidIndexId (contypid)
pg_conversion エンコード方式変換情報 ConversionRelationId
  • ConversionOidIndexId (oid)
  • ConversionDefaultIndexId (connamespace, conforencoding, contoencoding, oid)
  • ConversionNameNspIndexId (conname, connamespace)
pg_database データベース DatabaseRelationId
  • DatabaseOidIndexId (oid)
  • DatabaseNameIndexId (datname)
pg_db_role_setting ロール毎およびデータベース毎の設定 DbRoleSettingRelationId
  • DbRoleSettingDatidRolidIndexId (setdatabase, setrole oid_ops)
pg_default_acl オブジェクト種類のデフォルト権限 DefaultAclRelationId
  • DefaultAclOidIndexId (oid)
  • DefaultAclRoleNspObjIndexId (defaclrole, defaclnamespace, defaclobjtype)
pg_depend データベースオブジェクト間の依存関係 DependRelationId
  • DependDependerIndexId (classid, objid, objsubid)
  • DependReferenceIndexId (refclassid, refobjid, refobjsubid)
pg_description データベースオブジェクトの説明やコメント DescriptionRelationId
  • DescriptionObjIndexId (objoid, classoid, objsubid)
  • SharedDescriptionObjIndexId (objoid, classoid)
pg_enum 列挙型のラベルや値の定義 EnumRelationId
  • EnumOidIndexId (oid)
  • EnumTypIdLabelIndexId (enumtypid, enumlabel)
  • EnumTypIdSortOrderIndexId (enumtypid, enumsortorder)
pg_event_trigger イベントトリガ EventTriggerRelationId
  • EventTriggerOidIndexId (oid)
  • EventTriggerNameIndexId (evtname)
pg_extension 拡張(エクステンション) ExtensionRelationId
  • ExtensionOidIndexId (oid)
  • ExtensionNameIndexId (extname)
pg_foreign_data_wrapper 外部データラッパの設定 ForeignDataWrapperRelationId
  • ForeignDataWrapperOidIndexId (oid)
  • ForeignDataWrapperNameIndexId (fdwname)
pg_foreign_server 外部サーバーの設定 ForeignServerRelationId
  • ForeignServerOidIndexId (oid)
  • ForeignServerNameIndexId (srvname)
pg_foreign_table 外部テーブルの設定 ForeignTableRelationId
  • ForeignTableRelidIndexId (ftrelid)
pg_index インデックス IndexRelationId
  • IndexIndrelidIndexId (indrelid)
  • IndexRelidIndexId (indexrelid)
pg_inherits テーブル継承階層 InheritsRelationId
  • InheritsRelidSeqnoIndexId (inhrelid, inhseqno)
  • InheritsParentIndexId (inhparent)
pg_language 手続き言語 LanguageRelationId
  • LanguageOidIndexId (oid)
  • LanguageNameIndexId (lanname)
pg_largeobject ラージオブジェクト用のデータページ LargeObjectRelationId
  • LargeObjectLOidPNIndexId (loid, pageno)
pg_largeobject_metadata ラージオブジェクトのメタデータ LargeObjectMetadataRelationId
  • LargeObjectMetadataOidIndexId (oid)
pg_namespace 名前空間(スキーマ) NamespaceRelationId
  • NamespaceOidIndexId (oid)
  • NamespaceNameIndexId (nspname)
pg_opclass アクセスメソッド用の演算子クラス OperatorClassRelationId
  • OpclassOidIndexId (oid)
  • OpclassAmNameNspIndexId (opcmethod, opcname, opcnamespace)
pg_operator 演算子 OperatorRelationId
  • OperatorOidIndexId (oid)
  • OperatorNameNspIndexId (oprname, oprleft, oprright, oprnamespace)
pg_opfamily アクセスメソッド用の演算子族 OperatorFamilyRelationId
  • OpfamilyOidIndexId (oid)
  • OpfamilyAmNameNspIndexId (opfmethod, opfname, opfnamespace)
pg_pltemplate 手続き言語のためのテンプレートデータ PLTemplateRelationId
  • PLTemplateNameIndexId (tmplname)
pg_policy 行単位セキュリティポリシー PolicyRelationId
  • PolicyOidIndexId (oid)
  • PolicyPolrelidPolnameIndexId (polrelid, polname)
pg_proc 関数とプロシージャ ProcedureRelationId
  • ProcedureOidIndexId (oid)
  • ProcedureNameArgsNspIndexId (proname, proargtypes, pronamespace)
pg_range 範囲型 RangeRelationId
  • RangeTypidIndexId (rngtypid)
pg_replication_origin 登録されたレプリケーション起点 ReplicationOriginRelationId
  • ReplicationOriginIdentIndex (roident)
  • ReplicationOriginNameIndex (roname)
pg_rewrite 問い合わせ書き換えルール RewriteRelationId
  • RewriteOidIndexId (oid)
  • RewriteRelRulenameIndexId (ev_class, rulename)
pg_seclabel データベースオブジェクトのセキュリティラベル SecLabelRelationId
  • SecLabelObjectIndexId (objoid, classoid, objsubid, provider)
pg_shdepend 共有オブジェクトの依存関係 SharedDependRelationId
  • SharedDependDependerIndexId (dbid, classid, objid, objsubid)
  • SharedDependReferenceIndexId (dbid, classid, objid, objsubid)
pg_shdescription 共有オブジェクトに対するコメント SharedDescriptionRelationId
  • SharedDescriptionObjIndexId (objoid, classoid)
pg_shseclable 共有データベースオブジェクトのセキュリティラベル SharedSecLabelRelationId
  • SharedSecLabelObjectIndexId (objoid, classoid, provider)
pg_statistic プランナ統計情報 StatisticRelationId
  • StatisticRelidAttnumInhIndexId (starelid, staattnum, stainherit)
pg_tablespace テーブルスペース TableSpaceRelationId
  • TablespaceOidIndexId (oid)
  • TablespaceNameIndexId (spcname)
pg_transform 変換(データ型を手続き言語間の変換ルール) TransformRelationId
  • TransformOidIndexId (oid)
  • TransformTypeLangIndexId (trftype, trflang)
pg_trigger トリガー TriggerRelationId
  • TriggerOidIndexId (oid)
  • TriggerConstraintIndexId (tgconstraint)
  • TriggerRelidNameIndexId (tgrelid, tgname)
pg_ts_config 全文検索設定 TSConfigRelationId
  • TSConfigOidIndexId (oid)
  • TSConfigNameNspIndexId (cfgname, cfgnamespace)
pg_ts_config_map 全文検索設定のトークン写像 TSConfigMapRelationId
  • TSConfigMapIndexId (mapcfg, maptokentype, mapseqno)
pg_ts_dict 全文検索設定辞書 TSDictionaryRelationId
  • TSDictionaryOidIndexId (oid)
  • TSDictionaryNameNspIndexId (dictname, dictnamespace)
pg_ts_parser 全文検索パーサ TSParserRelationId
  • TSParserOidIndexId (oid)
  • TSParserNameNspIndexId (prsname, prsnamespace)
pg_ts_template 全文検索テンプレート TSTemplateRelationId
  • TSTemplateOidIndexId (oid)
  • TSTemplateNameNspIndexId (tmplname, tmplnamespace)
pg_type データ型 TypeRelationId
  • TypeOidIndexId (oid)
  • TypeNameNspIndexId (typname, typnamespace)
pg_user_mapping 外部サーバへのユーザーのマッピング UserMappingRelationId
  • UserMappingOidIndexId (oid)
  • UserMappingUserServerIndexId (umuser, umserver)

コメント

コメントを書き込む

TOP    掲示板    戻る
Written by NAKAMURA Minoru, Email: nminoru atmark nminoru dot jp, Twitter:@nminoru_jp