Dev Study
← 出して、戻して、直す — 変更と復旧の実務
レッスン 4 / 12·約 8 分·第24期 出して、戻して、直す

待っているだけのスキーマ変更が、後ろの全員を止める

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理
  7. DB保存今ここ

新旧の版が同じデータを読み書きする間の話から、そのデータの形を変える瞬間へ進みます。変更そのものは短くても、変更が始まるまでの待ちが利用者に届きます。

まず結論

PostgreSQL の `ALTER TABLE ... ADD COLUMN` は、条件がそろえば大きな表でも速く終わります。しかし取るロックは `ACCESS EXCLUSIVE` で、取れるまでは待ちます。

待っている間、後から来た素の `SELECT` もその後ろに並びます。実行が速いことと、始められるまで待つことは別の話です。変更側に `lock_timeout` を付ければ、後ろの待ちに上限を置けます。

? 読む前に予測

PostgreSQL 18 で、A が表を読んだままトランザクションを開いています。そこへ B の `ALTER TABLE ... ADD COLUMN` が来て待ち、続いて C の素の `SELECT` が来ました。C はどうなるでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

速い変更と、始められるまでの待ち

第6期で、素の SELECT を止めるロックは ACCESS EXCLUSIVE だけだと見ました。ALTER TABLE の文書は冒頭でこう書きます。

An ACCESS EXCLUSIVE lock is acquired unless explicitly noted.

列を足す変更は速いとも書かれています。非 volatile の DEFAULT を付けた ADD COLUMN は値を表のメタデータに置き、making the ALTER TABLE very fast even on large tables です。

速いのはロックを取った後の話です。取るまでの時間は、先にロックを持っている者が決めます。 読んだままのトランザクションが1つあれば、ALTER はそれが終わるまで待ちます。

問題はその間、後から来た者がどうなるかです。PostgreSQL 18 の利用者向け文書は2か所でこれを書いています。pg_dump の -j の注意書きは、排他ロックの要求が待たされると any other access to the table will not be granted either and will queue after the exclusive lock request と続けます。pg_blocking_pids の説明は、衝突するロックを持つ場合(hard block)に加えて、次の場合も相手を止めると定義します。

…or is waiting for a lock that would conflict with the blocked process's lock request and is ahead of it in the wait queue (soft block).

持っていなくても、前に並んでいれば止める。 13.3 のロックの章にはこの規則は書かれていません。

待っているだけの ALTER が行列の先頭をふさぐ
A: 読んだままのトランザクションAccessShareLock を保持
衝突するので待つ
B: ALTER TABLE ADD COLUMNAccessExclusiveLock を要求して待ち
B の後ろに並ぶ
C: 素の SELECTA とは衝突しないのに待ち
  • ・C を止めているのは A ではなく B(soft block)
  • ・B の実行そのものは速くても、C の待ちは A の長さで決まる
具体実物

本教材の実演の出力(スクリプト非掲載)

PostgreSQL 18.6 を Docker で立て、1000行の notes 表を作りました。A は表を読んだあと8秒トランザクションを開いたままにし、その1秒後に B が alter table notes add column tag text;、さらに1秒後に C が select count(*) from notes; を流します。まず時間です(秒数は docker exec と psql の起動を含むので、順序と桁だけを読んでください)。

A・B・C の終了までの時間(run 1、2026-09-26。2行目は C の結果)コマンド出力
PostgreSQL 18.6 (Debian 18.6-1.pgdg13+2) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
1000
B alter done after 7.097893000s
C select done after 6.092863000s

1000行を数えるだけの C が、秒の桁で止まりました。2回実行して揺れは 0.03 秒以内です。次に、待ちの最中(開始から約3秒)に誰が誰を止めているかを見ます。列は上から application_name・pid・wait_event_type・state・pg_blocking_pids(pid)、下は notes 表のロックのモードと granted です。

pg_stat_activity と pg_locks(run 1、PostgreSQL 18.6)コマンド出力
== 待ちの様子(t≈3s)
A_reader | 110 | Timeout | active | {}
B_alter | 117 | Lock | active | {110}
C_select | 124 | Lock | active | {117}
== pg_locks(notes 表)
A_reader | AccessShareLock | t
B_alter | AccessExclusiveLock | f
C_select | AccessShareLock | f

C は A と同じ AccessShareLock を求めているのに granted が f です。pg_blocking_pids が C について返したのは {117}、つまり ロックを1つも持たずに待っている B です。run 2 でも pid が変わるだけで同じでした。

✓ ここまでの確認

実演の `pg_blocking_pids(C)` が返したのはどのセッションで、それは何を意味しますか。

抽象原理

なぜ後ろに並ばせるのか、そして誰も打ち切らない

