skip to content
Samuel Edwin's Website

N+1 Query — GraphQL Performance Killer and Solution

/ 3 min read

GraphQL uses resolvers in order to handle complex queries.

Due to how resolvers work however, you can accidentally create a performance problem called the N+1 query.

What is N+1 query?

Consider this innocent-looking query:

# search for products by keyword
# and also show the product's shop
query SearchProducts {
searchProducts(keyword: "phone") {
id
name
price
shop {
id
name
}
}
}

This is what the GraphQL resolver will do to resolve this query:

  1. Query.searchProducts resolver will be called. Once the query is resolved,
  2. Product.shop resolver will be called for each product.

This is how the code looks like:

const resolvers = {
Query: {
searchProducts: async (_, { keyword }) => {
return await db.product.findMany({
where: { keyword: `like %${keyword}%` }
})
},
},
Product: {
shop: async (product) => {
return await db.shop.findById(product.shopId)
},
},
};

When the search returns 20 products, Product.shop resolver will call the database 20 times in a short time range.

N + 1 query diagram

This is what we call N+1 query problem.

The number of database calls grows according to the number of previous results returned.

How Dataloader and batching can solve this problem

We already know ahead of time that Product.shop resolver will be called multiple times with different product ids.

We can just batch the database calls to one single call with multiple product ids at once.

This is what Dataloader does.

Here’s how we can use Dataloader to solve the N+1 query problem:

dataloader.js
// create a dataloader that queries the database
// with an array of product ids
// and returns an array of shops
export const dataLoader =
new DataLoader(async (productIds) => {
return await db.shop.findMany({
where: { id: { in: productIds } }
})
})

And here’s how we can use the dataloader in the resolver:

resolver.js
import { dataLoader } from './dataloader'
const resolvers = {
Query: {
searchProducts: async (_, { keyword }) => {
return await db.product.findMany({
where: { keyword: `like %${keyword}%` }
})
},
},
Product: {
shop: async (product) => {
return await dataLoader.load(product.shopId)
},
},
};

How does Dataloader work?

  1. Dataloader is created with a batch function.
const dataLoader = new DataLoader(batchFn)
  1. When dataLoader.load is called, it will queue the request until the queue is full or some time has passed.
  2. When the above condition is met, dataLoader will call the batch function with an array of keys (in this case, shop ids). Dataloader diagram
  3. Then the dataLoader will split the array and return the requested item for each key. Dataloader returns data

This way instead of 21 database calls, we only make 2 database calls.

Where can I get these dataloader implementations?

There are many different dataloader implementations.

Popular languages and frameworks tend to have dataloaders implemented by the community.

This is an example of JavaScript implementation.

When should I use Dataloader?

Sometimes it’s hard to tell ahead of time whether a resolver will be called multiple times or not.

I tend to use Dataloader by default whenever I see a resolver requests for a single resource.

Conclusion

It is easy to create N+1 query problem in GraphQL and can happen very often.

Dataloader solves this problem by batching the database calls into a single call.

Take time to analyze your GraphQL schema and find spots where N+1 query problem can happen. Then use Dataloader to solve it.