Solving the N+1 Query Problem in Sequelize with Eager Loading
Stop killing your database performance with N+1 queries. Learn how to use Sequelize's 'include' property to implement Eager Loading and optimize your data retrieval.
09 Jun 2026, 21:55 UTC

The Silent Performance Killer: N+1 Queries
You have a dashboard that lists 50 recent orders, and for each order, you need to display the customer's name. If you fetch the orders first and then loop through them to fetch the customer for each individual order, you aren't making one request to the database—you are making 51. This is the N+1 query problem: one query to fetch the parent records (N), and one additional query for every record returned.
As your dataset grows, this pattern creates a massive bottleneck. The latency isn't caused by the database's inability to find the data, but by the repeated network round-trips between your application server and the database engine. The solution in Sequelize is Eager Loading, which tells the ORM to fetch associated data upfront in as few queries as possible.
Implementing Eager Loading with 'include'
Sequelize implements eager loading via the include property. Instead of fetching associations lazily (on-demand), you define the relationships you need at the moment of the initial query. By default, Sequelize uses a LEFT OUTER JOIN, meaning the primary record is returned even if the associated record does not exist.
Filtering with the 'required' Property
Sometimes you don't want every primary record; you only want records that actually have an association. By setting required: true within the include block, Sequelize switches from a LEFT OUTER JOIN to an INNER JOIN. This effectively filters the primary result set based on the existence of the associated data.
Handling Deep Relationships
For complex data models, you can nest include arrays. If an Order has a Customer, and a Customer has a Profile, you can fetch the entire chain in one operation. This prevents the need to write multiple sequential queries to traverse your data graph.
Worked Example: Orders, Customers, and Profiles
Assume you are using Sequelize v6+ with a PostgreSQL or MySQL backend. You have three models: Order, User, and Profile, where an Order belongs to a User, and a User has one Profile.
// Run this in your application logic/controller
// Required Permissions: Database read access
const orders = await Order.findAll({
where: { status: 'shipped' },
include: [{
model: User,
required: true, // Inner Join: Only return orders that have a valid user
include: [{
model: Profile,
attributes: ['avatarUrl', 'phoneNumber'] // Only fetch specific fields to save memory
}]
}]
});
Expected Result: Instead of 100+ queries, Sequelize will generate a complex JOIN query (or a optimized set of queries using IN clauses) that returns a nested JSON structure where each Order object contains a User object, which in turn contains a Profile object.
The Trade-off: Cartesian Products and Over-fetching
Eager loading is not a silver bullet. When you include multiple hasMany associations in a single query, you risk a Cartesian Product explosion. If an Order has 10 Items and 10 Shipments, a single JOIN query might return 100 rows for a single order, with the order data duplicated 100 times. This consumes significant memory on both the database and the application server.
To mitigate this, consider these strategies:
- Attribute Selection: Use the
attributesarray to fetch only the columns you need, reducing the payload size. - Separate Queries: For very large
hasManyassociations, it is often more performant to execute two separate queries and map them in the application layer. - Pagination: Be cautious when using
limitandoffsetwith eager loading; Sequelize may need to perform a subquery to ensure the limit applies to the primary model rather than the total number of joined rows.
Verifying the Result
To ensure your eager loading is working as intended and not falling back to lazy loading, enable SQL logging in your Sequelize configuration:
const sequelize = new Sequelize(dbConfig, { logging: console.log });
Check your terminal output. If you see a single SELECT statement with JOIN keywords, you have successfully solved the N+1 problem. If you see a stream of repetitive SELECT statements firing immediately after the first query, your associations are likely not configured correctly or you are accessing them outside the initial findAll call.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.