後から来た読み手を先に通せば C は待たずに済みます。しかし読み手が途切れなく来れば、書き手はいつまでも取れないかもしれません。ソースツリーの開発者向け文書 src/backend/storage/lmgr/README は、待ち手を起こす規則について Rule (b) ensures that conflicting requests are granted in order of arrival. と書きます。到着順は書き手にも番を回しますが、その代わり書き手の後ろが止まります。

この待ちには既定で終わりがありません。

So long as no deadlock situation is detected, a transaction seeking either a table-level or row-level lock will wait indefinitely for conflicting locks to be released.

デッドロックなら検出されて片方が打ち切られます。しかし A→B→C の待ちは循環していないのでデッドロックではなく、何も打ち切られません。 A が1時間開いたままなら、B も C もその後ろの全員も1時間待ちます。

上限を置く手が lock_timeout です。既定は 0(無効)で、Unlike statement_timeout, this timeout can only occur while waiting for locks. ロックを待っている文にだけ効きます。全体に設定することは文書が勧めていません。

Setting lock_timeout in postgresql.conf is not recommended because it would affect all sessions.

だから変更を流すセッションで SET します。

具体実物

同じ条件で、変更側に lock_timeout を付ける

同じ実演の後半で、B だけ set lock_timeout='2s'; を先に流しました。A と C は前半と同じです(同じ run 1 の出力の続き)。

B に lock_timeout='2s' を付けた場合(run 1 の後半。2行目は C の結果)コマンド出力
== with lock_timeout on B
ERROR: canceling statement due to lock timeout
1000
B2 exit
C2 select done after 1.073701000s

B は打ち切られ、C の待ちは約1秒に縮みました。C が B の後ろにいたのは、B が並んでいた間だけです。

「スキーマ変更には必ず停止が要る」も言い過ぎです。変更側が諦めて後で再試行すれば、利用者側の待ちは1回の試行あたり lock_timeout 程度で済みます。何秒にすべきかの一次ソースは無く、2s は例です。

✓ ここまでの確認

B に `SET lock_timeout='2s'` を付けて同じことをしたとき、実演では何が起きましたか。

⇄ 2つを見比べる
具体例 A

PostgreSQL 18 の表ロック

A が AccessShareLock を持つ間、B の AccessExclusiveLock は待ち、同じ AccessShareLock を求める C も B の後ろで待った。pg_blocking_pids(C) は B を返した。

実演の3セッション表
セッション 求めたロック 状態
A(読んだまま) AccessShareLock 保持
B(ALTER) AccessExclusiveLock 待ち
C(SELECT) AccessShareLock 待ち(B の後ろ)
具体例 B

Go の sync.RWMutex(go1.27.1)

R1 が RLock を持つ間に W が Lock を要求すると、後から来た R2 の RLock は R1 と衝突しないのに W の後ろで待った。文書も、書き手が取って離すまで新しい RLock は止まると書く。

本教材の実演の出力(2回とも同一)コマンド出力
go1.27.1
t=0.0s R1 RLock 取得
t=0.5s W Lock 要求
t=1.0s R2 RLock 要求
t=2.0s R1 RUnlock
t=2.0s W Lock 取得
t=2.5s W Unlock
t=2.5s R2 RLock 取得

データベースと言語の標準ライブラリで、同じ待ちが起きています。後から来た読み手を止めているのは、どちらでも何ですか。

後から来た読み手と衝突するロックを、その時点で実際に持っている者がいるかを見てください。

この章のまとめ

  • ALTER TABLE は特記が無い限り ACCESS EXCLUSIVE を取る。ADD COLUMN が速いのはロックを取った後の話。
  • 排他の要求が待つと、後から来た素の SELECT もその後ろに並ぶ。PostgreSQL 18 の文書はこれを soft block と呼ぶ。実演では pg_blocking_pids(C) が待っているだけの B を返した。
  • 待ち行列はデッドロックではないので打ち切られず、既定では無期限に待つ。
  • lock_timeout は変更を流すセッションで SET する(postgresql.conf は非推奨)。実演では B が打ち切られ、C の待ちは約1秒で終わった。
  • Go の sync.RWMutex も同じ順番を取る。書き手を守る規則が、書き手の後ろを止める。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

制約なし・非 volatile の DEFAULT を付けた `ADD COLUMN` について、PostgreSQL 18 の文書と合うのはどれですか。

用語集(3語)
ACCESS EXCLUSIVE
PostgreSQL で最も強い表ロック。素の SELECT を止める唯一のモードで、ALTER TABLE は特記が無い限りこれを取る。
soft block
pg_blocking_pids の文書の語。衝突するロックを持っていなくても、衝突する要求を持って待ち行列の前にいれば後ろを止める、という関係。
lock_timeout
ロック獲得の待ちがこの時間を超えた文を打ち切る設定。既定は 0(無効)。ロック待ちの間にしか発火しない。