発明のための再発明

内部動作のような、プログラムを書くときの参考になることを書きます

DBマイグレーションを行う技術

データベースのスキーマを変更するということはデータをいじる行為であり、最悪の場合データが消えます。
最悪の事態にはならなくとも、思わぬ場所に影響が起きたり、データの不整合が発生する恐怖と戦う必要が有ります。
テストや切り戻しを含めて計画し、大きな変更の場合にはダウンタイムまで考慮する必要があります。

そこで、RDBを対象にデータベースの変更を行う方法について書いていきます。

スキーマ変更

まずは、スキーマ変更について、

カラムを追加する

一番簡単で、影響も少ない変更です。
気をつけるのは、

といったところでしょうか。
大抵の場合は、スキーマの変更とソースコードの変更の順番にさえ気をつければ問題は発生しません。

カラム名を変更する

「ALTER」でさくっと変えたくなりますが、ソースコードの変更が同時に行われるわけではないので「カラムが見つからない」系のエラーが出てしまいます。
これを防ぐには、

  1. メンテナンス期間を設けて、システムを止める
  2. 新カラムを作成した後、元カラムを削除する

の2通りがあります。
前者の場合、コード修正は簡単なので開発コストはかからないのですが、メンテナンス期間が必要となることから、「メンテナンス期間はどの程度か」、「周知はどのように行うか」と言った、開発とは別の問題が出てくる上、「そこまでする価値はあるのか」というリファクタリングに対する疑問まで出てくるため、多くの人間を巻き込む必要が発生します。
しかも、説得する時間が長くなると開発側からも「コストが大きすぎる」という理由で変更が嫌煙されるようになってしまいます。
なので、個人としては、コード変更は面倒になるものの、後者が好みです。

後者の手順は、

  1. 新カラムを追加する
  2. トリガーを用いて旧カラムが更新された場合に、新カラムを更新するようにする
  3. 全行の新カラムに旧カラムの値を代入する
  4. 新カラムから読むようにコードを変更する
  5. 新カラムへ書き込むようにコードを変更する
  6. 旧カラムとトリガーを削除する

トリガーを用いることで、旧カラムと新カラムのどちらに書き込まれても新カラムが更新されるようになります。
トリガーではなくアプリケーションが両方のカラムに書き込みを行うようにするという対処も可能です。

カラムの型などを変更する

ALTERで大丈夫なら、ALTERを実行します。ダメなら、カラム名変更と同じ手順を踏む必要が有ります。
つまり、

  1. ALTERを使用する
  2. メンテナンス期間を設けて、システムを止める
  3. 新カラムを作成した後、元カラムを削除する

の方法があります。
ALTERが使える状況は、

  • NULL制約の変更
  • DEFAULT値の変更
  • 文字数制限やintからlongへの変更

のような、影響が少ない時です。
ただし、影響が少なくともロックが長くなることはあるので、事前に時間を計算する必要が有ります。
ロックが長くなるようであれば、ロックを短くする方法を模索するか、列追加を用いて変更するかで対処しましょう

カラムを削除する

DELETEを突然打ってはいけません。
カラムが突然消えると、「カラムが見つからない」系のエラーが起きるので、アプリケーションがカラムを使用していないことを確認してから、削除する必要が有ります。
手順は、

  1. カラムからの読み込みを止めるようにコードを変更する
  2. カラムへの書き込みを止める
  3. カラムを削除する

です。

  • リリース中は新旧のコードが同時に動く
  • ロールバックできなくなる

という理由から、最初に読み込みだけをやめ、その後に書き込みをやめることが必要です。
ただ、既に使われていないカラムである場合は、勇気を持っていきなりDELETEするしかありません。

テーブルの名前を変える

カラム追加と同じで、

  1. メンテナンス期間を設けて、システムを止める
  2. 新テーブルを作成した後、元テーブルを削除する

の2通りです。
読み込みや書き込みの変更範囲、外部キーの変更が大きくなりますが、手順は変わりません。

