Solving the GraphQL N+1 Problem with DataLoader
Fetch 50 books with their authors, and a naive server runs 1 query for the books plus 50 queries for authors. That's the N+1 problem, and it's the most common GraphQL performance issue.
Why it happens
Book: {
// called once per book, each with its own query
author: (book, _, { db }) => db.query('SELECT * FROM authors WHERE id = $1', [book.authorId]),
}
DataLoader batches the calls
DataLoader collects every load(id) call made in the same tick, then calls your batch function once with all the ids.
import DataLoader from 'dataloader';
function createLoaders(db) {
return {
authorById: new DataLoader(async (ids) => {
const { rows } = await db.query('SELECT * FROM authors WHERE id = ANY($1)', [ids]);
const byId = new Map(rows.map((a) => [String(a.id), a]));
return ids.map((id) => byId.get(String(id)) ?? null); // same order as ids!
}),
};
}
Create loaders per request
context: async ({ req }) => ({
db,
user: await userFromToken(req.headers.authorization),
loaders: createLoaders(db), // fresh cache per request, so users never see each other's data
}),
Book: {
author: (book, _, { loaders }) => loaders.authorById.load(book.authorId),
}
Now 50 books need just 2 queries: one for books, one for all their authors.
The two rules of batch functions
- Return an array the same length as the keys.
- In the same order as the keys (use
nullor anErrorfor missing items).
One-to-many relations
booksByAuthor: new DataLoader(async (authorIds) => {
const { rows } = await db.query('SELECT * FROM books WHERE author_id = ANY($1)', [authorIds]);
return authorIds.map((id) => rows.filter((b) => String(b.author_id) === String(id)));
}),
Log the number of SQL queries per GraphQL request in development so N+1 regressions are caught early.
Was this article helpful?
Comments
No comments yet. Have a question or a tip? Be the first to share it.