ぱと隊長日誌

ブログ運用もエンジニアとしての生き方も模索中

PostgreSQLのオブジェクト識別子(OID)とキャスト

目的

オブジェクト識別子(以下、OID)とキャストについては、すでに詳細な解説記事があります。
オブジェクト識別子を活用する | Let's Postgres
本エントリではこの解説記事をベースにしつつ、理解し辛い点を補足します。

本エントリの目次を示します。

検証環境

CentOS 7
PostgreSQL 9.6

参照するPostgreSQLのドキュメントのバージョンも9.6とします。

解説

オブジェクト識別子(OID)

OIDはPostgreSQLのシステムテーブル(システムカタログ)の主キーとして使用されています。また、OIDによってオブジェクト(テーブルやインデックス、関数、演算子、データ型定義など)を一意に識別できます。例えば、pg_classカタログではOIDとテーブル・インデックス・ビューなどが紐付けられています。
参考:50.11. pg_class

OIDの別名データ型(オブジェクト識別子データ型)とキャスト

OIDには別名データ型(オブジェクト識別子データ型)があります。この一覧はマニュアルに記載されています。

型名 参照 説明 値の例
oid すべて 数値オブジェクト識別子 564182
regproc pg_proc 関数名 sum
regprocedure pg_proc 引数の型を持つ関数 sum(int4)
regoper pg_operator 演算子 +
regoperator pg_operator 引数の型を持つ演算子 *(integer,integer) or -(NONE,integer)
regclass pg_class リレーション名 pg_type
regtype pg_type データ型名 integer
regrole pg_authid ロール名 smithee
regnamespace pg_namespace 名前空間 pg_catalog
regconfig pg_ts_config テキスト検索設定 english
regdictionary pg_ts_dict テキスト検索辞書 simple
8.18. オブジェクト識別子データ型

オブジェクト識別子データ型は型キャストの構文で型に指定します。参考までに、型キャストの構文を引用します。

PostgreSQLは型キャストに2つの等価な構文を受け付けます。

CAST ( expression AS type )
expression::type

CAST構文はSQLに準拠したものです。 ::を使用する構文は、PostgreSQLで伝統的に使用されている方法です。

4.2. 評価式

ここで重要なのが、オブジェクト識別子データ型であっても、キャストの挙動は変わらない、ということです。これに関連して、解説記事では以下の記述があります。

例えばregclassへ(スキーマ名.)テーブル名を渡す(キャストする)と、OIDへ変換してくれます。逆にOIDを渡すと(スキーマ名.)テーブル名に変換してくれます。

オブジェクト識別子を活用する | Let's Postgres

この記述からはregclassへのキャストでOIDとテーブル名を相互に変換できるように見えますが、実際にはregclassへのキャストはregclass型へ変換しているにすぎません。

実際の挙動から確認します。

-- WHERE句を見ると、'pg_class'::regclassでOIDに変換されて見える。
SELECT
  oid,
  relname
FROM
  pg_class
WHERE
  oid = 'pg_class'::regclass;

 oid  | relname
------+----------
 1259 | pg_class

-- だが、SELECTしてみるとOIDを取得できない。
SELECT 'pg_class'::regclass;

 regclass
----------
 pg_class

-- OIDを取得するにはregclassへのキャストを、さらにoidへキャストする必要がある。
SELECT 'pg_class'::regclass::oid;

 oid
 ------
 1259

-- テーブル名 -> regclass -> oid となるわけでもない。
SELECT 'pg_class'::regclass::regclass;

 regclass
----------
 pg_class

参考:50.11. pg_class

regclassへのキャストで、SELECT句ではOIDを取得できず、WHERE句ではOIDに変換できたのは、暗黙的な型変換が関係しています。

まず、pg_castカタログからregclassのデータ型変換パスを確認します。

SELECT
  castsource::regtype,
  casttarget::regtype,
  castcontext
FROM
  pg_cast
WHERE
  castsource = 'regclass'::regtype
  OR casttarget = 'regclass'::regtype
ORDER BY
  castsource,
  casttarget;

    castsource     | casttarget | castcontext
-------------------+------------+-------------
 bigint            | regclass   | i
 smallint          | regclass   | i
 integer           | regclass   | i
 text              | regclass   | i
 oid               | regclass   | i
 character varying | regclass   | i
 regclass          | bigint     | a
 regclass          | integer    | a
 regclass          | oid        | i

参考:50.10. pg_cast

