PostgreSQLにギャップロックはない!?MVCCと範囲ロックで理解する並行性制御

データベース・SQL

「PostgreSQLでギャップロックってどう使うの?」
「ファントムリードを防ぐにはどうすれば?」
「MySQLから移行したけど、ロックの動きが違う…」

こんな疑問を持っていませんか?

実は、PostgreSQLにはギャップロック(Gap Lock)という機能はありません。これはMySQLのInnoDBエンジン特有の機能なんです。

でも安心してください!PostgreSQLにはMVCC(多版型同時実行制御)という、もっと洗練された仕組みがあります。ギャップロックがなくても、ファントムリードをしっかり防げるんです。

この記事を読めば、PostgreSQLのロック機構を完全に理解でき、MySQLとの違いも明確になります!


スポンサーリンク

そもそもギャップロックって何?(MySQL視点)

MySQLのギャップロックとは

まず、MySQLのギャップロックについて簡単に説明します。PostgreSQLの仕組みを理解する上で、比較があると分かりやすいですから。

MySQLのギャップロック:

  • インデックスレコード間の「隙間」をロックする機能
  • ファントムリードを防ぐための仕組み
  • REPEATABLE READ分離レベルで自動的に使用

例(MySQLの場合):

-- MySQLでの動作例
BEGIN;
SELECT * FROM users WHERE age BETWEEN 20 AND 30 FOR UPDATE;
-- この時、20〜30の範囲だけでなく、その「隙間」もロックされる
-- 他のトランザクションは、age=25の新規レコードを挿入できない

PostgreSQLにギャップロックがない理由

PostgreSQLはMVCC(Multi-Version Concurrency Control)を採用しているため、ギャップロックは不要なんです。

PostgreSQLの哲学:

  • ロックを最小限に抑える
  • 読み取りは書き込みをブロックしない
  • 書き込みは読み取りをブロックしない

この設計により、パフォーマンスが向上し、デッドロックのリスクも減ります。


PostgreSQLのMVCCとスナップショット分離

MVCCの仕組み(これが重要!)

PostgreSQLは各トランザクションに「スナップショット」を提供します。

スナップショットの特徴:

  • トランザクション開始時点のデータベース状態を保持
  • 他のトランザクションの変更を見ない
  • 一貫性のあるデータビューを提供

実例で理解:

-- トランザクション1
BEGIN;
SELECT COUNT(*) FROM orders WHERE status = 'pending';
-- 結果: 10件

-- この間に別のトランザクションが5件追加しても...

SELECT COUNT(*) FROM orders WHERE status = 'pending';
-- 結果: まだ10件(スナップショットのおかげ)
COMMIT;

トランザクション分離レベルと動作

PostgreSQLの分離レベルごとの動作を理解しましょう。

READ COMMITTED(デフォルト):

BEGIN;
-- 各文の実行時に新しいスナップショットを取得
SELECT * FROM products WHERE price < 100;
-- 他のトランザクションがコミットした変更が見える

REPEATABLE READ:

BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- トランザクション開始時のスナップショットを使用
SELECT * FROM products WHERE price < 100;
-- 他のトランザクションがコミットしても見えない

SERIALIZABLE:

BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 最も厳格な分離レベル
-- 述語ロックを使用してファントムリードを完全防止

PostgreSQLの範囲ロックと述語ロック

述語ロック(Predicate Lock)の仕組み

SERIALIZABLE分離レベルでは、PostgreSQLは述語ロックを使用します。これがMySQLのギャップロックに最も近い機能です。

述語ロックの動作:

-- トランザクション1
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT * FROM accounts WHERE balance > 1000;

-- トランザクション2
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
INSERT INTO accounts (id, balance) VALUES (100, 1500);
-- これは競合する可能性がある!

競合検出の仕組み:

  • PostgreSQLは条件(述語)を記憶
  • 新しい操作が既存の述語と競合するか確認
  • 競合があればシリアライゼーション失敗

排他ロックによる範囲制御

明示的に範囲をロックしたい場合の方法です。

FOR UPDATEを使った範囲ロック:

BEGIN;
-- 特定範囲のレコードを排他ロック
SELECT * FROM inventory 
WHERE product_id BETWEEN 100 AND 200 
FOR UPDATE;

-- この範囲の既存レコードは他のトランザクションから更新不可
-- ただし、新規挿入は防げない(ギャップロックとの違い)

アドバイザリーロックの活用:

-- 範囲を表す仮想的なロックを取得
SELECT pg_advisory_lock(hashtext('products_100_200'));

-- 処理実行
INSERT INTO products (id, name) VALUES (150, 'New Product');

-- ロック解放
SELECT pg_advisory_unlock(hashtext('products_100_200'));

ファントムリードを防ぐ実践テクニック

方法1:SERIALIZABLE分離レベルを使う

最も確実な方法ですが、パフォーマンスへの影響を考慮する必要があります。

-- 完全にファントムリードを防ぐ
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- 範囲検索
SELECT * FROM orders 
WHERE created_at >= '2024-01-01' 
AND created_at < '2024-02-01';

