Stop Using .any? the Wrong Way in Rails
Source: Dev.to
Introduction
A single block passed to .any? can silently load thousands of records into memory—no warnings, no errors, just unnecessary objects. Most Rails developers don’t notice it.
Both .any? and .exists? answer the same simple question: Is there at least one record?
Under the hood, however, they can behave very differently. This article explains what happens when you call each method, when to use which, and how to avoid a common performance trap.
Basic Usage – Checking Existence Only
If you just need to know whether a relation contains any records, both .any? and .exists? generate the same efficient query:
user.posts.any?
#=> SELECT 1 AS one FROM "posts" WHERE "posts"."user_id" = 1 LIMIT 1
user.posts.exists?
#=> SELECT 1 AS one FROM "posts" WHERE "posts"."user_id" = 1 LIMIT 1
No objects are loaded into memory. Both methods ask the database a simple yes/no question and return immediately after finding the first match.
The same applies when you chain .where—as long as you don’t pass a block to .any?:
user.posts.where(published: true).any?
#=> SELECT 1 AS one FROM "posts"
# WHERE "posts"."user_id" = 1 AND "posts"."published" = true
# LIMIT 1
user.posts.where(published: true).exists?
#=> SELECT 1 AS one FROM "posts"
# WHERE "posts"."user_id" = 1 AND "posts"."published" = true
# LIMIT 1
If this is all you need, pick whichever reads better in your code; there’s no performance difference.
.any? With a Block
The moment you pass a block to .any?, Rails changes its behavior completely. Instead of asking the database, it loads every matching record into memory and filters in Ruby:
user.posts.any? { |post| post.published? }
#=> SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1
What Happens
- Loads all posts from the database.
- Instantiates an
ActiveRecordobject for each row. - Iterates over them in Ruby.
- Returns a boolean.
In development this may look harmless, but in production it can be disastrous. If a user has 50,000 posts, you’ve just loaded 50,000 objects into memory just to check whether one of them is published.
Implementation Insight
def any?(*args)
return false if @none
return super if args.present? || block_given?
!empty?
end
When a block is given, Rails delegates to Enumerable#any?, which requires the full collection in memory. You’ve effectively moved the filtering from SQL to Ruby.
Prefer SQL for Filtering
Push the condition into the database instead:
user.posts.exists?(published: true)
#=> SELECT 1 AS one FROM "posts"
# WHERE "posts"."user_id" = 1 AND "posts"."published" = TRUE
- One row, one column.
- Stops at the first match.
- No object instantiation.
Same result, far lower cost.
When .any? Is Actually the Right Choice
There is an important exception: if the relation is already loaded, .any? will not hit the database again.
users = User.includes(:posts)
users.each do |user|
user.posts.any? { |post| post.published? } # no extra query
end
Because posts were preloaded, .any? works entirely in memory. In this scenario:
.any?→ no additional query..exists?→ forces a new SQL query.
Using .exists? here could introduce unnecessary database calls and even create an N+1 pattern. Rails internally checks whether the relation is loaded; .any? behaves like a normal Ruby collection.
Guideline Summary
| Relation State | Recommended Method |
|---|---|
| Not loaded | exists? (or any? without a block) |
| Already loaded | any? (without a block) |
| Need to evaluate a block on an unloaded relation | Avoid .any? with a block; use SQL (exists? with conditions) |
Conclusion
Small differences in ActiveRecord APIs can have real production impact. Before calling .any?, ask yourself:
- Am I merely checking existence? → Use
exists?(orany?without a block). - Am I about to load an entire collection? → Avoid
.any?with a block on unloaded relations.
Understanding these nuances helps you write more efficient Rails code and prevents hidden performance pitfalls.