😽

「N+1問題」によってパフォーマンスが大幅に悪化する問題

に公開

「N+1問題」とは、データベースにアクセスする際に、必要以上に多くのクエリ(問い合わせ)が発行されてしまい、パフォーマンスが大幅に悪化する問題のことです。特に、O/Rマッパー(Object-Relational Mapper)を利用するWebアプリケーションなどでよく発生します。

名前の由来は、最初に1回のクエリで親となるデータを取得し、その後、その親データに紐づく子データを取得するために、親データの数(N個)だけ追加のクエリが発行されてしまう、合計「N+1」回のクエリが発生することから来ています。


N+1問題が起こる仕組み

この問題は、ループ処理の中で関連するデータを個別にデータベースから取得しようとするときに発生します。

例:ブログの記事(親)とその記事へのコメント(子)を取得する場合

  1. 最初の1クエリ: まず、すべてのブログ記事の一覧を取得します。
    SELECT * FROM posts;
    
  2. N回の追加クエリ: 次に、取得した各記事(N個)に対して、それぞれに紐づくコメントをループ処理で1件ずつ取得していきます。
    -- 1番目の記事のコメントを取得
    SELECT * FROM comments WHERE post_id = 1;
    -- 2番目の記事のコメントを取得
    SELECT * FROM comments WHERE post_id = 2;
    -- 3番目の記事のコメントを取得
    SELECT * FROM comments WHERE post_id = 3;
    -- ... N番目の記事のコメントを取得
    SELECT * FROM comments WHERE post_id = N;
    

この結果、記事が100件あれば、最初の1回+100回の合計101回のクエリがデータベースに送られます。データベースへの問い合わせは、それ自体が時間のかかる処理であるため、クエリの回数が増えれば増えるほど、アプリケーションの応答速度は著しく低下します。


N+1問題のデメリット 😵

  • パフォーマンスの悪化: クエリ回数に比例して、ページの表示速度などが遅くなります。
  • データベースへの高負荷: 大量のクエリが短時間に集中することで、データベースサーバーに大きな負荷がかかります。
  • スケーラビリティの低下: データ量が増えるほど問題が深刻化し、システムの拡張性を妨げます。

N+1問題の解決策

この問題を解決するには、**関連するデータをあらかじめ一括で読み込んでおく(Eager Loading)**のが一般的な方法です。これにより、ループ処理の中で都度クエリを発行する必要がなくなります。

主な解決アプローチ

  1. JOIN(ジョイン)を使う:
    親テーブルと子テーブルをJOINで結合し、1回のクエリで必要なデータをすべて取得します。

    SELECT * FROM posts
    LEFT JOIN comments ON posts.id = comments.post_id;
    

    この方法だと、1回のクエリで全記事とそれに関連する全コメントを取得できます。

  2. IN句を使う:
    まず親データを取得し、その後、取得した親データのIDをIN句にまとめて指定し、関連する子データを一括で取得します。

    -- 1. まず親データを取得
    SELECT * FROM posts;
    
    -- 2. 親データのIDリストを使い、子データを一括取得
    SELECT * FROM comments WHERE post_id IN (1, 2, 3, ..., N);
    

    この方法では、クエリは常に2回で済みます。

多くのフレームワーク(Ruby on Railsのincludesやeager_load、Laravelのwithなど)には、こうしたEager Loadingを簡単に行うための機能が用意されています。

まとめ

N+1問題は、開発の初期段階やデータ量が少ないうちは気づきにくいことが多いですが、アプリケーションの成長とともに深刻なパフォーマンス問題を引き起こすため、早期に発見し、対策を講じることが非常に重要です。

Discussion