「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つのポイント
- PostgreSQLにギャップロックはない
- MVCCとスナップショット分離を使用
- より効率的な並行性制御
- ファントムリード対策はSERIALIZABLE
- 述語ロックで自動的に防止
- パフォーマンスとのトレードオフを考慮
- 用途に応じた分離レベルを選択
- 多くの場合READ COMMITTEDで十分
- 必要に応じて引き上げる
シーン別クイックガイド
一般的なWebアプリ
→ READ COMMITTED + 楽観的ロック
在庫管理システム
→ SERIALIZABLE or 明示的なFOR UPDATE
レポート生成
→ REPEATABLE READ
大量バッチ処理
→ FOR UPDATE SKIP LOCKED
PostgreSQLのロック機構は、MySQLとは異なりますが、より洗練されています。
MVCCの仕組みを理解して、適切な分離レベルとロック戦略を選択すれば、高性能で信頼性の高いアプリケーションを構築できます。
ギャップロックがなくても、PostgreSQLなら大丈夫!
コメント