A lot of Rails performance problems do not start with obviously expensive code, but with code that looks harmless.
For example:
if order.line_items.present?
# ...
end
That reads like a simple boolean check, ordinary Ruby. and not like a database operation. But on an unloaded Active Record association, that line may issue SQL.
That is what makes it dangerous. Not the cost of one call, but the mismatch between how the code reads and what the system actually does.
In Rails, some predicates are not purely local questions. They can cross the database boundary to answer what looks like an in-memory check.
Once you see that, a lot of "mysterious" performance issues stop looking mysterious.
Why this is easy to miss
The method name is the trap.
present? sounds like a lightweight predicate. In most Ruby code, that intuition is fine. You ask a question about a value that already exists in memory, and you get an answer.
Associations break that expectation.
When you call:
order.line_items.present?
Rails may be able to answer from memory if line_items is already loaded. If not loaded, Rails has to hit the database to decide whether the association is empty.
The same line can behave differently depending on context the line itself does not show. That is a real problem.
Where this becomes expensive
One hidden query usually doesn't matter. A hundred do.
orders.each do |order|
next unless order.line_items.present?
# ...
end
Nothing about the loop looks suspicious. There is no where, no find_by, no explicit query call. The code reads cleanly. But if line_items has not been preloaded, that can turn into one query per order.
This is one reason N+1 issues survive in Rails applications. The expensive part often hides behind method calls that do not advertise that they might perform I/O.
What is happening under the hood
The exact implementation details are less important than the shape of the behaviour.
present? delegates to blank?. In an association, answering whether it is blank eventually depends on whether it is empty. If the association is not loaded, Active Record has to query the database to answer that question.
So the issue is not really present? versus blank?. The real issue is that predicate-like code can quietly depend on the database.
What to write instead
The fix is on intent. If your real question is "do related records exist in the database?", write that directly:
order.line_items.exist?
That makes the boundary explicit. A reader immediately knows this line may hit the database.
If your real intention is to use the records in memory, preload them first:
orders = Order.includes(:line_items)
orders.each do |order|
next unless order.line_items.present?
# ...
end
Now the check is operating on already-loaded data. The code reads more honestly.
That is the standard I keep coming back to: if a line might hit the database, it should either say so directly or sit inside a structure that makes the loading strategy visible.
This is bigger than present?
present? is just an easy example. Rails has other methods with the same shape. any?, empty?, and sometimes size can all blur the line between in-memory logic and database-backed behaviour when they are called on associations.
The details vary, but the lesson does not.
If a method looks local but depends on load state, it is worth pausing before scattering it through hot paths.
The habit that helps
In long-lived Rails systems, I try to make query boundaries visible.
Not because convenience methods are bad. But, because hidden I/O makes code harder to reason about. They hide costs and make reviews weaker, and let performance issues pass as innocent object logic.
present? is fine when the data is already in memory. It becomes a smell when it hides the moment your code stops being local.
Top comments (0)