はじめに
バックエンド側の開発を行っているとかならず遭遇するN+1問題。
今回は初心者向けにN+1問題はなぜ避けるべき対象とされているのか。例えを用いて分かりやすく解説していきたいと思います。
対象読者
- 初心者エンジニア
- Rails を触り始めたエンジニア
N+1問題とは
ループ処理の中で都度SQLを発行してしまい、大量のSQLが発行されてパフォーマンスが低下してしまう問題のことです。
まずはN+1問題の概要から。どうやらDB周りの処理によってパフォーマンスが低下してしまう問題のようですね。これだけだといまいちよく分からないかも知れません。ただパフォーマンスが低下するということはつまり非効率な動作を行なってしまっているという理解までできていれば問題ないです。
非効率性を身を持って体感してみよう
ではN+1を根底から理解するため、実際に検索行為という例えを通して非効率性を体感してみましょう。早速お好きなブラウザを開いて、以下の単語の検索結果を表示してください。そして一番最初の結果のURLをメモしておきましょう。
- ActiveRecord
- prisma
- TypeORM
メモしておきましたか?ありがとうございます。それでは次はまた頭から検索して、今度はその二番目の結果のURLを教えてください。さっきは言いませんでしたが、二番目のサイトも私は知りたかったのです。
え?だったら最初からそう言えって??素晴らしい!あなたはもうN+1問題を十分に理解しています。ええ、お分かりであるように三回で済むはずの検索行為が六回と二倍にまで膨れ上がってしまいました。この行為は非効率と呼ぶべき代物ですね。これがソフトウェアにおいてN+1問題と呼ばれる現象を現実に持ち込んだ一例になります。
実際の問題に当てはめてみる
ループ処理の中で都度SQLを発行してしまい、大量のSQLが発行されてパフォーマンスが低下してしまう問題
こちらを先ほどの例に当てはめてみましょう。
「URL取得処理の中で都度検索をしてしまい、大量の検索が行われてパフォーマンスが低下してしまう問題」
どうでしょうか。先ほどの見た時よりも理解しやすくなっているのではないでしょうか?本来少ない数で済むはずの行為が繰り返し処理によって無駄に処理が増える現象。これがN+1問題と呼ばれる状態を指すことが理解できたと思います。
それでは実際のN+1問題が発生しているコードも載せておきます。
# 全ての映画情報を取得
Movie.all.each do |movie|
# directorテーブルから名前を取得し、カンマ区切りで結合する
director_names = movie.directors.pluck(:name).join(",")
end
このようなコードは一度取得したあと再度別の情報にアクセスしているため、n+1問題が発生し、パフォーマンスが低下します。先ほどの三件ほどの検索でしたら少し筆者に腹が立つくらいで済むでしょうが、一万件ともいけば訴訟ものです。
対策
ではN+1問題の対策はどうすればいいのでしょうか?実は既にあなたは答えを導き出しています。
え?だったら最初からそう言えって?
そうです。最初のデータを取る前に必要なデータは何が必要か明示しておけば良いんですね。以下のコードのように最初に使う予定のテーブルもまとめて一度のクエリで取得してしまえば、効率的にデータを取得することができます。
# 全ての映画情報を取得
Movie.preload(:directors).all.each do |movie|
# directorテーブルから名前を取得し、カンマ区切りで結合する
director_names = movie.directors.pluck(:name).join(",")
end
※ Rails には preload
, eager_load
, includes
等いくつかN+1問題に関わるメソッドが存在しますがここではその違いについては解説しません。詳しい説明が欲しい方は以下記事を参考にすると良いかもしれません。
おわりに
N+1問題についていままで解説を読んでも理解できなかった方々へ少しでも理解につながれば幸いです。
また、検索対象とした文字列は全てORMと呼ばれるN+1問題に関わるライブラリです。最近勢いのある prisma
などはもとからN+1問題を防ぐ機構が備えてあるらしく、いずれN+1問題について気にせずに開発できる時代が来るかもしれません。
参考記事