深夜2時の移行の悪夢
Slackの通知が始まったのは午前1時45分でした。私たちが数ヶ月前から廃止しようとしていたレガシーなMySQL 5.7インスタンスがついに悲鳴を上げました。CPU使用率は99%に張り付き、コネクションプールはデッドロックされたスレッドの墓場と化していました。優れた並行処理とインデックス作成機能を求めてPostgreSQLへの移行を計画していましたが、そのスケジュールは「来月」から「今すぐ」に変わってしまったのです。
mysqldumpのことは忘れてください。睡眠不足の状態で400GBのデータベースに対して手動で検索・置換を実行するのは、データ破損への近道です。これら2つのエンジンの間にあるセマンティックなギャップを理解するツールが必要です。そこで私たちの本番環境を救ってくれたのがpgLoaderでした。これは単なるデータパイプではなく、スキーマ変換を自動化し、複雑な型マッピングを動的に処理する変換エンジンなのです。
クイックスタート(5分)
スキーマがクリーンであれば、コマンド1つでデータの移動を開始できます。UbuntuやDebianの場合、インストールは簡単です:
sudo apt-get install pgloader
インストール後、ソースとターゲットの文字列を渡すことで、直接移行を試みることができます。以下のようになります:
pgloader mysql://user:password@localhost/source_db \
postgresql:///target_db
このコマンドはスキーマを検出し、Postgresにテーブルを構築し、データをストリーミングします。小規模でシンプルなサイドプロジェクトならこれで十分です。しかし、本番データベースには通常、複雑なリレーションシップや、このワンライナーを失敗させる MySQLの「緩い」制約が含まれています。現実世界の複雑さに対処するには、ロードファイルを使用する必要があります。
ディープダイブ:ロードコマンドファイル
数百万件のレコードを移動する場合、正確さが重要になります。エンコーディングの不一致により5,000万行のテーブルで最初の試行がクラッシュしたとき、私は .load ファイルに切り替えました。この形式を使用すると、キャスティングルールや移行前のクリーンアップをきめ細かく制御できます。
migrate.load という名前のファイルを作成します:
LOAD DATABASE
FROM mysql://db_user:db_pass@source_host/old_db
INTO postgresql://pg_user:pg_pass@target_host/new_db
WITH include drop, create tables, create indexes, reset sequences,
workers = 8, concurrency = 1
CAST type tinyint to boolean drop typemod,
type datetime to timestamptz,
type double to precision
BEFORE LOAD DO
$$ drop schema if exists public cascade; $$,
$$ create schema public; $$
AFTER LOAD DO
$$ ALTER TABLE users ADD CONSTRAINT unique_email UNIQUE(email); $$;
CAST セクションに注目してください。MySQLはデータ型に対して「寛大」なことで有名ですが、PostgreSQLは厳格です。たとえば、MySQLはブーリアン型に tinyint(1) をよく使用します。明示的なキャスティングを行わないと、Postgresがスモールインテジャー(小さな整数)だと思っているカラムに true の値を挿入しようとした瞬間に、アプリケーションがクラッシュします。
移行中、煩雑なCSVファイルに含まれるレガシーな補助データも処理する必要がありました。これらをインポート用に準備するために、toolcraft.app/ja/tools/data/csv-to-json を使用しました。これは完全にブラウザ上で動作するため、機密性の高い本番環境のスニペットがローカルマシンから出ることはありませんでした。私は、ログイン不要で高速、かつプライバシーが守られているため、こうした簡単なフォーマット作業には ToolCraft を頼りにしています。
「ゼロ日付」の悪夢への対処
午前3時の私の悩みの種だった特定のエラーが、MySQLの 0000-00-00 00:00:00 タイムスタンプでした。PostgreSQLは当然ながら、これを無効な日付として拒否します。カラムが NOT NULL に設定されている場合、移行全体が停止してしまいます。単に無視することはできません。
最もスマートな解決策は、pgLoaderのキャスティングルールで直接変換を処理することです:
CAST type datetime when default "0000-00-00 00:00:00" to timestamptz drop default drop not null
巨大なテーブルを扱っている場合は、INCLUDING ONLY TABLE NAMES MATCHING 句を使用してチャンクごとに移行してください。これにより、重要度の低いログテーブルでの失敗によって、コアとなる users や transactions テーブルの移行がロールバックされるのを防ぐことができます。
INCLUDING ONLY TABLE NAMES MATCHING 'users', 'orders', 'products'
これらの変換をデバッグしている間、APIペイロードを確認するために https://toolcraft.app/ja/tools/developer/json-formatter を使用しました。新しいPostgresの構造がフロントエンドの期待するJSON形式を変更していないか確認する必要があったためです。クライアントサイドで動作するため、APIキーや顧客データがサードパーティのサーバーに送られる心配もありませんでした。
最終段階の実践的なヒント
3時間の試行錯誤の末、私たちの朝を救ってくれたチェックリストを作成しました:
- ANALYZEの実行: MySQLとPostgresでは統計情報の扱いが異なります。ロード直後にPostgresで
ANALYZE;を実行してください。これによりクエリプランナーが更新され、結合処理に時間がかかるのを防げます。 - シーケンスの確認: 自動インクリメントIDが同期されているか確認してください。
SELECT last_value FROM your_table_id_seq;を実行し、次のインサートで主キー制約違反が発生しないようにします。 - エンコーディングの標準化: MySQL hostのデータが
latin1のままなら、開始前にutf8mb4に変換してください。pgLoaderで変換も可能ですが、クリーンなUTF-8ソースから始める方がはるかに安全です。 - UUIDによる近代化: 移行中にスキーマを更新する場合は、主キーをUUIDに切り替えることを検討してください。私はステージング環境用のテストIDを素早く生成するために https://toolcraft.app/ja/tools/developer/uuid-generator を使用しました。
午前5時までにデータは同期され、アプリはPostgres上で稼働しました。APIのレイテンシは30%低下し、最も重い結合クエリの時間は800msからわずか45msに短縮されました。pgLoaderを使用することで、エッジケースに集中しながら、退屈な部分を自動化することができました。同様の移行に直面しているなら、適切な .load ファイルの作成に時間を投資してください。それが、切り替えの成功と、手動のSQL修正に追われる長い夜との分かれ道になります。

