search
LoginSignup
3

More than 1 year has passed since last update.

posted at

updated at

Organization

Railsのパフォーマンス向上

はじめに

Railsのパフォーマンスの向上に関して、少しまとめます。

いろいろ、誤解や間違いがあるかもしれないので、問題ありましたら指摘していただけると助かります。

N+1(includes)

N+1は100件のデータを取得する場合に、101回SQLが走る様な場合の問題のことです

例えば、

userモデルとuser_profileモデルが一対一の場合に

@users = User.all

@users.each do |user|
  user.user_profile.comment
end

の様にすると

user_profile.commentの取得時に毎回SQLが走ってN+1が走ります。

このN+1の回避方法としては下記の様にincludesを使うことで回避できます。

@users = User.includes(:user_profile).all

@users.each do |user|
  user.user_profile.comment
end

N+1(select)

includesで基本的なN+1は回避することができるのですが、下記の様な場合N+1は回避することができません。

userモデルとarticleモデルが一対多の場合

@users = User.includes(:article).all

@users.each do |user|
  user.articles.count
end

みたいにするとincludesをしていても↓の様にsqlがN+1回分走ります。

   (0.2ms)  SELECT COUNT(*) FROM "articles" WHERE "articles"."user_id" = ?  [["user_id", 1]]
   (0.1ms)  SELECT COUNT(*) FROM "articles" WHERE "articles"."user_id" = ?  [["user_id", 2]]
   (0.1ms)  SELECT COUNT(*) FROM "articles" WHERE "articles"."user_id" = ?  [["user_id", 3]]
   (0.1ms)  SELECT COUNT(*) FROM "articles" WHERE "articles"."user_id" = ?  [["user_id", 4]]
   (0.1ms)  SELECT COUNT(*) FROM "articles" WHERE "articles"."user_id" = ?  [["user_id", 5]]
   (0.1ms)  SELECT COUNT(*) FROM "articles" WHERE "articles"."user_id" = ?  [["user_id", 6]]

その場合は、自分はselectメソッドの中にサブクエリを記載します

@users = User.all.select('(SELECT COUNT(*) FROM articles WHERE articles.user_id = users.id) article_count')

@users.each do |user|
  user.article_count
end

この様にすることで、単純にincludesするだけては解決できない、N+1も解決できます。

exists

存在するかどうかのみを確認したい場合はpresent?よりexists?を使う方がパフォーマンスが良いです

発行されるSQLは下記の様に異なります。

User.all.exists?
=> User Exists? (0.2ms)  SELECT 1 AS one FROM "users" LIMIT ?
User.all.present?
=> User Load (0.4ms)  SELECT "users".* FROM "users"

present?の場合は、実際にusersのデータを全て取ってきてしまうのに対して、exists?は存在したら1を返すだけて、余計なデータを取ってくることはないので、早いです。

pluck

自分の場合データのidのみを取ってきたい場合にpluckを使う
pluckを使わず、 map(&:id)みたいにするのと比べると、pluckはSQLで必要なカラムのみ取ってくるのと、ActiveRecordのオブジェクトを作ることもしないので早い

実際に発行されるSQL↓

User.all.pluck(:id)
=> (0.3ms)  SELECT "users"."id" FROM "users"
User.all.map(&:id)
=> User Load (0.6ms)  SELECT "users".* FROM "users"

eachとfind_each

eachの場合は前件取ってきて、データをメモリに置くのに対して、
find_eachの場合は1000件ずつ持ってくる

件数が多い場合データをeachでループする場合はfind_eachを使う方が良いっぽい

終わりに

とりあえず、思いつくものを記載しました。
また、何か思いついたら追記していきます。

パフォーマンスに関してですが、基本的に

  • selectやpluckを使ってSQLで取ってくるカラムを制限する
  • N+1が起きない様にする

位で十分な気がしているんですが、どうなんでしょうか?

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
What you can do with signing up
3