はじめに
皆さんはデータベースのテーブルに列を追加しようとして、DDLがハングしてしまいデプロイが滞った経験はありませんか?
私はCDパイプラインで、デプロイが長時間化していたことをきっかけに、掲題の問題と遭遇しました。
この記事では現象をコードで再現し、原因を解説した上で、基本的な回避策まで紹介します。
環境情報
| 項目 | バージョン | 
|---|---|
| OS | macOS Sequoia 15.6.1 | 
| PostgreSQL | 18.0 | 
| MySQL | 9.4.0 | 
| Oracle Database | 23ai Free 23.0.0.0.0 | 
事象の再現
実際にDDLをハングさせる再現手順は次のとおりです。
事前準備
ALTER TABLE ADD COLUMNでのハングを再現させるために、テーブルとサンプルデータを作成します。
-- テストテーブル作成
CREATE TABLE test_table (
    id SERIAL PRIMARY KEY,
    data TEXT
);
-- サンプルデータ挿入
INSERT INTO test_table (data)
SELECT 'sample' || generate_series(1, 1000);
セッション1 (トランザクションA: 長時間のSELECT)
CDパイプラインでDDLを実行中に、アプリケーションのクエリが絡んでハングした状況をシミュレートします。
私の場合、CDパイプラインが走り出す前に、DDL実行対象のテーブルを参照するバッチ処理が動いていましたので、そのセッションを以下のクエリで再現します。
BEGIN;  -- トランザクション開始
SELECT * FROM test_table WHERE id > 0;  -- ここで結果を表示させたまま放置(AccessShareLock取得)
-- COMMITせずに待機(CDパイプラインの並行クエリを模擬)
セッション2 (ALTER TABLE: DDL実行)
次に、別のセッションでPostgreSQLへアクセスし、ALTER TABLEを実行します。
ALTER TABLE test_table ADD COLUMN new_column INTEGER;  -- ここでハング(AccessExclusiveLock待ち)
コマンドラインから実行すると、下図のようにカーソルが点滅し、ハングした状態になります。

セッション3 (トランザクションB: 別のSELECT)
さて、DDLがハングすることはここまでの手順で再現した通りになりますが、真に恐ろしいのは、ここから発生する現象です。例えば、無停止デプロイする際は以降に記述する現象と遭遇する可能性があります。
手順は簡単、新たなセッションでPostgreSQLに接続し、以下のクエリを実行します。
SELECT id FROM test_table;  -- ここでハング(連鎖)
もしこれがWebアプリケーションのAPIなどで発生すると、大騒ぎになるでしょう。
セッション1を完了
セッション1(トランザクションA)をcommitかrollbackして、トランザクションを終了させると、セッション2のDDLが完了し、即座にセッション3も結果を返します。

上図は ALTER TABLE ADD COLUMNが完了した状態です。ALTER文の後に ALTER TABLEメッセージが表示され、コンソールへの入力が可能な状態になります。
DDLとDMLがハングする原因
この現象の原因は、PostgreSQLのロック処理に起因します。
PostgreSQLにおいては、SELECT文を実行するタイミングで、テーブルに対しAccessShareLock(共有ロック)を取得します。これはPostgreSQLのロックの中では最も緩いロックで、複数のセッションが同時に取得可能です。
しかし、このロックは排他的な操作(例: ALTER TABLEのAccessExclusiveLock)と競合すると待機状態になります。
PostgreSQLはMVCC(Multi-Version Concurrency Control)という仕組みを採用しており、読み取り操作(SELECT)中にデータのバージョンをスナップショットして一貫性を保ちます。
しかし、テーブル構造の変更(DDL)が行われると、そのスナップショットが無効化される可能性があるため、AccessShareLockで読み取りを保護し、変更操作をブロックします。
これにより、データベースのACID特性(Atomicity, Consistency, Isolation, Durability)のうち、特に Consistency(一貫性)とIsolation(分離性) を担保している一方で、長時間のトランザクションが実行されている最中は、DDLがハングする原因ともなっています。
さらに、新しいSELECTが来るとロックのキューが積み上がり、ハングの連鎖も引き起こされます。
全体の流れ
本例の一連の流れを図にすると、以下のようになります。
FYI : ロックの状態をトレースする
PostgreSQLでテーブルのロック状況を確認する際のクエリ使用例です。
まず、ロック状況を表示するクエリは以下の通りです。(AccessShareLockの複数取得を確認できます)
SELECT * FROM pg_locks WHERE relation = 'test_table'::regclass;
 locktype | database | relation | page | tuple | virtualxid | transactionid | classid | objid | objsubid | virtualtransaction | pid |        mode         | granted | fastpath |           waitstart           
