What includes changes
Removing includes from a Rails lookup can be safe, but it is not a purely cosmetic change. It changes when associated records are loaded, how many queries later code may issue, and sometimes the shape of the SQL Rails generates.
includes tells Active Record that associations are likely to be needed. Rails can satisfy that with separate preload queries or, when conditions require it, with an eager-loading join. The call is both a performance hint and a signal about what later code expects to touch.
A plain find is narrower. It loads the record by primary key and nothing else.
What find alone means
Model.find(id) is direct and usually ideal when the code only needs the row itself. It keeps the initial query simple, avoids unused association work, and makes the lookup intention clear.
The risk appears after the lookup. If serializers, views, policy checks, decorators, or callbacks walk associations, those reads become lazy queries. One record may still be fine. A collection path can turn that same pattern into an N+1 problem.
That is why the safety question is not "does this line still return the record?" It is "what does the request do with the record after this line?"
Where N+1 hides
N+1 queries often hide outside the method being edited. A controller loads a record, a serializer reads comments, each comment reads an author, and the query count changes far away from the original lookup.
View partials and JSON builders are common hiding places. Authorization code can also load associations for ownership checks. Even logging or instrumentation can touch names, topics, or parent records.
The safest review follows the runtime path. Search the action, presenter, serializer, and templates. Then run the request with query logging or a query-count check before concluding that the include was unused.
When removal is safe
Removing includes is usually safe when the method returns one record, the downstream path does not touch the association, and tests or logs confirm that query count does not grow in the relevant request.
It is also reasonable when an include was copied from a collection query into a single-record path and no longer matches the code. In that case, deletion improves clarity because it removes a false promise.
If only some callers need associations, consider moving eager loading to the caller that renders those associations. A model-level scope that always includes everything can make unrelated code slower and harder to reason about.
Verification
A small verification pass is enough for most changes. Capture the SQL before and after. Exercise the screen or endpoint that uses the lookup. Confirm that the response shape is unchanged and that lazy queries did not increase.
For larger codebases, tools such as strict loading or query-count assertions can make this less manual. They turn hidden lazy loading into a visible failure during tests.
The rule is simple: remove eager loading when callers do not use it, but prove that with the caller path rather than with the lookup line alone.
The common mistake
The confusing case is calling a finder before the relation has been shaped. Once find returns a model instance, there is no relation left for includes to modify.
# Wrong mental model: find has already executed the query.
post = Post.find(params[:id])
post.includes(:comments) # No useful eager loading happens here.
# Shape the relation first, then execute it.
post = Post.includes(:comments).find(params[:id])
The same rule applies to scopes, ordering, selection, and strict loading. Build the relation first. Execute it last.
post = Post
.includes(comments: :author)
.strict_loading
.find(params[:id])
post.comments.each do |comment|
puts comment.author.name
end
For a single show page, this avoids a small N+1. For an index page, the same mistake can become much more expensive because every rendered row may trigger another set of association queries.
How to verify the query
The quickest verification is to watch the SQL. In development, the log should show the primary query and the expected association preload queries before rendering starts. If association queries appear while templates or serializers are running, eager loading did not cover the path being rendered.
For a more deliberate check, enable strict loading on the relation during development or in a focused test. That turns accidental lazy loads into visible failures. It is much easier to fix a missing preload when the exception points at the exact association access than after a slow endpoint has accumulated dozens of quiet queries.
The test data should include more than one associated row. A preload mistake can look harmless when the fixture has only one child record or when an association was already loaded by earlier test setup.
That makes the check resistant to false confidence. Eager loading is only proven when the rendered path touches the associations the page really uses.
Sources
- Rails Guides, eager loading associations. https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
- Rails API, ActiveRecord::QueryMethods#includes. https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-includes
- Rails API, ActiveRecord::FinderMethods#find. https://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-find