3/26 (土)
[Movie] バットマン v.s. スーパーマン
チネチッタで『バットパン v.s. スーパーマン』を観る。 マン・オブ・スティール(Man of Steel)の後の世界。 バットマンことブルース・ウィルスもゾット将軍の地球襲来でゴッサムシティが破壊されるのを目撃していたという設定。 なんというか地球を逆回転させるスーパーマンと鍛えただけのただの人間のバットマンが対比されるというのはシュールだよなぁ。
3/24 (木)
[PostgreSQL] VACUUM トランザクションの特殊性
PostgreSQL の VACUUM 処理はトランザクションとして実行されるので、独自のトランザクション ID (XID) を持っているのだが、この VACUUM の XID はコミットしたかどうか(TransactionIdDidCommit()
)、アボートしたかどうか(TransactionIdDidAbort()
)のチェックはできるのだが、現在実行中かどうか(TransactionIdIsInProgress()
)のチェックができないようだ。
セッションのスナップショットは GetSnapshotData()
という関数で取得される。
これはジャイアント・ロックを獲った上で PostgreSQL インスタンス内の全てのプロセスをスキャンして、現在稼働中のトランザクションのリストを作成する処理だ。
GetSnapshotData()
は同時に、全てのプロセスからみてコミット or アボートが確定した最古の XID を RecentXmin に設定する。
しかし GetSnapshotData()
は、VACUUM を処理しているプロセスは別扱いで、リストに加えず、RecentXmin の算出からも除外してしまう。
これが原因で状態によっては VACUUM のトランザクションがまだ生存しているにも関わらず TransactionIdIsInProgress()
が false を返すことがあるようだ。
まずことに PostgreSQL は TransactionIdIsInProgress(xid)
が false で、TransactionIdDidCommit(xid)
が false の場合、そのトランザクションはアボートまたはクラッシュしたと判定する。
そのため VACUUM トランザクションがテーブルに対して行を挿入・削除・更新した場合、VACUUM トランザクションが終わる前に別のトランザクションがテーブルをスキャンすると挿入・削除・更新がロールバックされたように見える。
さらにテーブルスキャンはトランザクション判定の結果を行(タプル)のフラグ領域(t_infomask)に書き込んでしまうので、VACUUM トランザクションが完了した後からテーブルを見直しても挿入・削除・更新が行われなかったように見える。
ドキュメント化されていないが、VACUUM 処理の延長線上で行える処理は以下のような制限を課す必要があるようだ。
- テーブルに挿入・削除・更新はダメ。
- VACUUM トランザクションの XID をリレーションに書き出すような処理はダメ。
- トランザクションがアボートしたとしても、それまでリレーションに書き出した内容をロールバックしなくても済むような内容
3/19 (土)
[PostgreSQL] PostgreSQL で使われるロックの公平性
PostgreSQL の各種操作はテーブルに対してテーブルレベルロックを設定する。 通常の SQL コマンドの SELECT はテーブルに対して AccessShare のロックモードでロックを適用する。 UPDATE の場合は RowExclusive ロック、ALTER TABLE の場合は AccessExclusive ロックである。
AccessShare、RowShare、RowExclusive はちょうど Read-Writer ロックの Reader 側のように同じロックモードであれば複数のプロセスが同時にロックを獲得することができる。 ただしこれには罠があるようだ。 AccessShare、RowShare、RowExclusive でロックされている時に、後参のプロセスが同じロックモードでロックをかけたらマニュアル仕様的にはロックが可能なように思える。 その中間に別のロックモードでロックをかけてブロックされたプロセスが居た場合、FIFO でブロックされるようだ。
例えば AccessExclusive は他の全てのロックモードのロックを排他する。 AccessExclusive で同時にロックできるのは 1 プロセスだけになる。
- #1 プロセスが AccessShare ロックを獲った。
- #2 プロセスが AccessExclusive ロックを獲ろうとした。この場合、AccessShare ロックがすでに掛けられているためにロックは獲得できず、#2 プロセスはブロックする。
- #3 プロセスが AccessShare ロックを獲ろうとした。この場合、マニュアル仕様上ロックが獲れそうだが、先行している #2 プロセスがブロックされているため # プロセスもブロックされる。
このあたりのロックの挙動を把握しておかないとどつぼに嵌る。
3/9 (水)
[PostgreSQL] 出力レコード不定の Set Returing Function の作り方
PostgreSQL の関数には Set Returing Fuction(SRF) と呼ばれるものがある。
「集合」を返す関数で、1回の関数呼び出しで複数行の結果を返すことになる。
下の SQL サンプルだと generate_series
がそれにあたる。
CREATE TABLE testtable (i int);
INSERT INTO testtable (i) VALUES (1), (2);
SELECT i, generate_series(1,3) FROM testtable;
i | generate_series
---+-----------------
1 | 1
1 | 2
1 | 3
2 | 1
2 | 2
2 | 3
(6 rows)
SRF 関数をユーザー定義することも可能である。
PostgreSQL 9.4.x文書だと35.4.8 集合を返すSQL関数、35.9.8 行(複合型)を返す、35.9.9 集合を返すにやり方が書かれている。
CREATE FUNCTION でユーザー定義関数を定義する際の戻り値の型として SETOF sometype
を付けるのが決める。
一方、SETOF
を利用した場合でも出力行(レコード)の列の定義が必要になる。
これも CREATE FUNCTION 時に定義する。
やり方は複数ある。
- RETURNS tablename を使って既存のテーブル定義を利用して出力形式を決める。
- RETURNS typename を使って既存のデータ型を利用して出力形式を決める。
- 出力パラメータ を利用して
- RETURNS TABLE を使って出力形式を決める。
ところで出力レコードが不定な SRF 関数は作れないのだろうか? 例えばテーブル名を引数にとり、そのテーブルの内容を返すような SRF 関数である。 テーブルの定義はテーブル毎に異なるので、この関数の出力レコードは不定となる。
PostgreSQL 9.4.x文書には、そのような作り方が載ってないのだが、PostgreSQL の既存関数の中で JSON を処理する json_to_recordset()
、jsonb_to_recordset()
がちょうどそのような関数である。
この赤字の AS x(a int, b text) は必須で省略できない。
PostgreSQL はクエリーのプランを作る段階で、各プランノードの型情報が決まっていなければ成らない。
json_to_recordset()
がどのような出力レコードを返すかは json_to_recordset()
を実行してみるまで分からないので、これを補う必要がある。
SELECT * FROM json_to_recordset('[{"a":1,"b":"foo"},{"a":"2","c":"bar"}]') AS x(a int, b text);
a | b
---+-----
1 | foo
2 |
ちょっと面倒だが、この文法を受け入れるなら出力レコードが未定義な SRF 関数を C 言語で作ることができる。
仮に json_to_recordset()
をユーザー定義関数として実装しよとすると、その CREATE FUNCTION は以下のように出力レコードを未定のまま定義する。
CREATE FUNCTION public.json_to_recordset(IN arg json) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C VOLATILE STRICT;
AS x(a int, b text) の部分の出力レコードの定義は 35.9.8 行(複合型)を返すにある get_call_result_type()
を使うとタプル・ディスクリプター(TupleDesc) として取り出すことができる。
このタプル・ディスクリプターをコピーして で定義した FuncCallContext
構造体の tuple_desc に格納してやればよい。
参考として出力レコードが未定な SRF ユーザー定義関数の例として exec_sql_twice エクステンションを公開する。
このエクステンションに含まれる sql_exec_twice()
は引数として与えられた SQL クエリー文字列を実行して出力する。
ただしクエリーを 1 回実行した後に巻き戻して 2 回目の実行を行うので、通常のクエリーの 2 倍の出力がえられる。
CREATE EXTENSION exec_sql_twice; -- テスト用のテーブルを定義 CREATE TABLE testtable (i int); INSERT INTO testtable (i) VALUES (1), (2); -- 通常の実行 SELECT * FROM testtable; i --- 1 2 (2 rows) -- exec_sql_twice で実行する SELECT * FROM sql_exec_twice('SELECT * FROM testtable;') AS x(i int); i --- 1 2 1 2 (4 rows)