概要
Railsを開発する上でパフォーマンスを低下させないためのTipsをメモ
Tips
実行時間の大きい処理はJobで非同期実行する
リクエスト内で複数な処理を実行してしまうと、レスポンスが遅くなりユーザビリティの低下につながります
リクエスト内で実行する必要がないものは、ActiveJob等で非同期に実効するか、バッチ処理で定期的に処理するように
することでユーザビリティの向上が図れます
条件式判定よりもSQLで絞る
取得してきたデータを1件ずつ条件反映して処理をするよりは、
取得するデータそのものをSQLで絞る方がはるかにパフォーマンスが良くなります
ループの中で毎回条件式で反映してしまうと、ループの中で毎回SQLが走ってしまうこともあるので、
パフォーマンスが要求される場合はSQLで絞るか、SQLで絞れる設計にすることをおすすめします
Array
よりも ActiveRecord::Relation
Array
オブジェクトはデータを全てメモリ上にもった上で処理を実効するのに対し、
ActiveRecord::Relation
オブジェクトはSQLの構造だけをメモリ上に持ち、
処理する直前に遅延評価を実行し、値をメモリ上に展開します
ActiveRecord::Relation
を使える場合は、そちらを使う方がパフォーマンスが良くなります
in_batches
ActiveRecord::Relation
オブジェクトを分割して取得したい場合、 Rails5からは in_batches
メソッドが使えます(※Rails5以前では find_in_batches
が使えます)
in_batches
はオプションで of
を受けることができ、分割で取得するレコード数を指定できます(デフォルトは1000件です)
Array
で渡してしまうとパフォーマンスが低下してしまう場合は in_batches
を使って処理することをおすすめします
Log.in_batches do |logs|
# ActiveRecord:: Relationオブジェクトをblockで受け取れる
end
each
よりも find_each
大量のデータを処理する際、DBから取得してきたデータを each
のループで回してしまうと、
ループを回す直前にデータを全てメモリ上に展開してしまいます
取得するデータ量が多い場合は、 find_each
を使ってDBからデータを分割して取得するようにし、
メモリ上に展開されるデータ数を絞る方がパフォーマンスが良い場合があります
ちなみに、 find_each
は in_batches
で取得したデータをeachで返しています
※ 追記
find_each では、取得結果がprimary_keyで一度whereもしくはpluckされるため、order等でソートしていた場合は
ソートの順序が無視されてしまうため注意が必要です
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation/batches.rb#L223
× データ数が多いと一度に全てをメモリ上に展開してしまうためパフォーマンスが悪い
Log.all.each do |log|
# 処理を実行
end
◯ DBから分割して取得してくれるので、必要とするメモリの量を抑えながら実行できる
Log.find_each do |log|
# 処理を実行
end
CSVの取込は分割して取得する
大量のデータをCSVから一度に取得してしまうと、メモリに大量のデータが展開されてしまい、
パフォーマンスが低下してしまいます
この場合、 foreach
メソッドを使って分割して取得することでパフォーマンスの低下を防ぐことができます
CSV.foreach(filename, headers: true, skip_blanks: true).with_index do |row, index|
# CSVを1行ずつ処理する
end
下記例では、each_slice
と組み合わせて1000件ずつCSVデータを取得し処理を実行します
CSV.foreach(filename, headers: true, skip_blanks: true).each_slice(1000).with_index do |rows, index|
# CSVを1000件ずつまとめて処理する
end
バッチ処理では不要になった変数に明示的に nil
を設定する
HTTPのリクエストで完結する処理に関しては、リクエスト終了後に展開された変数は全てメモリから開放されますが、
バッチ処理等で展開した変数を何度も使いまわした場合、変数がGCされる対象にならずメモリに残り続けてしまう場合があります
この場合、ループ処理の最後で不要になった変数に明示的に nil
を設定することで
使用するメモリ量を抑えることができ、パフォーマンスの改善が図れる場合があります
大量にデータを挿入する場合はBULK INSERTをする
activerecord-importを使うことで、DBへのINSERTを1回のSQLで実行できます
大量のデータの取込処理がある場合は導入を検討してみることをおすすめします
下記例では、CSVから1000件ずつデータを取得し、取得した1000件のデータを1回のSQLでINSERTしています
fields = [:title, :body, :created_at, :updated_at]
CSV.foreach('blog.csv', headers: true, skip_blanks: true).each_slice(1000).with_index do |rows, index|
values = []
rows.each do |row|
values << [row['title'], row['body'], row['created_at'], row['updated_at']]
end
Blog.import fields, values, validate: false, timestamps: false, on_duplicate_key_update: fields
# @note 使用済みの変数に明示的にnilを設定してメモリから開放する
values = nil
rows = nil
end
destroy_all
vs delete_all
destroy_all
で削除を実行した場合、レコードを1件ずつ each
でまわし、 1件ずつ destroy
を実行するため、
レコードごとにバリデーションとコールバックが実行されます
レコード数が多い場合はDBが詰まってしまう可能性があるため、
バリデーションもコールバックも不要な場合は、 delete_all
を使い、SQLから一括削除する方が良い場合があります
※ 外部参照制約に要注意
destroy_all
と delete_all
の違いについては下記リンクでもまとめたので、参照ください
Cardinalityが大きいカラムに対する絞込やソートにはINDEXを貼ろう
Railsアプリケーションのパフォーマンスが悪い場合の、よくある例がINDEXの貼り忘れです
SlowQueryを検出してくれるツールを導入し、該当するテーブルにINDEXを貼ることでパフォーマンスが劇的に改善します
ツールの例として、ヘルスチェックも合わせて実行してくれるNewRelicを紹介しておきます
また、複数のカラムを組み合わせて絞込やソートを実行している場合は複合INDEXを貼ることでパフォーマンスが変わることがあります
SQLで explain
を実行し、SQLの実行計画を調査してみると良いです
ActiveRecord::Relation
オブジェクトにも explain
メソッドがあるので、 rails console から確認することも可能です