The N+1 query problem is one of the most common performance bottlenecks in GraphQL applications. Let me show you exactly what it is and how to fix it.
Understanding the Problem
Imagine you have this GraphQL query:
query { posts { id title author { id name } } }
With a naive implementation, here's what happens:
- One query to fetch all posts (10 posts)
- Ten queries to fetch the author for each post
Total: 11 queries for what should be 1-2 queries!
The Code That Causes N+1
const resolvers = { Query: { posts: () => db.posts.findAll(), }, Post: { // ⚠️ This runs once PER post! author: (post) => db.users.findById(post.authorId), }, };
For 10 posts, author resolver runs 10 times = 10 separate database queries!
The Solution: DataLoader
DataLoader batches and caches requests within a single request cycle.
Installation
npm install dataloader
Basic Implementation
import DataLoader from 'dataloader'; // Batch function: receives array of IDs, returns array of Users const batchUsers = async (ids) => { const users = await db.users.findAll({ where: { id: ids } }); // IMPORTANT: Return users in same order as IDs! return ids.map(id => users.find(user => user.id === id) ); }; // Create DataLoader const userLoader = new DataLoader(batchUsers); // Use in resolvers const resolvers = { Post: { author: (post) => userLoader.load(post.authorId), }, };
Real-World Example
Here's a complete setup for a blog application:
// loaders.js import DataLoader from 'dataloader'; import { User, Post, Comment } from './models'; export function createLoaders() { // User loader const userLoader = new DataLoader(async (ids) => { const users = await User.findAll({ where: { id: ids } }); return ids.map(id => users.find(u => u.id === id) || null); }); // Posts by user loader const postsByUserLoader = new DataLoader(async (userIds) => { const posts = await Post.findAll({ where: { authorId: userIds } }); return userIds.map(userId => posts.filter(post => post.authorId === userId) ); }); return { users: userLoader, postsByUser: postsByUserLoader, }; }
Conclusion
DataLoader is essential for production GraphQL APIs. Key takeaways:
- Always use DataLoader for related data fetching
- Create per-request - New loaders for each request
- Maintain order - Return results in same order as keys
- Handle nulls - Return null for missing items
- Monitor - Log batch sizes to verify it's working
This pattern has helped me reduce database queries by 90%+ in production GraphQL APIs.
Happy optimizing! 🚀