PostgreSQL 入門

PostgreSQL ロック機構

ロック機構(Locking mechanisms)は、データベースシステムにおけるデータへの並行アクセス(Concurrent access)を管理するための鍵となります。複数のトランザクションが同時に実行される際、データの整合性(Integrity)と一貫性(Consistency)を保証します。適切な並行性制御(Concurrency control)がなければ、トランザクション間で干渉が発生し、データの破損や不整合な結果を招く可能性があります。

本章では、PostgreSQLで使用可能な異なるタイプのロックと、それらがマルチユーザー環境でどのようにデータ整合性の維持に役立つかを包括的に紹介します。各種ロックのタイプ、それらの互換性(Compatibility)、そしてトランザクションの実行動作にどのような影響を与えるかを探っていきましょう。

1. PostgreSQLにおけるロックの理解

ロックは、PostgreSQLの並行性制御における基礎となる要素です。これらは、複数のトランザクションがデータの不整合を引き起こす可能性のある方法で同じデータにアクセスするのを防ぐための防御メカニズムです。

あるトランザクションが特定のデータベースオブジェクト(テーブル、ロウ、ページなど)のロックを取得すると、そのオブジェクトにアクセスしようとする他のトランザクションは、保持されているロックのタイプや実行しようとしている操作に応じて、ブロック(待機状態)されることがあります。

2. ロックのタイプ

PostgreSQLは多種多様なロックタイプを提供しており、それぞれの厳格さが異なります。並行処理を効率的に処理できるアプリケーションを設計するには、これらのロックタイプを理解することが不可欠です。以下に主要なロックタイプの詳細を解説します。

  • ACCESS SHARE(アクセスシェア): 最も制限の少ないロックレベルです。複数のトランザクションが同時に同じテーブルに対してアクセスシェアロックを保持できます。通常、テーブルデータを読み取る際(SELECT文の実行中など)に取得されます。これはアクセス排他(ACCESS EXCLUSIVE)ロックのみと競合します。
  • ROW SHARE(ロウシェア): SELECT ... FOR UPDATE/SHARE を使用して更新のために行(Row)を選択する際に取得されます。並行読み取りを許可しますが、他のトランザクションが排他(EXCLUSIVE)またはアクセス排他(ACCESS EXCLUSIVE)ロックを取得するのをブロックし、他の並行する SELECT ... FOR UPDATE/SHARE が同じ行をロックすることも防ぎます。
  • ROW EXCLUSIVE(ロウ排他): ロウシェアに似ていますが、同じ行に対して他のトランザクションがロウシェアまたはロウ排他ロックを取得するのもブロックします。更新(UPDATE)、挿入(INSERT)、削除(DELETE)操作を実行する際に自動的に取得されます。
  • SHARE UPDATE EXCLUSIVE(共有更新排他): 並行するテーブル構造の変更(Schema changes)や VACUUM FULL 操作を防ぐために使用されます。共有(SHARE)ロックよりも制限は少ないですが、書き込み(WRITE)アクセスをブロックします。
  • SHARE(共有): 並行読み取りを許可しますが、書き込みをブロックします。通常、インデックスの並行作成(Concurrent index creation)などの操作中に取得されます。
  • SHARE ROW EXCLUSIVE(共有ロウ排他): 並行読み取りを許可しますが、他のトランザクションによるテーブルの変更を禁止します。このロックモードはテーブルを並行データ変更から保護し、自己排他的(Self-exclusive)であるため、同時に1つのセッションしか保持できません。
  • EXCLUSIVE(排他): 1つのトランザクションのみがテーブルにアクセスすることを許可します。通常、REFRESH MATERIALIZED VIEW CONCURRENTLY(マテリアライズドビューの並行リフレッシュ)などの操作中に取得されます。他のトランザクションの読み書き操作をブロックします。
  • ACCESS EXCLUSIVE(アクセス排他): 最も制限の厳しいロックレベルです。他のすべてのトランザクションがいかなる方法でもそのテーブルにアクセスするのを阻止します。通常、テーブルの削除(DROP TABLE)やテーブル全体の書き換え操作を実行する際に取得されます。

3. ロックの互換性マトリックス

異なるロックタイプ間の互換性は、データベースがどのように並行性を管理するかを理解する上での鍵です。ロック互換性マトリックスは、どのロックタイプが同じリソース上に同時に存在できるかを示しています。以下は簡略化されたマトリックスです。

