N+1問題がなんもわからんというだけの記事
N+1問題がなんもわからん。なんもわからんので記事にしてなんもわからん人口を増やしてやるという記事です。
N+1問題って何?
例えば User -> Friendという1対他の関係があるデータをDBに保管しているときに、下記のように問い合わせをやってしまうとN+1問題になります。
const users = User.findAll();
for (const user in users) {
const friends = user.findFriend();
// ...
}
簡単に言えば1回ユーザーのリストを取得し、そのユーザーに紐づくフレンドをループでN回取得しようとすることによってパフォーマンス問題を起こしてしまうというものになります。
解決策は簡単で
const users_with_friends = User.findUserWithFriends();
// ...
というように1度の取得でフレンドまで持ってくれば良いというので簡単ですね。
N+1問題完全に理解した!やったー!!!
何がわからんの?
これで終わればよかったのですが、ひょんなことでN+1問題を考える機会があり実はN+1問題の問題認識が人によってズレてるのでは・・・?ってなりました。
参考までにN+1問題でググってでる記事の説明を見てみましょう。
合計でN+1回のクエリを実行している状態です。
(1+N問題と考えた方が理解しやすい)
Nが大きいときは処理に非常に時間がかかるため、対応が必要になります。
ループ処理の中で都度SQLを発行してしまい、大量のSQLが発行されてパフォーマンスが低下してしまう問題のことです。
日常生活で例えるなら、スーパーで商品を1点ずつお会計するようなもの。
それだけ無駄なことを行なっている状態を指します。
DBアクセスが合計 N+1 回実行される問題
N+1問題とは、データベースからデータを取り出す際に、大量のSQLが実行されて動作が重くなるという問題
SQLが実行されるときには1回あたりわずかですが時間がかかります。
5回ぐらいの実行であれば動作にそれほど影響はないですが、これが100万回あったときはどうでしょうか?
読み込むまで相当な時間がかかってしまいますよね。
ですのでこの問題が起きないよう気をつける必要があります。
DjangoやRailsなど、ORMを利用するWebフレームワークなどの開発では、よく 「N+1問題」 というのが話題になります。ORMでは、あるモデルが参照している別のモデルを参照するとその時点でSQLが発行されてしまうため、気が付かないうちにパフォーマンスが低下する場合がある、というやつですね。
N+1問題とは簡単に言うと、データベースへのアクセス回数が余計に多くなってしまう現象です。加えてこれは、モデル間のアソシエーションを利用する場合に発生します。
私のわからんがわかるでしょうか。
そう、パフォーマンスが低下するのはわかる。わからんのは何が問題で何処のパフォーマンスが低下するのかです。
このあたりを読み漁ったり、人に聞いて整理してみると大きく2種類の話があるように見えます。
- 直列にSQLをN回発行するこで、アプリケーションのパフォーマンスが劣化する
- SQLをN回発行することで、データベースに負荷がかかり結果としてパフォーマンスが劣化する
この記事を読んでいる皆さんはN+1問題と聞いてどちらの問題だと思いますか?それとも両方の問題があると考えますか?
私は1が問題だと思っていて、ケースによっては2もあるかもねぐらいのスタンスですが、どうも聞いてみると2が問題であると考えてる人もいるようで、N+1問題って何が問題なの・・・?となんもわからなくなりました。
N+1問題なんもわからん。
直列に発行することで起きるパフォーマンス劣化の掘り下げ
1の直列にSQLをN回発行するこで、アプリケーションのパフォーマンスが劣化するはまぁわかりやすいです。
N+1回の問い合わせが直列に起きるため、Nが増えれば増えるほど問い合わせ回数が増え、線形に実行時間が伸びていきます。
であれば、下記のように並行、非同期化することでも解決できると思うんですよね。
const users = User.findAll();
const promises = users.map(user => user.findFriend().promise())
Promise.all(promises).then( ... )
(擬似コードだしJSであることはあんまり重要じゃないです。大事なのは並行に処理するあたり)
この場合、理論上はNが増えたとしても一定の時間で解決可能です。(実際にこれをトレースすると階段状になりそうな気はするし、設定次第な話にもなりそうだけど)
え、これじゃだめなの?なんでみんなクエリを一つにしたがるの・・・?
N+1問題なんもわからん。
データベースに負荷がかかりパフォーマンス劣化の掘り下げ
これに関してはデータベースで何を利用しているのか、テーブル構造はどうなっているのか、どのようなSQLが発行されるのか、どういったユースケースで使われてるのかで変わると思うんですよね。
そういうDBに負荷がかかるケースがあってもおかしくはないけど、N+1問題で出てくる例で常に出てくるとも思えないですし。
それに最近の私のデータベース感覚だと、小さなクエリでキャッシュに乗せて並行実行した方がよくない?みたいなのがあってツイッターで呟いていたらこんな同意をもらえました。
まぁ実際のところ程度問題だとは思っていて、1つのAPIの呼び出しでSQLが1万回発行されます!!は死ねそうと思うので要はバランスではと思いますけど。
(そもそも青天井や多くのN件のデータが必要な時点で何かが間違っていて、アクセスするデータ数を一定の件数におさめてコントローラブルにしようよと思うけど)
ただ、もしここが問題だと考えるならもっとN+1問題の解説でDBの種類と負荷の状況を記載しても良いと思うんですけどね・・・。
N+1問題なんもわからん。
クエリを1つにまとめてはいけないケース
個人的にクエリを1つにまとめてはいけないケースは知っていて、それもあって常にN+1問題の解決で常にクエリをまとめるべきかというと疑問があります。
そのケースはAWSのAurora for MySQLの5.6環境でReader(ReadreplicaではなくReader)に遅いクエリを頻発させるとHistry List Lengthが増加し、このHLLを解放するさいにWriterに影響を与えてDBをスローダウンさせるケースがあります。
つまりReaderだからと遅いクエリは投げてはいけないということですね。
この辺りは設定やユースケースによるので一概には言えませんが、クエリをまとめて遅いクエリを発行するのN+1問題の解決のためとはいえやってはいけない行為です。テーブル設計からやり直すべきでは?
え、じゃあN+1問題どうすればいいの・・・なんもわからん。
なんもわからん
もうなんもわからん。私はどう生きて、どう死ぬべきかもわからん。
今まで無意識にN+1問題が出たらクエリをまとめてN+1にしないようにしていたけど、よくよく考えて調べてみるとなんもわからんくなりました。
この記事を見てN+1問題がよくわからなくなった方。ようこそ仲間に^^
N+1問題ちょっとわかる方。僕の仲間が増える前にどう生きるべきかを教えてください。
まぁ、クエリをまとめて、そのクエリに問題があればそれ直すだけで答えを得たからと何か変わる訳ではないけど。N+1問題が何が問題かわからないの小骨が刺さったような気持ち悪さ。
Discussion
N+1問題を2種類に分けたけど、3つ目はさすがにいないよね・・・・・?
良記事ありがとうございます。
私も改めて説明求められる機会があったのですが、本質的な問題は "N" であることではないかと考えます。
複数回実行されることによってスループットの悪化を避けたいというのが直接的な目的にはなりますが、とはいえ実際の挙動はインデックスが効いたクエリで5、6回程度実行したとて、レスポンスタイムの著しい悪化につながることはまずないでしょう。(この際、レイテンシーは同じネットワークにいて考えないとして)
しかし、ユーザーデータのように常に肥大化し続けていくデータを考慮した場合、今5、6回で済んでいるものが運営を続けることで1000回になりえるかもしれません。
つまり私はこの、増加し続ける可能性がある "N" という可能性そのものの問題を指しているのではないかと考えます。