💡

【データベース】N+1問題について

2024/12/18に公開

N+1問題とは

N件の行を持つテーブルの前データ取得でクエリを1回実行
別のテーブルから、前述のテーブルの各行に紐づくデータを1件ずつ取得するためクエリをN回実行
合計でN+1回のクエリを実行している問題のことです。
Nが大きくなるとクエリの数も増えてしまうので、処理に時間がかかってしまいます。

レンタルDVDの全データを取得し、それぞれのDVDを借りている人の情報を取得するとします。

まず、レンタルDVDの情報を全データ取得します。

SELECT * FROM DVD;

次に、各DVDを借りている人のIDを見て、対応する利用者の情報を取得します。

SELECT * FROM 利用者 WHERE 利用者ID=1;
SELECT * FROM 利用者 WHERE 利用者ID=2;
SELECT * FROM 利用者 WHERE 利用者ID=3;

このように関連する情報を取得する際に、何度もクエリを実行してしまうことがN+1問題になります。

解決策

JOIN句を使う

JOINでテーブルを結合し、1回のクエリ実行で関連する情報も丸ごと取得する方法です。

SELECT DVD.ID, DVD.DVD名, 利用者.利用者名
FROM DVD JOIN 利用者 ON DVD.利用者ID = 利用者.ID

Eager Loadingを使う

テーブルの取得に1回クエリ発行
別テーブルから、今後の処理に必要なデータを1回のクエリでまとめて取得
その後アプリ側で、データの結合などの処理を行う

この場合は、2回のクエリ実行を行います。

まずDVDテーブルのデータを全件取得します。

SELECT * FROM DVD;

各DVDの利用者IDを格納する配列を作成します。

userIDs = [1, 2, 3]

利用者テーブルから、利用者情報を取得します。

SELECT * FROM 利用者 WHERE ID IN (1, 2, 3);

NestJSで実装してみた

下記コードはJOIN句を使った処理になります。
こうすることで、1回のクエリ実行で必要なデータを取得することができます。

SELECT
  user.id AS user_id,
  user.name AS user_name,
  user.email AS user_email,
  books.id AS books_id,
  books.name AS books_name,
  books.userId AS books_userId,
FROM
  user
LEFT JOIN
  book AS books
ON
  user.id = books.userId;
async getUsersWithBooks(): Promise<User[]> {
    return this.userRepository
      .createQueryBuilder("user")
      .leftJoinAndSelect("user.books", "books") // ユーザーと本を結合
      .getMany();
}

参考Project

下記のプロジェクトにJOIN句で1回クエリ実行してデータ取得するコードを掲載しています。
よければ参考にしてください!
https://github.com/John-Thailand/demo-migration-tools/tree/feature/n-plus-one-problem

参考になった記事

https://qiita.com/muroya2355/items/d4eecbe722a8ddb2568b

Discussion