
PostgreSQL PWI - 2. LOCK > Relation-level Lock
目次[非表示]
PWI(PostgreSQL Wait Interface) - LOCK
PostgreSQLのWait Eventを扱う前に、まずPostgreSQLで使用されるLockの全般的な仕組みについて説明します。
PostgreSQLは、Relationといったオブジェクトを保護する Heavyweight Lock(HWLock)、その一種として実装されている Row-level Lock、そして共有メモリのデータ構造へのアクセスに利用される Lightweight Lock(LWLock)など、さまざまなタイプのLockを提供しています。
本記事(PWI-LOCK)では、PostgreSQLにおけるLockの種類や特徴、動作の仕組みを整理し、実際の使用例を通じてLockの獲得・解除の流れを確認していきます。
Relation-level Lock
PostgreSQLでは、データベースオブジェクトのうち、テーブルのような構造を持つものを Relationと呼びます。代表的なものとして、テーブル、インデックス、ビュー、シーケンスがあります。
Relation-level Lock(リレーションレベルロック)は、同時アップデートからRelationを保護し、またRelationの構造を変更している間はそのRelationへのアクセスを禁止する役割を持ちます。
Relation-level Lockは Heavyweight Lockの一種で、PostgreSQL内部のメモリ空間に保持され、Lock Managerによって管理されます。そのため、ビュー pg_locksで locktype ='relation'
として確認することができます。
Lock Mode
Relation-level Lock には、 Access Share, Row Share, Row Exclusive, Share Update Exclusive, Share, Share Row Exclusive, Exclusive, Access Exclusiveの8種類のモードが用意されています。
このように複数のモードが存在するのは、Relationに対して実行可能な同時コマンド数を最大化するためです。
Lock Modeは、実行されるSQLコマンドによって選択されます。また、あるLock Modeが他のモードと同時に使用可能かどうか、すなわち互換性を確認するには、以下の表を参照する必要があります。
Locking Mode | Access Share | Row Share | Row Exclusive | Share Update Exclusive | Share | Share Row Exclusive | Exclusive | Access Exclusive | SQL Commands |
---|---|---|---|---|---|---|---|---|---|
Access Share | X | SELECT | |||||||
Row Share | X | SELECT FOR UPDATE/SHARE | |||||||
Row Exclusive | X | X | X | X | INSERT, UPDATE, DELETE | ||||
Share Update Exclusive | X | X | X | X | X | X | VACUUM, ALTER TABLE(1), CREATE INDEX CONCURRENTLY | ||
Share | X | X | X | X | X | CREATE INDEX | |||
Share Row Exclusive | X | X | X | X | X | X | CREATE TRIGGER, ALTER TABLE(2) | ||
Exclusive | X | X | X | X | X | X | X | REFRESH MATERIALIZED VIEW CONCURRENTLY | |
Access Exclusive | X | X | X | X | X | X | X | X | DROP, TRUNCATE, VACUUM FULL, LOCK TABLE, ALTER TABLE(3), REFRESH MATERIALIZED VIEW |
ALTER TABLE
コマンドにはさまざまな種類があり、その内容に応じて必要となるロックレベルが異なります。
上の表を見ると、3つの異なるロックモードに対して ALTER TABLE
が表示されています。
具体的な ALTER TABLE
のコマンド例は以下のとおりです。
ALTER TABLE(1)
ALTER TABLE VALIDATE CONSTRAINT
ALTER TABLE SET WITHOUT CLUSTER
ALTER TABLE SET TOAST
ALTER TABLE SET STATISTICS
ALTER TABLE SET N_DISTINCT
ALTER TABLE SET FILLFACTOR
ALTER TABLE SET AUTOVACUUUM
ALTER TABLE DETACH PARTITION CONCURRENTLY (PARENT)
ALTER TABLE CLUSTER ON
ALTER TABLE ATTACH PARTITION (PARENT)
ALTER INDEX (RENAME)
ALTER TABLE(2)
ALTER TABLE ENABLE/DISABLE TRIGGER
ALTER TABLE ADD FOREIGN KEY NOT VALID (PARENT)
ALTER TABLE ADD FOREIGN KEY NOT VALID (CHILD)
ALTER TABLE ADD FOREIGN KEY (CHILD)
ALTER TABLE(3)
ALTER TABLE SET/DROP DEFAULT
ALTER TABLE SET TABLESPACE
ALTER TABLE SET STORAGE
ALTER TABLE SET SEQUENCE
ALTER TABLE SET DATA TYPE
ALTER TABLE SET COMPRESSION
ALTER TABLE RESET STORAGE
ALTER TABLE RENAME
ALTER TABLE INHERIT PARENT
ALTER TABLE ENABLE/DISABLE RULE
ALTER TABLE ENABLE/DISABLE ROW LEVEL SECURITY
ALTER TABLE DROP EXPRESSION
ALTER TABLE DROP CONSTRAINT
ALTER TABLE DROP COLUMN
ALTER TABLE DETACH PARTITION (PARENT)
ALTER TABLE DETACH PARTITION (TARGET/DEFAULT)
ALTER TABLE DETACH PARTITION CONCURRENTLY (TARGET/DEFAULT)
ALTER TABLE ATTACH PARTITION (TARGET/DEFAULT)
ALTER TABLE ALTER CONSTRAINT
ALTER TABLE ADD COLUMN
ALTER TABLE ADD CONSTRAINT
ALTER INDEX SET TABLESPACE
ALTER INDEX SET FILLFACTOR
ALTER INDEX ATTACH PARTITION
Access Shareモードは他のモードとの互換性が最も広く、
Access Exclusiveは自分自身を含め、すべてのモードと競合します。
(つまり、SELECT
はほとんどのコマンドと同時に実行できますが、DROP
やTRUNCATE
のようにデータ構造自体を変更するコマンドは、VACUUM
のようなシステムバックグラウンド作業やSELECT
のような軽い読み取り処理とも同時実行できません。)表の上位4つのモード(Access Share, Row Share, Row Exclusive, Share Update Exclusive)は、テーブルデータの同時変更を許可しますが、下位4つのモード(Share, Share Row Exclusive, Exclusive, Access Exclusive)は許可しません。
一般的にインデックス作成(
CREATE INDEX
)は Shareモードを使用します。このモードはデータ変更作業と同時に実行できないため、インデックス作成と他の更新作業は並行できず、作業間で同時実行の制約を受けます。
📝 Create IndexCREATE INDEX Commandを使用した場合、①Relationに対して
ShareLock
モードでLockを獲得し、②他のプロセスで実行したUPDATE Commandは同じRelationに対してRowExclusiveLock
モードでLockを要請しましたが、 'granted'値がfalse
で、Lock獲得に失敗したことが確認できます。しかし、上記のように①CONCURRENTLYオプションを追加して実行すると、①
ExclusiveLock
モードのLockを獲得するように動作し、②SELECT Commandを実行したプロセスも当該Relationに対してLockを獲得したことが確認できます。 ( 'granted'=true
)
- REFRESH MATERIALIZED VIEW Commandもオプションによって異なるモードが選択できます。 一般的にMATERIALIZED VIEWをREFRESHする作業は、 'Access Exclusive'モードが選択されるため、読み取り作業と互換性がありませんが、CONCURRENTLYオプションを使用すると、「Exclusive」モードが選択され、読み取り作業との互換性を持ちます。
📝 Refreshing MVREFRESH MATERIALIZED VIEW Commandを使用すると、①Relationに対して 'Access Exclusive Lock'モードでLockを獲得するので、②他のプロセスで同じRelationに対して実行した単純なSELECT CommandもLockを獲得できずに失敗したことが分かります。 ( 'granted'=
false
)しかし、上記のように①CONCURRENTLYオプションを追加して実行すると、①
ExclusiveLock
モードのLockを獲得するように動作し、②SELECT Commandを実行したプロセスも当該Relationに対してLockを獲得したことが確認できます。 ( 'granted'=true
)
Locks on Transaction IDs
次の例では、Relation Lockの確認のために照会した pg_locksの結果のうち、 locktypeの値が transactionid
と virtualxid
と表示されることが確認できます。
それぞれはTransactionIDとVirtual TransactionIDに対するLockで、これらはHeavyweight Lockに該当し、 "Exclusive'と 'Share'の2つのロックモードを提供します。
すべてのトランザクションは、Virtual Transaction IDに対するLockを "Exclusive'モードで取得し、トランザクション終了時まで維持します。 そしてTransactionIDが割り当てられた場合(一般的にトランザクションがデータベースの状態を変更する場合)、TransactionIDについても 'Exclusive'モードでロックを獲得します。
これとは別に、あるトランザクションが別のトランザクションの終了まで待たなければならないと判断すると、別のトランザクションのID(状況に応じてVirtual TransactionIDまたはTransactionID)に対するロックを 'Share'モードで獲得しようと試みます。 もちろん、当該トランザクションにより既に 'Exclusive'モードでLockを獲得している状態ですので、トランザクションが終了するまで獲得に失敗し、Sleep状態で待機します。

