45
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

気軽な.countに気をつけよう(N+1問題)

Posted at

はじめに

Ruby on Railsを使っているとサクサクコードが書けて楽しいですね。
文法さえいくつか覚えてしまえばまるでプログラミングを意識せずに書きたいことをサクサク書いていけます。
Databaseに対してどういう命令を出しているかも意識せずに・・・

この代表的な問題点がいわゆるN+1問題です。

N+1問題の解決策などはこのあたりに詳しく書いてありますが、今回はcountメソッドを使った場合に起きる問題に焦点を当てて解説します。

問題点: Array?ActiveRecord::Associations?

配列の数を数えるメソッドとして、

.length
.size
.count

などがあります。
これらを使うことで、簡単に要素の数を返してくれます。

今回の問題点は .count を使った際にせっかく includes などを利用してもN+1問題が発生してしまうことの詳細です。

例えば、記事に対するコメント数を出す場合に

app/models/article.rb
class Article < ActiveRecord::Base
  has_many :comments
end
app/models/comment.rb
class Comment < ActiveRecord::Base
  belongs_to :article
end

というモデルがあったとします。

とある一覧画面に記事の一覧とコメントの数を列挙しようとした際に、何も考えずに書くと

app/controllers/articles_controller.rb
~~

  def index
    @articles = Article.all
  end

~~

※ページングも何も考慮してません

app/views/articles/index.html.erb
~~略

<%= @articles.each do |article| %>
<div class="article_panel">
  <div class="article_title"><%= article.title %></div>
  <div class="comment_count"><%= article.comments.count %></div>
</div>
<% end %>

~~略

おめでとうございます。コレであなたもN+1問題へ無事突入しました。

一般的な解決策としては app/controllers/articles_controller.rbincludes をすることなのですが、

app/controllers/articles_controller.rb
~~

  def index
    @articles = Article.includes(:comments).all
  end

~~

これだけだと app/views/articles/index.html.erb.count メソッドを使っているため、解決しません。

.count メソッドは ActiveRecord::Associations::CollectionProxy にも定義されており、対象のテーブルに COUNT(*) 句のクエリを発行してしまいます。

残念ながらここまででは、N+1は未解決です。

解決策

app/controllers/articles_controller.rb
~~

  def index
    @articles = Article.includes(:comments).all
  end

~~

includes に加えて、

app/views/articles/index.html.erb
~~略

<%= @articles.each do |article| %>
<div class="article_panel">
  <div class="article_title"><%= article.title %></div>
  <div class="comment_count"><%= article.comments.size %></div>
</div>
<% end %>

~~略

.count.size (もしくは .length )に書き換えることで、N+1問題は解決します。

結論

せっかくincludeしたActiveRecord::Associationsに対しての数を数えるときは、 .size もしくは .length を使いましょう。
そうすることで COUNT(*) 句のクエリは発行されず、要素の数だけを数えて返してくれます。

※ちなみにですが、 comments.sum(:like) などを使った際にも SUM(comments.like) などでクエリが発行されますので注意が必要です。

おわりに

Ruby on Rails は本当に気軽にかける素晴らしいフレームワークです。
我々の会社でも数多くのプロダクトでこのフレームワークを使うことによる開発速度の恩恵を受けてきました。

正しく理解し、正しく使うことが非常に重要です。

何に対してどんな命令を出すことで、どこのリソースに対してどんな負荷がかかるのかを意識し、この記事がN+1退治に役立てれば幸いです。

45
28
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
45
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?