A single block passed to .any? can silently load thousands of records into memory.
No warnings. No errors. Just unnecessary objects.
And most Rails developers don’t notice it.
You’ve probably used both .any? and .exists? in your Rails app without thinking twice. They both answer the same simple question:
Is there at least one record?
But under the hood, they can behave very differently.
In this article, we’ll look at what actually happens when you call each method, when to use which, and how to avoid a common performance trap.
The Basics: Same Query, Same Result
If you just need to check whether a relation contains any records at all, 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.
No full table scan.
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
So if this is all you need, pick whichever reads better in your code.
There’s no performance difference here.
Where Things Get Dangerous: .any? With a Block
The moment you pass a block to .any?, Rails completely changes its behavior.
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
That single line:
- Loads all posts
- Instantiates an ActiveRecord object for each one
- Iterates over them in Ruby
- Just to return boolean
It might look harmless in development.
But in production?
If a user has 50,000 posts, you just loaded 50,000 objects into memory to check if one of them is published.
Why This Happens
Here’s how .any? is implemented in Rails:
def any?(*args)
return false if @none
return super if args.present? || block_given?
!empty?
end
If a block is given, Rails delegates to Enumerable#any?.
And Enumerable#any? needs the full collection in memory.
You’ve effectively moved filtering from SQL to Ruby.
The Fix: Let the Database Do the Work
Push the condition into SQL:
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. Much lower cost.
When .any? Is Actually the Right Choice
There is one important exception.
If the relation is already loaded, .any? will not hit the database again.
For example:
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 case:
-
.any?→ no additional query -
.exists?→ forces a new SQL query
So using .exists? here could actually introduce unnecessary database calls — potentially even an N+1 pattern.
Rails internally checks whether the relation is loaded.
If it is, .any? behaves like a normal Ruby collection.
A Better Rule of Thumb
- Relation not loaded → prefer
.exists? - Relation already loaded →
.any?is perfectly fine -
.any?with a block → avoid on ActiveRecord relations -
.present?for existence → avoid on ActiveRecord relations
Final Thought
Before calling .any?, ask yourself:
Am I checking existence — or am I about to load an entire collection?
Small differences in ActiveRecord APIs can have real production impact.
Have you ever spotted this in a real codebase?
This is part of a small series exploring subtle ActiveRecord behaviors that can impact performance.
Top comments (0)