オフィスで誰かが部屋の中に鍵を忘れて、全部のドアがロックされちゃう状況を想像してみて。PostgreSQLのロックもまさにそんな感じ。もしクエリやトランザクションがリソースをロックしたら、同じリソースにアクセスしようとする他の操作は、そのロックが終わるまで待たされる。こういう状況は遅延やコンフリクトを引き起こして、最悪の場合システムが止まっちゃうこともあるんだ。
ロックはいつ発生するの?
PostgreSQLのロック(Locks)は、データへの同時アクセスを管理するために使われるよ。ロックが発生するタイミングは:
- 書き込み操作を実行するとき:
UPDATE、DELETE、INSERT。 - トランザクションが必要以上に長くリソースを保持しているとき。
- 同じリソースを複数のトランザクションが取り合ってコンフリクトしたとき。
リアルなデータベースはリソースの「バトルフィールド」だし、システムが完璧に動いてると思ってても、うっかりしたトランザクション一つで全部「詰まる」こともある。Gitの失敗したmergeみたいなもんだね。
ロック分析ツール:pg_locks
pg_locksは、PostgreSQLのシステムビューで、今どんなロックがトランザクションに保持・待機されてるかを表示してくれる。「誰がロックを持ってて、誰が待ってるの?」って疑問に答えてくれるやつ。
pg_locksの主なカラム:
locktype: ロックの種類(例:relation、transaction、page、tuple)。database: データベースのID。relation: テーブルのID(テーブルに関するロックの場合)。mode: ロックモード(例:RowExclusiveLock、AccessShareLock)。granted: ロックが付与されてるかどうかのフラグ(trueなら付与済み、falseならまだ待機中)。
注意:PostgreSQLは「階層的ロックモード」ってやつを使ってる。つまり、操作によっては緩いロック(例:データ読み取り用のAccessShareLock)や、もっと厳しいロック(例:テーブル構造変更用のExclusiveLock)がかかるってこと。
例:現在の全ロックを表示
SELECT *
FROM pg_locks;
でも、pg_locksの中身を全部出すとノイズが多すぎる。もっと意味のあるやつを見てみよう!
例:まだ付与されてないロック(つまりトランザクションが待ってるやつ)
SELECT pid, locktype, relation::regclass AS table_name, mode, granted
FROM pg_locks
WHERE NOT granted;
ここで何が起きてる?
granted = falseのレコードだけフィルタしてる、つまりまだロックが付与されてないやつ。relation::regclassでテーブルIDをテーブル名に変換して、見やすくしてる。
出力例はこんな感じ:
| pid | locktype | table_name | mode | granted |
|---|---|---|---|---|
| 1234 | relation | students | RowExclusiveLock | false |
| 4321 | relation | courses | RowShareLock | false |
こういうクエリで、どのテーブルやリソースがロックされてて、どのトランザクションが原因か特定できるよ。
コンフリクト分析:pg_blocking_pids()
ロックだけならまだマシだけど、もし一つのトランザクションが他のやつをブロックしてたらどうする?PostgreSQLには「犯人」を特定する便利な関数pg_blocking_pids()があるよ。
pg_blocking_pids()は、今のトランザクションをブロックしてるプロセスID(pid)のリストを返してくれる。
例:他をブロックしてるトランザクションを探す
SELECT pid, pg_blocking_pids(pid) AS blocking_pids
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0;
ここで何が起きてる?
pg_stat_activityビューで、システム内のアクティブなプロセスを取得してる。pg_blocking_pids(pid)で各pidのブロック元プロセスのリストを返す。リストが空じゃなければ(長さが0より大きければ)、そのプロセスはブロックされてるってこと。
結果例:
| pid | blocking_pids |
|---|---|
| 4567 | {1234, 5678} |
| 6789 | {4321} |
pid = 4567のトランザクションは1234と5678のプロセスにブロックされてる。これで「犯人」発見!
ブロックしてるプロセスの終了
ブロックしてるプロセスが特定できたら、pg_terminate_backend()関数で止めることができる:
SELECT pg_terminate_backend(1234); -- プロセス1234を「殺す」
でも注意して!強制終了すると、そのトランザクションのデータはロールバックされる。これは「核ボタン」みたいなもんだから、本当に困ったときだけ使おう。
実践:ロック分析シナリオ
例えば、大学のデータベースがあって、studentsとenrollmentsテーブルがあるとする。複数のトランザクションが同時にenrollmentsに書き込もうとして、ロックが発生したケースを考えよう。
- ロックの特定:
SELECT pid, locktype, relation::regclass AS table_name, mode, granted
FROM pg_locks
WHERE NOT granted;
- ブロックしてるプロセスの特定:
SELECT pid, pg_blocking_pids(pid) AS blocking_pids
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0;
- ロックの解消:
コンフリクトしてるプロセスの一つを強制終了する:
SELECT pg_terminate_backend(1234); -- プロセス1234を終了
注意:プロセスを「殺す」前に、なぜロックが発生したのかちゃんと調べよう。トランザクションのロジックを見直すのも大事だよ。
よくあるミスとその防止策
ロックはトランザクション管理がうまくできてないときによく発生する。例えば:
ミス:トランザクションが何も操作せずにロックを長時間保持してる("idle in transaction"状態)。
対策:pg_stat_activityでトランザクションの状態をしっかり監視して、「ハング」してるやつは終了させよう。
SELECT pid, state, query
FROM pg_stat_activity
WHERE state = 'idle in transaction';
ミス:クエリでインデックスを使い忘れて、テーブル全体にロックがかかっちゃう。
対策:よく使う条件にはインデックスを追加して、クエリを最適化しよう。
「誰が誰を待ってるか」テーブルの出力
診断を楽にするために、どのトランザクションがどれをブロックしてるかの依存ツリーを作ることもできるよ:
WITH RECURSIVE blocking_tree AS (
SELECT pid, pg_blocking_pids(pid) AS blocked_by
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0
UNION ALL
SELECT a.pid, pg_blocking_pids(a.pid)
FROM pg_stat_activity a
JOIN blocking_tree b ON a.pid = ANY(b.blocked_by)
)
SELECT pid, blocked_by FROM blocking_tree;
結果:
| pid | blocked_by |
|---|---|
| 4567 | {1234} |
| 1234 | {5678} |
| 5678 | {} |
ここでは、プロセス5678が1234をブロックしてて、1234が4567をブロックしてるのが分かるね。
GO TO FULL VERSION