はじめに
これは ActiveRecord のメモリ節約や処理速度向上等のパフォーマンスに関していくつか調べたり知り得たことを書き留めたメモです。どちらかというと API よりバッチ処理に主眼をおいた Tips になります。
またパフォーマンスを重視する場合、コードの読みづらさだったり、バグを仕込んでしまったりとのデメリットとのトレードオフになることも多いのでその点はご留意ください。
インスタンス化を避ける
ActiveRecord のインスタンス化はかなり時間の掛かる処理です。
例えば CSV を元に何千何万件の登録を行うような処理で、事前に各レコードに対して ActiveRecord のバリデーションやフックアクション(before_validation
とか)を通したいことがあります。
この時に毎回 new
して valid?
していくとめちゃくちゃ時間がかかりますが、一度初期化したインスタンスにデータだけを上書きして使い回すようにするとかなり早くなります。ただし、使い回すことによる副作用もあると思いますのでその点は十分ご注意ください。
eager_load
しない
あるテーブルの条件を使って関連テーブルの値の一覧を取得したい場合、eager_load
や preload
と map
を組み合わせるより pluck
を使える形に直したほうが早くなります。
※ N+1 クエリの解消等の目的で使う分には当然 eager_load
等したほうが良いです。
以下のようなテーブル構造で Device
の一覧を元に Account#code
を取得したい場合、
class Device
belongs_to :user
has_one :account, through: :user
end
class User
has_one :account, dependent: :destroy
has_many :devices, dependent: :destroy
end
class Account
belongs_to :user
end
Device
から引くよりも Account
から pluck
したほうが良いです。
# 前提条件
devices = Device.where(id: 1..10000)
# eager_load して map するよりも
devices.eager_load(user: :account).map { |d| d.user.account.code }
# merge で条件指定して pluck したほうが速い
Account.joins(user: :devices).merge(devices).pluck(:code)
レコード数にもよりますがだいたい10倍ほど速かったです。
マルチスレッド内ではコネクションプールは共有する
これはパフォーマンスチューニングというより注意ですが、マルチスレッド等で DB 処理を行う場合はスレッド数分だけコネクションを貼ろうとして、設定上限数をオーバーする可能性があるので ActiveRecord::Base.connection_pool.with_connection
でコネクションプールの共有を行う必要があります。
# parallel gem を使った例
Parallel.each(arrays, in_threads: 8) do |array|
ActiveRecord::Base.connection_pool.with_connection do
# コネクションプールが共有される
end
end
その他随時追記予定です。