リクエストされたロック / 現在保持されているロックACCESS SHAREROW SHAREROW EXCLUSIVESHARE UPDATE EXCLUSIVESHARESHARE ROW EXCLUSIVEEXCLUSIVEACCESS EXCLUSIVE
ACCESS SHAREYesYesYesYesYesYesNoNo
ROW SHAREYesYesNoYesYesNoNoNo
ROW EXCLUSIVEYesNoNoNoNoNoNoNo
SHARE UPDATE EXCLUSIVEYesYesNoNoNoNoNoNo
SHAREYesYesNoNoNoNoNoNo
SHARE ROW EXCLUSIVEYesNoNoNoNoNoNoNo
EXCLUSIVENoNoNoNoNoNoNoNo
ACCESS EXCLUSIVENoNoNoNoNoNoNoNo
  • Yes: これらのロックタイプは互換性があり、同じリソース上で同時に保持できます。
  • No: これらのロックタイプは互換性がなく、2番目のロックタイプをリクエストしたトランザクションは、最初のロックが解放されるまでブロックされます。

4. 暗黙的ロック vs. 明示的ロック

PostgreSQLは、暗黙的および明示的なロックメカニズムを併用します。

  • 暗黙的ロック (Implicit Locking): データベースシステムが実行中のSQLコマンドに基づいて自動的にロックを取得します。例えば、データを1行更新すると、PostgreSQLはその行に対して自動的にロウ排他(ROW EXCLUSIVE)ロックを取得します。
  • 明示的ロック (Explicit Locking): LOCK TABLE コマンドを使用して手動でロックを取得できます。これにより、ロック動作をより精密に制御できます。
LOCK TABLE my_table IN SHARE MODE;

このステートメントは my_table テーブルに対して共有(SHARE)ロックを取得し、そのロックを保持している間、他のトランザクションがこのテーブルを変更するのを防ぎます。

4.1 ロックの持続時間

ロックの保持時間は、トランザクションの分離レベル(Isolation level)および実行中の操作タイプに依存します。通常、トランザクション中に取得されたロックは、そのトランザクションがコミット(COMMIT)またはロールバック(ROLLBACK)されるまで保持され続けます。

5. ロックの実戦例

実際の例を通じて、PostgreSQLのロックがどのように機能するかをデモンストレーションします。

5.1 例 1:並行更新

以下のような構造の accounts テーブルがあると仮定します。

CREATE TABLE accounts (
    id SERIAL PRIMARY KEY,
    balance DECIMAL(15, 2) NOT NULL
);

INSERT INTO accounts (balance) VALUES (1000.00), (500.00);

今、2つのトランザクションが同時に同じアカウントを更新しようとしている状況を考えます。

トランザクション 1:

BEGIN;
UPDATE accounts SET balance = balance - 100.00 WHERE id = 1;
-- ... その他の操作 ...
COMMIT;

トランザクション 2:

BEGIN;
UPDATE accounts SET balance = balance + 50.00 WHERE id = 1;
-- ... その他の操作 ...
COMMIT;

適切なロック機構がなければ、これら2つのトランザクションが互いに干渉し、「更新の紛失(Lost update)」を招く可能性があります。しかし、PostgreSQLは更新対象の行に対して自動的にロウ排他(ROW EXCLUSIVE)ロックを取得します。そのため、トランザクション1が先に開始された場合、トランザクション2はトランザクション1がコミットまたはロールバックされるまでブロック(待機)されます。これにより、最終的な残高計算の正確性が保証されます。

5.2 例 2:明示的ロックの使用

特定のテーブルに対して一連の操作を実行し、そのプロセス中に他のトランザクションがそのテーブルを変更するのを防ぎたいシナリオを考えます。明示的ロックを使用してこれを実現できます。

BEGIN;
LOCK TABLE accounts IN EXCLUSIVE MODE;
-- accounts テーブルに対して一連の操作を実行
UPDATE accounts SET balance = balance * 1.05; -- 利息の計算
-- ... その他の操作 ...
COMMIT;

この例では、LOCK TABLE コマンドが accounts テーブルに対して排他(EXCLUSIVE)ロックを取得し、現在のトランザクションが完了するまで、他のトランザクションによるテーブルへの読み書きを阻止します。

5.3 例 3:デッドロック(Deadlock)シナリオ

2つ以上のトランザクションが無期限にブロックされ、互いに相手が保持しているロックの解放を待っている状態をデッドロックと呼びます。

トランザクション 1:

BEGIN;
UPDATE accounts SET balance = balance - 100.00 WHERE id = 1;
UPDATE accounts SET balance = balance + 50.00 WHERE id = 2;
COMMIT;

トランザクション 2:

BEGIN;
UPDATE accounts SET balance = balance + 50.00 WHERE id = 2;
UPDATE accounts SET balance = balance - 100.00 WHERE id = 1;
COMMIT;

トランザクション1が id = 1 の行をロックし、同時にトランザクション2が id = 2 の行をロックしたとします。その後、各トランザクションが相手によって既にロックされている行のロックを取得しようとすると、デッドロックが発生します。しかし、PostgreSQLはデッドロックを自動的に検出し、デッドロックを解消するために強制的にどちらか一方のトランザクションをロールバックさせます。