この結果より、text ⇒ regclass, regclass ⇒ oidへの変換があることが分かります。

"SELECT 'pg_class'::regclass;"は"text ⇒ regclass"でregclass型へ変換し、その結果としてpgclass.relnameを出力したと考えられます。

次に、左辺もしくは右辺がoid型の"="演算子について、pg_operatorカタログから確認します。

SELECT
  oprname,
  oprleft::regtype,
  oprright::regtype
FROM
  pg_operator
WHERE
  oprname = '='
  AND (
    oprleft = 'oid'::regtype
    OR oprright = 'oid'::regtype);

 oprname | oprleft | oprright
---------+---------+----------
 =       | oid     | oid

参考:50.34. pg_operator

"="演算子は左辺・右辺共にoid型であることを要求していることが分かります。

"SELECT oid, relname FROM pg_class WHERE oid = 'pg_class'::regclass;"の場合、WHERE句の左辺の"oid"列がoid型ですので、右辺のregclass型が暗黙的にoid型へ変換されたものと考えられます。

PostgreSQLのマニュアルでも、以下の2つのSQLはほぼ同等であることが示されています。

-- オブジェクト識別子データ型のキャストを利用したSQL
SELECT
  *
FROM
  pg_attribute
WHERE
  attrelid = 'mytable'::regclass;

-- オブジェクト識別子データ型のキャストを利用しないSQL
SELECT
  *
FROM
  pg_attribute
WHERE
  attrelid = (
    SELECT
      oid
    FROM
      pg_class
    WHERE
      relname = 'mytable');

ただし、後者のSQLスキーマの考慮がされておらず、完全に同等ではありません。スキーマパスを考慮する前者のSQLのほうが望ましいとされています。本節の主題から外れるため、詳細は割愛します。詳しくはマニュアルをご参照ください。
参考:8.18. オブジェクト識別子データ型

オブジェクト識別子データ型のキャストの使いどころ

オブジェクト識別子データ型のキャストの使い方の一例として、システムカタログの参照が挙げられます。システムカタログにはPostgreSQL固有の情報が格納されています。例えば、テーブルや列の情報などのスキーマメタデータ、及び内部的な情報です。

pg_locksビュー(50.65. pg_locks)で現在のロックを確認するSQLの例がありました。

SELECT
  l.pid,
  l.granted,
  d.datname,
  l.locktype,
  relation,
  relation::regclass,
  transactionid,
  l.mode
FROM
  pg_locks l
  LEFT JOIN pg_database d
    ON l.database = d.oid
WHERE
  l.pid != pg_backend_pid()
ORDER BY
  l.pid;

SQLを見やすくするため、オリジナルに対して整形を行っています。

【PostgreSQL-0007】ロック(Lock)の確認方法とデットロックの解除 - OSS ERP Compiere Distribution Lab

relation列は参照先がpg_class.oidとなっており、オブジェクト識別子データ型の一覧より、regclassでキャスト可能なことが分かります。
これに対し、database列は参照先がpg_database.oidとなっており、オブジェクト識別子データ型の一覧にありません。そこで、pg_locks.databaseとpg_database.oidをキーに結合して参照しています。

regprocとregoper別名型の制限

マニュアルに以下の記述があります。

regprocとregoper別名型は、一意な(オーバーロードしていない)名前のみを入力として受け入れるため、これらの使用には限度があります。 ほとんどの場合、regprocedureまたはregoperatorを使用するのが適切です。

8.18. オブジェクト識別子データ型

この記述を実際の挙動から確認します。

-- pg_procカタログでpiとsumの定義数を確認する
SELECT
  proname,
  count(*)
FROM
  pg_proc
WHERE
  proname IN ('pi', 'sum')
GROUP BY
  proname;

 proname | count
---------+-------
 pi      |     1
 sum     |     8

-- pg_procカタログ内で関数名が一意な場合
SELECT 'pi'::regproc;

 regproc
---------
 pi

-- pg_procカタログ内で関数名が一意でない場合
SELECT 'sum'::regproc;

ERROR:  more than one function named "sum"

-- 引数の型を指定してregprocedureでキャストする
SELECT 'sum(integer)'::regprocedure;
 regprocedure
--------------
 sum(integer)

参考:50.38. pg_proc

regprocでは一意な名前を指定しないとエラーになりました。このような場合、マニュアルに記載の通り、引数の型を指定してregprocedureでキャストする必要があります。

