先日、行ロック待ちについて調査した際にpg_locksを確認しましたが、表示内容と内部動作の対応関係を十分に理解できていないことに気づきました。
本記事では、調査・整理した内容をもとに、行ロック待ちの内部動作とpg_locksの表示の対応関係を解説します。
実行環境
まず、調査した内容を再現するために、dockerでPostgreSQL環境を構築しました。
- PostgreSQLバージョン:16.12
- テーブル・データ作成
-- idを持つテーブルを作成
CREATE TABLE sample_table (
id SERIAL PRIMARY KEY,
value TEXT
);
-- テスト用データ投入
INSERT INTO sample_table (value) VALUES ('A'), ('B'), ('C');
pg_locks確認SQL
手順実施毎にpg_locksを確認するSQLを実行します。
SELECT
pl.pid, --プロセス単位で割り振られるid
pl.locktype, --何の単位に対してロックしているか
pc.relname, --テーブル、インデックス、ビューなどの名前
pl.relation, --ロックの対象となるリレーションのOID
pl.transactionid, --ロックの対象となるトランザクションID。(行ロック待機状態の追跡に使用)
pl.mode, --ロックモードの名称
pl.granted, --trueの場合はロックが保持されている。falseの場合はロックが待ち状態。
psa.state, --バックエンドの総体的な状態
psa.application_name --バックエンドに接続したアプリケーションの名前
FROM
pg_locks pl
LEFT JOIN pg_stat_activity psa ON pl.pid = psa.pid
LEFT JOIN pg_class pc ON pl.relation = pc.oid
WHERE
pl.mode <> 'AccessShareLock' --読み取り用の共有ロックは除外
and pl.pid <> pg_backend_pid() --このクエリのpidは除外
ORDER BY pid;
用語の補足
| 用語 | 説明 |
|---|---|
| リレーション | テーブル、ビュー、シーケンスなどのデータベースオブジェクトの総称 |
| OID | PostgreSQLが内部的にオブジェクト(テーブル、ビュー、関数など)に割り振る一意の番号。 つまり、sample_tableやsample_tableのprimary keyに対してもOIDが内部的に設定されている。 |
| トランザクションID | トランザクション開始~コミット/ロールバックまで一意のID。 トランザクションの整合性管理に使われる。 |
| バックエンド | PostgreSQLではSQLを実行するためにセッション単位で専用プロセス(=バックエンド)を起動している。 |
手順
1. transaction_1で行ロックを取得
特定の行に対してselect … for updateで排他ロックを取得します。
BEGIN;
SET application_name = 'transaction_1';--application_nameを設定
SELECT * FROM sample_table WHERE id = 1 FOR UPDATE;
その後、pg_locks確認SQLを実行した結果:
pid | locktype | relname | relation | transactionid | mode | granted | state | application_name
-----|--------------|-------------------|----------|---------------|---------------|---------|----------------------|-----------------
789 | relation | sample_table_pkey | 16404 | | RowShareLock | true | idle in transaction | transaction_1
789 | relation | sample_table | 16398 | | RowShareLock | true | idle in transaction | transaction_1
789 | virtualxid | | | | ExclusiveLock | true | idle in transaction | transaction_1
789 | transactionid| | | 751 | ExclusiveLock | true | idle in transaction | transaction_1
確認ポイント
-
locktype=relation
- select for updateによって取得されたリレーションに対するロック。
- sample_tableとsample_table_pkeyに対して、RowShareLockが付与されている。
- RowShareLock(行共有ロック)とは、「特定の行を読み取り専用でロックし、他の人がその行を更新・削除できないようにする」こと
-
locktype=virtualxid
- transaction_1自身の仮想IDに対するロック。
- 仮想IDとは、トランザクションを開始する前にセッション単位で割り振られるID。デッドロック検出やロック待ちの管理に使われる。
- 内部管理用なので、今回は特に気にしなくてOK。
-
locktype=transactionid
- transaction_1が獲得したトランザクションIDに対してのロック。
2. transaction_2から同じ行に対して行ロックを実行
別セッションを立ち上げ、transaction_1で行ロックした行に対してselect … for updateを実行します。
BEGIN;
SET application_name = 'transaction_2';--application_nameを設定
SELECT * FROM sample_table WHERE id = 1 FOR UPDATE;
pg_locks の確認結果:
pid | locktype | relname | relation | transactionid | mode | granted | state | application_name
-----|--------------|-------------------|----------|---------------|---------------------|---------|----------------------|-----------------
789 | relation | sample_table_pkey | 16404 | | RowShareLock | true | idle in transaction | transaction_1
789 | relation | sample_table | 16398 | | RowShareLock | true | idle in transaction | transaction_1
789 | virtualxid | | | | ExclusiveLock | true | idle in transaction | transaction_1
789 | transactionid| | | 751 | ExclusiveLock | true | idle in transaction | transaction_1
843 | relation | sample_table_pkey | 16404 | | RowShareLock | true | active | transaction_2
843 | relation | sample_table | 16398 | | RowShareLock | true | active | transaction_2
843 | tuple | sample_table | 16398 | | AccessExclusiveLock | true | active | transaction_2
843 | virtualxid | | | | ExclusiveLock | true | active | transaction_2
843 | transactionid| | | 751 | ShareLock | false | active | transaction_2
ポイント
postgreSQLは、行自体を待機するのではなく、その行を更新しているトランザクションを待つ仕組みになっている。
そのため、実際の待機状況はtransactionid行を確認するのがポイント。
relation/tuple行は、あくまでもロックしたい行に対しての意図を表している。
-
transaction_2のlocktype=relation/tuple
- transaction_1のrelationが入っている。
→どの行やテーブルを待機しているかを示している。 - tupleとは、PostgreSQL内部で行(レコード)を格納する単位のこと。
- transaction_1のrelationが入っている。
-
transaction_2のlocktype=tuple
- 対象行のロックにおいて、transaction_1のロックを待機するために作られる。
- タプルに対して、予約としてロックをかけているのではなく、ロック解放を待っている状態。
- タプルロックは「このタプルをロックしたい」という意図を表している。
-
transaction_2のlocktype=transactionid
- transaction_1のtransactionidが入っている。
→transaction_1の解放待ち状態であることを示す。 - 仕組み上、行自体が解放されるのを待っているのではなく、transaction_1が終了することで行が解放されるのを待っている。
- そのため、
granted=falseになっている。
- transaction_1のtransactionidが入っている。
3. transaction_1をコミット
COMMIT; --transaction_1をコミット
pg_locks の確認結果:
pid | locktype | relname | relation | transactionid | mode | granted | state | application_name
-----|--------------|-------------------|----------|---------------|----------------|---------|----------------------|-----------------
843 | relation | sample_table_pkey | 16404 | | RowShareLock | true | idle in transaction | transaction_2
843 | relation | sample_table | 16398 | | RowShareLock | true | idle in transaction | transaction_2
843 | virtualxid | | | | ExclusiveLock | true | idle in transaction | transaction_2
843 | transactionid| | | 752 | ExclusiveLock | true | idle in transaction | transaction_2
ポイント
-
transaction_2のlocktype=tuple
- transaction_2が行ロックを獲得したため、以前待機を表していたtupleロックが消えている。
-
transaction_2のlocktype=transactionid
-
granted=trueになり、transaction_2のトランザクションIDが割り当てられる。
→transaction_2が対象行のロックを保持している状態であることを示す。
-
まとめ
本記事では、行ロックや待機状態の挙動をpg_locksで追い、トランザクションがどのようにロックを管理しているかを確認しました。
pg_locksの見方や、行ロック挙動の理解の一助になれば幸いです。
