ぱと隊長日誌

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

PostgreSQLの一部の関数はトランザクション分離レベルに従わない

はじめに

PostgreSQL 8.4.1 で pg_dump と index の再作成を同時に実行すると "ERROR: cache lookup failed for index" の発生することがあるのはなぜか?との質問に、tom lane さんがこんな回答をしていました。

pg_dump runs in a serializable transaction, so it sees a consistent snapshot of the database including system catalogs. However, it relies in part on various specialized backend functions like pg_get_indexdef(), and those things tend to run on SnapshotNow time, ie they look at the currently committed state.

PostgreSQL: Re: Cache lookup failure for index during pg_dump

pg_dump はシリアライザブルトランザクションPostgreSQL 9.1以降のリピータブルリードに相当)で実行されるが、pg_get_indexdef() のような一部の関数は現在のコミット済みの状態を返すとあります。

PostgreSQL 9.1 以降はトランザクション分離レベルが変更されていますが、これによって挙動の変化はあったのでしょうか?本エントリで確認結果をまとめます。

検証

検証環境・方針

PostgreSQL 10.3 で検証を行いました。

トランザクション1(T1)でインデックスを drop し、トランザクション2(T2)で pg_get_indexdef() を実行して挙動を確認しました。また、以下の条件を組み合わせています。

  • DROP INDEX コミット前/後
  • リピータブルリード/シリアライザブル

今回の検証ではリピータブルリード/シリアライザブルで結果に差異がありませんでした。そこで、検証結果はシリアライザブルの例でまとめます。

DROP INDEX コミット前に pg_get_indexdef() を実行する

-- T1

=# CREATE TABLE index_test(value integer);

=# CREATE INDEX value_idx ON index_test(value);

=# BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;

=# SELECT oid FROM pg_class WHERE relname = 'value_idx';
  oid
-------
 16929
(1 row)
-- T2

=# BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;

=# SELECT oid FROM pg_class WHERE relname = 'value_idx';
  oid
-------
 16929
(1 row)

=# SELECT pg_get_indexdef(16929);
                         pg_get_indexdef
-----------------------------------------------------------------
 CREATE INDEX value_idx ON public.index_test USING btree (value)
(1 row)
-- T1

=# DROP INDEX value_idx;
-- T2

=# SELECT oid FROM pg_class WHERE relname = 'value_idx';
  oid
-------
 16929
(1 row)
-- DROP INDEX 発行後もOIDが取得できる。
-- スナップショットを利用するため、この挙動は妥当といえる。

=# SELECT pg_get_indexdef(16929);
-- レスポンスが保留される。
-- T1

=# COMMIT;
-- T2

保留されていたレスポンスが戻り、
"ERROR:  cache lookup failed for relation 16929"
と表示される。
インデックスが削除された状態、つまりT1コミット後の状態を見ている。

DROP INDEX コミット後に pg_get_indexdef() を実行する

-- T1

=# CREATE TABLE index_test(value integer);

=# CREATE INDEX value_idx ON index_test(value);

=# BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;

=# SELECT oid FROM pg_class WHERE relname = 'value_idx';
  oid
-------
 16931
(1 row)
-- T2

=# BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;

=# SELECT oid FROM pg_class WHERE relname = 'value_idx';
  oid
-------
 16931
(1 row)

=# SELECT pg_get_indexdef(16931);
                         pg_get_indexdef
-----------------------------------------------------------------
 CREATE INDEX value_idx ON public.index_test USING btree (value)
(1 row)
-- T1

=# DROP INDEX value_idx;

=# COMMIT;
-- T2

=# SELECT oid FROM pg_class WHERE relname = 'value_idx';
  oid
-------
 16931
(1 row)
-- DROP INDEX 発行後かつコミット後もOIDが取得できる。
-- スナップショットを利用するため、この挙動は妥当といえる。

=# SELECT pg_get_indexdef(16931);
 pg_get_indexdef
-----------------

(1 row)
-- CREATE INDEX 文を取得できない(T1コミット後の状態を見ている)が、エラーにはならなかった。

まとめ

PostgreSQL 10 かつ シリアライザブル分離レベルであっても、pg_get_indexdef() はスナップショットではなくコミット済みの状態を返しました。また、DROP INDEX のコミット前後によって pg_get_indexdef() の挙動が変わることもわかりました。PostgreSQL 10 以外のバージョンでも同様にふるまう可能性が高そうです。

冒頭で紹介した回答にはこんな記述もあります。

The right fix for this is to make all those inquiry functions use the calling query's snapshot; but duplicating a lot of backend infrastructure is going to be a major pain in the rear, so the discussion has kind of petered out every time it's come up in the past.

PostgreSQL: Re: Cache lookup failure for index during pg_dump

今回のふるまいについてマニュアルを確認しましたが、該当する記述を見つけられませんでした。TRUNCATEのようにMVCCセーフでないとマニュアルに明記されているコマンドもありますが、記載がなくともトランザクション分離レベルに従わないふるまいをする関数があるということです。

また、該当する関数を直接使わずとも、pg_dump のように利用しているケースがあります。今回のようなケースがあることを頭の片隅に入れておくとよいでしょう。

更新情報

2018/10/03

検証においてT1のBEGINの直後にSELECTコマンドを追加することで、参照するスナップショットをより明確にしました。サンプルのふるまい及びエントリ全体の結論には影響ありません。
参考:PostgreSQL のシリアライザブル分離レベルにおけるスナップショットのタイミング - ぱと隊長日誌