Tech Learning Daily

2026-09-24 (Thu) — 第 69 号
AI が毎朝届ける、ソフトウェア技術の基礎解説
DB 🌿 基礎 ⏱ 約 7 分

同じ ALTER TABLE のはずなのに、なぜ一瞬で終わる時と本番を止める時があるのか?

Rails で add_column のマイグレーションを書いて rails db:migrate を叩いたことは何度もあるはずだ。たいていは一瞬で終わり、拍子抜けするほど何も起きない。しかしある日、似たような1行の ALTER TABLE が本番のテーブルを何十秒もロックし、その間 API のリクエストが軒並みタイムアウトした経験はないだろうか。

🎯 3 行まとめ
  • ALTER TABLE は PostgreSQL では ACCESS EXCLUSIVE という最も強いロックを取ることが多く、これは単純な SELECT とすら共存できない。
  • 新しい列の追加は PostgreSQL 11 以降、定数のデフォルト値ならメタデータの書き換えだけで一瞬に終わるが、既存の列に NOT NULL を課す操作は全行スキャンを伴い、その間ずっと ACCESS EXCLUSIVE を握り続ける。
  • NOT VALID な CHECK 制約 → VALIDATE CONSTRAINT → SET NOT NULL という3段階に分ければ、重いスキャンを読み書きを止めない軽いロックの間に逃がせる。

ロックは一枚岩ではない

PostgreSQL では、テーブルへの操作の種類ごとに要求するロックの強さが違う。通常の SELECTACCESS SHAREINSERT/UPDATE/DELETEROW EXCLUSIVEという軽いロックしか取らないため、複数のトランザクションが同時に読み書きできる。これらは互いに競合しない設計になっている。

一方で ALTER TABLE の多くの操作はテーブルの定義そのものを変えるため、ACCESS EXCLUSIVEという最も強いロックを要求する。公式ドキュメントによれば ACCESS EXCLUSIVE は ACCESS SHARE を含む他のすべてのロックモードと競合し、そのテーブルへのあらゆるアクセスを独占する。つまり ALTER TABLE がこのロックを要求した瞬間、単純な SELECT ですら順番待ちの列に並ばされる。

列の追加は、今は意外と一瞬で終わる

PostgreSQL 11 より前は、デフォルト値付きの列を追加すると既存の全行に値を書き込むテーブル全体の書き換えが発生し、その間 ACCESS EXCLUSIVE を握り続けていた。11 以降は「fast default」という最適化が入り、デフォルト式が定数や STABLE な式であれば、その評価結果をカタログ(pg_attributeattmissingval)に1回だけ保存し、古い行を読むときにその値を補完で返すようになった。行数に関係なくミリ秒オーダーで終わる。

-- 速い: 定数なのでカタログに1回保存するだけ
ALTER TABLE users ADD COLUMN plan text DEFAULT 'free';

-- 遅い: volatile関数なので全行を書き換える
ALTER TABLE users
  ADD COLUMN uid uuid DEFAULT gen_random_uuid();

この近道が使えるのは「呼ぶたびに結果が変わらない」式に限られる。gen_random_uuid() のような volatile 関数をデフォルトに指定すると、行ごとに異なる値を書き込む必要があるため、PostgreSQL は全行を書き換えるテーブルリライトに戻ってしまう。

🍱 たとえるなら

ACCESS EXCLUSIVE は「本日休業」の張り紙を出してビルの玄関ごと閉め、中にいる客も外から来る客も一切通さない状態に近い。一方 SHARE UPDATE EXCLUSIVE は、廊下の一部だけ「工事中、他の通路からお回りください」と規制するだけで、店は普段どおり営業を続けられる。同じ「工事」でも、玄関を閉めるか廊下の一部だけ塞ぐかで、客が受ける影響はまったく違う。

危ないのは「既存の列」に NOT NULL を課すとき

実務でよくあるのは、列を NULL 許容で追加してデータをバッチで埋め、後から NOT NULL を課す2段階の進め方だ。この最後の一手が重い。ALTER TABLE t ALTER COLUMN v SET NOT NULL; は「既存の行に1つも NULL が無い」ことを保証するために全行を読む必要があり、PostgreSQL はこの検証を ACCESS EXCLUSIVE ロックを握ったまま実行する。数百万行のテーブルなら、その間ずっと読み書きすべてが止まる。