上記の結果を見ると、① pidが 1162505
のプロセスが実行したトランザクションのIDが 1772
であり、当該 transactionid
に対して ExclusiveLock
モードでLockを獲得しました。 そして、② pidが 114406
の別のプロセスが、 transactionid
が 1772
に対するLockを sharedLockモードで要請したが、獲得に失敗し、Sleep状態で待機中であることが確認できます。 ( granted= false
)
その後、先行トランザクションが終了すると、当該 transactionid
に対する Exclusive Lock
モードのLockが解除されるので、Sleep状態のプロセスがLockを獲得できるようになり、この過程で先行トランザクションの終了を知ることができます。
このように、 トランザクションID(Virtual Transaction ID、Transaction ID)に対するロックは、トランザクションの開始/終了時点の追跡を目的に使用することができます。
📝Virtual TransactionIDと TransactionID
PostgreSQLは、トランザクションを識別するためにVirtual Transaction IDとTransaction IDを使用します。 違いはLocalとGlobalにあります。
Virtual TransactionID
Virtual Transaction IDは、トランザクションが開始する時点ですぐに割り当てられ、Backend IDとLocal XIDで構成されます。 例の9/294
は、Backend IDは9、Local XID 294を意味します。(ここで、Backend IDはオペレーティングシステムのプロセスIDではなく、PostgreSQLでプロセスを識別するために使用するIDであり、Local XIDは各Backendに順次付与されたLocal IDです。)この値は、ディスクに保存されることなくメモリにのみ存在します。 そのため、MVCCに使用されず、Vacuumの対象にならないため、Vacuum作業と関連がないという特徴があります。TransactionID
トランザクションが最初の書き込み操作を行う時点、つまりデータの修正が必要な場合にのみ使用します。 PostgreSQL Cluster内に存在するGlobal Counterによって順次付与されたIDで、データベース変更の手順を示します。 この値は、ディスクに保存され、永続的な値です。
一部のトランザクションは読み取り専用であるか、空のトランザクションのようにデータベースの変更を引き起こさないため、Transaction ID を使用することは無駄になる可能性があります。 このため、データベースに変化を与えないトランザクションについては、Virtual Transaction ID のみ割り当て、Transaction ID には割り当てません。
そして、一般的にVirtual Transaction IDは、オブジェクトを一意に識別するために使用します。 Virtual Transaction IDがメモリで管理されるため、ロック競合発生時により効率的に処理できるからです。次のように、Lockを獲得または待機しているトランザクションのVirtual Transaction IDを pg_locksの Virtual Transactionカラムを通じて提供します。pg_locksで locktypeに表示される
virtualxid
は、ロックの対象となるObjectがvirtualTransactionIDであることを意味し、 virtualtransactionカラムはそれぞれのロックを保有したり、待機中のtransactionのvirtualtransactionID値(ex.9/294
)を表記します。
Lock Contention
Relation Lockに対する理解をもとに、実際に複数のトランザクションが発生する状況でRelation Lockの動作過程を見てみましょう。
【CASE 1】ロックモード間の互換性がない場合
二つのトランザクションで同じRelationに対して互換性のないLock Modeを選択するCommandを同時に実行したとき、どのように処理されるのか、以下のテストを通じて見てみましょう。
まずトランザクション1を開始し、次のようにトランザクションとプロセスIDを確認した後、UPDATE Command を実行します。
-- トランザクション1開始
BEGIN;
-- PID, transactionID 確認
SELECT pg_backend_pid(), txid_current();
pg_backend_pid | txid_current
----------------+----------------
2487936 | 1572
-- UPDATE実行
UPDATE lock_test SET c3 = c3 + 100.00 WHERE c1 = 1;

