【ITニュース解説】How we sped up our rails migration setup in 90%
2025年09月04日に「Dev.to」が公開したITニュース「How we sped up our rails migration setup in 90%」について初心者にもわかりやすいように丁寧に解説しています。
ITニュース概要
Railsプロジェクトで、増え続けたデータベース変更(マイグレーション)処理の遅延を解決した。古い変更をまとめてSQLファイルで一括適用し、データ操作は別で管理。この手法でデータベース構築時間を90%短縮し、新規環境作成を効率化した。
ITニュース解説
この記事は、Ruby on RailsというWebアプリケーション開発フレームワークを使ったプロジェクトにおいて、データベースの変更管理が遅くなるという問題と、その解決策について解説している。Railsでは「マイグレーション」という仕組みを使い、データベースの構造変更(テーブルの作成・変更・削除など)をコードとして管理する。アプリケーション開発を進める中で、このマイグレーションファイルがどんどん増えていくと、特に新しい開発環境を構築したり、ブランチを切り替えたりする際に、データベースの再構築に非常に時間がかかるようになるという深刻な問題が発生していた。
このプロジェクトでは6年分のマイグレーションファイルが蓄積されており、データベースの再構築にはかなりの時間を要していた。また、同じ構造を持つデータベースを複数作成するプロセスも遅くなっており、新しい開発環境を立ち上げるのが困難になっていた。
マイグレーションが遅くなる主な原因は二つある。一つは、Railsのマイグレーションがデフォルトで一つずつ独立した「トランザクション」として実行されることだ。トランザクションとは、一連のデータベース操作を一つのまとまりとして扱い、すべて成功するか、すべて失敗するかを保証する仕組みである。これはデータの一貫性を保つ上で非常に重要だが、大量のマイグレーションがそれぞれ個別のトランザクションとして順番に実行されると、その分処理にオーバーヘッドが生じ、全体として非常に遅くなってしまう。特に、新規環境のデータベースをゼロから構築する場合や、開発者がブランチを頻繁に切り替えてデータベースを再構築する必要がある場合に、この遅さが顕著になる。
もう一つの原因は、過去のマイグレーションファイルの中に、データベース構造の変更だけでなく、アプリケーションのデータを更新したり、新しいレコードを作成したりするRubyコードが大量に含まれていたことである。これらのコードは、マイグレーションが実行された時点では意味があったが、現在では不要なものも多く、毎回実行されることで無駄な処理時間となっていた。
このような問題に対して、Railsには「schema:load」という別の方法でデータベーススキーマ(構造)を構築する機能も存在する。これはdb/schema.rbというファイルからデータベース構造を読み込むもので、マイグレーションを一つずつ実行するよりもはるかに高速にデータベースを構築できる場合が多い。しかし、schema:loadにはいくつかの制限があった。例えば、SQLで直接作成されたビューや、マイグレーション内のRubyコードで作成されたデータ(例えばUser.create!(name: 'John')のようなレコード作成)は反映されない。そのため、本番環境での利用や、データ作成を含むマイグレーションがある場合には、schema:loadだけでは不十分だった。
Railsは、どのマイグレーションが実行済みかを判断するために、schema_migrationsというテーブルにマイグレーションのタイムスタンプ(バージョン)を記録している。新しいマイグレーションを実行しようとする際、Railsはこのテーブルに該当のタイムスタンプがあるかを確認し、あればスキップ、なければ実行するという仕組みだ。この仕組みを利用すれば、特定のマイグレーションを特定の環境で実行したくない場合に、schema_migrationsテーブルにそのマイグレーションのタイムスタンプを手動で追加することで、実行をスキップさせることが可能となる。
このプロジェクトの課題は、schema:loadの高速さを利用しつつ、マイグレーション内に含まれるビューの作成やデータの作成・更新といった処理も適切に実行できる方法を見つけることだった。
そこで考えられた解決策は、古いマイグレーションをまとめて一つの大きなSQLファイルに変換し、それを直接データベースに読み込ませるというものだ。Railsにはstructure.sqlというファイル形式もあり、これはschema.rbよりも詳細なデータベース構造(例えばビューなど)を記録できるため、このstructure.sql形式を利用することにした。
具体的には、ある時点(このプロジェクトでは2024年)までの古いマイグレーションをまとめてSQLファイルに変換し、「ベースライン」とした。それ以降の新しいマイグレーション(2025年以降)は通常通り実行することにした。これにより、古い大量のマイグレーションを個別に実行する手間を省き、新しいマイグレーションの管理は柔軟性を保つことができる。
しかし、この方法にも問題があった。structure.sqlはデータベースの構造は記録できるものの、マイグレーション内で実行されたデータ作成や更新の記録はできない。通常、新規環境で必要なデータはdb/seeds.rb(シードファイル)に記述されるべきだが、すべてのデータ操作がシードファイルに適切に移行されている保証はなかった。数千ものマイグレーションファイルを手作業で確認するのは非現実的で、エラーも発生しやすい。
この問題を解決するため、データ作成や更新を行うマイグレーションを自動的に特定するスクリプトを開発した。これは、マイグレーション実行中にデータベースに対するINSERTやUPDATE、DELETEといったデータ操作のSQLクエリをログに記録するもので、プロジェクトの初期設定ファイル(config/initializers/migration_data_logger.rb)として一時的に導入された。このスクリプトを使うことで、数千あったマイグレーションの中から、データ操作を行う約70個のマイグレーションを特定することができた。
特定されたマイグレーションに含まれるデータ作成・更新コードは、その後手作業でdb/seeds.rbに移行された。これにより、古いマイグレーションからデータ作成の懸念を取り除き、安心してSQLファイルに変換できるようになった。
具体的な実装手順は以下の通りである。まず、config/application.rbにconfig.active_record.schema_format = :sqlという設定を追加し、Railsがデータベース構造をSQL形式でダンプするように変更した。次に、2025年以降の新しいマイグレーションファイルを一時的に別のフォルダに移動し、データベースを再構築した。これにより、2024年以前のすべてのマイグレーションが適用された状態のデータベース構造がstructure.sqlとして生成された。このファイルをstructure_baseline.sqlという名前に変更して保存した。
その後、一時的に移動した新しいマイグレーションファイルを元のフォルダに戻し、2024年以前の古いマイグレーションファイルはすべて削除した。
既存の環境で、このstructure_baseline.sqlの適用をスキップさせるため、2024年の最後のマイグレーションよりも後のタイムスタンプを持つ架空のマイグレーションを作成し、そのタイムスタンプを既存の全環境のschema_migrationsテーブルに直接追加した。こうすることで、既存の環境ではstructure_baseline.sqlを読み込むマイグレーションは実行されず、新しい環境でのみ実行されるようになる。
そして、structure_baseline.sqlを読み込むための特別なマイグレーションファイルを作成した。このマイグレーションはdisable_ddl_transaction!という設定を持つ。これは、データベースの操作をトランザクションなしで実行するという意味で、非常に大きなSQLファイルを読み込む際に速度を大幅に向上させる効果がある。ただし、途中でエラーが発生した場合にロールバック(変更を取り消すこと)ができなくなるため、新規環境でのみ使用し、問題が発生してもデータベースを再構築すれば良いという状況でのみ推奨される。
この作業が完了した後、config/application.rbに追記したconfig.active_record.schema_format = :sqlの行を削除するか、config.active_record.schema_format = :rubyに戻した。
最後に、データベースを再構築した際に、Railsが自動で作成するar_internal_metadataとschema_migrationsというテーブルとそれらのインデックスが、structure_baseline.sqlにも含まれており、重複作成エラーが発生するという問題があった。これを解決するため、structure_baseline.sql内の該当するCREATE TABLEやALTER TABLE文にIF NOT EXISTS句を追加し、もしテーブルやインデックスが存在しなければ作成するという条件付きのSQLに修正した。これにより、db:migrateタスクが正常に動作するようになった。
また、特定のユーザー権限がない場合に問題となる可能性のある、PostgreSQLの拡張機能pg_trgmに対するCOMMENT ON EXTENSIONの行も削除することが推奨された。
これらの対策を講じた結果、db:migrateタスクの実行速度が劇的に改善し、約90%もの高速化を達成することができた。
ただし、この解決策には注意点がある。一つは、SQLファイルを使用することで、Railsの持つ「データベース非依存性」という利点が失われることだ。生成されるSQLファイルは、使用している特定のデータベース(例:PostgreSQL)に特化したものとなり、他の種類のデータベース(例:MySQL)ではそのまま使えなくなる可能性がある。また、Railsが公式に推奨する手法ではなく、変更履歴の追跡や変更の巻き戻し(リバーシビリティ)といったマイグレーションの本来の利点が一部失われるため、マイグレーションの遅さが深刻な問題でない限り、この方法は推奨されない場合もある。しかし、このプロジェクトのように大規模で、マイグレーションの遅さがボトルネックとなっている場合には、非常に有効な解決策となる。