
はじめに
本記事は BASE アドベントカレンダー 2025 の 14 日目の記事です。
BASE BANK Dept で フルサイクルエンジニア をしている 02 です。
2025年4月、BASEは新しい振込申請機能「最速振込」をリリースしました。最短10分、土日祝日を含む365日対応での入金が可能になり、ショップオーナーさんのキャッシュフロー改善に大きく貢献しています。
本記事では、最速振込の実装で使用したスキーマ変更とMySQL INSTANT DDLを活用したマイグレーションについて解説します。なお、テーブル名・カラム名は説明用に簡略化しています。
プロジェクト管理やリーダーシップの観点、振込申請や最速振込の詳細については、以前の記事「最速振込の舞台裏:プロジェクトのリードの実践と学び」で紹介しました。よろしければそちらもご覧ください。
既存のテーブル設計課題と新しいカラム
最速振込を追加する前、BASEの振込申請には通常振込、お急ぎ振込、定期振込の3種類がありました。当時、振込種別は複数のテーブルを参照して判定していました。
- 通常振込:振込手数料テーブルのお急ぎ振込フラグがOFF
- お急ぎ振込:振込手数料テーブルのお急ぎ振込フラグがON
- 定期振込:定期振込ログテーブルにレコードが存在
しかし、この設計には課題がありました。
- 振込種別を判定するために複数テーブルをJOINする必要がある
- フラグでは2種類しか表現できず、お急ぎ振込か否かしか判断できない
- 新しい振込種別を追加するたびに判定ロジックが複雑化する
これらの課題を解決するため、振込申請テーブルに振込種別カラムを追加しました。
transfer_type varchar(20) COMMENT '振込種別(通常振込: normal、お急ぎ振込: express、定期振込: scheduled、最速振込: instant)'
これにより、振込種別の判定が振込申請テーブルの1カラムで完結します。カラム追加には、MySQLのINSTANT DDLを活用しました。
MySQL INSTANT DDLの活用
INSTANT DDLとは
MySQL 8.0.12で導入されたALTER TABLEのアルゴリズムです。従来のCOPYやINPLACEと異なり、メタデータの更新のみで操作が完了するため、非常に高速です。
| アルゴリズム | 動作 | 特徴 |
|---|---|---|
| COPY | テーブルをコピーして入れ替え | テーブルロックが発生、最も遅い |
| INPLACE | その場で変更(内部的には再構築) | DML操作は可能だが、時間がかかる |
| INSTANT | メタデータのみ更新 | 非常に高速、テーブルサイズに依存しない |
INSTANT DDLは行データの再配置や再構築を行わず、メタデータだけを更新します。
結果として、アプリケーション視点では接続断やタイムアウト、強制リトライが生じず、読み書きトラフィックを止めないまま変更を完了できます。これが本記事で述べる「完全無停止」の根拠です。
MySQL 8.0.29以降、INSTANT DDLは以下の操作に対応しています。
- カラムの追加(任意の位置に追加可能)、削除
- カラム名の変更
- ENUM/SETへの要素を追加
- DEFAULTの追加、変更、削除
- 仮想カラムの追加、削除
- テーブル名の変更
- インデックスのデータ構造(BTREEやHASH)の変更
ただし、以下のような制限があります。
- INSTANTのアルゴリズムに対応していない構文との併用はできない
ROW_FORMAT=COMPRESSEDのテーブルでは使用できないFULLTEXTインデックスを含むテーブルでは使用できない- INSTANT DDLの操作回数には上限があり、テーブルごとに64回(超過時はOPTIMIZE TABLEでリセット)まで
詳細については、MySQLの公式ドキュメントをご参照ください。
なぜINSTANT DDLを選んだのか
振込申請テーブルは数百万件のレコードを持つ、追加・更新頻度の高いテーブルです。従来のINPLACE DDLでは長時間のロックが発生するため、メンテナンス時間の確保か、gh-ostを使用したカラム追加が必要だと考えていました。
しかし、INSTANT DDLを使用すればサービスへの影響を最小限に抑えられ、メンテナンス時間も不要です。さらに、マイグレーション手順を適切に設計すれば、DDLの実行だけでカラム追加が完結します。
本番相当のデータ量を持つ検証環境で検証した結果、無停止で実現できると確信しました。
- INPLACE DDL: 3分ほど
- INSTANT DDL: 374ms
マイグレーション手順と安全なカラム追加の戦略
新しいカラムを追加する際、単純にカラムを追加してアプリケーションを更新するだけでは不十分です。過去のレコードとの整合性が取れないため、以下の手順で段階的にマイグレーションを行いました。
なお、今回出てくるクエリは、例として名称は一部置き換えています。
Step 1: DEFAULT句ありでカラム追加
まず、振込種別カラム transfer_type を DEFAULT句 'normal'(通常振込)で追加します。これにより、既存レコードの transfer_type には自動的に 'normal' が設定されます。
ALTER TABLE 振込申請テーブル ADD transfer_type varchar(20) DEFAULT 'normal' COMMENT '振込種別', ALGORITHM=INSTANT, LOCK=DEFAULT;
'normal'(通常振込)をデフォルト値として選んだのは、通常振込が最も多く使われている振込種別だからです。Step 4で過去データをマイグレーションする際、更新するレコード数を最小限に抑えられます。最もレコード数の多い通常振込を更新対象から除外できるため、この方法が効率的だと判断しました。
Step 2: アプリケーションの対応
振込申請作成時に振込種別カラムを設定するようアプリケーションを更新します。
- 通常振込作成時:
transfer_type = 'normal' - お急ぎ振込作成時:
transfer_type = 'express' - 定期振込作成時:
transfer_type = 'scheduled'
Step 3: DEFAULTを検知用の値に変更する
次にDEFAULT句を検知用の値である 'unsupported' に変更します。
ALTER TABLE 振込申請テーブル ALTER transfer_type SET DEFAULT 'unsupported', ALGORITHM=INSTANT, LOCK=DEFAULT;
この変更後、transfer_type = 'unsupported' のレコードが発生しないことを監視します。もし発生した場合は、対応漏れがあることを意味するため、アプリケーションコードを修正します。
Step 4: 過去データのマイグレーション
レコードを監視して対応漏れがないことを確認したら、過去のレコードを関連テーブルの情報をもとに更新します。
-- お急ぎ振込の過去レコード(振込手数料テーブルのお急ぎ振込フラグを参照) UPDATE 振込申請テーブル LEFT JOIN 振込手数料テーブル ON 振込申請テーブル.振込申請id = 振込手数料テーブル.id SET 振込申請テーブル.transfer_type = 'express' WHERE 振込手数料テーブル.お急ぎ振込フラグ = 1; -- 定期振込の過去レコード(定期振込ログテーブルの存在を参照) UPDATE 振込申請テーブル LEFT JOIN 定期振込ログテーブル ON 振込申請テーブル.id = 定期振込ログテーブル.id SET 振込申請テーブル.transfer_type = 'scheduled' WHERE 定期振込ログテーブル.id IS NOT NULL;
Step 1でDEFAULT句を追加した際、既存レコードのtransfer_typeには自動的に'normal'が設定されています。そのため、通常振込の過去レコードについてはUPDATE文を実行する必要はありません。
Step 5: 参照ロジックの切り替え
最後に、振込種別の判定ロジックを新しいカラムtransfer_typeを参照するように切り替えます。また、任意のタイミングでDEFAULT句を削除します。
ALTER TABLE 振込申請テーブル ALTER transfer_type DROP DEFAULT, ALGORITHM=INSTANT, LOCK=DEFAULT;
こういった手順で振込申請テーブルに振込種別カラムを追加しました。
おわりに
最速振込の実装では、MySQL INSTANT DDLを活用することで、更新頻度の高い数百万行のテーブルへ、メンテナンスなしでスキーマ変更を実現できました。
MySQL 8.0.29以降を使用している方は、ぜひINSTANT DDLの活用を検討してみてください。
BASE BANK Deptでは、プロダクト開発をリードできるエンジニアを募集しています。興味のある方は、ぜひ採用情報をご覧ください!
明日は、BASEアドベントカレンダーは @capi さんです!お楽しみに!