Upgrade to Pro — share decks privately, control downloads, hide ads and more …

RailsとRidgepoleの マイグレを120倍早くする

Avatar for Hazumi Ichijo Hazumi Ichijo
August 07, 2025
48

RailsとRidgepoleの マイグレを120倍早くする

Avatar for Hazumi Ichijo

Hazumi Ichijo

August 07, 2025
Tweet

Transcript

  1. Copyright © Henry, Inc. All rights reserved. Roppongi.rb #33 一條端澄

    (@rerost/hazumirr) RailsとRidgepoleの マイグレを120倍早くする
  2. Copyright © Henry, Inc. All rights reserved. 自己紹介 一條 端澄

    (X: @hazumirr, GitHub: @rerost) 株式会社ヘンリー エンジニア 普段はKotlinを書いています 趣味 • 小さいツールを長くメンテする • CI/CD周りの改善・パフォーマンス改善
  3. Copyright © Henry, Inc. All rights reserved. 意図せず > RidgepoleによるDBスキーマの宣言的定義に移行して便利になりました

    (ちょっと高速化もしました) > https://blog.smartbank.co.jp/entry/2025/08/01/103000 と完全に被りました • PostgreSQL • ConnectionAdaptersを実装する で、微妙にやっていることが違うので温かい目で見てもらえると (一応、僕のほうがちょっとだけGitHubの公開が早かった) 謝罪
  4. Copyright © Henry, Inc. All rights reserved. Railsで手元から本番やQAなどのリモートDBへマイグレーションをしたときに、 謎の待ち時間があった経験ありません? •

    なんかやたらSQLが表示される ◦ SELECT … FROM pg_attribute …みたいな • マイグレーションも終了しており、カラム・テーブルはあるのになぜか待た される...? Railsでのマイグレーション時の問題
  5. Copyright © Henry, Inc. All rights reserved. マイグレーション後にdb:schema:dump を行いdb/schema.rbの更新を行ってい るのが原因

    対処法 • config.active_record.dump_schema_after_migration = false ◦ db:schema:dump をスキップする ◦ https://github.com/rails/rails/issues/38927 • config.active_record.schema_format = :sql ◦ DBのネイティブなツールでdumpする あたりで回避可能 Railsでのマイグレーション時の問題
  6. Copyright © Henry, Inc. All rights reserved. Railsの db/schema.rb を書くと、その状態にDBを変更してくれるくん

    • 添付のように、schema.rbと同じ記法が使える • Railsのmigrationを書きschema.rbが生成される流れと逆。sqldefと類似 • 接続先DBにusersテーブルがないとき => DBにテーブルを作る • 接続先DBにusersテーブルがあるとき => カラムやnull制約などが変 わっていたら、DBに反映 Ridgepoleについて create_table :users, force: true do |t| t.string :name, null: false t.timestamps end
  7. Copyright © Henry, Inc. All rights reserved. だんだんサービス成長し、Ridgepoleでの反映に20分かかっていた。 ローカルだと数秒なので、原因としては •

    GitHub Actionsのマシンスペック • ネットワークレイテンシー • … のどれかなはず Ridgepoleについて
  8. Copyright © Henry, Inc. All rights reserved. Ridgepoleの処理の流れとしては 1. 接続先のDBのスキーマをdump

    • ActiveRecord::SchemaDumper.dumpを使い、接続先のDBのスキーマ をロード 2. 差分計算 3. マイグレーションの実行 Ridgepoleについて
  9. Copyright © Henry, Inc. All rights reserved. Ridgepoleの処理の流れとしては 1. 接続先のDBのスキーマをdump

    • ActiveRecord::SchemaDumper.dumpを使い、接続先のDBのスキーマ をロード <- ここがボトルネック 2. 差分計算 3. マイグレーションの実行 Ridgepoleについて
  10. Copyright © Henry, Inc. All rights reserved. ActiveRecord::SchemaDumperについて def tables(stream)

    sorted_tables = @connection .tables.sort not_ignored_tables = sorted_tables.reject { | table_name| ignored?( table_name) } not_ignored_tables.each_with_index do |table_name, index| table(table_name, stream) stream.puts if index < not_ignored_tables.count - 1 end # dump foreign keys at the end to make sure all dependent tables exist. if @connection .supports_foreign_keys? foreign_keys_stream = StringIO.new not_ignored_tables.each do |tbl| foreign_keys( tbl, foreign_keys_stream) end foreign_keys_string = foreign_keys_stream.string stream.puts if foreign_keys_string.length > 0 stream.print foreign_keys_string end end def table(table, stream) columns = @connection .columns(table) https://github.com/rails/rails/blob/v8.0.2/activerecord/lib/active_record/schema_dumper.rb#L134-L159
  11. Copyright © Henry, Inc. All rights reserved. ActiveRecord::SchemaDumperについて def tables(stream)

    sorted_tables = @connection.tables.sort not_ignored_tables = sorted_tables.reject { | table_name| ignored?( table_name) } not_ignored_tables.each_with_index do |table_name, index| table(table_name, stream) stream.puts if index < not_ignored_tables.count - 1 end # dump foreign keys at the end to make sure all dependent tables exist. if @connection.supports_foreign_keys? foreign_keys_stream = StringIO.new not_ignored_tables.each do | tbl| foreign_keys( tbl, foreign_keys_stream) end foreign_keys_string = foreign_keys_stream.string stream.puts if foreign_keys_string.length > 0 stream.print foreign_keys_string end end def table(table, stream) columns = @connection .columns(table) https://github.com/rails/rails/blob/v8.0.2/activerecord/lib/active_record/schema_dumper.rb#L134-L159
  12. Copyright © Henry, Inc. All rights reserved. connection.columnsを辿っていくと、PostgreSQLの場合、以下にたどり着く ActiveRecord::SchemaDumperについて def

    column_definitions (table_name) query(<<~SQL, "SCHEMA") SELECT a.attname, format_type(a.atttypid, a.atttypmod), pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod, c.collname, col_description(a.attrelid, a.attnum) AS comment, #{supports_identity_columns? ? 'attidentity' : quote('')} AS identity, #{supports_virtual_columns? ? 'attgenerated' : quote('')} as attgenerated FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum LEFT JOIN pg_type t ON a.atttypid = t.oid LEFT JOIN pg_collation c ON a.attcollation = c.oid AND a.attcollation <> t.typcollation WHERE a.attrelid = #{quote(quote_table_name( table_name))}::regclass AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum SQL end https://github.com/rails/rails/blob/v8.0.2/activerecord/lib/active_record/connection_adapters/postgresq l_adapter.rb#L1034-L1049
  13. Copyright © Henry, Inc. All rights reserved. カラム定義以外にも主キーの判定や外部キー判定でもN+1が発生 高速化方針 •

    Preloadをする(ただしActiveRecordが使えない) • 影響範囲は最小限にする。標準のConnectionAdaptersに手を入れると、 SchemaDumper以外にも問題が出るので 対応方法: 専用のConnectionAdaptersを作成。普通はDBの種類ごとに作られる が、今回はSchemaDumperにユースケースを絞り、高速化したものを作成 • postgresql:///... -> bulk-postgresql://... • adapter: postgresql -> adapter: bulk-postgresql で差し替えられるようにする(Ridgepoleでも利用可能) SchemaDumperのN+1について
  14. Copyright © Henry, Inc. All rights reserved. 再掲 SchemaDumperのN+1について def

    column_definitions (table_name) query(<<~SQL, "SCHEMA") SELECT a.attname, format_type(a.atttypid, a.atttypmod), pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod, c.collname, col_description(a.attrelid, a.attnum) AS comment, #{supports_identity_columns? ? 'attidentity' : quote('')} AS identity, #{supports_virtual_columns? ? 'attgenerated' : quote('')} as attgenerated FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum LEFT JOIN pg_type t ON a.atttypid = t.oid LEFT JOIN pg_collation c ON a.attcollation = c.oid AND a.attcollation <> t.typcollation WHERE a.attrelid = #{quote(quote_table_name( table_name))}::regclass AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum SQL end https://github.com/rails/rails/blob/v8.0.2/activerecord/lib/active_record/connection_adapters/postgresq l_adapter.rb#L1034-L1049
  15. Copyright © Henry, Inc. All rights reserved. 再掲 SchemaDumperのN+1について def

    column_definitions (table_name) query(<<~SQL, "SCHEMA") SELECT a.attname, format_type(a.atttypid, a.atttypmod), pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod, c.collname, col_description(a.attrelid, a.attnum) AS comment, #{supports_identity_columns? ? 'attidentity' : quote('')} AS identity, #{supports_virtual_columns? ? 'attgenerated' : quote('')} as attgenerated FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum LEFT JOIN pg_type t ON a.atttypid = t.oid LEFT JOIN pg_collation c ON a.attcollation = c.oid AND a.attcollation <> t.typcollation WHERE a.attrelid = #{quote(quote_table_name( table_name))}::regclass AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum SQL end https://github.com/rails/rails/blob/v8.0.2/activerecord/lib/active_record/connection_adapters/postgresq l_adapter.rb#L1034-L1049
  16. Copyright © Henry, Inc. All rights reserved. 改善後: 以下のようにPreloadを挟む。PreloadするタイミングはAdapter作成時 SchemaDumperのN+1について

    def preload_column_definitions (table_names) table_name_map = ( query(<<~SQL, "SCHEMA") SELECT (a.attrelid::regclass)::text , a.attnum, a.attname, format_type(a.atttypid, a.atttypmod), pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod, c.collname, col_description(a.attrelid, a.attnum) AS comment, #{supports_identity_columns? ? 'attidentity' : quote('')} AS identity, #{supports_virtual_columns? ? 'attgenerated' : quote('')} as attgenerated FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum LEFT JOIN pg_type t ON a.atttypid = t.oid LEFT JOIN pg_collation c ON a.attcollation = c.oid AND a.attcollation <> t.typcollation WHERE a.attnum > 0 AND NOT a.attisdropped SQL ).group_by(&:first) ... https://github.com/rerost/activerecord_bulk_postgresql_adapter/blob/master/lib/activerecord_bulk_po stgresql_adapter.rb#L56-L70
  17. Copyright © Henry, Inc. All rights reserved. 改善の結果 • SchemaDumperのN+1はすべて解消。10種類以上のN+1があった

    • 結果として、20分かかっていたのが10秒程度で終了 感想 • ActiveRecordナシでPreloadするのしんどい • 片っ端からN+1を潰すのは辛いので、Claude Codeにやってもらった ちなみにN+1を潰そうとする動きはある https://github.com/rails/rails/pull/53930 結果
  18. Copyright © Henry, Inc. All rights reserved. https://note.com/henry_app 会社ブログやってます We

    are hiring!! https://henry.jp/ Thank you https://dev.henry.jp/ 技術ブログやってます