----------+----------+----------+------+-------+------------+---------------+---------+-------+----------+--------------------+-----+---------------------+---------+----------+------------------------------ 
 relation |        5 |    16388 |      |       |            |               |         |       |          | 5/6                | 183 | AccessExclusiveLock | f       | f        | 2025-10-12 07:38:43.520741+00
 relation |        5 |    16388 |      |       |            |               |         |       |          | 7/3                | 222 | AccessShareLock     | f       | f        | 2025-10-12 07:38:48.707213+00
 relation |        5 |    16388 |      |       |            |               |         |       |          | 3/9                | 152 | AccessShareLock     | t       | f        | 
(3 rows)
次に、待機クエリを確認するクエリは以下の通りです。
SELECT * FROM pg_stat_activity WHERE state = 'active';
 datid | datname  | pid | leader_pid | usesysid | usename  | application_name | client_addr | client_hostname | client_port |         backend_start         |          xact_start           |          query_start          |         state_change          | wait_event_type | wait_event | state  | backend_xid | backend_xmin | query_id |                         query                          |  backend_type  
-------+----------+-----+------------+----------+----------+------------------+-------------+-----------------+-------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+-----------------+------------+--------+-------------+--------------+----------+--------------------------------------------------------+----------------
     5 | postgres | 315 |            |       10 | postgres | psql             | 172.17.0.1  |                 |       48696 | 2025-10-12 07:39:08.712211+00 | 2025-10-12 07:43:35.621448+00 | 2025-10-12 07:43:35.621448+00 | 2025-10-12 07:43:35.621471+00 |                 |            | active |             |          768 |          | SELECT * FROM pg_stat_activity WHERE state = 'active'; | client backend
     5 | postgres | 222 |            |       10 | postgres | psql             | 172.17.0.1  |                 |       39596 | 2025-10-12 06:50:51.150299+00 | 2025-10-12 07:38:48.703999+00 | 2025-10-12 07:38:48.703999+00 | 2025-10-12 07:38:48.704043+00 | Lock            | relation   | active |             |          768 |          | SELECT id FROM test_table;                             | client backend
     5 | postgres | 183 |            |       10 | postgres | psql             | 172.17.0.1  |                 |       41728 | 2025-10-12 06:32:16.164868+00 | 2025-10-12 07:38:43.520006+00 | 2025-10-12 07:38:43.520006+00 | 2025-10-12 07:38:43.520038+00 | Lock            | relation   | active |         768 |          768 |          | ALTER TABLE test_table ADD COLUMN new_column INTEGER;  | client backend
