N+1問題が発生したコードが遅いのは周知の事実だと思いますし、私も実体験として遅いことは確認していますが、
何が原因で遅いのでしょうか?
例えば以下のコードでN+1が発生しますが、(雰囲気を伝えるためのコードなので実際には動きません)
for group in groups.limit(10): for user in group.users: print(user.name)
クエリをgroup一覧を取得する際にeager loadingやjoinによってuser内容を一度に取得することにより実行速度が向上します。
両者の違いはクエリの実行数ですが、DBにおけるクエリの実行はそれほどオーバーヘッドがあるものなのでしょうか?それとも別の要因で遅くなっているのでしょうか?
オーバーヘッドの要因として思いついたものは以下の要因です
- クエリのパースや実行計画作成が遅い
- ディスクアクセスが発生する場合にシーケンシャルアクセスができない
- DBとプログラムとの通信が遅い
これらの要因が少ない場合にはN+1問題が発生した場合にも影響は少ないのでしょうか?
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
回答3件
0
ベストアンサー
例えDBが
- オンメモリ(すべてメモリ上に展開されるため、ディスクアスクセスが存在しない)
- 組み込み(そのプロセスの中で実行されるため、プロセス間通信もネットワーク間通信も存在しない)
という場合でもN+1問題は発生する場合があるようです。サンプルを作成し、ベンチマークを取得してみました。
Ruby
1# frozen_string_literal: true 2require 'active_record' 3require 'sqlite3' 4require 'benchmark' 5 6# DB接続 SQLite オンメモリ 7ActiveRecord::Base.establish_connection( 8 adapter: 'sqlite3', 9 database: ':memory:' 10) 11 12# テーブル及びモデル 13ActiveRecord::Base.connection.create_table :groups do |t| 14 t.text :name, null: false 15end 16 17ActiveRecord::Base.connection.create_table :users do |t| 18 t.text :name, null: false 19 t.integer :group_id, null: false 20end 21 22class Group < ActiveRecord::Base 23 has_many :users 24end 25 26class User < ActiveRecord::Base 27 belongs_to :group 28end 29 30# データ作成 31group_size = 100 32user_size = 10000 33 34group_size.times do |i| 35 group_name = "group#{i}" 36 Group.create(name: group_name) 37end 38 39user_size.times do |i| 40 user_name = "user#{i}" 41 group_name = "group#{i % group_size}" 42 User.create(name: user_name, group: Group.find_by(name: group_name)) 43end 44 45# ベンチマーク 46Benchmark.bm 10 do |r| 47 r.report 'none' do 48 list_none = [] 49 User.all.each do |user| 50 list_none << [user.name, user.group.name] 51 end 52 # SELECT "users".* FROM "users" 53 # SELECT "groups".* FROM "groups" WHERE "groups"."id" = ? LIMIT ? 54 # user_size回繰り返す、N+1問題あり 55 end 56 57 r.report 'eager_load' do 58 list_eager_load = [] 59 User.eager_load(:group).all.each do |user| 60 list_eager_load << [user.name, user.group.name] 61 end 62 # SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, 63 # "users"."group_id" AS t0_r2, "groups"."id" AS t1_r0, 64 # "groups"."name" AS t1_r1 FROM "users" 65 # LEFT OUTER JOIN "groups" ON "groups"."id" = "users"."group_id" 66 end 67 68 r.report 'preload' do 69 list_preload = [] 70 User.preload(:group).all.each do |user| 71 list_preload << [user.name, user.group.name] 72 end 73 # SELECT "users".* FROM "users" 74 # SELECT "groups".* FROM "groups" 75 # WHERE "groups"."id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) 76 end 77 78 r.report 'includes' do 79 list_includes = [] 80 User.includes(:group).all.each do |user| 81 list_includes << [user.name, user.group.name] 82 end 83 # SELECT "users".* FROM "users" 84 # SELECT "groups".* FROM "groups" 85 # WHERE "groups"."id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) 86 end 87end
実行結果
user system total real none 3.930000 0.050000 3.980000 ( 3.987493) eager_load 0.480000 0.010000 0.490000 ( 0.502292) preload 0.480000 0.010000 0.490000 ( 0.487678) includes 0.530000 0.010000 0.540000 ( 0.543553)
noneがN+1問題があり、10001回クエリを発行します。eager_loadは1回、preloadとincludesは2回です。
結果はDBやORMによって変わる可能性があります。しかし、ディスクアクセスもプロセス間通信/ネットワーク間通信だけが原因となるわけではなく、クエリやORMの処理そのもの(クエリを作る、クエリ結果をオブジェクトに変換する)も負荷が高く、呼び出し回数が膨大の場合は問題となるようです。
ただ、注目すべくは、単純にクエリ数に比例するわけでは無いと言うことです。none(N+1問題あり)はeager_loadに比べて9倍ぐらいしか遅くありません(クエリの実行回数は10000倍違います)。N+1のNが少なければそれほどの差は出なくなるかも知れません。
上のベンチマークは組み込みでオンメモリなSQLiteでActiveRecordを使った場合に過ぎません。ORMを使わない、別のORMにする、と言った場合はどうなるか、ストアドプロシージャを使った実行計画のキャッシュがあった場合はどうなるか、OracleやMariaDBなど別のDBではどうなるかは実測しないとわかりません。テーブル構造やクエリ内容によっても変わってくると思います。結局は実際にベンチマークを取らないとどれだけ遅いかはわからないと思います。
投稿2016/10/10 22:04
総合スコア21735
0
コンピュータ処理において、データのやり取りは、
- CPU - 二次キャッシュ
- CPU - メインメモリ
- CPU - メインメモリ - 別プロセス
- CPU - メインメモリ - HDD
- CPU - メインメモリ - ネットワーク
の順で、数倍から数十倍遅くなります。
N + 1 が起きてない場合、以下が1回だけ行われます。(SQLの生成は省いています)
A APサーバから 5 (SQLの送信)
B DBサーバ内で 3 (SQLの受信)
C DBサーバ内で 2と4 (SQLの実行)
D DBサーバから 5 (結果の転送)
E APサーバ内で 3 (結果の受信)
N + 1 問題が起きると、以下がN + 1回 繰り返されます。
A' APサーバから 5 (SQLの送信)
B' DBサーバ内で 3 (SQLの受信)
C' DBサーバ内で 2と4 (SQLの実行)
D' DBサーバから 5 (結果の転送)
E' APサーバ内で 3 (結果の受信)
C'の部分は、繰り返しを合計した処理量と、1回で処理した C とは、ほぼ一致します。
しかし、コンピュータ処理で特に遅い
CPU - メインメモリ - 別プロセス
CPU - メインメモリ - ネットワーク
の部分が、N回増えることになりますから、それがパフォーマンスが著しく劣化する原因となります。
投稿2016/10/09 06:19
総合スコア295
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2016/10/10 05:45
2016/10/10 06:07
2016/10/10 06:38
2016/10/10 14:48 編集
0
「N+1問題」と言った場合、普通は「データベースからN件のデータを取得する過程で、N+1件のクエリが発行される」ということを指すと思います。
まず、次の二つの処理にかかる時間を考えてみて下さい。
(a) データベースに対してクエリを発行してその結果のデータを受け取る処理。
(b) メモリ内にあるデータを加工してメモリの別の場所に格納する処理。
前者 (a) の処理は後者 (b) よりはるかに時間がかかることは、直感的にわかると思います。
次に、「N+1問題」が発生していない場合と発生している場合をくらべてみます。
-
「N+1問題」が発生していない場合、1回のクエリ発行で全件のデータを取得してメモリに格納します。
-- (a) が1回、(b) がN回行われます。 -
「N+1問題」が発生している場合、N+1回のクエリ発行で全件のデータを取得してメモリに格納します。
-- (a) がN+1回、(b) がN回行われます。
あとのほうが、(a)N回分だけ、余計に時間がかかることがわかりますね。たとえば、目的のテーブルに100万件のレコードがあったとしたら、クエリ100万回分だけ時間がかかります。
また、はじめに考えた通り、(a)N回分は、同じ回数の(b)N回分よりも、桁違いに長い時間です。ですから、「N+1問題」が発生すると、処理全体が目に見えて遅くなります。
投稿2016/09/28 03:17
編集2016/09/28 03:28総合スコア4315
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2016/09/28 13:48
2016/09/28 13:50
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2016/10/11 14:50 編集