← Reference · Nestor G Pestelos Jr · Print this page
Software Development · Ruby on Rails
ActiveRecord Eager Loading
Reference entry · last updated October 13, 2026
ActiveRecord eager loading loads specified associations alongside a set of model records, so later association access can reuse those records.[1][4]
This entry covers synchronous record loading in Rails 8.1.3. Examples assume persisted Author records with an ordinary, unscoped has_many :books association; Book has author_id, title, and published columns. Ruby and SQL examples are illustrative, not executed query logs. SQL spelling depends on the database adapter.
First principles and definitions
A parent query selects authors. Loading an association fills each author's cached books collection. Selecting parents and loading their children are separate operations: an SQL join alone can select authors without filling that collection.[2][3]
Lazy loading fetches an association when its records are first needed. In an N+1 pattern, one parent query is followed by a child query for each parent. A preloader groups association loading across the selected parents.[1][4]
joins, includes, preload, and eager_load
| Call | SQL shape | Parent selection | Loads books? |
|---|---|---|---|
joins(:books) | INNER JOIN | Only authors with matching books; duplicate author rows are possible | No |
includes(:books) | Normally separate queries; can switch to a join | All authors without a child filter | Yes |
preload(:books) | Parent query, then separate association queries | Determined by the parent query | Yes |
eager_load(:books) | LEFT OUTER JOIN | All authors without a child filter | Yes |
includes switches to joined loading when the relation references an included table or also joins the same included association. preload leaves association loading separate even when the parent query contains a join.[3]
A simple nonempty parent set with one ordinary association normally needs one parent query and one preload query. Nested associations can need more. Joined loading can also need a preliminary query when a collection is limited.[4][6]
N+1 queries
authors = Author.order(:id).limit(3).to_a
# Assume the result contains authors 1, 2, and 3.
authors.each do |author|
author.books.each { |book| puts book.title }
end
-- Illustrative SQL before preloading: SELECT authors.* FROM authors ORDER BY authors.id LIMIT 3; SELECT books.* FROM books WHERE books.author_id = 1; SELECT books.* FROM books WHERE books.author_id = 2; SELECT books.* FROM books WHERE books.author_id = 3;
With initially unloaded collections, this pattern requests a books query for each author. The database query cache can affect physical database round trips.[1][11]
authors = Author.order(:id).limit(3).preload(:books).to_a
authors.each do |author|
author.books.each { |book| puts book.title }
end
-- Illustrative SQL after preloading: SELECT authors.* FROM authors ORDER BY authors.id LIMIT 3; SELECT books.* FROM books WHERE books.author_id IN (1, 2, 3);
The second query fills the authors' book collections. Enumerating those loaded collections reuses their records.[4][10]
Association conditions and loaded records
Author.includes(:books).where(books: { published: true })
Author.includes(:books)
.where("books.published = ?", true)
.references(:books)
The hash condition supplies the table reference. A SQL-string condition needs references for joined loading. includes takes association names; references takes table names.[2]
Both examples select authors with published books and fill each loaded books collection with only the matching books. The child predicate excludes authors without a match, despite the LEFT OUTER JOIN.[2][4]
authors = Author.joins(:books)
.where(books: { published: true })
.distinct
.preload(:books)
.to_a
Here the join selects authors, distinct removes repeated parent rows, and a separate preload fetches all their books. The parent filter does not restrict that preload. Association scopes still apply; a scoped association loads only its scope's records.[2][3][8]
Joined rows and collection limits
A collection join repeats parent columns across its matching child rows. Ordinary joins can return duplicate parents unless the query removes them. During eager-loaded record construction, Rails groups joined rows by parent primary key and builds the associated collections. This reduces duplicate Ruby parent objects but leaves repeated parent data in the SQL result.[1][5]
Author.eager_load(:books).order(:id).limit(3).to_a
In Rails 8.1.3, the normal ungrouped eager-loading path checks limit or offset against collection reflections. When required, it first selects distinct parent IDs, then loads joined records for those IDs with the row limit removed.[6][7]
-- Illustrative two-stage shape, assuming IDs 1, 2, and 3: SELECT DISTINCT authors.id FROM authors LEFT OUTER JOIN books ON books.author_id = authors.id ORDER BY authors.id LIMIT 3; SELECT authors.id AS t0_r0, books.id AS t1_r0, ... FROM authors LEFT OUTER JOIN books ON books.author_id = authors.id WHERE authors.id IN (1, 2, 3) ORDER BY authors.id;
This parent-ID stage prevents the SQL row limit from truncating the joined collection for the selected parents. Its SQL varies by adapter and ordering. An eager_load call therefore does not guarantee one SQL query; a singular association can take a different path.[6][7]
strict_loading and counting
author = Author.strict_loading.first # Assuming an author exists and books remain unloaded: author.books.to_a # raises ActiveRecord::StrictLoadingViolationError loaded = Author.strict_loading.preload(:books).first # Assuming an author exists: loaded.books.to_a # uses preloaded records
With the default violation action, strict_loading raises when an association tries to load its records lazily. Preloading that association makes its records available before access. Rails can instead log violations through action_on_strict_loading_violation.[1][8]
loaded.books.size # uses the loaded collection loaded.books.count # requests a SQL count calculation
Strict loading guards lazy association record loading. Blockless count follows a calculation path and normally requests SQL even for a preloaded collection. A query-cache hit or known-empty result can avoid a database round trip. A loaded collection's size uses its records.[9][10][11]
See also
References
- Rails 8.1 Guides, Active Record Query Interface, sections 15 and 16.
- Rails source, tag v8.1.3, query_methods.rb: joins, includes, preload, eager_load, references, and distinct.
- Rails source, tag v8.1.3, relation.rb: eager_loading? and preload_associations.
- Rails source, tag v8.1.3, preloader.rb.
- Rails source, tag v8.1.3, join_dependency.rb: instantiate.
- Rails source, tag v8.1.3, finder_methods.rb: apply_join_dependency and using_limitable_reflections?.
- Rails source, tag v8.1.3, schema_statements.rb: distinct_relation_for_primary_key.
- Rails source, tag v8.1.3, association.rb: find_target and violates_strict_loading?.
- Rails source, tag v8.1.3, calculations.rb: count and calculate.
- Rails source, tag v8.1.3, collection_proxy.rb: count and size.
- Rails source, tag v8.1.3, query_cache.rb: select_all and cache_sql.