LoginSignup
3
3

More than 3 years have passed since last update.

Railsのパフォーマンス向上

Last updated at Posted at 2020-12-01

はじめに

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が起きない様にする

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

3
3
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
3
3