N+1問題

f:id:utouto97:20210706233746p:plain

前回、ORMについてまとめました。
SQLを書かなくてもDB操作を簡単にできて便利なORMですが、
N+1問題という問題があります。

N+1問題とは

N+1問題とは、ループ処理の中で、1回ずつクエリを発行してしまう問題です。
これにより、無駄なクエリが発行され、パフォーマンスの低下を招きます。

より具体的にいうと、Todoの一覧を取得し、Todo一つずつの担当ユーザー(User)を表示しようとするとき
ループを使って記述すると、合計でN+1回のクエリが発行されます。

  • Todoの一覧を取得(1回)
  • Todoの担当ユーザーを取得(N回、ループによる)

しかし、あらかじめユーザーの一覧を取得しておき、Todoの担当ユーザーの情報をそこからもってくれば、合計2回のクエリで済みます。

  • ユーザーの一覧取得(1回)
  • Todoの一覧を取得(1回)

RailsでN+1問題を試す

N+1を確認する

↓の感じで、ループ内部で一つずつ表示してみます。
ちなみに、@todos = Todo.allとしています。

<% @todos.each do |todo| %>
  <div>
    <%= todo.title %> : <%= todo.user.name %>
  </div>
<% end %>

ログを確認してみます。

rails_1  | Processing by TodosController#index as HTML
rails_1  |   Rendering layout layouts/application.html.erb
rails_1  |   Rendering todos/index.html.erb within layouts/application
rails_1  |   Todo Load (0.2ms)  SELECT "todos".* FROM "todos"
rails_1  |   ↳ app/views/todos/index.html.erb:3
rails_1  |   User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
rails_1  |   ↳ app/views/todos/index.html.erb:5
rails_1  |   CACHE User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
rails_1  |   ↳ app/views/todos/index.html.erb:5
rails_1  |   User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
rails_1  |   ↳ app/views/todos/index.html.erb:5
rails_1  |   User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
rails_1  |   ↳ app/views/todos/index.html.erb:5
rails_1  |   User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 4], ["LIMIT", 1]]
rails_1  |   ↳ app/views/todos/index.html.erb:5
rails_1  |   CACHE User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 4], ["LIMIT", 1]]
rails_1  |   ↳ app/views/todos/index.html.erb:5
rails_1  |   Rendered todos/index.html.erb within layouts/application (Duration: 29.9ms | Allocations: 15572)
rails_1  | [Webpacker] Everything's up-to-date. Nothing to do
rails_1  |   Rendered layout layouts/application.html.erb (Duration: 81.8ms | Allocations: 22040)
rails_1  | Completed 200 OK in 104ms (Views: 86.5ms | ActiveRecord: 1.5ms | Allocations: 28480)

SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?がTodoと同じ数だけ発行されています。
CACHEとついてるので、キャッシュされているかもしれませんね。

終わり