質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.49%
ORM

ORM(オブジェクト関係マッピング)はオブジェクト指向のシステムとリレーショナルデータベースの間でマッピングを行う技術です。

データベース

データベースとは、データの集合体を指します。また、そのデータの集合体の共用を可能にするシステムの意味を含めます

Q&A

解決済

3回答

5303閲覧

N+1 はなぜ遅いのでしょうか

mrasu

総合スコア21

ORM

ORM(オブジェクト関係マッピング)はオブジェクト指向のシステムとリレーショナルデータベースの間でマッピングを行う技術です。

データベース

データベースとは、データの集合体を指します。また、そのデータの集合体の共用を可能にするシステムの意味を含めます

0グッド

0クリップ

投稿2016/09/24 10:20

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ページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答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

raccy

総合スコア21735

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

mrasu

2016/10/11 14:50 編集

ありがとうございます。 確かに、僕の環境でも同じ傾向が現れました。 簡単な原因だけではないのですね。納得できました。 原因が深いこと、またこれ以上はおっしゃるとおり各実装に依存する部分が大きいと思いますので、 自分で調べてみようと思います。本当にありがとうございました。
guest

0

コンピュータ処理において、データのやり取りは、

  1. CPU - 二次キャッシュ
  2. CPU - メインメモリ
  3. CPU - メインメモリ - 別プロセス
  4. CPU - メインメモリ - HDD
  5. 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

kantomi

総合スコア295

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

mrasu

2016/10/10 04:38

ありがとうございます。 3,5が遅いこと理解しました。 また、回数がN回増えることも理解しました。 では、データのやり取りが遅いということは、データ量が重要な要素となると思うのですが、 その場合、N+1問題が発生するかどうかにかかわらずデータ量が変化しない場合(十分な量を一度に取らずにN+1回に分けて取得する場合)にはなぜ遅くなるのでしょうか? データ量よりもやり取りの回数のほうが重要な要素なのでしょうか?
kantomi

2016/10/10 05:45

処理に必要なデータ量は、1回で処理しても、N+1で処理しても同じなのです。 違うのなら、「違う処理」ということになりますね。
mrasu

2016/10/10 06:07

はい。そのつもりで質問しました。 ということは、やはりやり取りをするという「行為」自体が負荷になるということでしょうか? であれば、どんなに速いDB(例えばRedis)であってもN+1の影響は免れず、 同一サーバー上であってもネットワークの影響が消えるだけ(といってもかなり速くなると思いますが)で、 いくらIOを削ろうとも別プロセスと通信すること自体が速度低下の原因であり、 本質的にはDBに限った問題ではなく、偶々DBとのやり取りが多かったためにDBの部門で有名になったということなのですね。
kantomi

2016/10/10 06:38

比較するとき、ベースを変えたら意味がない! RedisはKVSなので、N+1しか無理でしょう。 HDDへのアクセスはありませんし、データの保存形式も、RDBMSと違って直接データを読むので高速です。 上の文章でいうと、Cの構造が全く違うため、比較しても意味がありません。
mrasu

2016/10/10 14:48 編集

申し訳ありません。ベースを変えたつもりがありませんでした。 「N+1が遅いのは、プロセス間通信やネットワーク通信によるオーバーヘッドが原因である」ということを確認したつもりでした。
guest

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
ikedas

総合スコア4306

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

mrasu

2016/09/28 13:48

回答ありがとうございます。 ただ、申し訳ありません。私が知りたかったのは、 > 前者 (a) の処理は後者 (b) よりはるかに時間がかかることは、直感的にわかると思います。 この部分を理解したく質問しました。 なぜ、データベースへのアクセスが遅いのかを知りたかったのです。 単一のプログラムではない以上遅くなることは理解できますが、どのような要因で遅くなっているのかを知りたいのです。
goute

2016/09/28 13:50

データベースに限った話ではなくファイルなどでも同じですが、物理的にほかのところへアクセスする(IOがある)というのはコストが大きいです。 相手を探しに行く→接続→読み取り(書き込み)→接続の解放(ほかにもあります)などの処理がありますので。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.49%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問