ログイン中のQiita Team
ログイン中のチームがありません

Qiita Team にログイン
コミュニティ
OrganizationイベントアドベントカレンダーQiitadon (β)
サービス
Qiita JobsQiita ZineQiita Blog
Rails
251
どのような問題がありますか?

この記事は最終更新日から3年以上が経過しています。

投稿日

更新日

N+1問題

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

はい。

ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
この記事は以下の記事からリンクされています
hiro273sql出力とn+1問題からリンク
pli8xxx_yuクリスマスのN + 1からリンク
過去の2件を表示する

コメント

この記事にコメントはありません。
あなたもコメントしてみませんか :)
ユーザー登録
すでにアカウントを持っている方はログイン
記事投稿イベント開催中
マイクロソフト認定資格を取得する際の学習方法や経験談、おすすめ学習リソースなどを紹介しよう!
~
251
どのような問題がありますか?
ユーザー登録して、Qiitaをもっと便利に使ってみませんか

この機能を利用するにはログインする必要があります。ログインするとさらに下記の機能が使えます。

  1. ユーザーやタグのフォロー機能であなたにマッチした記事をお届け
  2. ストック機能で便利な情報を後から効率的に読み返せる
ユーザー登録ログイン
ストックするカテゴリー