テーブルを削除する

これも、カラム削除と同じです。

大量にデータがあるテーブルに対する変更

データ量が大きいことは、ただそれだけで問題を引き起こします。
注意点として、実行時間の予測に本番データをコピーした環境を使った場合には、本番では別処理が走っているせいで、コピーした環境よりも遅くなることを考慮する必要が有ります。

大量データに対する基本方針としては、

  • ロックを長時間かけない
  • 徐々に移行する

です。
例えば、DEFAULT値のついたカラム追加をすると、既存の全行に対して更新がかかるせいで時間がかかるけれど、DEFAULT値をつけなければ処理が殆ど無いので速いということが有ります。この場合には、一旦nullを許可したカラムを追加してUPDATEでデータを代入した後にDEFAULT値を入れると良いでしょう。
また、UPDATEを適当な行数(例えば1,000行)づつ行うことで、他の処理への影響を少なくすることができます。
他にも、dbによっては列を無効にすることで参照出来なくすることが出来ることもあります。

本当に辛い時は、別テーブルを用意して全行コピーすることが必要になることも有ります。

切り戻しを高速化する

どんなに注意しても、切り戻しが必要になることは避けられないので、切り戻しがしやすい変更方法を検討します。
次のような方法を取ることで、1世代前のコードにすぐ戻せるはずです。
ただし、防御策は作業量が多くなる傾向があるので、問題が発生しなさそうな変更は「ダメだったらシステム停止する」という思い切りも必要です。

前のデータを待機させる

データを消さなければ、万が一必要だった場合にもソースコードを戻すことで運用が続けられます。
DBがカラムを無効にする機能を持っているのであれば、無効にしてしまうのが簡単です。
カラムの無効化をDBがサポートしていないのであれば、カラム名を変更することでも、参照が出来なくするように出来ます。

新旧同時に稼働させる

変更前のデータに対しても更新が続けられていれば、万が一変更後のスキーマが使えないことがわかってもソースコードの変更で済みます。
これは、

ことで、出来ます。トリガーのほうが簡単ですが書き込み先を変更した後には戻れなくなるので、両カラムへ書くほうが被害は減ります。

フラグで新旧を切り替える

ソースコード上に、新スキーマを使うものと旧スキーマを使うものを同時に存在させ、フラグでどちらを使用するか制御します。
フラグの置き場所は適当なテーブルを使用すれば十分です。Redisを使ってもいいでしょう。

フラグを使用すると、「リリース」や「切り戻し」というおおげさなものではなくなり、「使いはじめる」という感覚になります。
更に、変更範囲をユーザーやグループ単位に限定し、新旧同時に稼働することが出来るようになるので、影響範囲も限定出来ます。

手順は、

  1. フラグを使用して、新旧を切り替えるコードをリリースする
  2. バグが出ても良いユーザー(社員のアカウントやテスト用アカウント)に対して新スキーマを使用するようにする
  3. バグが出ないことを確認しつつ、新スキーマを使用するユーザーを徐々に拡大する
  4. 全員が新スキーマを使用する
  5. スキーマを使用するコードを削除する
  6. カラムやテーブルを削除する

です。
影響範囲や切り戻しがかなり早くなりました。

複数アプリケーションが同一のDBを使用しているため、影響範囲が測りづらい

最後に、よくある辛い状況について、
複数アプリケーションが同じDBを使っていると気軽な変更が予期しない影響を及ぼすことが有りますね。

頑張って、分離しましょう。
あるいは、共有データを扱う第3のアプリケーションを作成し、既存のアプリケーションはそのアプリケーションを利用して共有データをいじるように変更しましょう。
これが大変なら、もう・・・

まとめ

DBマイグレーションを行う方法を色々書きました。
このような話は、

に載っているので、参考になると思います。
「データベース・リファクタリング」は絶版になってしまっていて、手に入れるのが難しいですが、「Migrating to Microservice Databases」は無料で手に入ります。