仕組み

N+1は、一覧取得の1回に加えて、各要素の関連データ取得をN回行う状態です。

たとえば、記事20件と、その著者名を表示する場合です。

const articles = await getArticles({ limit: 20 }); // 1回

for (const article of articles) {
  article.author = await getUser(article.authorId); // 20回
}

合計21回の問い合わせになります。

ORMでは、関連プロパティを参照しただけでSQLが発行される場合もあるため、コードから気付きにくいことがあります。Prisma:N+1問題

対策①:関連データをまとめて取得する

-- 1回目:記事を取得
SELECT id, title, author_id
FROM articles
ORDER BY id DESC
LIMIT 20;
-- 2回目:必要な著者をまとめて取得
SELECT id, name
FROM users
WHERE id IN (...);

取得後に、アプリケーション側で author_id と id を対応付けます。

この例では、記事ごとの問い合わせを2回の問い合わせにまとめられます。

対策②:JOINする

SELECT
  a.id,
  a.title,
  u.name AS author_name
FROM articles AS a
LEFT JOIN users AS u ON u.id = a.author_id
ORDER BY a.id DESC
LIMIT 20;

記事と著者のような関係では、JOINでまとめて取得できます。

対策③:DataLoaderなどでバッチ化する

GraphQLのように、各フィールドが個別に関連データを要求する構造では、複数の要求を集めてまとめて取得する方法があります。

ただし、認可条件が異なる利用者間で結果を共有しないようにします。リクエスト単位でバッチやメモリ上のキャッシュを管理する設計がよく使われます。

注意点:SQLを1回にすること自体が目的ではない

記事にコメントやタグをまとめてJOINすると、組み合わせにより結果行が大きく膨らむことがあります。

また、JOINした結果に単純に LIMIT 20 を付けると、「記事20件」ではなく「結合後の20行」になる場合があります。

並列化だけにも注意が必要です。

await Promise.all(
  articles.map(article => getUser(article.authorId))
);

待ち時間が短くなる可能性はありますが、問い合わせ回数は減りません。DBへの同時負荷が増えることもあります。

設計上の要点
問い合わせ回数、取得行数、転送量、実行時間を確認し、必要なデータを適切なまとまりで取得します。