Help us understand the problem. What is going on with this article?

N+1問題

More than 1 year has passed since last update.

SQLがたくさん発行されて動作が重くなる、という問題。インターン先で一度出くわして、ちょうど忘れかけてたので思い出す意味で書いていく。

SQLとは、データベースとの対話で用いるプログラミング言語。
僕の理解で言えば、あるデータベースのテーブルで別のテーブルのデータが必要になった際に、その2つを繋いでくれるもの、である。

例えば次の2つのテーブルがあったとする。

スクリーンショット 2017-12-06 13.52.42.png
スクリーンショット 2017-12-06 13.53.06.png

ホテルの利用者であるUserテーブルと、そのホテルの予約であるReservationテーブル。それぞれの関係は1対1、Userが1人いれば予約も1つである。その逆も同様、予約に対して予約者は1人である関係だ。
(他にも、大学と学生の関係であれば、大学は1つに対しても所有する学生数は多である1対多や、Twitterのフォローフォロワーのように多対多の関係というのがあるが、ややこしくなるからここでは言及しない。)

この2つのテーブルがあった際に、利用者がどのホテルに泊まるかを、user_controllerで呼び出したいとする。通常、user_controllerであればUserテーブルのデータしかとってこれないが、SQLが手伝ってReservationテーブルの情報を取ってくれるのである。

さて、では本題のN+1問題に入る。

例として、さっきのテーブルたちの下で、Reservationのviewに各user_nameを表示させたいとする。ReservationコントローラーでUserテーブルのデータを利用したいという場合である。

まず初めに、ReservationテーブルとUserテーブルをつなぐため、その関係をReservationモデルに記す。

Reservation.rb
has_one :user

次に、reservation_controllerでのuserテーブルのカラムの呼び出し方だが、
例えば、reservation_idが1の予約者を@userに代入したいとすると、

@reservation = Reservation.find_by(reservation_id: 1)
@user = @reservation.user.user_name

これでできる。

Reservationテーブルのデータに対して、userテーブルのデータを呼び出したい際には、先ほどの”has_one :user”と記しておけば、reservationのデータに対して後ろに“.user”とつけることでuserテーブルのカラムやメソッドが使えるようになるのである。

使い方がわかってきたので、次は、実際にReservationのviewで予約者一覧を表示したいとき。

まず思い浮かぶのは、each do文を使う方法だろう。

@reservations = Reservation.all
@reservations.each do |@reservation|
    @user = @reservation.user.user_name
    put @user
end

これでできるはずだ。しかしこのコードだと以下のようにSQLが発行される。これがいわゆるN+1問題である。

SELECT ‘reservations’.* FROM ‘reservations’ # Reservation.all の実行
SELECT 'users'.* FROM 'users' WHERE 'users'.'user_id' = 1
SELECT 'users'.* FROM 'users' WHERE 'users'.'user_id' = 2
SELECT 'users'.* FROM 'users' WHERE 'users'.'user_id' = 3
SELECT 'users'.* FROM 'users' WHERE 'users'.'user_id' = 4
SELECT 'users'.* FROM 'users' WHERE 'users'.'user_id' = 5

@reservation@userに代入される度にSQLが発行されるのである。

これを解決するには、”includes”を使えばいい。
Includesとは簡単に言うと2つのテーブルを結合してくれるメソッドである。

具体的なコードは

@reservations = Reservation.all.includes(:user)

こうすると、

SELECT ‘reservation’.* FROM ‘reservations’
SELECT 'users'.* FROM 'users' WHERE 'users'.'user_id' IN (1,2,3,4,5)

というように2つのSQLが発行されるだけになり、解決する。

はい。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした