最終更新日: 2026-08-25
TOP(About this memo)) > 一覧(RDB) > ロック・同時実行制御(PostgreSQL)
接続中クライアントの確認:
select * from pg_stat_activity;
ロック状況の確認:
SELECT l.pid,l.granted,d.datname,l.locktype, l.tuple, l.relation,l.relation::regclass,l.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;
pg_locksの各項目内容については以下を参照。
このテーブルには「テーブルロック」と「未獲得の行ロック」しか含まれない。したがって、
獲得済みの行ロックは確認できない。
ロックモード(「FOR UPDATE」「FOR NO KEY UPDATE」「FOR SHARE」「FOR KEY SHARE」)の区別がつかない。
(参考) pg_locksについて - Qiita
サーバーログにロック内容や待機状態も出力される(?)。
分離レベルの構文:
SHOW TRANSACTION ISOLATION LEVEL
トランザクション開始コマンドと一緒に使う:
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
MVCCの詳細はトランザクション分離レベルを参照。
ほとんどのPostgreSQLコマンドでは自動的にロックを取得するので注意が必要。
NOWAIT は FOR UPDATE のみに利用できる。以下はすべてエラーとなる。delete from users where id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' NOWAIT
select from users where id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' NOWAIT
select from users where id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' for select NOWAIT
update users set name='dummy name' where id = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb' nowait
先に SELECT FOR UPDATE NOWAIT でロックをかけてから、その後各処理をすればよさそう(?)。
LOCK コマンドを使用する。
LOCK TABLE user_setting;
モードの指定がないため、もっとも強力なACCESS EXCLUSIVEを取得する。
RowExclusiveLockはかかるが、競合しない限りロック待ちは発生しない。
パターン1: ユニーク制約のあるテーブルへinsert
前提として users は uid でユニーク。
セッション1:
begin;
INSERT INTO users (name, uid) VALUES ('name', 'aaaaaaa');
select * from users;
(1 row)
セッション2:
select * from users;
(0 row)
INSERT INTO users (name, uid) VALUES ('name', 'aaaaaaa');
待機状態になる。ロック競合の検知において、ユニーク制約はちゃんと見ているようだ。pg_locksを確認すると、usersでRowExclusiveLockがかかっている。
セッション1:
commit;
セッション2:
ERROR: duplicate key value violates unique constraint "uniq__users__uid"
DETAIL: Key (uid)=(aaaaaaa) already exists.
パターン2: ユニーク制約のないテーブルへinsert
前提として comments にはユニーク制約はない。
セッション1:
begin;
INSERT INTO comments (user_id, comment) VALUES ('cccccccc-cccc-cccc-cccc-cccccccccccc', 'aaaaaaa');
select * from comments;
(1 row)
セッション2:
select * from comments;
(0 row) /* コミットされていないためまだ見えない */
INSERT INTO comments (user_id, comment) VALUES ('cccccccc-cccc-cccc-cccc-cccccccccccc', 'aaaaaaa');
select * from comments;
(1 row)
待機状態にはならない。制約がなければロック競合にはならないようだ。pg_locksを確認すると、commentsでRowExclusiveLockがかかっている。
セッション1:
select * from comments;
...
(2 rows) // ファントムリード
SELECT FOR UPDATE文、unique keyを更新するDELETE/UPDATE文SELECT FOR NO KEY UPDATE文、unique keyを更新しないDELETE/UPDATE文SELECT FOR SHARE文SELECT FOR KEY SHARE文/*
これはエラーになる。(FOR UPDATEは集約関数と同時に利用できない)
SELECT COUNT(*) FROM comment_favorites WHERE user_id = 'dddddddd-dddd-dddd-dddd-dddddddddddd' FOR UPDATE;
*/
以下はBEGIN内で実行(単なる実行だと即時終了して確認できないため)。
単なるSELECT文を実行:
select * from users where id = '10101010-1010-1010-1010-101010101010'
pg_locksを確認すると、relationに対するAccessShareLock(ACCESS EXCLUSIVEロックモードとのみ競合)が確認できる。
「FOR UPDATE(FOR NO KEY UPDATE)」「FOR SHARE」を実行してみる:
select * from users where id = '10101010-1010-1010-1010-101010101010' for update;
select * from users where id = '10101010-1010-1010-1010-101010101010' for share;
update users set name='taro' where id = '10101010-1010-1010-1010-101010101010';
pg_locksを確認すると、relationに対するRowShareLock(EXCLUSIVEおよびACCESS EXCLUSIVEロックモードと競合)が確認できる。
別のpidからリソースを取得しようとしてみる:
select * from users where id = '10101010-1010-1010-1010-101010101010' for share nowait;
-> could not obtain lock on row in relation "users"
元pidの方の処理がFOR SHAREのSQLの場合はエラーにならない。
MySQLは、インデックスが貼られていない列に対してのWHEREはテーブルロックとして共有ロックや排他ロックがかかってしまう(詳細はロック・排他制御(MySQL)参照)。
PostgreSQLは大丈夫そう(?)。
begin;
select * from users where name = 'jiro' for share;
(他のpidで)
update users set name='taro' where name='jiro'; # これは当然待機状態になる
update users set name='taro' where name='saburo'; # これは即時完了
以下は例。
/* トランザクションでロック */
BEGIN;
/* (X) */SELECT * FROM comment_favorites WHERE user_id = 'dddddddd-dddd-dddd-dddd-dddddddddddd' FOR UPDATE;
ROLLBACK;
/* (X)の直後のタイミングで別のセッション(コネクション)で以下を実行する */
/* insert: これはエラーにならない */
insert into comment_favorites (user_id, from_user_id, comment, comment_id) values ('dddddddd-dddd-dddd-dddd-dddddddddddd', 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', 'test', 'ffffffff-ffff-ffff-ffff-ffffffffffff');
/* update: これはロック待ちとなる */
UPDATE comment_favorites SET comment='' WHERE user_id = 'dddddddd-dddd-dddd-dddd-dddddddddddd';
/* select for update: これは即時エラーとなる */
SELECT * FROM comment_favorites WHERE user_id = 'dddddddd-dddd-dddd-dddd-dddddddddddd' FOR UPDATE NOWAIT;
上記の通り、ロッキングリードでは「まだ存在しない行」はロックできない。そもそもINSERTは一意インデックス以外ではブロックされない。
一意インデックスのないテーブルへのINSERTは同時実行中の処理によりブロックされることはありません。
検証:
/* トランザクションでロック */
begin;
/* (X) */SELECT pg_try_advisory_xact_lock(hashtext('comments_dddddddd-dddd-dddd-dddd-dddddddddddd'));
rollback;
/* (X)の直後のタイミングで別のセッションで実行 */
SELECT pg_try_advisory_xact_lock(hashtext('comments_33333'));/* これは通る */
SELECT pg_try_advisory_xact_lock(hashtext('comments_dddddddd-dddd-dddd-dddd-dddddddddddd'));/* これは通らない */
例えば、「同じuser_idのレコードはXX個まで登録可能」とする仕様の場合を考える。
numberとする)を追加する。user_id, number)を作成する。user_idの全データを取得して、numberについて1〜XXの範囲で空きのあるものがあればinsertする。ない場合はエラーとする。user_id, numberに対してinsertが発生しても、ユニーク制約によってエラーとなる。デメリットは実装が面倒であること。
BEGIN; SELECT * FROM user_setting WHERE xxx = 1; を実行した(ACCESS SHARE)。LOCK TABLE user_setting を行う既存処理が存在した(つまりACCESS EXCLUSIVE)。この処理が手作業によるロック解除待ちになった。このロック解除待ちによって、他のSELECT処理も待機になる。LOCK TABLEで、多数のスレッドがSELECTで止まってしまい、データベースとのコネクションプールが枯渇。システムダウンした。副構文によって異なるが、ほとんどはテーブルに対してACCESS EXCLUSIVEを取得する。
要求されるロックレベルはそれぞれの副構文によって異なることに注意してください。特に記述がなければACCESS EXCLUSIVEロックを取得します。
自身が明示的にロックした行以外に対しては悲観ロックはかからない。取得する行について、分離レベルに応じてファントムリードが発生するかどうかは変わる。
begin;
select * from users where uid in ('sample_uid_a') for update;
(0 rows)
ここで、別のpidでinsertする。
INSERT INTO "users" ("uid") VALUES ('sample_uid_a');
select * from users where uid in ('sample_uid_a') for update;
(1 row) // ファントムリード
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
select * from users where uid in ('sample_uid_a') for update;
(0 rows)
ここで、別のpidでinsertする(※ ここはロックされない。MySQLと違ってギャップロックがない感じ(?))。
INSERT INTO "users" ("uid") VALUES ('sample_uid_a');
select * from users where uid in ('sample_uid_a') for update;
(0 rows) // ファントムリードは起きない
begin;
select * from users where uid in ('sample_uid_a') for update;
別pid:
delete from users where uid in ('sample_uid_a'); // 待機になる
begin;
BEGIN
select * from users where uid in ('sample_uid_0001', 'sample_uid_0002') for update LIMIT 1;
...
(1 row)
別のpidで以下を実行する。
select * from users where uid in ('sample_uid_0001') for share nowait;
ERROR: could not obtain lock on row in relation "users"
select * from users where uid in ('sample_uid_0002') for update nowait;
...
(1 row) // 取得できる
SERIALIZABLEでも結果は同じ。
したがって、CREATEやALTERなどもロールバックされる。TRUNCATEもロールバックされるようだが、副作用等がどのように働くのかは未確認(TODO)。
OracleやMySQLではロールバックされない。