GraphQLとN+1問題
目次
N+1問題とは
N+1問題とは、データベースへのアクセスが、本来は1回(または少数回)で済むはずの処理にもかかわらず、取得したデータN件それぞれに対して追加の問い合わせを発行してしまうことで、アクセス数が件数に比例して線形増加する性能問題です。
特にORMを使用する際のアンチパターンという文脈で目にする方も多いと思います。
具体的には、まず一覧(複数件)を取得するためのアクセスが1回発生し、その後、各行の関連データ取得などのためにN回の追加アクセスが発生します。
まさに N+1回 のアクセスになるため、N+1問題と呼ばれます。
// 例: 顧客一覧を取得して、各顧客の担当者も取得する
// customers が N 件のとき、DBアクセスは 1 + N 回になる
const customers = await db.customer.findMany(); // 1回
for (const customer of customers) {
// 各顧客ごとに担当者を取得している(N回)
customer.staff = await db.staff.findUnique({
where: { id: customer.staffId },
});
}
N+1問題を通してみる、REST APIとGraphQLの違い
REST API はエンドポイントごとにレスポンス形状が固定されるため、画面に必要な関連データ(例:顧客+担当者)をサーバー側でJOIN等によりまとめて取得し、少ないDBアクセスで返す設計にしやすいです。その結果、N+1が入り込みにくくなります。
一方、GraphQLは取得フィールドがリクエストごとに変わり、フィールド単位でリゾルバが実行されます。
たとえば、次のクエリは宣言的な見た目ですが、
{
customers {
id
name
staff { id name }
}
}
実装次第で「customersは1回取得したが、staffは顧客件数だけ個別取得している」という状態になり得ます。リゾルバ内で関連取得を素朴に書くとN+1問題が発生しやすくなります。
どう防ぐ?
1. リレーションをまとめて取得する
基本は「行ごとの追加取得」をやめ、関連データを一括で取ります。ORMのeager loading(例:include)や、SQLのJOIN相当の発想で、1+N回を少数回に圧縮します。
2. DataLoaderパターンを併用する
GraphQLでは同じ型・同じフィールドの取得が散発的に繰り返されがちです。DataLoaderで取得要求をリクエスト内で集約し、まとめて問い合わせることで重複アクセスを減らします。
3. クエリ設計を見直す
深いネストを取りすぎない、1画面で大量の関連を取らない、ページングや条件指定を前提にする。GraphQLは見た目が短くても裏のDBアクセスが増えやすいため、どのフィールドが何回DBに行くかを意識して設計します。
実装テクニックだけでは不十分
GraphQLのN+1対策は、DataLoaderなどの実装テクニックだけで完結する話ではありません。GraphQLはクエリが柔軟であるぶん、どのフィールドをどれだけ取得するかによって、実行コストが簡単に増えます。
柔軟性とパフォーマンスはトレードオフになりやすいため、柔軟なツールを使うときほど、ボトルネック化しうる点がないか常に目を配る必要があります。
株式会社オートプロジェクトでは、中小企業向けのシステム・アプリケーション開発 / 外注サービスを提供しております。
貴社のニーズに応じた柔軟なサポートを行いますので、ぜひお気軽にご相談ください。