さらに厄介なのは、ACCESS EXCLUSIVE の取得自体が長時間トランザクションの後ろに並ばされる点だ。誰かが同じテーブルに対して重いレポートクエリを実行中なら、その軽い ACCESS SHARE ロックが解放されるまで ALTER TABLE は待たされる。そして待っている ALTER TABLE のさらに後ろには、新しく来た SELECT まで列を作って詰まっていく。

安全な3段階 — NOT VALID → VALIDATE → SET NOT NULL

重い検証を軽いロックの間に逃がす手順として PostgreSQL が用意しているのが、CHECK 制約の NOT VALID オプションである。

素朴な1行:
ALTER TABLE t ADD COLUMN c bool
  NOT NULL DEFAULT false;
→ ACCESS EXCLUSIVE(全停止)のまま
   全行を検査して書き込む

安全な3段階:
① ADD CONSTRAINT chk NOT VALID
   ACCESS EXCLUSIVE だが一瞬
② VALIDATE CONSTRAINT chk
   SHARE UPDATE EXCLUSIVE
   (読み書きは止めない)
③ SET NOT NULL
   ACCESS EXCLUSIVE だが
   ②のおかげでスキャン省略
-- ① 既存行は検査せず、制約だけ追加(一瞬)
ALTER TABLE t
  ADD CONSTRAINT v_not_null
  CHECK (v IS NOT NULL) NOT VALID;

-- ② 全行スキャンはここで行うが、通常の
--    読み書きとは共存できるロックで済む
ALTER TABLE t VALIDATE CONSTRAINT v_not_null;

-- ③ PostgreSQL 12+では②の検証結果を再利用し
--    スキャンなしで一瞬で完了する
ALTER TABLE t ALTER COLUMN v SET NOT NULL;

①はまだ既存行を検査しないため ACCESS EXCLUSIVE を取っても一瞬で終わる。②の VALIDATE CONSTRAINT が要求するのはSHARE UPDATE EXCLUSIVEロックで、これは通常の SELECT/INSERT/UPDATE/DELETE と共存できる。時間がかかっても本番のトラフィックは流れ続ける。③の SET NOT NULL 自体は依然として ACCESS EXCLUSIVE を取るが、PostgreSQL 12 以降は②で検証済みの CHECK 制約が「NULL が存在しない」ことを既に証明しているとみなし、再スキャンを省略する。

MySQL では事情が違う

MySQL(InnoDB)は 8.0.12 以降、列追加などの一部 DDL を ALGORITHM=INSTANT で実行できる。これはテーブルのデータファイルに触れずデータディクショナリのメタデータだけを書き換えるため、行数によらず即座に終わる。ただし対象になる行バージョンの上限は 64 件で、行フォーマットが COMPRESSED のテーブルには使えないといった制限があり、条件から外れると従来どおりテーブルコピーを伴う重いアルゴリズムにフォールバックする。PostgreSQL の fast default と発想は近いが、対応する操作の範囲や制限は別物なので、使っている RDBMS のバージョンごとにドキュメントを確認する必要がある。

💼 実務でどう出会うか

Rails で列を NULL 許容のまま追加してデータをバックフィルし、最後に NOT NULL を課す2段階マイグレーションは、まさにこの ACCESS EXCLUSIVE ロックの問題を避けるための定石だ。strong_migrations のような gem は危険なマイグレーションのパターンを検知してブロックし、NOT VALID を使った安全な代替手順を提案してくれる。Go やその他の言語でマイグレーションツールを自作している場合も、SQL を手書きする以上は同じロックの理屈がそのまま当てはまる。

⌨️ 手を動かす(5 分)

「軽そうな SET NOT NULL が、実は長時間トランザクションの後ろで待たされる」様子を Docker の PostgreSQL で再現する。

docker run -d --rm --name pgdemo \
  -e POSTGRES_PASSWORD=pass -e POSTGRES_DB=demo \
  -p 5544:5432 postgres:16
until docker exec pgdemo pg_isready -U postgres \
  >/dev/null 2>&1; do sleep 1; done
docker exec pgdemo psql -U postgres -d demo \
  -c "CREATE TABLE t(id serial primary key, v int);"
docker exec pgdemo psql -U postgres -d demo \
  -c "INSERT INTO t(v) SELECT g FROM generate_series(1,50000) g;"

# セッションA: 8秒間トランザクションを保持したままにする
docker exec pgdemo psql -U postgres -d demo \
  -c "BEGIN; SELECT pg_sleep(8) FROM t LIMIT 1; COMMIT;" &
