古いデータベースカラムを安全に削除できるタイミングとは?Expand-ContractパターンのContractフェーズ

新しいカラム形式にアプリケーションコードをすべて移行しました。デプロイはスムーズに完了し、ログにもエラーはありません。チームは古いスキーマを掃除して、次の機能に進む準備ができています。

しかし、今すぐそのカラムを削除すべきでしょうか?

答えはほぼ常に「いいえ」です。古いデータベース構造を早すぎるタイミングで削除することは、突如として発生するように見える本番インシデントの最も一般的な原因のひとつです。月に一度実行されるバッチジョブ、6ヶ月前に誰かが書いたレポートクエリ、四半期末の処理中にだけ起動するレガシーサービス——これらはすべて、実際に実行が必要になる瞬間まで静かに失敗し続ける可能性があります。

Expand-Contractパターンの最終フェーズはContract(収縮)フェーズと呼ばれます。この名前は、もはや使用されていない構造を縮小または削除するという考えに由来します。しかし、このフェーズに到達するには、メインアプリケーションが切り替わったことを確認するだけでは不十分です。

他に誰が古い構造に触れているのか?

何かを削除する前に、ひとつの質問に答える必要があります。それは、「古いスキーマに対して、他に誰が読み取りや書き込みを行っているのか?」です。

主要なアプリケーションは移行済みかもしれません。しかし、考慮していなかった他のコンシューマーが存在する可能性があります。

  • 夜間に実行され、まだ古いカラムを参照しているバッチジョブ
  • データチームがアドホック分析のために実行する手動クエリ
  • 古いテーブルを使って毎月PDFを生成するレポートシステム
  • 6ヶ月前にデプロイされ、その後更新されていない内部ツール
  • 古いフォーマットでデータを送信するサードパーティ連携

これらの依存関係は見落とされがちです。なぜなら、それらはメインアプリケーションと同じデプロイサイクルに従わないからです。同じリポジトリにすら存在しないかもしれません。中にはコードですらないものもあります——誰かのラップトップに保存されたSQLスクリプトかもしれません。

隠れた依存関係を検出する方法

これらの依存関係を見つける最も簡単な方法は、データベースログを確認することです。PostgreSQL、MySQL、Oracleなどのデータベースは、実行されたクエリを記録します。削除予定のカラムやテーブルを参照するクエリをフィルタリングできます。

以下のフローチャートは、古いカラムを削除しても安全かどうかを判断するための意思決定プロセスをまとめたものです。

flowchart TD A[古いカラムの削除を計画?] --> B{バッチジョブはあるか?} B -->|はい| C[まだ削除しない] B -->|いいえ| D{レポートクエリはあるか?} D -->|はい| C D -->|いいえ| E{内部ツールはあるか?} E -->|はい| C E -->|いいえ| F{サードパーティ連携はあるか?} F -->|はい| C F -->|いいえ| G[カラムを削除]

データベースがデフォルトでクエリをログに記録しない場合は、一時的に有効にできます。PostgreSQLにはクエリ統計を追跡するpg_stat_statementsがあり、MySQLにはPerformance Schemaがあります。どちらも、どのクエリがまだ古い構造に触れているかを確認できます。

2つ目のアプローチは、アプリケーションの切り替え後にエラーを監視することです。何かがまだ古いカラムに書き込もうとしている場合、アプリケーションログにエラーが表示される可能性が高いです。ただし、これはアクティブなコンシューマーしか捕捉できません。四半期に一度実行されるスクリプトは、数ヶ月間エラーを引き起こさないかもしれません。

これが、削除前の待機期間が重要である理由です。すべてのアプリケーションが切り替わってから、1回の完全なデプロイサイクル(通常2週間)を待つチームもいます。特に手動クエリや非技術チームが実行するレポートがある場合、さらに長く待つチームもいます。普遍的なルールはありませんが、制御不能な依存関係が多ければ多いほど、待機期間を長くすべきです。

実際の削除

依存関係が残っていないと確信できたら、削除を進めます。SQLコマンドは単純です。

以下は、実行するSQLの実践的な例と、他のデータベースオブジェクトがそのカラムに依存していないことを確認するクエリです。

-- まず、そのカラムを参照しているビュー、関数、トリガーがないか確認
SELECT DISTINCT
    OBJECT_SCHEMA,
    OBJECT_NAME,
    OBJECT_TYPE
FROM INFORMATION_SCHEMA.ROUTINES
WHERE ROUTINE_DEFINITION LIKE '%old_column_name%'
UNION
SELECT
    TABLE_SCHEMA,
    TABLE_NAME,
    'VIEW'
FROM INFORMATION_SCHEMA.VIEWS
WHERE VIEW_DEFINITION LIKE '%old_column_name%';

-- 安全が確認できたら、カラムを削除
ALTER TABLE users DROP COLUMN old_legacy_status;

-- 古いカラムのためだけに存在していたインデックスをクリーンアップ
DROP INDEX IF EXISTS idx_old_status ON users;

最初に依存関係チェックを実行し、その後でDROPを実行します。チェックで行が返された場合、安全に削除する前に更新が必要な隠れたコンシューマーが見つかったことになります。

  • カラムの場合: ALTER TABLE ... DROP COLUMN
  • テーブルの場合: DROP TABLE
  • 制約の場合: ALTER TABLE ... DROP CONSTRAINT

しかし、順序が重要です。まず使用される可能性が最も低いカラムやテーブルを削除し、数日待ってから残りを削除します。この段階的アプローチにより、セーフティネットが確保されます。最初の削除後に何かが壊れた場合でも、より重要な構造が削除される前に対応する時間があります。

インデックスとトリガーを忘れずに

よく見落とされるのが、関連するデータベースオブジェクトのクリーンアップです。古いカラムのためだけに存在していたインデックスは削除すべきです。古いテーブルを参照するトリガーも削除します。これらを残しておくと、データベースにデッドウェイトが蓄積され、書き込みが遅くなり、将来のマイグレーションが複雑になります。

Contractフェーズを完了する前のクイックチェックリスト:

  • アクティブなクエリが古い構造に触れていないことを確認(少なくとも1回の完全なサイクルでログを確認)
  • すべてのバッチジョブ、レポート、手動スクリプトが更新されていることを確認
  • 古いカラムのみにサービスを提供していたインデックスを削除
  • 古いテーブルを参照するトリガーを削除
  • 段階的に削除:リスクが最も低いものから始め、待機し、その後続行
  • 各削除後、24〜48時間アプリケーションエラーを監視

本当の教訓

Contractフェーズはスピードに関するものではありません。確信に関するものです。古いスキーマを早すぎるタイミングで削除すると、検出が手遅れになるまで難しいリスクが生じます。追加の待機期間は無駄な時間ではなく、存在すら知らなかった隠れた依存関係に対する安全バッファーです。

古いスキーマが削除されれば、Expand-Contractパターンは完了です。新しい構造を追加し、すべてのコンシューマーを移行し、本番インシデントを引き起こすことなく古い構造を安全に削除しました。それがこのパターンの要点です——チームがデータベース変更を恐れなくて済むように、安全なものにすることです。