データベース Schema ドリフト:history、schema、query の三ビューで特定する
技術ノート
公開 / DavidHLPL
結論を先に:Flyway Community の validate が比較するのは「適用済み migration history」と「ローカルで解決できる migration」であり、「実際の database schema」と「期待 schema」ではない。Unknown column、列の重複追加、migration 失敗に遭遇した場合は、history、schema、query の三ビューを必ず同時に確認すること。repair だけを実行しても history テーブルの整合性は回復するかもしれないが、DDL を再生・取り消し・クリーンアップすることはない。
適用バージョンと証拠の強度
このページは Flyway 13.2.0(tag flyway-13.2.0、commit 5c5d90da)と公式 mysql:8.4 イメージ内の MySQL 8.4.11 に固定する。
- Flyway のセマンティクスは公式ドキュメントと固定ソースによる。今回のバッチでは Flyway CLI の実行に成功しなかったため、具体的な CLI 出力は検証済み事実として扱わない。
- MySQL の
1060、1054、1091、単一の複数句ALTER TABLEの原子ロールバック、外部 DDL ドリフトは、使い捨てコンテナで実測した。 - 他の Flyway/MySQL のメジャーバージョン、ストレージエンジン、レプリケーショントポロジは再検証が必要。
三ビューは互いに代替できない
| ビュー | 答える質問 | 最小チェック | 見えないもの |
|---|---|---|---|
| migration history | どの migration が適用済み・失敗・欠落・乱順として記録されているか | flyway info、flyway validate、schema history table |
Flyway を経ない外部 DDL |
| actual schema | 現在のテーブル・列・インデックスは実際に何か | SHOW CREATE TABLE、SHOW COLUMNS、information_schema |
アプリコードが実際に参照しているもの |
| application query | 失敗 SQL はどのオブジェクトをどの句で参照しているか | 最小再現 SQL、エラーコードと SQLSTATE | オブジェクトが現在の状態になった理由 |
したがって:validate が通っても schema ドリフトがないことは証明できない。schema に列が存在しても、すべてのアプリインスタンスの query と serializer がアップグレード済みとは証明できない。query のエラーも、それだけで migration が未実行であることを直接証明できない。
Flyway の本当の境界
validate
validate は適用済み migration とローカルで解決された migration を比較する。名前・タイプ・checksum の違い、適用済み migration がローカルで消えている、またはローカル migration がまだ適用されていない場合に失敗する。SQL migration の checksum は CRC32 を使用する。
固定ソースの DbValidate は MigrationInfoService 経由で history と resolved migrations を検査し、ユーザーテーブルの構造は読まない。そのため外部で:
ALTER TABLE users ADD COLUMN ext_drift INT;
を実行しても、実際の schema は変わるが migration history は生成されない。Community の validate はこれだけでは drift を自動報告しない。公式の schema snapshot/drift analysis は Enterprise の能力だ。
repair
repair は失敗レコードの削除、checksum/description/type の整合、欠落 migration の deleted マークができる。ただし history しか直さない:
- migration を再実行しない;
- 実行済み DDL を取り消さない;
- 失敗 migration が残したオブジェクトを削除しない;
- MySQL が DDL transaction を提供しない場合、人手での確認とクリーンアップが引き続き必要。
したがって repair をロールバック扱いにしてはいけない。まず実際の schema と目標状態を確認し、その上で元の migration を復元するか、前方修復 migration を書くか、制御されたウィンドウで手動クリーンアップするかを決める。
outOfOrder
outOfOrder のデフォルトは false。例えば 1.0 と 3.0 が適用済みで、後から 2.0 が現れた場合、これを有効にして初めて実行され Out of Order と記録される。「ゼロからの再現」と「既存環境の段階的アップグレード」で異なる結果になり得るため、空 DB とアップグレード済み DB の両方で先に検証すること。
エラーコードはまず SQL コンテキストで振り分ける
MySQL 8.4.11 の最小実験の結果:
| 現象 | エラー | 意味 |
|---|---|---|
再度 ADD COLUMN email |
1060 (42S21) Duplicate column name |
migration が二重実行された、または列が外部操作で作成済み |
SELECT no_such_col ... |
1054 (42S22) Unknown column ... in 'field list' |
存在しない列を query が参照 |
DROP COLUMN no_such_col |
1091 (42000) Can't DROP ... |
DROP 対象が存在しない。1054 ではない |
1 つの ALTER TABLE の後続句が列を重複 |
1060、文全体がロールバック |
同一 InnoDB DDL 文は原子 |
「単一 DDL が原子」は「migration ファイルがトランザクション化される」を意味しない。MySQL の DDL は暗黙コミットする:同じ migration ファイル内の最初の DDL が成功し二番目が失敗した場合、最初の DDL は残る可能性があり、Flyway history は失敗を記録する。このとき repair で失敗レコードを削除しても、最初の文が残したオブジェクトはクリーンアップされない。
最小診断の順序
- 失敗 SQL とエラーコードを固定する:SELECT・ADD・DROP を区別し、すべての「列問題」を 1054 に分類しない。
- history を見る:version、success、checksum、description、state を確認する。
validateは migration history の整合性だけを判定する。 - actual schema を見る:
SHOW CREATE TABLE/SHOW COLUMNSで列・インデックス・制約を照合し、history の外の変化を探す。 - migration ファイルと照合する:同一バージョンが書き換えられたか、欠落、乱順、または非トランザクション DDL を複数含むかを確認する。
- 前方修復を選ぶ:本番では新しい migration を追加して実状態を唯一の目標に進めるのが優先。履歴レコード自体が誤りと確認できた場合だけ
repairを実行する。 - 二経路で検証する:空 DB でゼロから一度 migration、既存 DB を現バージョンから一度アップグレードし、その後アプリの最小 query を実行する。
最小 MySQL 再現
以下は使い捨てのローカルコンテナ専用。TEMP_PASSWORD を実環境に使い回さないこと:
docker run -d --name schema-drift-mysql \
-e MYSQL_ROOT_PASSWORD=TEMP_PASSWORD \
-e MYSQL_DATABASE=driftdb mysql:8.4
# コンテナ内の mysqld が実際に接続を受け付けるまで待つ。docker run -d は起動を証明するだけである。
until docker exec -e MYSQL_PWD=TEMP_PASSWORD schema-drift-mysql \
mysqladmin -uroot ping --silent; do sleep 1; done
docker exec -e MYSQL_PWD=TEMP_PASSWORD schema-drift-mysql \
mysql -uroot driftdb -e \
"CREATE TABLE users (id BIGINT PRIMARY KEY); ALTER TABLE users ADD COLUMN email VARCHAR(100);"
docker exec -e MYSQL_PWD=TEMP_PASSWORD schema-drift-mysql \
mysql -uroot driftdb -e \
"ALTER TABLE users ADD COLUMN c2 INT, ADD COLUMN email INT; SHOW COLUMNS FROM users;"
docker rm -f schema-drift-mysql
2 番目のコマンドの重複列は 1060 を報告し、c2 は列リストにないことが期待される。この実験は MySQL 側の挙動だけを証明する。Flyway の挙動は、対象バージョンの隔離 DB で info、validate、migrate を別途実行すべきだ。
証拠と不確実性
- 情報源の事実:
database-schema-drift-contractは Flyway のドキュメント/ソースと MySQL 8.4.11 の実験に固定し、validate/repair/outOfOrder の境界とエラーコード行列を支える。 - 本ページの総合:migration history、actual schema、application query を三ビュー診断法として整理し、前方修復の順序を示す。
- 未確認:今回のバッチでは Flyway CLI を実走していない。duplicate-version の具体的なエラーテキスト、パーティション/外部キー/レプリケーションのシナリオ、他のストレージエンジン、他のメジャーバージョンは未検証。
関連ページ
出典と関連文書
研究記録
- 元の文書: src/content/note/ja/database-schema-drift.md
- 出典リビジョン: f49953fb0d4bd52a39bf81167b0c4b7dd7d78a64
- 証拠の状態: 有効