Synchronized Snapshot お試し:PostgreSQL Advent Calendar #7

このエントリはPostgreSQL Advent Calendarの12/7分です。

現在開発中のPostgreSQL 9.2でSynchronized Snapshot(同期スナップショット?)なる機能が導入されます。開発の動機はpg_dumpallで複数のデータベースのバックアップを一貫性のある内容で取りたいということのようで、あまり派手な機能ではないのですが、開発中の pgsql_fdw と組み合わせて面白いことできないかな、と思って調べたので内容を整理してみます。

そもそも「Snapshot(スナップショット)」って何?

PostgreSQLにおける「スナップショット」とは、MVCC で使われる情報の一つで、大雑把に言えばあるトランザクションからどの時点でのデータが可視であるかを判定する基準です。Synchronized Snapshotでは、このスナップショットを複数のトランザクションで共有することによって同一内容のデータを参照できるようになります。

スナップショットを同期する流れ

参照するデータの準備

まず、参照するデータを用意します。デフォルトの分離レベルはREAD COMMITTEDなので、この時点で他のトランザクションから1,000件が参照できます。


postgres=# CREATE TABLE foo (
postgres-# 	id int,
postgres-# 	name text,
postgres-# 	CONSTRAINT foo_pkey PRIMARY KEY (id)
postgres-# );
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "foo_pkey" for table "foo"
CREATE TABLE
postgres=# INSERT INTO foo SELECT id, to_char(id, '00000000') FROM generate_series(1, 1000) id;
INSERT 0 1000
同期元トランザクションの開始

まず、基準となるトランザクションがいないことには話が始まりませんので開始します。今回はデータを作成したトランザクションとは別に同期元トランザクションを開始してみます。この時点で参照できるのは1,000件です。


postgres=# BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN
postgres=# SELECT count(*) FROM foo;
 count
            • -
1000 (1 row)
スナップショットのエクスポート

同期元のトランザクションでpg_export_snapshot()関数を実行してスナップショットをエクスポートします。関数の戻り値がスナップショット識別子なので、後で使うまで覚えておきましょう。なお、READ COMMITTED分離レベルの場合は、トランザクション進行中に複数回実行すると異なるスナップショット識別子が返り、複数の別トランザクションでそれぞれの時点のデータを参照することもできます。


postgres=# select pg_export_snapshot();
 pg_export_snapshot
                                      • -
000007CE-1 (1 row)
データ追加

同期元とは別のセッションで、先ほどのテーブルにデータを1,000件追加してコミットしておきます。同期元トランザクションはREAD COMMITTED分離レベルなので、参照できるデータは2,000件になります。


postgres=# INSERT INTO foo SELECT id, to_char(id, '00000000') FROM generate_series(1001, 2000) id;
INSERT 0 1000

postgres=# SELECT count(*) FROM foo;
 count
            • -
2000 (1 row)
同期先トランザクションの開始

別のセッションでトランザクションを開始します。が、同期元との間に分離レベルの制約があります。

同期元 同期先
SERIALIZABLE REPEATABLE READまたはSERIALIZABLE
REPEATABLE READ REPEATABLE READ
READ COMMITTED REPEATABLE READ

ここでは同期元がREAD COMMITTEDなので、REPEATABLE READを使います。


postgres=# BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN
スナップショットのインポート

同期先のトランザクションで先ほど取得したスナップショット識別子を指定してpg_import_snapshot()関数を実行します…と言いたいところですが、インポートにはSET TRANSACTION SNAPSHOT文を使うことになっています。エクスポートした時点と同じく参照できるのは1,000件が参照ですね。


postgres=# SET TRANSACTION SNAPSHOT '000007CE-1';
SET
postgres=# SELECT count(*) FROM foo;
 count
            • -
1000 (1 row)

なお、SET TRANSACTION SNAPSHOT文はそのトランザクションの最初のクエリでなければならない、という制約があります。私は最初にpsqlでSET TRANSACTION文を打つときに補完機能を使ってしまい、裏でSELECT文が発行されてトランザクション開始からやりなおすはめになりました…

別のスナップショットをエクスポート/インポート

同期元トランザクションから2,000件参照できる状態でエクスポートしたスナップショットを使うと、同じデータが参照できます。


postgres=# select pg_export_snapshot();
 pg_export_snapshot
                                      • -
000007BB-1 (1 row) postgres=# BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN postgres=# SET TRANSACTION SNAPSHOT '000007CE-1'; SET postgres=# SELECT count(*) FROM foo; count
            • -
2000 (1 row)

まとまってないまとめ

  • トランザクション分離レベルの制約があるので、利用シーンは限られるかも。まだ英語のみですが、詳細はPostgreSQL: Documentation: devel: 9.26. System Administration Functionsにあります。
  • psqlの補完機能は裏でSQL文を発行するので、SET TRANSACTION SNAPSHOT文に使っちゃダメ。
  • スナップショットは実は$PGDATA/pg_snapshotにテキストファイルで保存されている。書き換えは試してませんがきっと未定義動作でしょう。
  • あくまで「スナップショット同期」であって同一トランザクションになるわけではないので、別トランザクションが更新した未コミットのデータは見えない(当たり前ですが)。これができればpgsql_fdwとか非同期クエリとか組み合わせてパラレルクエリができるかな、と思ってたんですがそうは問屋が卸さないようです。

このエントリ読んだ方で何か面白い使い道を思いついた方がいたら、ぜひ教えてください。m(_ _)m

明日のPostgreSQL Advent Calendarは〜♪

fujii_masaoさんです。