見出し画像

N+1問題について語る

本日はITの話題です。世間様とはずれた視点で真面目に語ります。
興味ない方はすっ飛ばしてちょ。
SQLをかじる程度に知ってる人にはぜひ読んで欲しいです。

N+1問題とは

この言葉を今日知りました。
ループ処理内でSELECTすると何回も呼び出しを行うから遅くなるという問題だそうです。
個人的にびっくりしたのは・・・
この事象に名前がついてるってことです。
てっきり「巡回セールスマン問題」などを想像してしまいました。
要は「しょぼいロジック書いてんじゃねーよ!」という事例のひとつなんです。
名前いるんかい???
ひどい記事では、「N+1問題も知らないのか」みたいな卑下する言葉もありました。知らんかったばい。すまんのう。

ぐぐるといろいろ出てくるのですが、そのあほらしさについてあえて解説しよう。

よくあるシステム構成

フツーのITシステムはこんな感じですね。※かなり端折ってます

題材として「商品を注文したときの注文明細リストを出したい」としよう。
こんなやつね。

DB上では商品のマスターデータと注文データは別に管理されています。
下のようなイメージで表すのがふつーです。
RDB的な話なので少し端折りますが、注文データには商品コードは記録するけど、商品名は記録しない。必要なときに商品マスターを見に行けばいい。

話は戻ります。
画面に注文明細を表示したいと思ったら通常はSQLでDBアクセスします。
このときのロジックをざくっと書くとこんなことになる。
(こんな言語は存在しませぬ)

rs1 = con.query("select * FROM 注文トランザクション WHERE 注文番号=x0010111;")
do while rs.eof = false
  @shcd = rs1.col(商品コード);
 商品マスターを検索して1行ごとの名称を取得する
 rs2 = con.query("select * FROM 商品マスター WHERE 商品コード= @shcd;")
  明細をこりこり書きだす
loop  

よって、欲しい明細行数(N行)+1回分のSELECTを実行することになる。
1つの明細データを出力するのに多数のSELECTをしないといけないから処理が遅くなってしまうことがあるんです。

これが「N+1問題」 と呼ばれる所以です。

ちなみに解決策としてはJoinして1回のSELECTで済ませましょう って書いてありました。

しけしけしけ!しょぼっ!

そんだけかよ!
こんなのに名前をつけんなよ!
と思ったんですが、名前が付いてるということはそれだけよくあることなんだろうか。

時を遡ること25年前。
2000年問題よりも前の時代にはIT業界という名前もなかった頃。
当時のコンピューターシステムでは、上記のN+1問題は問題ではなかったケースも多かった。
というのも、ITシステムは冒頭の絵で書いたような構成ではなかったから。
・DBサーバーとアプリサーバー(相当)は同じサーバーで動作していた
・SQLが使えなかった

この環境だと、”N+1問題” は問題ではなくて、スタンダードな手法なのです。
当然ながら、このやり方が卑下されることはない。
アプリとDBは同一筐体にあるので、N+1処理はまったく遅くない。

問題の本質はN+1ではない

あんまり偉そうなことを書けなかったりするのですが、何が問題の本質なのかについて語りたい。
不思議なことに、N+1問題の記事ではあまり触れられていない気がする。

明細Loopの中でSELECTをやっていけないかというと、そうでもないんです。
呼び出しコストや時間の合計が大きくなりすぎないようにしないといけないってことなんです。

仮に100件の明細を返すデータだったとしよう。
まず、N+1の場合
・明細データのSELECT   1秒
・商品名の取得 1回につき 1秒
⇒合計 101秒
となる

Join あり で1回で取得する
・JOIN明細データのSELECT   10秒
⇒合計10秒となる

これを見るとJoinありがいいよね!ってなりますね。

コストについてあまり触れませんでしたが、処理時間以外にもディスクIOやCPU使用率、ネットワーク帯域の使用量もコストの一部です。
この処理はちゃんと動くけど、他が止まったら目も当てられない。

もしも商品名取得のコストが 0.01 秒だったら

合計時間は2秒で済むのでN+1のほうが速いってことになります。

トータルでどうなのよ?ということを考えないといけません。
別の言い方をすると、
どこで誰がどんな処理を行っているのかを意識しないといけないってことです。
冒頭の絵では サーバー室に2台のサーバーがありましたが、この場合だとN+1はそんなに遅くなりません。
アプリサーバーをオフィス側に移設すると劇遅になります。
サーバー間にインターネットが挟まることで、通信速度がネックになって処理時間が増大するんです。

かなり前の話ですが、JOINが遅すぎたせいで、N+1に書き直したことがあります。条件次第では逆転することもあるんです。(あんまりないけどね)

もしも商品名取得時にDBに接続していたら

今度は逆に劇遅になるケースです。
ループ内のDBコネクションを生成してSQLを実行したらどうなるでしょう。
SQLよりもDBコネクションの生成と破棄のほうが時間がかかります。
場合によっては数秒かかることがあります。1回5秒かかったとして、100明細で500秒。
こんだけ遅ければ作ってる奴はおかしいと思うよね。
しかし、、、こんなのを平気な顔して作るやつは世の中にはざらにいます。

問題の本質はどこよ?

十分なITスキルがない人は、自分が組んだプログラムコードのことしか想像できません。
DBサーバーがSQLを動かしてるので、プログラムを書いた人はデータを収集しているという実感がないんです。まぁ、そうだろうけど。
でも、動いてる以上はそのコストや影響がどこかで発生しているから加味しないといけないんです。

ちなみにJOINで記述した場合でも”N+1問題”と同じ挙動が別の場所で起きています。
それはDBサーバーの内部です。SQLのJOIN1回で取得してるように見えるけど内部ではN+1をやるんです。
上の段落で書いた「どこで誰がどんな」が違ってくるということです。

総合的に判断してちょ

いまどきの一般的な構成だとN+1記述をしないで、JOIN一発がよいことが多いですが、総合的に判断するのが正しいです。

・サーバーの配置
・ネットワークの速度
・サーバーの使用料と性能差
・処理回数
・処理時間
・CPU負荷
・JOIN の複雑さ
・目標値
・コーディングルール
・保守性

おわりに

”N+1問題” とは、実は問題ではないと思ってるのですが、非機能要件やシステムアーキテクチャを意識してITシステムを構築するという意味ではとても基本的で大事な課題なんじゃなかろうか。
これからIT業界に関わる人は気にしてほしいなと思います。


この記事が気に入ったらサポートをしてみませんか?