【初心者向け】カラム追加で始末書を書かないためにやるべきこと

# はじめに
初めて携わったプロジェクトで知識・経験がなくて大量データが入ったテーブルへのカラム追加でやらかしちゃったことがあります。
今となっては同じ失敗は二度としなくなりましたが、当時は何も知らなかったので、いま新人の人たちが私と同じような失敗をしないよう失敗談を記事としたいと思います。
※この記事は実話をもとにしたフィクションです。本当に始末書を書かされるかは分かりません。やらかし度合いによると思います

# 動作確認環境
- PHP 8.0
- Laravel 8.0
- MySQL 8.0

※下位環境でも動作する場合がございます

# 失敗談

カラム追加は今までに何度もやったことがあったので、いつものようにマイグレーションを書き、機能実装してました。

```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class Test extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->bigInteger('test_id')->unsigned()->comment('test');
        });
    }
}
```

私：よしっ！ローカル環境で動作確認、マイグレーションを実行！
（すぐにマイグレーションが完了）
私：あ、カラム位置が間違えてた。修正しよう...。

```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class Test extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->bigInteger('test_id')->unsigned()->after('name')->comment('test');
        });
    }
}
```

私：よしっ！もう一度ローカル環境で動作確認、マイグレーションを実行！
（すぐにマイグレーションが完了）
私：Laravelはカラム追加も楽ちんだなぁ。プルリクエスト作成してタスク完了！

...数日後。

私：ステージングでも動作確認。カラムがちゃんと追加されていること確認！これで準備万端！
私：次のメンテナンスも問題なさそう...。一安心。

偉い人：「次のアップデート、どれくらいの作業時間かかりそう？いつもと同じで大丈夫そう？」
私：「はい、（作業としてはカラム追加しちょっとロジック調整しただけの修正で影響も少ないし）大丈夫です。」

...アップデート当日。

偉い人：「メンテナンス入ります。作業お願いします。」
私：「はーい。マイグレーション実行！」

...10分後。

私：「あれ？完了にならない。操作間違えたかな...。」
（間違えてないよね）
私：「（そわそわ）」

...30分後。

私：「あれまだ終わらない...。」
偉い人：「そろそろ作業終わった？」
私：「実はまだ終わってなくて...。もう少しだけ時間もらえますでしょうか？」

...10分後。

偉い人：「そろそろ作業終わった？」
私：「実はまだ終わってなくて...。」
偉い人：「え！どういうこと？あとどれくらいかかるか教えて」
私：「（え...。なんで終わらないの？！）」
私：「すみません。本番サーバーの影響からなのか、時間かかっていて、まだもう少しかかりそうです。完了目処未定でお願いします」
偉い人：「え！どういうこと？！今日は大した作業ないはずじゃ...。」
偉い人：「メンテナンス延長しておくけれど、あとで報告書ちょうだい。」
私：「はい、申し訳ございません。できる限り早く作業終わるように努めます」
（でも今から止めることもできず、ただ待つばかり...）

# 原因

ベテランの方であれば、すぐにピンとくるかと思いますが、無知ゆえに「大量データが入ったテーブルへのカラム追加」にこれほどの時間がかかると思ってはいませんでした。
ローカルでもステージングでもすぐにマイグレーションが完了していたので、本番の多いデータ量でも長くても数分くらいと思ってました。

# 計測

では、カラム追加にはどれくらいの時間がかかってしまうのか。
今回この記事作成にあたり、改めて計測してみました。

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f313439303736372f32643464326134622d343464322d646164362d346363312d3962623731626661313833642e706.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1490767/c1ecb432-bbdf-4d2a-d4ca-9c6b95f43885.png)

```bash
Migrating: 2021_06_26_010052_hogehoge
Migrated:  2021_06_26_010052_hogehoge (1,607,466.00ms)

real    26m49.743s　← 約26分
```

※累計1500万レコードで計測。Indexの量やカラム構成、スペックによっても経過時間は変化します。


# 教訓

- カラム追加を行う場合は、実行にかかる時間を本番と同じレコード数で想定しておく
    - 理想は、本番と同じデータ量のコピーを別途用意し、事前にマイグレーション実行時間を計測しておく
- 本番サーバーへのカラム追加は容易にはできないことを意識し、初期設計をしっかりとしておく
- マイグレーションを行う場合は長めのメンテナンスを実施する

# おまけ知識
カラム追加ですが、カラムの一番後方に追加する場合は劇的に早く追加することができます。
1500万レコードが入っているテーブルへのカラム追加であっても数秒で終わります。

```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class Test extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->bigInteger('test_id')->unsigned()->comment('test');
        });
    }
}
```

# おまけ知識2
カラム追加は、Indexなどが付いているテーブルなほど時間がかかります。
そこで、カラム追加時のみその制限を外すことで少しだけ時短することができます。
当記事でのサンプルデータでは、数分の短縮が見込めました。

```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class Hogehoge extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        DB::statement('SET UNIQUE_CHECKS=0');
        DB::statement('SET FOREIGN_KEY_CHECKS=0');

        Schema::table('users', function (Blueprint $table) {
            $table->bigInteger('test_id')->unsigned()->after('name')->comment('test');
        });

        DB::statement('SET UNIQUE_CHECKS=1');
        DB::statement('SET FOREIGN_KEY_CHECKS=1');
    }
}
```

# 関連記事

https://docs.oracle.com/cd/E17952_01/mysql-8.0-ja/optimizing-innodb-bulk-data-loading.html

https://qiita.com/hondy12345/items/4ebf7130bef833d0a3ea

https://ukonlly.hatenablog.jp/entry/2021/02/11/235200

https://ysh.hateblo.jp/entry/2016/02/19/220556