← Recipes

阅读中文版本

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.

~35 minperformancedatabasegraphql

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

  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 result: Concurrent per-item loads are coalesced into one batched query.

  1. 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.