(3 rows)
FYI : 他のRDBMS
PostgreSQL以外のRDBMSでも、同様の現象が発生するか確かめてみました。
MySQL
MySQLでもDDLのハングと、それに続くSELECT文のハングが再現しました。
「Instant ADD COLUMN」が導入され、特定条件下ではメタデータの変更のみで列を追加できるようになりましたが、今回のトランザクション例においては効果がありませんでした。
OracleDB
Oracleでは通常のSELECT文によってDDLはハングしません。ただし、FOR UPDATEにより行ロックをかけた場合、DDLはハングします。
また、DDLがハングしている間にトランザクションBからSELECT文を実行した場合、トランザクションBはハングせずにクエリの結果を返します。
回避策
DDLのハングを予防する策はあまり多くありません。
ダウンタイムゼロを実現するには、高度なマイグレーション戦略(例:論理レプリケーションを使用したBlue/Greenデプロイや、専用のオンラインマイグレーションツールなど)が必要であり、本記事で紹介する基本的な対策では困難です。
DDLのロック待ち対策
DDL実行時に、タイムアウト値を設定することでAccessShareLock解放待ちによる長時間化を予防する措置です。
SET lock_timeout = '5s';
ALTER TABLE test_table ADD COLUMN new_column INTEGER;
lock_timeoutを設定することで、ロック解放待ちが長時間化した際に、下図のようなエラーが発生するようになります。

待機セッション強制終了
DDLを実行する前に、待機しているセッションを強制終了させる措置です。
SELECT
    pg_terminate_backend(pid)
FROM
    pg_stat_activity
WHERE
    pid <> pg_backend_pid()
AND datname = current_database();
上記のクエリはクリティカルな処理を中断させる可能性を伴います。本番環境ではもちろん、開発環境でも原則使うべきではありません。どうにもならない時の最後の手段として捉えてください。
その他の手段
ロックを最小限に抑えるツールとしてpgroll(ゼロダウンタイムマイグレーション)がありますが、こちらを使用してもAccessShareLockとDDLの競合は回避できませんでした。
一部の生成AIは、このツールを利用することで対策可能と回答1しましたが、バージョン0.14.3時点では直接的な対策としては採用できません。
FYI : 分散化による対策ができるか
「片方のノードでDDLを実行しても、もう片方で読み取りを継続する」といったアーキテクチャを想定する方もいるかもしれません。しかし、PostgreSQLにおいてネイティブにマルチプライマリ(マルチライト)構成をサポートする仕組みは存在しません。
選択肢としては以下が考えられます。
- 
レプリケーションを活用したリードレプリカ構成
通常のストリーミングレプリケーションでは、DDLはプライマリで実行され、レプリカへ伝播します。レプリカはhot_standby_feedbackの設定などで読み取りを継続可能ですが、DDL適用の瞬間には一時的なストールが発生する可能性があります。 - 
サードパーティ製のマルチプライマリ拡張
例として Bucardo や pglogical といった論理レプリケーション系の仕組みを活用することで「マルチライト」的な構成を実現可能ですが、DDLの伝播はサポート外または制限が多いため、DDLの無停止適用という文脈では解決になりません。 - 
商用ディストリビューション (例: Postgres-BDR)
一部の商用ディストリビューションではマルチプライマリを実現していますが、こちらもDDLの即時反映には制約があります。 
総じて「マルチプライマリだからDDLが無停止で行える」という保証はなく、DDLの扱いはどの構成でも厳格に設計する必要があります。
最後に
本記事では、ALTER TABLE ADD COLUMNがハングする現象を再現し、原因となるロック競合の仕組みを解説しました。
まとめると以下の通りです。
- PostgreSQLの
ALTER TABLEはAccessExclusiveLockを必要とし、SELECTなどのAccessShareLockと競合する - 長時間トランザクションが存在すると、DDLが待たされ、さらに新しいSELECTも連鎖的にハングする
 - 回避策は限られている
- 運用上「DDLはサービス影響が小さい時間帯に実行する」
 - 
lock_timeoutを設定して長時間化を防ぐ 
 
他のRDBMSと比較しても、DDLの無停止実行は依然として難しい課題です。したがって、「DDLを本番でどう扱うか」こそがDBエンジニアやバックエンドエンジニアの腕の見せどころと言えるでしょう。
この記事が、皆さんの運用・設計のヒントとなれば幸いです。
- 
2025/10/12時点 ↩
 
