pgLoaderを使用したMySQLからPostgreSQLへの移行:深夜2時のサバイバルガイド

Database tutorial - IT technology blog
Database tutorial - IT technology blog

深夜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 句を使用してチャンクごとに移行してください。これにより、重要度の低いログテーブルでの失敗によって、コアとなる userstransactions テーブルの移行がロールバックされるのを防ぐことができます。

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修正に追われる長い夜との分かれ道になります。

Share: