Rails

【Rails】N+1問題とは?初心者向けに解説します【解決策あり】

どうも、シューヘーです。普段はRailsエンジニアとして、サーバーサイドの開発を行っております。

 

「N+1問題ってなんだ??」という方に向けて、以下のことを記事にまとめました。

 

本記事の内容

・N+1問題とは

・N+1問題の解決策

「N+1問題」という言葉を初めて聞いた人や、聞いたことはあるけど意味を知らない人の助けになれば幸いです。

 

※動作確認環境

・Ruby 2.6.3

・Rails 6.0.0

【Rails】N+1問題とは?初心者向けに解説します

N+1問題とは、「データベースに必要以上の回数(N+1回)問い合わせてしまう、無駄の多い処理」のことです。

 

例えば、段ボール箱の中に100個みかんが入っているとして、僕があなたに「みかんをすべて持ってきて〜」とお願いしてときに、あなたが1個ずつみかんを運ぶ作業を100回繰り返す。というイメージです。

 

段ボールの箱ごと100個いっぺんに持ってくればいいのに…ってなりますよね。

 

では、これをRailsの処理に置き換えて具体的にN+1問題が何なのか解説します。

 

Twitterのようなアプリを想定して、UserがPost(投稿)を複数持っており、Postは1人のUserに属する場合を例に挙げます。

 

app/models/user.rb

class User < ApplicationRecord
  has_many :posts
end

 

app/models/post.rb

class Post < ApplicationRecord
  belongs_to :user
end

 

railsコンソールを立ち上げて、以下のようにPostモデルから10個データを取り出し、それぞれのPostに紐付くUserの名前を取得してみます。(このとき、取り出した10個のPostに紐付くUserのidはすべて異なることとします)

# Postモデルからデータを10個取り出す
> posts = Post.limit(10)
  Post Load (2.3ms)  SELECT `posts`.* FROM `posts` LIMIT 10

# postに紐付くuserの名前を表示する
> posts.each do |post|
>   puts post.user.name
> end
  Post Load (0.6ms)  SELECT `posts`.* FROM `posts` LIMIT 10
  User Load (0.7ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
  User Load (0.4ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1
  User Load (0.3ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 3 LIMIT 1
  User Load (0.3ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 4 LIMIT 1
  User Load (0.3ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 5 LIMIT 1
  User Load (0.4ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 6 LIMIT 1
  User Load (0.4ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 7 LIMIT 1
  User Load (0.4ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 8 LIMIT 1
  User Load (0.3ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 9 LIMIT 1
  User Load (0.3ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 10 LIMIT 1

userの名前を表示する処理を行うときに発行されるSQLに注目すると、最初にPostを1回取得した後、Userテーブルに10回問い合わせてデータを取得してきていることが分かります。

 

postsの数が10回なので、1+10で合計11回SQL文が発行されています。これが、N+1問題と呼ばれる現象です。(1+N問題と呼んだ方が分かりやすいかもしれません)

 

たくさんSQLを発行すると何が問題かというと、データベースを何回も行ったり来たりすることになるので、すべてのデータを取得するまでに時間がかかります。結果的に、アプリの動作が遅くなってしまいます。

 

今回は10回の問い合わせで済みましたが、これが10000回とかになると影響が大きくなるのは容易に想像がつきます。

 

アプリをサクサク高速で動かす仕組みを作るのもエンジニアの大事な仕事の1つなので、これはなんとかせねばなりません。

【Rails】N+1問題の解決策

N+1問題を解決するためには、「includes」メソッドを使います。includesメソッドを使うと、先程の例でいうところのUserのデータを、あらかじめまとめて取得しておくことで、1回1回データベースに問い合わせる無駄な処理をなくします。

 

例えるなら、みかんが100個入っている段ボール箱を箱ごと手元に持ってきておいて、みかんが必要になったらその場ですぐに取り出す、というイメージです。

 

Railsで具体的にみてみましょう。

# postsテーブルから10個データを取り出す(このとき、関連するuserデータも一緒に取り出す)
> posts = Post.includes(:user).limit(10)
   (0.8ms)  BEGIN
  Post Load (1.7ms)  SELECT `posts`.* FROM `posts` LIMIT 10
  User Load (0.7ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

# postに紐づくuserの名前を表示する
> posts.each do |post|
>   puts post.user.name
> end
  Post Load (9.7ms)  SELECT `posts`.* FROM `posts` LIMIT 10
  User Load (0.6ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

上記を見ると分かるように、post.user.nameを取得したときに発行されるSQLの回数が2回で済みます。これでN+1問題が解決できました。

 

発行されるSQLを見てずらーっと何回もSQLが発行されているのを発見したら、N+1問題が起きている可能性を疑ってみてください。

さいごに

今回は、N+1問題の解決策としてincludesメソッドを紹介しましたが、他にもjoins,  eager_load, preloadといったメソッドがActive Recordで用意されています。

 

それぞれ特性が異なるので、気になる方は調べてみてください。

参考文献

以下のサイトを参考にさせていただきました。本記事だけではあまり理解ができなかったという方は、こちらも読んでみると良いでしょう。

【Ruby on Rails】N+1問題ってなんだ?

RailsのN+1問題の解決方法

Railsガイド Active Record クエリーインターフェース