sleep 1
# セッションB: lock_timeoutを2秒にしてSET NOT NULLを試す
time docker exec pgdemo psql -U postgres -d demo \
  -c "SET lock_timeout='2s';
      ALTER TABLE t ALTER COLUMN v SET NOT NULL;"
wait
docker stop pgdemo

セッション A の pg_sleep(8) は単なる SELECT なので ACCESS SHARE ロックしか持たないが、それでも ALTER TABLE が求める ACCESS EXCLUSIVE とは共存できない。セッション B は2秒の lock_timeout に引っかかり canceling statement due to lock timeout で失敗するはずだ。lock_timeout を外して再実行すると、今度は A の8秒間 B がブロックされ続ける挙動が確認できる。

🙅 よくある誤解
  • PostgreSQL 11 以降なら ALTER TABLE での列追加は常に一瞬 — 非 volatile な定数デフォルトなら一瞬だが、gen_random_uuid() のような volatile 関数をデフォルトにすると全行書き換えのテーブルリライトに戻る。
  • NOT NULL を課すのが重いのは、制約の内容そのものが複雑だから — 重いのは制約の複雑さではなく全行スキャンの有無であり、NOT VALID な CHECK 制約で既に保証済みなら SET NOT NULL 自体はスキャンを省略して一瞬で終わる。
  • ALTER TABLE が一瞬で終わるなら本番への影響はない — ACCESS EXCLUSIVE ロックの取得自体が既存のどんな軽いロックとも共存できないため、長時間トランザクションの後ろに並ばされ、さらにその後ろに新しいクエリが列を作ることがある。
📖 用語ミニ辞典
ACCESS EXCLUSIVE ロック
PostgreSQL で最も強いロックモード。他の全モードと競合し、対象テーブルへのあらゆるアクセスを独占する。
SHARE UPDATE EXCLUSIVE ロック
通常の SELECT/INSERT/UPDATE/DELETE とは共存できるが、他の DDL や VACUUM とは競合するロックモード。
fast default
PostgreSQL 11 で入った最適化。定数デフォルトの評価結果をカタログに1回だけ保存し、全行への書き込みを省略する仕組み。
NOT VALID 制約
追加時に既存行を検査しない CHECK/外部キー制約のオプション。追加自体を軽くし、検証を別ステップに遅延させる。
VALIDATE CONSTRAINT
NOT VALID な制約が既存行を満たすか確認するコマンド。SHARE UPDATE EXCLUSIVE ロックのみで実行できる。
ALGORITHM=INSTANT
MySQL 8.0.12 以降で使えるオンライン DDL の方式。データファイルに触れずメタデータのみを書き換える。
🔗 もっと深く
最近の記事

画像の URL を1つ受け取っただけなのに、なぜそれがクラウドの認証情報を盗む入口になるのか?

SSRFはユーザー指定のURLをサーバーに取得させ、外部からは届かない内部ネットワークやクラウドのメタデータサービスに到達する攻撃。ブロックリストがDNSリバインディングやリダイレクトで破られる理由と、IMDSv2のPUT+トークン方式・既定ホップリミット1による防御を解説。
Security🌿 基礎⏱ 約 7 分2026-09-23

setState しただけなのに、なぜ React は新しいツリーをまるごと作り直すのか?

Reactはstate変更のたびに軽量なJSオブジェクトである仮想DOMツリーを丸ごと生成し、前回ツリーとdiffしてから差分だけを実DOMへ適用する。要素の同一性が「ツリー内の位置」で決まるためkeyが必須になる理由と、FiberによるスケジューリングやReact18のautomatic batchingを解説。
Frontend🌿 基礎⏱ 約 7 分2026-09-22

レスポンスは 200 のはずなのに、なぜ通信量が 0 バイトになるのか?

HTTPキャッシュは鮮度(サーバーに聞かずに使える期間)と検証(聞いてから使う判断)の2層構造で、no-cacheは保存禁止ではなく毎回検証を強制する指示。ETag/If-None-Matchの検証が一致すると304 Not Modifiedで本文送信を省略する仕組みを解説。
Network🌿 基礎⏱ 約 6 分2026-09-21

負荷が落ち着いたのに、なぜ HPA はしばらく Pod を減らしてくれないのか?

HPAは既定15秒間隔の制御ループでdesiredReplicas=ceil(現在数×実測/目標)を計算するだけの仕組み。既定10%のトレランスでフラッピングを防ぎ、scaleUpは0秒・scaleDownは既定300秒のstabilizationWindowSecondsという非対称設計で急な縮小を避ける理由を解説。
Container🌿 基礎⏱ 約 7 分2026-09-20