はじめに
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が起きない様にする
位で十分な気がしているんですが、どうなんでしょうか?