Edited at

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が発行されるだけになり、解決する。

はい。