まとめ

オブジェクト識別子データ型のキャストを活用することで、SQLを正確にそして簡潔に記述することができます。オブジェクト識別子データ型は必要な全てのパターンが用意されているわけではないため、参照先と結合することが必要な場合もあります。

pg_rman の「最新の状態までリカバリ」の処理内容

目的

pg_rman で復元ポイントを指定せずにリストアを実行し、PostgreSQLを起動すると、可能な限り障害直前の状態までリカバリされます。pg_rman のマニュアルで「リストア・オプション」の節に以下の説明があります。

  • --recovery-target-timeline TIMELINE
    • どの時系列でリカバリを行うかを指定します。省略時は現在のタイムライン($PGDATA/global/pg_control から取得)でリカバリを行います。
  • --recovery-target-time TIMESTAMP
    • リカバリ処理をどこまで行うかを示す時刻を指定します。省略時は可能な限り最新の状態までリカバリを行います。
pg_rman

pg_rman がどのようにして障害直前の状態にまでリカバリしているのか、リストア処理の概説と共に説明します。

環境

今回、参考にした pg_rman のソースコードは以下のリンクからダウンロードしました。
https://github.com/ossc-db/pg_rman/archive/v1.3.4.zip

挙動確認は以下の構成で行いました。
CentOS 7
PostgreSQL 9.6
pg_rman-1.3.4-pg96

説明

ベースバックアップからのリストアは以下の流れで行われます。

  1. オンラインWALとサーバログのバックアップ
  2. $PGDATA 配下の削除
  3. バックアップから $PGDATA 配下のリストア
  4. バックアップからアーカイブWALのリストア
  5. オンラインWALのリストア(先ほどバックアップしたオンラインWAL)
  6. recovery.conf の作成

このリストア処理が完了すると、ベースバックアップ+アーカイブWAL+オンラインWALによる完全リカバリを行える状況になります。見方を変えると、 --recovery-target-time を指定しなければ完全リカバリになります。任意の時点(例えばバックアップ時点)に戻したい場合は --recovery-target-time を指定して、不完全リカバリを実行する必要があります。

補足

バックアップされていないアーカイブWALのリストア時の扱い

バックアップされていないアーカイブWALに対し、リストア処理では何もしません。つまり、そのまま残ります。これは完全リカバリを行うために必要です。

リストア処理でのオンラインWALとサーバログのバックアップ

pg_rman のマニュアルには以下の記載があります。

pg_rmanがリストア中に未アーカイブの WAL をバックアップしつつ、その他のデータを削除します。 未アーカイブの WAL のバックアップは次に全体バックアップを取得するまで保持されるので、リカバリ結果が望んだものでなかった場合は再度リストアからやり直すことが可能です。

pg_rman

実際には未アーカイブのWAL(オンラインWAL)だけでなく、サーバログもバックアップされています。

コピーは以下のように行われます。

対象 コピー元 コピー先
オンラインWAL $PGDATA/pg_wal $BACKUP_PATH/backup/pg_xlog
サーバログ $SRVLOG_PATH $BACKUP_PATH/backup/srvlog

※各環境変数については pg_rman マニュアルの「パラメータの指定方法」(http://ossc-db.github.io/pg_rman/index-ja.html#waytopassoptions)を参照ください。

該当処理のソースコードを抜粋します。
restore.c (778)

/* backup online WAL */
snprintf(pg_wal_path, lengthof(pg_wal_path), "%s/pg_wal", pgdata);
snprintf(work_path, lengthof(work_path), "%s/%s/%s", backup_path,
	RESTORE_WORK_DIR, PG_XLOG_DIR);
dir_create_dir(work_path, DIR_PERMISSION);
dir_copy_files(pg_wal_path, work_path);

/* backup serverlog */
snprintf(work_path, lengthof(work_path), "%s/%s/%s", backup_path,
	RESTORE_WORK_DIR, SRVLOG_DIR);
dir_create_dir(work_path, DIR_PERMISSION);
dir_copy_files(srvlog_path, work_path);

pg_xlog と pg_wal

pg_rman は対応する PostgreSQL のバージョン毎にパッケージを用意しています。
pg_rman-1.3.4 のパッケージ間で PG_XLOG_DIR の定義に差異がありました。

ダウンロード元
Release 1.3.4 · ossc-db/pg_rman · GitHub

Source code (zip) - pg_rman.h

#define PG_XLOG_DIR "pg_wal"

pg_rman-1.3.4-pg96.tar.gz - pg_rman.h

#define PG_XLOG_DIR "pg_xlog"

この差異について、リリースノートの「変更点」に説明がありました。

PostgreSQL 10では'pg_xlog'が'pg_wal'に変更されることや、SQL関数名の中の'xlog'も'wal'に変更されることに対応しました。

Release 1.3.4 · ossc-db/pg_rman · GitHub

ベースとなる "Source code" は最新のバージョン(現時点で開発中の PostgreSQL 10)をターゲットとしているようです。

リストアとリカバリの違い

「リストア」と「リカバリ」という用語は pg_rman のマニュアルで共につかわれています。
一例を挙げます。

リストア完了後にpg_rmanが生成したrecovery.confの内容を確認し、必要に応じて修正を行います。 今回の場合は、最新の状態までリカバリするため特に修正せず、そのままPostgreSQLを起動してPITRを実施させます。

pg_rman

「リストア」と「リカバリ」の定義(使い分け)を調べたところ、Oracle Database を例に解説したエントリがありました。

Oracleデータベースのリストアとは、バックアップ媒体から元の場所もしくは新しい場所へデータベースを構成する物理ファイルをコピーして復元することを指します。
Oracleデータベースのリカバリとは、REDOログファイル(バックアップ取得~現在までのトランザクションの変更情報が保存されています)を使用してバックアップ後に作成されたデータベースへ変更情報を反映してデータを復旧することを指します。
参照元より抜粋)

Oracle:リカバリとリストアの違い | 覚え書き.com

この解説を踏まえて pg_rman のマニュアルを読み直したところ、この解説に沿った使い分けがなされているようでした。そこで、本エントリでもこの解説にそった使い分けを行いました。

pg_rman によるバックアップで、バックアップ対象ファイルのタイムスタンプがシステム時刻より進んでいるとエラーとなる

事象

pg_rman によるバックアップ中に、以下のエラーが表示されて、処理が中断される。

$ pg_rman backup --backup-mode=full --with-serverlog --progress
INFO: copying database files
Processed 1183 of 1183 files, skipped 0
INFO: copying archived WAL files
FATAL: cannot take a backupipped 0
DETAIL: There is a file with future timestamp from system time.
Current system time may be rewound.
HINT: The file is /mnt/data/pgsql/archivedir/000000020000000000000006.
If this is a database file, please retry with the full backup mode.
If this is a server log or archived WAL file, change the timestamp.

再現環境

CentOS 7(PostgreSQLサーバ、NFSサーバ共に)
PostgreSQL 9.6
pg_rman-1.3.4-pg96

NFSサーバをWALアーカイブ先に指定しています。

環境やコマンドによっては、先述のエラーメッセージが異なるかもしれません。

原因

pg_rman によるバックアップ実行時、pg_rman を実行したマシンのシステム時刻と、バックアップ対象ファイルのタイムスタンプを比較し、ファイルのタイムスタンプが進んでいる場合にエラーとなるためです。

pg_rman のバックアップ対象は以下のファイル群です。

  • データベース($PGDATA、データディレクトリ)
  • アーカイブWAL(archive_command 構成パラメータの指定先)
  • サーバログ(log_directory 構成パラメータの指定先)

このうち、アーカイブWALファイルは本エラーの対象となりやすいです。NFSサーバをWALアーカイブ先としている場合、DBサーバとNFSサーバの時刻ずれによって、本エラーの発生する可能性があるからです。

このことは pg_rman のマニュアルにも記載されています。

WALアーカイブ先が別サーバ上に存在する場合、DBサーバよりもシステム時刻が進んでいるとバックアップ/リカバリに失敗します。

pg_rman

解決策

DBサーバとNFSサーバで同じNTPサーバを基準に時刻同期します。同じNTPサーバを基準にすることで、時刻ずれをなるべく抑えます。それでもサーバ間の時刻ずれがわずかに生じますが、今回の問題に関してはほぼ影響ないと思われます。

参考

回避パッチの提案

本問題に関連して、サーバ間の時刻ずれを考慮し、ファイルのタイムスタンプの比較に余裕時間を加えてはどうか、との提案が出ていました。
Error on Backup into a network filesystem.
ただ、この提案に対しては反応がないようです。

私としては、このパッチが受け入れられることはないように思います。なぜなら、安全・確実なバックアップを行うために取り入れた仕組みを迂回することになるためです。