railsで子モデルの要素をcountした際にN+1になった話
こんにちは、taniyuです。 今回は、railsでお馴染みのN+1問題についてお話します。
概要
冒頭でも述べましたが、今回は、eager loading(関連モデルの読み込み)を行った後に、countを使うとSQLが発行されるということについてお話します。
また、今回は、下記の構造を持つ、ユーザ(user)が複数の記事(post)を投稿できるようなシステムを例に挙げて説明します。
class User < ActiveRecord::Base has_many :posts end
class Post < ActiveRecord::Base belongs_to :user end
問題が発生するコード
以下の様に、ユーザ情報と一緒に記事の投稿数を表示するようなview(index.html.slim)を考えます。
table thead tr th | 名前 th | 投稿数 tbody - @users.each do |user| tr td = user.name td = user.posts.count /<= この行に注目
user.posts.count
という処理があります。この行は、N+1問題となります。
そのため、eager loading をするために、コントローラーで以下のような処理を記述すると思います。
class UsersController < ApplicationController def index @users = User.includes(:posts) end end
今回は、includesを使っていますが、他にも色々あります。
それらの違いについては、以下のサイトが参考になります。
ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い - Qiita
しかし、eager loading をしても、サーバーのログを見ると、大量のクエリが流れています。
あらかじめ読み込んでいるはずなのになぜでしょうか?
問題の原因
原因は呼び出されるメソッドにあります。
countメソッドを呼び出すと、ActiveRecordのcountメソッドが呼ばれます。
countメソッドでは、内部で SELECT count(*) ~
を実行しており、
既に読み込んでいても毎回クエリが発行されるため、N+1問題が発生してしまいます。
では、どのようにすればよいでしょうか?
対策
対策について説明します。対策方法は、以下の様なものがあります。
- Userテーブルにカラム(posts_count)を追加する
- sizeメソッドを使う
「1. Userテーブルにカラム(posts_count)を追加する」 について
1.は単純にUserテーブルにカラムを追加し、そっちでデータを保存するという方法になります。
このようにすることで、 user.posts.count
から user.posts_count
という書き方にかわり、
関連するモデルを読み込む必要がなくなります。そのため、N+1問題が発生しなくなります。
しかし、子モデル(今回であれば、post)のデータが、追加・削除される際には、userモデルの
posts_countを更新する必要があり、SQLが発行されるようになります。
「2. sizeメソッドを使う」 について
2.は、使うメソッドを変えるというものです。sizeメソッドは、countメソッドと異なり、
既に読み込んでいる場合は、そのデータを使います。つまり、クエリが生成されることはありません。
そのため、解決できます。しかし、あらかじめ、includes等で読み込む処理が入るので、
ページ表示までの時間が1の方法よりも長くなります。
まとめ
1のようにカラムを追加する場合、子モデルの追加・削除時にSQLが発行されるようになります。
対して、2のようにeager loadingを使う場合、ページ表示までの時間が増加しますが、
子モデルの追加・削除時にSQLが発行されることはありません。
どちらか一方が優れているというわけではないため、どちらの実装方法を利用するかは、
ページがどのくらい利用されるか等を考慮した上で決めた方が良いです。
その他(条件を満たす要素を数える場合)
ちなみに、指定した要素の数を数えたい場合は、以下のように select と size を使うことで作成できます。
user.posts.select { |post| "条件文" }.size
参考
今回は以下のサイトを参考にさせていただきました。ありがとうございます。
countじゃなくてsizeを使った方がいい!! アソシエーションがLoadされてるか調べて常に高速なメソッドを作ろう! - Qiita