LoginSignup
363
255

More than 5 years have passed since last update.

N+1問題

Last updated at Posted at 2017-12-07

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

はい。

363
255
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
363
255