Eager loading: preload vs includes vs eager-load
Accessing an association in a loop runs one query per record — the N+1 problem. Eager loading fixes it by fetching the associations up front. Three methods differ in how they fetch.
preload — a separate query per association
preload loads each association in its own batched query and caches the result.
No JOIN, so you can't filter on the association.
1 2 3 | |
Without preload, .pages on each of N users would fire N queries. With it,
the pages load in one additional query and populate the per-record cache.
eager-load — one LEFT OUTER JOIN
eager-load fetches everything in a single query with a LEFT OUTER JOIN:
1 | |
Use this when you want one round-trip, or when you need to filter on the joined table.
includes — preload by default, JOIN when referenced
includes is the one to reach for by default: it preloads (separate queries)
unless something forces a JOIN, then it switches to one.
1 | |
It promotes to a JOIN when you reference the association — explicitly with
references, or implicitly by mentioning the joined table in a where or
order:
1 2 3 4 5 6 7 8 9 10 11 | |
Each of these emits LEFT OUTER JOIN profiles, filters or orders on it, and
still caches the association. references(profile => False) suppresses the
promotion.
Starting from the class
preload, includes, and eager-load are available directly on the model, so
you can drop the leading where({}) when there is no condition:
1 2 3 | |
These are the same as starting from User.where({}) or User.all; add a
where later to filter.
Loading several associations
Pass multiple names to load them all:
1 | |
Nested eager loading
Use a Pair to descend one level, and a hash to go deeper:
1 2 3 4 5 | |
1 2 3 | |
This works through :through associations as well, caching the join collection
along the way (see Through associations):
1 | |
Which one?
| Method | Strategy | Filter on association? |
|---|---|---|
preload(:a) |
separate query | no |
includes(:a) |
separate query, JOIN when referenced | yes, once referenced |
eager-load(:a) |
one LEFT OUTER JOIN | yes |
Reach for includes by default; it does the right thing whether or not you end
up filtering on the association. Use preload to force separate queries, and
eager-load to force a single join.