Skip to content

Commit ace1e13

Browse files
committed
n1 and dataloader
1 parent 5adeb7f commit ace1e13

File tree

1 file changed

+145
-0
lines changed

1 file changed

+145
-0
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
---
2+
title: Solving the N+1 Problem with `DataLoader`
3+
---
4+
5+
When building a GraphQL API with `graphql-js`, it's common to
6+
run into performance issues caused by the N+1 problem: a pattern that
7+
leads to a large number of unnecessary database or service calls,
8+
especially in nested query structures.
9+
10+
This guide explains what the N+1 problem is, why it's relevant in
11+
GraphQL field resolution, and how to address it using
12+
[`DataLoader`](https://github.com/graphql/dataloader).
13+
14+
## What is the N+1 problem?
15+
16+
The N+1 problem happens when your API fetches a list of items using one
17+
query, and then issues an additional query for each item in the list.
18+
In GraphQL, this ususally occurs in nested field resolvers.
19+
20+
For example, in the following query:
21+
22+
```graphql
23+
{
24+
posts {
25+
id
26+
title
27+
author {
28+
name
29+
}
30+
}
31+
}
32+
```
33+
34+
If the `posts` field returns 10 items, and each `author` field fetches
35+
the author by ID with a separate database call, the server performs
36+
11 total queries: one to fetch the posts, and one for each post's author
37+
(10 total authors). This doesn't scale well as the number of parent items
38+
increases.
39+
40+
Even if several posts share the same author, the server will still issue
41+
duplicate queries unless you implement deduplication or batching manually.
42+
43+
## Why this happens in GraphQL.js
44+
45+
In `graphql-js`, each field resolver runs independently. There's no built-in
46+
coordination between resolvers, and no automatic batching. This makes field
47+
resolvers composable and predictable, but it also creates the N+1 problem.
48+
Nested resolutions, such as fetching an author for each post in the previous
49+
example, will each call their own data-fetching logic, even if those calls
50+
could be grouped.
51+
52+
## Solving the problem with `DataLoader`
53+
54+
[`DataLoader`](https://github.com/graphql/dataloader) is a utility library designed
55+
to solver this problem. It batches multiple `.load(key)` calls into a single `batchLoadFn(keys)`
56+
call and caches results during the life of a request. This means you can reduce redundant data
57+
fetches and group related lookups into efficient operations.
58+
59+
To use `DataLoader` in a `graphpql-js` server:
60+
61+
1. Create `DataLoader` instances for each request.
62+
2. Attach the instance to the `contextValue` passed to GraphQL execution.
63+
3. Use `.load(id)` in resolvers to fetch data through the loader.
64+
65+
### Example: Batching author lookups
66+
67+
Suppose each `Post` has an `authorId`, and you have a `getUsersByIds(ids)`
68+
function that can fetch multiple users in a single call:
69+
70+
```js
71+
import {
72+
graphql,
73+
GraphQLObjectType,
74+
GraphQLSchema,
75+
GraphQLString,
76+
GraphQLList,
77+
GraphQLID
78+
} from 'graphql';
79+
import DataLoader from 'dataloader';
80+
import { getPosts, getUsersByIds } from './db.js';
81+
82+
const UserType = new GraphQLObjectType({
83+
name: 'User',
84+
fields: () => ({
85+
id: { type: GraphQLID },
86+
name: { type: GraphQLString },
87+
}),
88+
});
89+
90+
const PostType = new GraphQLObjectType({
91+
name: 'Post',
92+
fields: () => ({
93+
id: { type: GraphQLID },
94+
title: { type: GraphQLString },
95+
author: {
96+
type: UserType,
97+
resolve(post, args, context) {
98+
return context.userLoader.load(post.authorId);
99+
},
100+
},
101+
}),
102+
});
103+
104+
const QueryType = new GraphQLObjectType({
105+
name: 'Query',
106+
fields: () => ({
107+
posts: {
108+
type: GraphQLList(PostType),
109+
resolve: () => getPosts(),
110+
},
111+
}),
112+
});
113+
114+
const schema = new GraphQLSchema({ query: QueryType });
115+
116+
function createContext() {
117+
return {
118+
userLoader: new DataLoader(async (userIds) => {
119+
const users = await getUsersByIds(userIds);
120+
return userIds.map(id => users.find(user => user.id === id));
121+
}),
122+
};
123+
}
124+
```
125+
126+
With this setup, all `.load(authorId)` calls are automatically collected and batched
127+
into a single call to `getUsersByIds`. `DataLoader` also caches results for the duration
128+
of the request, so repeated `.loads(id)` calls for the same ID don't trigger
129+
additional fetches.
130+
131+
## Best practices
132+
133+
- Create a new `DataLoader` instance per request. This ensures that caching is scoped
134+
correctly and avoids leaking data between users.
135+
- Always return results in the same order as the input keys. This is required by the
136+
`DataLoader` contract. If a key is not found, return `null` or throw depending on
137+
your policy.
138+
- Keep batch functions focused. Each loader should handle a specific data access pattern.
139+
- Use `.loadMany()` sparingly. While it's useful in some cases, you usually don't need it
140+
in field resolvers. `.load()` is typically enough, and batching happens automatically.
141+
142+
## Additional resources
143+
144+
- [`DataLoader` GitHub repository](https://github.com/graphql/dataloader): Includes full API docs and usage examples
145+
- [GraphQL field resovlers](https://graphql.org/graphql-js/resolvers/): Background on how field resolution works.

0 commit comments

Comments
 (0)