What is the N+1 Query Problem?
Imagine you're a librarian fetching books for a display. First, you get a list of 10 featured books (1 query). Then, for each book, you make a separate trip to get its author's information (10 more queries). That's 11 total trips - or N+1 queries, where N is the number of books. This is exactly what happens in code when we don't properly structure our database queries.
Identifying N+1 Queries
Let's look at some common patterns that lead to N+1 queries:
Example 1: The Classic N+1 Pattern
// ❌ N+1 Query Pattern
const getBlogPosts = async () => {
// First query to get all posts
const posts = await Post.findAll();
// Then a separate query for each post's author
for (const post of posts) {
post.author = await User.findByPk(post.authorId);
}
return posts;
};
Why is this bad? If you have 100 posts, this will make 101 database queries:
1 query to get all posts
100 separate queries to get each post's author
Example 2: Hidden N+1 in Lazy Loading
// ❌ N+1 Query Pattern (less obvious)
const getComments = async () => {
const comments = await Comment.findAll();
// This looks innocent, but creates N+1 queries!
comments.forEach(comment => {
console.log(comment.User.name); // Triggers lazy loading
});
};
Fixing N+1 Queries
Let's look at how to fix these problems using eager loading:
Solution 1: Using Include
// ✅ Efficient Query Pattern
const getBlogPosts = async () => {
// Single query with joined data
const posts = await Post.findAll({
include: [{
model: User,
as: 'author',
attributes: ['id', 'name', 'email']
}]
});
return posts;
};
Solution 2: Proper Eager Loading
// ✅ Efficient Query Pattern
const getComments = async () => {
const comments = await Comment.findAll({
include: [{
model: User,
attributes: ['name']
}]
});
comments.forEach(comment => {
console.log(comment.User.name); // No additional queries
});
};
Real World Complex Examples
Let's look at more complex scenarios you might encounter:
E-commerce Product Listing
// ❌ N+1 Query Pattern
const getProductsWithDetails = async () => {
const products = await Product.findAll();
for (const product of products) {
product.category = await Category.findByPk(product.categoryId);
product.reviews = await Review.findAll({
where: { productId: product.id }
});
product.inventory = await Inventory.findOne({
where: { productId: product.id }
});
}
return products;
};
// ✅ Efficient Query Pattern
const getProductsWithDetails = async () => {
return await Product.findAll({
include: [{
model: Category,
attributes: ['name', 'description']
}, {
model: Review,
attributes: ['rating', 'comment'],
limit: 5,
order: [['createdAt', 'DESC']]
}, {
model: Inventory,
attributes: ['quantity', 'location']
}],
order: [['name', 'ASC']]
});
};
Advanced Scenarios
Nested Associations
// ❌ N+1 Query Pattern
const getOrders = async () => {
const orders = await Order.findAll();
for (const order of orders) {
order.customer = await Customer.findByPk(order.customerId);
order.items = await OrderItem.findAll({
where: { orderId: order.id }
});
for (const item of order.items) {
item.product = await Product.findByPk(item.productId);