なぜデータベースのカラムをすぐに削除してはいけないのか
チームが新しいデータベーススキーマを使うようにアプリケーションコードを更新し終えたとします。古いカラムはもはや死重です。片付けたいと思うでしょう。そこで、あなたはそれを削除します。
数分後、エラーがログにあふれ始めます。ユーザーから、5分前まで使えていた機能が今は真っ白なページを返すと報告が来ます。夜勤の誰かから、真夜中に実行されるバッチジョブがクラッシュしたと電話がかかってきます。あなたは原因究明に次の2時間を費やすことになります。
このシナリオは、大小さまざまなチームで発生します。古いスキーマをすぐに削除したいという衝動は理解できますが、本番システムではほとんどの場合、裏目に出ます。その理由を説明します。
古いインスタンスがまだ動作している
スキーマ削除が失敗する最も一般的な理由は、すべてのアプリケーションインスタンスがまだ更新されていないことです。本番環境では、デプロイは段階的に行われます。1台のサーバーを更新し、次に次のサーバー、さらに次のサーバーと進みます。その間、一部のサーバーはまだ古いコードで動作しています。
古いインスタンスが、もはや存在しないカラムに対して読み取りや書き込みを試みると、データベースはエラーを返します。そのエラーはリクエスト失敗となり、ユーザーの苦情へとつながります。問題は新しいコードにあるのではなく、デプロイ手順のタイミングのギャップにあります。
ブルーグリーンデプロイメントやカナリアリリースを使用している場合でも、同じ原則が適用されます。ロールアウト中のどの時点でも、アプリケーションの複数のバージョンが同時に稼働しています。それらはすべて同じデータベースを共有しています。すべてのインスタンスが追いつく前にスキーマが変更されると、何かが壊れます。
データ移行はほぼ瞬時には完了しない
2つ目の問題はデータです。古いカラムやテーブルには、新しい構造にまだ移動されていないデータが残っていることがよくあります。ステージング環境では移行スクリプトが正常に実行されたかもしれませんが、本番環境ではデータセットが10倍大きいかもしれません。手動での確認が必要な外部キー関係があるかもしれません。移行自体に何時間もかかり、1回のメンテナンスウィンドウでは完了できないかもしれません。
すべてのデータが移行される前にスキーマを削除すると、そのデータは失われます。回復するにはバックアップからのリストアが必要であり、ダウンタイムが発生し、バックアップ取得後に書き込まれたデータが失われるリスクがあります。書き込み量の多いテーブルでは、そのギャップは大きくなる可能性があります。
スキーマを削除する前に、別のステップでデータを移行できると想定するチームもいます。理論的には機能しますが、実際には常にエッジケースが表面化します。誰も覚えていないスケジュールジョブ、月に1回実行されるレポートクエリ、特定の条件下でのみ起動するレガシーインテグレーション。これらの古いスキーマの隠れた消費者は、カラムが削除された後にのみ姿を現します。
隠れた依存関係は至る所にある
これが最も難しい問題、つまり未知の依存関係につながります。データベーススキーマのすべての消費者が文書化されているわけではありません。すべての消費者が、あなたが管理するアプリケーションであるとは限りません。
以下のシナリオを考えてみてください。
- 別のチームが作成したバッチジョブが毎晩実行され、削除しようとしているテーブルから読み取る。
- レポートツールが、誰もメンテナンスしていないダッシュボードのために古いカラムをクエリする。
- 監視スクリプトが特定のテーブルをチェックして、データの鮮度を確認する。
- サードパーティのインテグレーションが、古いスキーマに書き込むエンドポイントにデータを送信する。
これらはアプリケーションコードには見えません。リポジトリのgrepにも表示されません。それらが壊れたときに初めて見えるようになります。
最悪なのは、これらの障害の中には静かなものがあることです。削除されたカラムを参照するクエリは、すぐにはクラッシュしないかもしれません。NULL値や空の結果セットを返す可能性があり、消費側のシステムはそれを有効なデータとして解釈するかもしれません。結果として、破損したレポート、誤ったダッシュボード、または誤った出力を静かに生成するデータパイプラインができあがります。誰かが気づく頃には、根本原因は下流の処理の層の下に埋もれています。
不可逆的な変更はリスクを増幅させる
スキーマを直接削除することの根本的な問題は、それが不可逆的であることです。カラムやテーブルが一度なくなると、それを元に戻す唯一の方法はデータベースの完全リストアです。つまり、ダウンタイムが発生します。データ損失の可能性があります。チームは何かを迅速に修正するプレッシャーにさらされ、まさにミスが起こりやすい状況になります。
新しいカラムを追加することと比較してみてください。追加は可逆的です。何か問題が発生した場合、追加したばかりのカラムを削除できます。削除はそうではありません。一度削除を実行すると、簡単にはロールバックできない道にコミットしたことになります。
この非対称性こそが、経験豊富なチームがスキーマ削除を単一のアクションではなく、複数ステップのプロセスとして扱う理由です。すべての消費者が移行したと確信できるまで、古いスキーマを削除しません。そして、その確信を仮定ではなく観察によって構築します。
より安全なアプローチ:Expand(拡張)してからContract(縮小)する
古いスキーマを削除して何も壊れないことを願う代わりに、より良いパターンが存在します。これには2つのフェーズがあります。
以下のフローチャートは、2つのパスを対比しています。
まず、拡張(Expand)します。古いものを維持したまま、新しいカラムまたはテーブルを追加します。両方の構造が同時に存在します。アプリケーションコードは、両方に書き込むか、必要に応じて古いものにフォールバックしながら新しいものから読み取るように更新されます。このフェーズでは、エラーを監視し、データが正しく書き込まれていることを確認し、すべての消費者が新しい構造を使用していることを確認します。
次に、縮小(Contract)します。古いスキーマに依存するものが何もないという証拠が得られたら、それを削除します。これは推測ではありません。ログ、メトリクス、クエリ分析により、古いカラムが妥当な期間アクセスされていないことが示されています。その場合にのみ、それを削除します。
このパターンはExpand-Contractと呼ばれ、後方互換性のないスキーマ変更を安全に行うための標準的なアプローチです。時間はかかりますが、単純なクリーンアップが全員参加のデバッグセッションに変わるような本番インシデントを防ぎます。
以下のSQLスニペットは、リスクの高い1ステップ削除と、より安全なマルチステッププロセスを対比しています。
-- 危険: カラムを即座に削除
ALTER TABLE users DROP COLUMN old_plan;
-- 安全: Expand-Contract アプローチ
-- ステップ1: 新しいカラムを追加
ALTER TABLE users ADD COLUMN new_plan VARCHAR(50);
-- ステップ2: 古いカラムから新しいカラムにデータをバックフィル
UPDATE users SET new_plan = old_plan WHERE new_plan IS NULL;
-- ステップ3: 両方のカラムに書き込むようにアプリケーションを更新
-- (コードで処理。SQLではない)
-- ステップ4: 古いカラムへの読み取りがないことを確認した後、削除
ALTER TABLE users DROP COLUMN old_plan;
スキーマ削除前の実用的なチェックリスト
カラムやテーブルを削除する前に、以下の条件を確認してください。
- すべてのアプリケーションインスタンスが、少なくとも1回の完全なデプロイサイクルにわたって新しいコードを実行している。
- 本番環境で、少なくとも1週間、古いスキーマを参照するクエリがなかった。
- 古いスキーマを使用する可能性のあるすべてのバッチジョブ、レポート、およびインテグレーションが更新または廃止されている。
- データ移行が完了し、履歴レコードも含めて検証されている。
- データベースの完全リストアを必要としないロールバック計画が存在する。
これらの条件のいずれかが満たされていない場合、削除の準備はできていません。
まとめ
データベースのカラムを削除することは、クリーンアップタスクではありません。不可逆的な結果を伴う本番変更です。安全に行う方法は、誰も必要としなくなったという証拠が得られるまで、古いスキーマを生かしておくことです。その証拠を集めるには時間がかかりますが、以前は正常に動作していた機能が壊れたという夜中の電話を避ける唯一の方法です。