PostgreSQLでのシーケンス取得方法(currvalとlast_valueの違い)

By nakayama - 09/04/17 - このエントリをはてなブックマークに追加このエントリをYahoo!ブックマークに追加このエントリをdel.icio.usに追加このエントリをFC2ブックマークに追加

プログラムを書いていて、データベースにデータをインサートした後、そのレコードのシーケンスが欲しい場合があります。

PostgreSQLではシーケンスを取得するのにlast_value とcurrvalがありますが、
pearやcakeなど有名なライブラリでもこの2つの使い方が結構あいまいになっているケースがあり注意が必要です。
 
参考:Shin x blog – CakePHP PostgreSQLではgetInsertID()に注意

INSERT INTO  sample (hoge) values ('foo');
SELECT last_value FROM sample_id_seq; // seq=1 

この場合何が問題かというとINSERT から SELECT の間に
別のプログラムからのINSERT が実行されてしまうと正しいシーケンス番号が取れなくなります。

INSERT INTO  sample (hoge) values ('foo');
INSERT INTO  sample (hoge) values ('bar");
SELECT last_value FROM sample_id_seq; // seq=2
SELECT last_value FROM sample_id_seq; // seq=2

カートのシステムで会員情報をINSERT、そのシーケンスを注文情報と関連付ける為のキーとして使っていた場合、別の会員と注文情報が結びつく恐ろしい事態に。。

currvalの場合、同一セッション中で設定された直前のシーケンス値を返すことを保証してくれますので、INSERTが頻繁に起こるシステムでも安心です。

INSERT INTO  sample (hoge) values ('bar");
SELECT currval(sample_id_seq); 

PDO(Zend_DB)lを使っている場合は、lastInsertIdを使えば安心です。
参考:PHP マニュアル – PDO::lastInsertId
 

Zend Frameworkドキュメント – 13.1.4.2. 生成された値の取得

“SELECT MAX(id) FROM table” じゃあダメなんですか?

たしかにこのクエリは、最後にテーブルに追加された主キーの値を返すこともあります。 しかしこれは、複数のクライアントがデータベースにレコードを追加するという環境では 安全ではありません。 つまり、データを追加してから MAX(id) の値を取得するまでの間に 他のクライアントが別のデータを追加する可能性があるということです。 この場合、クエリが返す結果はあなたが実際に追加した行の ID とは異なり、 誰か他の人が追加した行の ID となってしまいます。 しかも、もしそのような状況になっていたとしても あなたにはそれを知ることはできません。

たとえば “repeatable read” のようなトランザクション分離モードを使用すれば、 この危険性を減らせます。しかし、 このレベルのトランザクション分離をサポートしていない RDBMS もあります。また、そのアプリケーション自体がもっと緩いレベルの トランザクション分離モードを想定して作成されているかもしれません。

さらに、新しい主キーの値を生成する際に “MAX(id)+1” のような式を使うのも同様に危険です。ふたつのクライアントから同時にこのクエリを実行すると、 どちらも同じ値を取得することになり、同じ値で INSERT を行なうことになってしまいます。

どんな RDBMS でも、一意な値を生成する機能や 最後に生成した値を返す機能は持っています。 この機能はトランザクション分離レベルとは無関係に機能するはずなので、 ふたつのクライアントで同じ値が重複してしまうことはありません。 また、他のクライアントで作成した値が あなたの接続で「最後に生成した値」として返されることもありません。