0
0

ActiveRecord のパフォーマンス関連 Tips

Posted at

はじめに

これは ActiveRecord のメモリ節約や処理速度向上等のパフォーマンスに関していくつか調べたり知り得たことを書き留めたメモです。どちらかというと API よりバッチ処理に主眼をおいた Tips になります。
またパフォーマンスを重視する場合、コードの読みづらさだったり、バグを仕込んでしまったりとのデメリットとのトレードオフになることも多いのでその点はご留意ください。

インスタンス化を避ける

ActiveRecord のインスタンス化はかなり時間の掛かる処理です。
例えば CSV を元に何千何万件の登録を行うような処理で、事前に各レコードに対して ActiveRecord のバリデーションやフックアクション(before_validationとか)を通したいことがあります。
この時に毎回 new して valid? していくとめちゃくちゃ時間がかかりますが、一度初期化したインスタンスにデータだけを上書きして使い回すようにするとかなり早くなります。ただし、使い回すことによる副作用もあると思いますのでその点は十分ご注意ください。

eager_load しない

あるテーブルの条件を使って関連テーブルの値の一覧を取得したい場合、eager_loadpreloadmap を組み合わせるより 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

その他随時追記予定です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0