ロック・同時実行制御 - RDB(PostgreSQL)

最終更新日: 2026-08-25

TOP(About this memo)) > 一覧(RDB) > ロック・同時実行制御(PostgreSQL)

ロック・同時実行制御(PostgreSQL)

SQLやロックの確認

接続中クライアントの確認:

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の各項目内容については以下を参照。

pg_locksで確認できる情報について

このテーブルには「テーブルロック」と「未獲得の行ロック」しか含まれない。したがって、

サーバーログへのロック内容の出力

サーバーログにロック内容や待機状態も出力される(?)。

トランザクションの分離レベル

分離レベルの構文:

SHOW TRANSACTION ISOLATION LEVEL

トランザクション開始コマンドと一緒に使う:

BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;

MVCC

MVCCの詳細はトランザクション分離レベルを参照。

PostgreSQLコマンドではロックを自動的に取得する

ほとんどのPostgreSQLコマンドでは自動的にロックを取得するので注意が必要。

NOWAIT

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するにはどうすればよいか

先に SELECT FOR UPDATE NOWAIT でロックをかけてから、その後各処理をすればよさそう(?)。

明示的なロック取得

LOCK コマンドを使用する。

LOCK TABLE user_setting;

モードの指定がないため、もっとも強力なACCESS EXCLUSIVEを取得する。

insertによるロックとユニーク制約の検証

RowExclusiveLockはかかるが、競合しない限りロック待ちは発生しない。

パターン1: ユニーク制約のあるテーブルへinsert

前提として usersuid でユニーク。

セッション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) // ファントムリード

ロックモード

ロッキングリードは集約関数と同時に利用できない

/*
これはエラーになる。(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は一意インデックス以外ではブロックされない。

一意インデックスのないテーブルへのINSERTは同時実行中の処理によりブロックされることはありません。

方法1: 勧告的ロック用関数を利用する

検証:

/* トランザクションでロック  */
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'));/* これは通らない */

方法2: unique制約を利用する

例えば、「同じuser_idのレコードはXX個まで登録可能」とする仕様の場合を考える。

デメリットは実装が面倒であること。

ロックによる障害の例

ALTER TABLEによるロック

副構文によって異なるが、ほとんどはテーブルに対してACCESS EXCLUSIVEを取得する。

要求されるロックレベルはそれぞれの副構文によって異なることに注意してください。特に記述がなければACCESS EXCLUSIVEロックを取得します。

(検証)分離レベルとロックの範囲

自身が明示的にロックした行以外に対しては悲観ロックはかからない。取得する行について、分離レベルに応じてファントムリードが発生するかどうかは変わる。

READ COMMITTEDで、ファントムリードが起きることを確認

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) // ファントムリード

SERIALIZABLEでファントムリードが起きないことを確認

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'); // 待機になる

明示的にロックしていない行はロックされないことを確認(LIMITのケース)

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でも結果は同じ。

PostgreSQLはDDLもロールバックの対象

したがって、CREATEやALTERなどもロールバックされる。TRUNCATEもロールバックされるようだが、副作用等がどのように働くのかは未確認(TODO)。

OracleやMySQLではロールバックされない。