はじめに
今は MySQL Shell dump utilities などのより高速な手段が推奨されつつありますが、MySQLの論理バックアップに mysqldump を使った場合、以下のようなダンプファイルが出力されます。
-- MySQL dump ...
--
-- Host: 127.0.0.1 Database: appdb
-- ------------------------------------------------------
-- ...
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
--
-- Table structure for table `users`
--
DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `users` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`login_id` varchar(64) NOT NULL,
`email` varchar(255) DEFAULT NULL,
`full_name` varchar(128) DEFAULT NULL,
`tel` varchar(32) DEFAULT NULL,
`profile` json DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `users_login_id_uq` (`login_id`),
KEY `users_email_idx` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `users`
--
LOCK TABLES `users` WRITE;
/*!40000 ALTER TABLE `users` DISABLE KEYS */;
INSERT INTO `users` VALUES (1,'alice','alice@example.com','山田 太郎','090-1111-2222','{\"lang\": \"ja\"}','2026-01-05 12:00:00'),(2,'bob','bob@example.com','鈴木 花子',NULL,'{\"lang\": \"en\"}','2026-01-06 09:30:00');
/*!40000 ALTER TABLE `users` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
見ての通り、普通のDDL(CREATE TABLE) + DML(INSERT INTO)の構成ですが、行数が多い場合、これのロードの遅さは並大抵ではありません。
また、ロードデータについて、PII(個人情報)をマスクするなど後ろの加工工程があると更に遅くなります。 加えてPIIマスクをSQLで表現していると複雑な型を壊さずマスクしたい場合など、マスク表現力も厳しくなってきます。
担当システムでのバックアップ復元試験工程ではこれらで2時間をオーバーすることもしばしばでした。
今回は バックアップ形式を一切変更せず、これをAIを用いて 高速化 していこうと思います。(ちなみに 最終的には2時間 → 30分になりました )
高速化のために行えること
結論からいうと、以下を複合的に行うことでデータロードが高速になりました。
- DDLを最初に流し、全テーブルからINDEXを剥がして、データロード完了後に付け戻す
- 挿入の都度インデックス更新が走るのは単純に無駄です。データを入れ終わってから、ソート済みの状態でまとめて構築するのが一番無駄がありません。
- ちなみに、改善後のデータロードで一番遅いのもここの部分です。総じてインデックスの構築・維持にはコストがかかります。
- PIIを後からマスクせず、流し込む時にマスクする
- 一旦流し込んだ後の巨大テーブルへのUPDATE操作はトランザクションログ書き込みも発生し(一応DBの設定で緩和はできますが)非常にコストが高いです。流し込む時に加工するのが一番効率がいいでしょう。
- INSERT文をやめて、
LOAD DATA文にする- MySQLに複雑な構文パージングをやめさせ、ほぼダイレクトデータとして取り込むことにより高速化します
- (効果小) バックアップ置き場のS3から ローカルにダウンロードしないでストリーミングのまま扱う
- S3からのコピーをローカルに保存せず、そのままネットワークストリーミングで処理します。無駄なディスク書き込みが減るほか、ローカルにPIIマスクされていないバックアップデータも残らないので一石二鳥です。
処理の流れはこうなります。
'Reader::table_data' はMySQLの標準構文ではなく、Go の go-sql-driver/mysql が提供する機能です。mysql.RegisterReaderHandler で登録した任意の io.Reader を LOAD DATA LOCAL INFILE の入力として流し込めます。なお、検証は MySQL 8.x で行っています。
とはいえ、実装が難しい
高速化のための方法を示しましたが、論理的に出来ることは分かるものの、言うは易く行うは難しで、これらの実際の実装はかなり難しいものです。
特にPIIのマスクと LOAD DATA への変換が難しいです。流れてくるINSERT文を ストリーミングで まともに パージング しつつ 書き換え なければなりません。
INSERT INTO `users` VALUES (1,'alice','alice@example.com','山田 太郎','090-1111-2222','{\"lang\": \"ja\"}','2026-01-05 12:00:00'),(2,'bob','bob@example.com','鈴木 花子',NULL,'{\"lang\": \"en\"}','2026-01-06 09:30:00');
-- ↓↓↓↓ 上記を読み取りつつ以下で出力してMySQLに流す ↓↓↓↓
LOAD DATA LOCAL INFILE 'Reader::table_data' INTO TABLE `users`
CHARACTER SET binary
FIELDS TERMINATED BY '\t' ESCAPED BY '\\' LINES TERMINATED BY '\n'
(`id`,`login_id`,`email`,`full_name`,`tel`,@v5,`created_at`)
SET `profile` = CONVERT(@v5 USING utf8mb4);
-- ↑の 'Reader::table_data' が読むデータ本体(タブ区切り。email は復元ごとのランダム鍵で HMAC-SHA256、full_name/tel は <MASKED:文字数> に置換済み)
1 alice 7cefb62898447f475b6a3b2ac74caa03305a44f9f3a8b2f027843a42caa377db@example.com <MASKED:5> <MASKED:13> {"lang": "ja"} 2026-01-05 12:00:00
2 bob 424ae02593af77e9422192494823263a41b44788fc99491cb5a08a3ba1a9f2ab@example.com <MASKED:5> \N {"lang": "en"} 2026-01-06 09:30:00
では、どのように実装したか?
以前 開発用適当ツールはGoで作るのがオススメ という記事を書いたのですが、手軽さ・速度の面から、これらを行うGoツールをClaude Codeにコーディングさせました。Goであれば go run ./cmd/path-to-main だけで依存性DL〜ビルド〜動作まで完了します。
内容としては、INSERT文を必要に応じて正確にパースして、かつタプル部分の位置とカラムの対応も見ながらレストアするプログラムになります。
パース能力的にはMySQL INSERT についての lexer があれば十分ですが、Goには今のところストリーミング処理向きの丁度いいMySQL用 lexer が存在しませんでした(TiDB parser や vitess の sqlparser といったフルパーサはありますが、文全体をASTに組み立てる方式のため、巨大なINSERT文のストリーミング処理には不向きです)。なので、lexer込みでストリーミング処理を実装してもらいました。当然、大量のユニットテストやサンプルデータをテストハーネスとして用意させながら実装してもらいます。
とはいえ、本用途だと必要なのは「文字列リテラルがどこで終わるか」と「タプル (...) 内の値の境界がどこか」の2つだけなので、かなりコンパクトな実装になりました。また補助情報として information_schema もロードして使います。
INSERT INTO `users` VALUES (1,'alice','alice@example.com','山田 太郎',NULL);
↓ lexer が返すもの(テーブル名と、各値の範囲・種別だけ。値の中身は読まない)
table = users
pos=0 [28,29) Number
pos=1 [30,37) String
pos=2 [38,57) String
pos=3 [58,73) String ← full_name 列。ここだけ置換する
pos=4 [74,78) Null
AIがあればこういったパージングありのツールを書くのもそこまで苦労せずに終わってしまいます。
ちょっとした実装上のポイント
- 古い方式によるレストア結果をオラクルとして、新しい方式によるレストア結果を比較する
- こういったスナップショット比較のハーネスがあるとAIのほうで勝手に改善できます。AI実装の際は必ず意識しましょう
-
LOAD DATA絡みの罠- ダンプの明示的設定タイムスタンプ:UTC ⇔
LOAD DATAを解釈するMySQLのタイムゾーン: JST でズレます -
CHARACTER SET binaryにしないと壊れる列があったり、工夫しないとJSONが壊れたりします - エラーで壊れた行が暗黙にスキップされる。必ず処理した行数と投入した行数が一致するかチェックしましょう
- ダンプの明示的設定タイムスタンプ:UTC ⇔
- とにかく想定外の場合はエラーで終了するようにしましょう
その他色々ありますが、おそらく状況によって変わるので、AIに試行錯誤してもらいましょう。
まとめ 〜 この記事の意義
以上、AIでインフラツールを作成し、MySQLのダンプロードを理想的に変更した事例の紹介でした。
冒頭でも触れた通り、様々な工夫の結果、2時間超から30分にダンプロードスピードが向上し、長年不便に思っていたところも機能追加することができました。
実装詳細は多すぎるので書き切れていませんが、この記事をClaude等に読ませて実装すれば、だいたい同じ結果が得られると思います。
そもそも現時点でAIコーディングしている人がほとんどでしょうから、手法を紹介する技術記事はAIに向けて書くことが今後は主流になっていくのではないでしょうか。