Eager loading
Read this page in the documentation
Eager loading include loads associated rows alongside the parents. This guide is about the mechanism — what statements actually run, in what order, and which include options are honoured. For how to declare the associations themselves, see Associations. Separate queries, not JOINs Prorm resolves an include with one additional query per association level, matching children to parents in memory. It never widens the parent SELECT into a JOIN. Two statements for any number of users — the N+1 problem is gone, and because the parent query is never joined, LIMIT 20 on the parent means twenty users, not twenty joined rows. The trade-off is that you cannot sort parents by a child column in one statement; do that with $alias.column$ filtering plus an ordinary parent order, or drop to raw SQL. Nesting adds one query per level, not per row: One query per level Three levels, three statements — not one per row, and not a join. The count grows with the depth of the include, never with the number of rows, which is why LIMIT 20 on the parent still means twenty users. What gets attached Results are attached under the association's alias, and mirrored onto dataValues: Association | Attached value | --- | --- | belongsTo, hasOne | a single instance, or null | hasMany, belongsToMany | an array (empty when there are no children) | Per-association direction belongsTo — the key lives on the parent, so Prorm collects the foreign keys it already has and looks up the targets by primary key: hasMany / hasOne — the key lives on the child: belongsToMany — the junction table is queried first, then the targets by the ids it produced: Filtering includes restrict the parents An include carrying where, or marked required: true, means inner join semantics: parents with no matching child are dropped from the result. Because that must happen before LIMIT, Prorm resolves the matching parent keys first and constrains the main query with them. required: true does the same thing without a predicate — "only users that have at least one post". If no parent can qualify, findAll short-circuits and returns [] without running the main query. $alias.column$ A where key of the form $alias.column$ names a column on an included association. It is lifted onto that include, which makes it a parent-restricting filter with the same three-statement shape as above: Naming an alias you did not include throws an AssociationError telling you to add the include, rather than dropping the condition silently. Include options Option | Honoured | Effect | --- | --- | --- | model | yes | The model to load. Required — an entry without one is skipped. | as | yes | Which association to use when several link the same two models. | where | yes | Filters the children and restricts the parents (see above). | attributes | yes | Column list for the child query. | order | yes | ORDER BY on the child query. | required | yes | Inner-join semantics. | include | yes | Nested includes, to any depth. | limit / offset | yes | A window per parent, not across the whole result — see below. | { all: true } | yes | Expands to one include per association. { all: 'alias' } and { all: true, nested: true } too. | separate | n/a | Every include is already a separate query. | limit / offset are a per-parent window "The 3 most recent posts for each user" is what you almost always mean, and it is what you get: Because one query serves every parent, the LIMIT cannot simply go into that statement — LIMIT 3 there would return three rows in total and leave every parent but the first empty. So the database applies the ORDER BY (the window is over the ordering you asked for, not insertion order) and the slice happens per parent as rows are grouped onto their parents: That is correct, but it does not reduce what crosses the wire: every matching child is still fetched. For a genuinely large child table, query the child model directly instead. One case does push down. With exactly one parent row — findByPk/findOne plus an include, the common shape — the shared query's window is the per-parent window, so it becomes real SQL, for hasMany only: belongsToMany never pushes down: the junction means fetched rows and attached array elements are not one-to-one, so a SQL LIMIT would count the wrong thing. On a to-one include (belongsTo / hasOne) a window is meaningless; it is ignored and reported rather than silently applied. A limit/offset that is not a non-negative integer raises an AssociationError instead of being coerced. { all: true } Each entry expands to one concrete include per association before anything is loaded, so the result is identical to writing them out by hand — one query per association. Options on the entry carry onto every expanded include. Recursion is bounded two ways: a depth cap of 10, and a per-branch record of the models already expanded. The chain stops re-expansion, not the association itself, so a circular User ⇄ Post graph settles in three queries rather than growing exponentially. An unknown { all: 'alias' } throws. { all: true } composes with required: true — expansion now happens before the parent-filtering pass, so every expanded association is required and the result matches writing the includes out by hand: Association scopes A scope declared on the association is a fixed condition applied every time that association is traversed. The include's own where wins where the two overlap. Counting instead of loading When you only need the size of a collection, withCount avoids building instances for every child: It runs one query per association, not per parent: The rows are tallied in JavaScript, so this saves instance construction and round trips but still transfers the child rows. For a large child table, a grouped count() against the child model is cheaper. belongsTo counts are resolved without any query at all — the answer is 0 or 1 depending on whether the parent's foreign key is null. Lazy alternatives When you have a single instance in hand, the association accessors are usually clearer than an include: These accessors are generated from the association alias — see Associations for the full naming rules. src/models/model.ts also defines generic getRelated(alias) / setRelated / createRelated / hasRelated helpers, but they live on the unreachable createModel() factory (see Streaming), not on the instances define() / addModel() produce. Use the generated accessors above. Reach for include when you have many parents; reach for the accessors when you have one. Errors you may hit Error | Cause | --- | --- | AssociationError: X has no association to Y | The association was never declared, or is declared in the other direction. | AssociationError: X has N associations to Y (…) | Ambiguous — pass as to pick one. | AssociationError: … has no association to Y aliased 'z' | The as does not match any association's effective alias. | EagerLoadError | The child query itself failed; the cause is attached. | Next: Validation — rejecting bad data before it reaches the database. Related reading Associations — declaring relationships Querying — the rest of FindOptions Scopes Query optimization