- ブラウザURL入力
- DNS名前→IP
- ルータ経路選択
- ロードバランサ振り分け
- Nginx受け口
- アプリ処理
- 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 のロックの章にはこの規則は書かれていません。
- ・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 の起動を含むので、順序と桁だけを読んでください)。
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-bit1000B alter done after 7.097893000sC select done after 6.092863000s
1000行を数えるだけの C が、秒の桁で止まりました。2回実行して揺れは 0.03 秒以内です。次に、待ちの最中(開始から約3秒)に誰が誰を止めているかを見ます。列は上から application_name・pid・wait_event_type・state・pg_blocking_pids(pid)、下は notes 表のロックのモードと granted です。
== 待ちの様子(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 | tB_alter | AccessExclusiveLock | fC_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 の出力の続き)。
== with lock_timeout on BERROR: canceling statement due to lock timeout1000B2 exitC2 select done after 1.073701000s
B は打ち切られ、C の待ちは約1秒に縮みました。C が B の後ろにいたのは、B が並んでいた間だけです。
「スキーマ変更には必ず停止が要る」も言い過ぎです。変更側が諦めて後で再試行すれば、利用者側の待ちは1回の試行あたり lock_timeout 程度で済みます。何秒にすべきかの一次ソースは無く、2s は例です。
B に `SET lock_timeout='2s'` を付けて同じことをしたとき、実演では何が起きましたか。
PostgreSQL 18 の表ロック
A が AccessShareLock を持つ間、B の AccessExclusiveLock は待ち、同じ AccessShareLock を求める C も B の後ろで待った。pg_blocking_pids(C) は B を返した。
セッション 求めたロック 状態A(読んだまま) AccessShareLock 保持B(ALTER) AccessExclusiveLock 待ちC(SELECT) AccessShareLock 待ち(B の後ろ)
Go の sync.RWMutex(go1.27.1)
R1 が RLock を持つ間に W が Lock を要求すると、後から来た R2 の RLock は R1 と衝突しないのに W の後ろで待った。文書も、書き手が取って離すまで新しい RLock は止まると書く。
go1.27.1t=0.0s R1 RLock 取得t=0.5s W Lock 要求t=1.0s R2 RLock 要求t=2.0s R1 RUnlockt=2.0s W Lock 取得t=2.5s W Unlockt=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も同じ順番を取る。書き手を守る規則が、書き手の後ろを止める。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
制約なし・非 volatile の DEFAULT を付けた `ADD COLUMN` について、PostgreSQL 18 の文書と合うのはどれですか。
用語集(3語)
- ACCESS EXCLUSIVE
- PostgreSQL で最も強い表ロック。素の SELECT を止める唯一のモードで、ALTER TABLE は特記が無い限りこれを取る。
- soft block
- pg_blocking_pids の文書の語。衝突するロックを持っていなくても、衝突する要求を持って待ち行列の前にいれば後ろを止める、という関係。
- lock_timeout
- ロック獲得の待ちがこの時間を超えた文を打ち切る設定。既定は 0(無効)。ロック待ちの間にしか発火しない。
出典(一次ソース)
- PostgreSQL 18 Documentation — ALTER TABLE ↗
- PostgreSQL 18 Documentation — 13.3. Explicit Locking ↗
- PostgreSQL 18 Documentation — 19.11. Client Connection Defaults(lock_timeout) ↗
- PostgreSQL 18 Documentation — 9.27. System Information Functions and Operators(pg_blocking_pids) ↗
- PostgreSQL 18 Documentation — pg_dump(-j の注意書き) ↗
- PostgreSQL ソースツリー src/backend/storage/lmgr/README(REL_18_STABLE。開発者向け文書) ↗
- Go 標準ライブラリ sync パッケージ(go1.27.1) ↗