SQLがたくさん発行されて動作が重くなる、という問題。インターン先で一度出くわして、ちょうど忘れかけてたので思い出す意味で書いていく。
SQLとは、データベースとの対話で用いるプログラミング言語。
僕の理解で言えば、あるデータベースのテーブルで別のテーブルのデータが必要になった際に、その2つを繋いでくれるもの、である。
例えば次の2つのテーブルがあったとする。
ホテルの利用者である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モデルに記す。
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が発行されるだけになり、解決する。
はい。