Reduce N+1 Queries with a DataLoader
Batch and cache per-request lookups with a DataLoader so list endpoints stop issuing one query per item.
Task
Introduce request-scoped batching to collapse N+1 queries into one batched query.
When to use this
A list endpoint issues one database query per row, multiplying latency as the list grows.
Prerequisites
- A GraphQL or REST backend
- A query layer you can batch
- Query logging to verify counts
Steps
1. Wrap the lookup in a per-request DataLoader.
const userLoader = new DataLoader(async (ids) => {
const rows = await db.query('SELECT * FROM users WHERE id = ANY($1)', [ids]);
const byId = new Map(rows.map(r => [r.id, r]));
return ids.map(id => byId.get(id) || null);
});Expected output: Concurrent per-item loads are coalesced into one batched query.
2. Verify the query count drops.
queryLog.length; // expect 1 instead of N
for (const post of posts) await userLoader.load(post.userId);Expected output: The query log shows a single batched query for the whole list.
Troubleshooting
Problem
Cache leaks across requests
Solution
Create a fresh loader per request so cached values do not bleed between users.
Problem
Batch size too large
Solution
Cap batch size or use a query timeout to avoid huge IN clauses.
Limitations
- Requires batching-capable data access.
- Per-request caching only dedupes within a request.
- Large batches can exceed query limits.
What this recipe does
Introduce request-scoped batching to collapse N+1 queries into one batched query.
A list endpoint issues one database query per row, multiplying latency as the list grows.
Prerequisites
- A GraphQL or REST backend
- A query layer you can batch
- Query logging to verify counts
Procedure
- Wrap the lookup in a per-request DataLoader.
const userLoader = new DataLoader(async (ids) => {
const rows = await db.query('SELECT * FROM users WHERE id = ANY($1)', [ids]);
const byId = new Map(rows.map(r => [r.id, r]));
return ids.map(id => byId.get(id) || null);
});
Expected result: Concurrent per-item loads are coalesced into one batched query.
- Verify the query count drops.
queryLog.length; // expect 1 instead of N
for (const post of posts) await userLoader.load(post.userId);
Expected result: The query log shows a single batched query for the whole list.
How to confirm it worked
Run the commands above and confirm every expected result in order. If a step does not produce the stated output, use the troubleshooting notes before continuing. A passing run shows the query log shows one batched query per request instead of one per item, with identical results.
Troubleshooting
Cache leaks across requests — Create a fresh loader per request so cached values do not bleed between users.
Batch size too large — Cap batch size or use a query timeout to avoid huge IN clauses.
Limitations
- Requires batching-capable data access.
- Per-request caching only dedupes within a request.
- Large batches can exceed query limits.