-- 同じ条件で再度検索しても結果は変わらない
-- 他のトランザクションが新規注文を追加しても見えない

COMMIT;

方法2:明示的なテーブルロック

小さなテーブルや、バッチ処理で使える方法です。

BEGIN;
-- テーブル全体をロック
LOCK TABLE products IN SHARE ROW EXCLUSIVE MODE;

-- この間、他のトランザクションはINSERT/UPDATE/DELETE不可
SELECT * FROM products WHERE category = 'electronics';

-- 処理実行
UPDATE products SET price = price * 1.1 WHERE category = 'electronics';

COMMIT;

方法3:アプリケーションレベルの制御

データベースレベルではなく、アプリケーション側で制御する方法。

-- 一意制約やチェック制約を活用
ALTER TABLE reservations 
ADD CONSTRAINT unique_time_slot 
UNIQUE (room_id, date, time_slot);

-- INSERTが失敗したら再試行
-- アプリケーション側でリトライロジックを実装

MySQLからPostgreSQLへの移行時の注意点

ロック動作の違い

MySQLからPostgreSQLに移行する際の重要な違いです。

MySQL(InnoDB):

  • ギャップロックで範囲を自動保護
  • REPEATABLE READでファントムリード防止
  • ネクストキーロックの使用

PostgreSQL:

  • MVCCによるスナップショット分離
  • REPEATABLE READではファントムリード発生可能
  • SERIALIZABLEで完全防止

移行時の対処法

-- MySQLのコード
BEGIN;
SELECT * FROM users WHERE age BETWEEN 20 AND 30 FOR UPDATE;
INSERT INTO users (name, age) VALUES ('John', 25);
COMMIT;

-- PostgreSQL版(SERIALIZABLE使用)
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT * FROM users WHERE age BETWEEN 20 AND 30;
INSERT INTO users (name, age) VALUES ('John', 25);
COMMIT;

パフォーマンスを考慮した設計

ロックを最小限にする設計

PostgreSQLの強みを活かす設計方法です。

楽観的ロックの活用:

-- versionカラムを使った楽観的ロック
UPDATE products 
SET 
    price = 150,
    version = version + 1
WHERE 
    id = 100 
    AND version = 5;  -- 期待するバージョン

-- 更新件数が0なら競合発生

バッチ処理の工夫:

-- 大量データ処理時は範囲を分割
WITH batch AS (
    SELECT id FROM large_table 
    WHERE processed = false 
    LIMIT 1000 
    FOR UPDATE SKIP LOCKED  -- ロックできない行はスキップ
)
UPDATE large_table 
SET processed = true 
WHERE id IN (SELECT id FROM batch);

よくあるトラブルと解決法

シリアライゼーション失敗

ERROR: could not serialize access due to concurrent update

対処法:

-- アプリケーション側でリトライ
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 処理
COMMIT;
-- エラーの場合は自動リトライ

デッドロック

ERROR: deadlock detected

予防策:

-- 常に同じ順序でロックを取得
-- 例:IDの昇順でロック
SELECT * FROM accounts 
WHERE id IN (1, 2, 3) 
ORDER BY id 
FOR UPDATE;

ベストプラクティス

適切な分離レベルの選択

使い分けの指針:

READ COMMITTED:

  • デフォルト設定
  • 一般的なWebアプリケーション
  • パフォーマンス重視

REPEATABLE READ:

  • レポート生成
  • 一貫性が重要な参照処理

SERIALIZABLE:

  • 金融取引
  • 在庫管理
  • 絶対的な一貫性が必要

モニタリングとチューニング

-- ロック状況の確認
SELECT 
    pid,
    usename,
    application_name,
    state,
    query
FROM pg_stat_activity
WHERE wait_event_type = 'Lock';

-- ロックの詳細
SELECT * FROM pg_locks;

まとめ:PostgreSQLのロック戦略をマスターしよう!

ここまで読んでいただき、ありがとうございました!

覚えておくべき3つのポイント

  1. PostgreSQLにギャップロックはない
  • MVCCとスナップショット分離を使用
  • より効率的な並行性制御
  1. ファントムリード対策はSERIALIZABLE
  • 述語ロックで自動的に防止
  • パフォーマンスとのトレードオフを考慮
  1. 用途に応じた分離レベルを選択
  • 多くの場合READ COMMITTEDで十分
  • 必要に応じて引き上げる

シーン別クイックガイド

一般的なWebアプリ
→ READ COMMITTED + 楽観的ロック

在庫管理システム
→ SERIALIZABLE or 明示的なFOR UPDATE

レポート生成
→ REPEATABLE READ

大量バッチ処理
→ FOR UPDATE SKIP LOCKED

PostgreSQLのロック機構は、MySQLとは異なりますが、より洗練されています。

MVCCの仕組みを理解して、適切な分離レベルとロック戦略を選択すれば、高性能で信頼性の高いアプリケーションを構築できます。

ギャップロックがなくても、PostgreSQLなら大丈夫!

コメント

タイトルとURLをコピーしました