トランザクション1を開始したプロセス 248936
が持っているLockに対する結果は、 pg_locksを照会して確認することができます。
Locks on Transaction IDs で説明したように、①トランザクション1 のトランザクションID に対し、 Exclusive Lock
モードでLock ( transactionid
, virtualxid
) を獲得します。 そして、②UPDATEの対象Relationである lock_test
については、 RowExclusiveLock
モードでLock獲得に成功したことが分かります。 (**granted **= true
)
続いて、別のプロセスでトランザクション 2 を開始し、同じテーブルにインデックスを作成します。
-- トランザクション2開始
BEGIN;
-- PID, transactionID 確認
SELECT pg_backend_pid(), txid_current();
pg_backend_pid | txid_current
----------------+--------------
2487939 | 1573
-- CREATE INDEX実行
CREATE INDEX lock_idx ON lock_test (c2);
上記のように pg_locks結果を照会してみると、①トランザクション2も自分のトランザクションIDに対するLockを獲得した状態です。 ②また、インデックス生成対象のRelationである
lock_test
に対して ShareLock
モードでLockを要請しましたが、該当Lockは granted値が false
であることから獲得に失敗したことが分かります。 ③これは、 lock_test
に対してトランザクション1が RowExclusiveLock
でLockを既に獲得しているためであり、 RowExclusiveLock
と ShareLock
は相互互換性がないため、トランザクション2はトランザクション1終了時まで待機することになります。
【CASE 2】ロックモード間で互換性のある場合
次のケースでは、二つのトランザクションが要求するRelationのLock Modeが互換性がある場合の例を見てみましょう。
-- トランザクション1開始
BEGIN;
-- PID, transactionID 確認
SELECT pg_backend_pid(), txid_current();
pg_backend_pid | txid_current
----------------+--------------
2493574 | 1574
-- 2. UPDATE 実行
=> UPDATE lock_test SET c3 = c3 + 100.00 WHERE c1 = 1;
トランザクション1では、[CASE 1]と同じUPDATE Commandを実行しました。 pg_locksを照会すると、[CASE1]と同様に①トランザクション1のトランザクションIDについて、ExclusiveLockモードでLock(transactionid、virtualxid)を獲得し、②UPDATEの対象Relationであるlock_testについてはRowExclusiveLockモードでLock獲得に成功したことが分かります。 ( granted= true)
そして、トランザクション2では、[CASE 1]とは異なり、SELECT FOR UPDATE Commandを実行し、 pg_locksの結果を確認します。
-- トランザクション2開始
BEGIN;
-- PID, transactionID 確認
SELECT pg_backend_pid(), txid_current();
pg_backend_pid | txid_current
----------------+--------------
2493575 | 1575
-- 2. SELECT FOR UPDATE 実行
=> SELECT * FROM lock_test WHERE c1 = 1 FOR UPDATE;
結果を確認してみると、①トランザクション2も自分のトランザクションIDに対するロックを獲得した状態です。 しかし、[CASE 1]とは異なり、②トランザクション2を開始したプロセス2492575は、Relation lock_testに対してRowShare LockモードでRelation Lock獲得に成功したことが分かります。 ( granted=true)これは同じRelationに対する要請ですが、RowShareLockモードが③トランザクション1がlock_testに対して獲得したRowExclusiveLockモードと同時作業が可能な互換性のあるモードだからです。
📝 上記の pg_locks結果から *7番目と9番目のRowに表示されたtransactionid( lockid:1574)とtupleに対するLockは、後で説明するRow-level Lockと関連します。
まとめ
今回は、PostgreSQLのLock、その中でもRelation-levelのLockについて解説しました。内容を整理すると、以下のようになります。
- RelationはDatabase Objectのうち、テーブルのような構造を持つObjectを指す。
- Relation-level Lockは、同時アップデートからRelationを保護し、Relationの構造を調整する間、当該Relationに対する使用を禁止する役割をする。
- Relation Lock은 Access Share, Row Share, Row Exclusive, Share Update Exclusive, Share, Share Row Exclusive, Exclusive, Access Exclusiveの 8つのモードを提供する。
- トランザクションを識別するためにVirtual Transaction ID と Transaction ID を使用し、トランザクションの開始と終了時点の追跡を目的としてトランザクション ID (Virtual Transaction ID, Transaction ID) に対するLock を使用する。
次回は、より細かな制御を行うRow-level Lockについて詳しく解説していきますので、ぜひご覧ください。
※本記事は、以下のブログ記事を日本語向けに翻訳・再構成したものです:
🔗 DB インサイト | PWI - LOCK > Relation-level Lock (EXEM本社ブログ)