N+1問題とは?
N+1問題は、主にデータベースのクエリを扱う際に出くわす可能性のあるパフォーマンス上の問題で、特にオブジェクト関係マッピング (ORM) ツールを使用しているときによく見られます。
この問題は、リレーションシップが存在する2つのデータセット(例えば、親と子の関係にあるデータ)を取得する際に発生します。親データを1つ取得し、その後、その親に関連する各子データを個別に取得するという方法でデータを取得すると、データベースに対するクエリ数が増え、それによってパフォーマンスが低下する可能性があります。
N+1問題を具体的な例で説明します。
あるブログサイトを考えます。このサイトには多数の投稿があり、それぞれの投稿にはいくつかのコメントがついています。このブログサイトの情報をデータベースから取得したいとします。具体的には、各投稿とそれに対するコメントを全て取得したいと考えています。
N+1問題が発生するシナリオ:
まず、全ての投稿を取得するためのクエリを1つ実行します。
SELECT * FROM posts;
次に、取得した各投稿に対して、その投稿に対するコメントを取得するためのクエリを投稿ごとに実行します。例えば、投稿が10件あった場合、次のクエリを10回(投稿の数だけ)実行します。
SELECT * FROM comments WHERE post_id = ?;
このシナリオでは、データベースに対するクエリは合計で11回(1(投稿の取得)+ 10(各投稿に対するコメントの取得)= 11)実行されます。これがN+1問題です。
N+1問題を解決するシナリオ:
N+1問題を解決するには、全ての投稿とそれに対するコメントを1つのクエリで一度に取得します。具体的には、JOIN
句を使用して次のようなクエリを実行します。
SELECT * FROM posts LEFT JOIN comments ON posts.id = comments.post_id;
このクエリでは、posts
テーブルとcomments
テーブルを結合し、各投稿とそれに対するコメントを一度に取得します。その結果、データベースに対するクエリは1回だけになり、N+1問題は解決されます。
N+1問題を解決するその他の方法:
Batching or Bulk Fetching:
バッチ処理やバルクフェッチは、必要なデータを一度に取得することでクエリの数を減らします。例えば、特定のユーザーのリストを持っていて、それぞれのユーザーに対応する情報をデータベースから取得する場合、各ユーザーに対して個別にクエリを実行するのではなく、一度のクエリで全てのユーザー情報を取得します。SQLのIN句を使って、一つのクエリで複数のIDにマッチするレコードを取得することができます。
ユーザーのIDリストがあり、それぞれのユーザーの情報を個々に取得します:
SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE id = 2;
SELECT * FROM users WHERE id = 3;
...
SELECT * FROM users WHERE id = N;
上記のように書くと、ユーザーの数だけクエリが発行され、これがN+1問題になります。
それに対して、Batchingを使用すると、次のように一度のクエリで全てのユーザー情報を取得することができます:
SELECT * FROM users WHERE id IN (1, 2, 3, ..., N);
この場合、データベースへのクエリは1回だけで、必要な全てのユーザーの情報を取得することができます。これにより、クエリの数を大幅に削減し、パフォーマンスを改善することができます。
ただし、IN句に大量の要素を指定すると、クエリのパフォーマンスに影響を及ぼす可能性があるため、適度な数に分割してクエリを実行することが推奨されます。
Data Loader Pattern:
これは主にGraphQLなどのAPIで使われる手法です。リクエストが来た時にすぐにデータを取得するのではなく、一定時間待ってから、その間に集まった同じ種類のクエリを一つのクエリにまとめて実行します。例えば、あるリクエストでユーザーAの情報を取得し、すぐに別のリクエストでユーザーBの情報を取得する場合、2つのクエリを一つにまとめて、ユーザーAとBの情報を一度に取得します。
Data Loader Patternは、特にFacebookのGraphQLにおいてよく用いられます。Data Loaderは、GraphQLがデータベースからデータを取得するときにN+1問題を解消するために設計されたツールです。
以下に、JavaScriptを用いたData Loaderの実装例を示します。ここでは、dataloader
というライブラリを使います。
まず、dataloader
とデータベースとのやり取りを行うライブラリ(ここではpg
)をインストールします。
npm install dataloader pg
次に、Data Loaderを使ってデータを取得するための実装をします。以下の例では、ユーザーのIDを元にユーザーの詳細情報を取得します。
const DataLoader = require('dataloader');
const pg = require('pg');
// PostgreSQL clientを作成
const client = new pg.Client('postgres://username:password@localhost:5432/mydatabase');
client.connect();
// Data Loaderを作成
const userLoader = new DataLoader(async (userIds) => {
const { rows } = await client.query(
'SELECT * FROM users WHERE id = ANY($1)',
[userIds]
);
return userIds.map((id) => rows.find((row) => row.id === id));
});
// 使用例
async function getUserDetails() {
const user1 = await userLoader.load(1);
const user2 = await userLoader.load(2);
console.log(user1, user2);
}
getUserDetails();
この例では、DataLoader
は内部的にload
メソッドで要求された全てのIDをバッチ化し、一度にデータベースに問い合わせます。これにより、各IDに対して個別にクエリを発行することなく、複数のユーザーの詳細を効率的に取得できます。これがData Loader Patternの一例です。
このパターンは、必ずしもGraphQLやJavaScriptだけでなく、他の言語やフレームワークでも同様の方法で適用することが可能です。要点は、要求されたデータを一度に取得することでデータベースへの問い合わせを最小限に抑えることです。