In my recent project, I was working with scopes across various models, and I needed to apply a nested scope for more advanced querying.
To achieve this with performant query I used different concepts, this blog is about the same!!
Lets revise the basics!
Firstly, lets start with what is query optimisation ?
Optimising queries is key for better application performance
By using various ORM techniques like includes, eager_load, preload etc we can reduce the database calls and avoid N+1 queries!!
What are N+1 queries ?
N+1 queries occur when the application executes one query to retrieve the main data and then executes additional queries for each related record.
class Post < ApplicationRecord
has_many :comments
end
@posts = Post.all
@posts.each do |post|
post.comments.count
end
Here we can see,
- A single query retrieves all (N) posts
- For each post, (+1) a separate query retrieves its comments
This leads to multiple database calls, significantly slowing down the application..
Solving N+1 queries
Mainly preload and includes are used for solving this!
When to use preload:
preload is used when we only need to iterate over associated records without filtering or querying on them. preload does not join tables; instead, it fires a separate query for the associated records.
Example: If we want to display all comments of all posts
@posts = Post.preload(:comments)
@posts.each do |post|
puts "Post: #{post.title}"
post.comments.each do |comment|
puts "Comment: #{comment.body}"
end
end
- Post.preload(:comments) fetches all posts and comments in two separate queries.
- No table joins are performed, making it efficient for simple iteration.
When to use includes:
includes is used when filtering or querying on associated records. However, it does not always behave the same way — it dynamically decides whether to use preload or eager_load, depending on whether the associated records are used in filtering.
If the associated records are not filtered, includes behaves like preload (executing separate queries).
If filtering is applied on the associated records, Rails automatically converts includes into eager_load, which performs a SQL LEFT OUTER JOIN instead.
Example: If we want first user whose post has title “Rails”
user = User.includes(:posts).where(posts: { title: "Rails" }).first
Here,
- includes joins the table posts with users
- The where clause filters on posts.title
- Because filtering is used, Rails treats includes as eager_load, ensuring everything is fetched in a single query.
Key difference in preload and includes:
- Key difference in preload and includes:

Lets go advance!
Now, lets see the nested scopes scenario!!
See the below models and query scenario:
class User < ApplicationRecord
has_many :posts
has_many :comments
has_many :recent_posts, -> { recent }, class_name: "Post"
end
class Post < ApplicationRecord
belongs_to :user
has_many :comments
has_many :recent_comments, -> { recent }, class_name: "Comment"
scope :recent, -> { where(date: (Date.yesterday..Date.today)) }
end
class Comment < ApplicationRecord
belongs_to :post
belongs_to :user
scope :recent, -> { where(date: (Date.yesterday..Date.today)) }
end
Here, we have users, posts and comments table associated, and we have “recent” scope in posts and comments to get recent (last 2 days) posts and comments. I have added new association recent_posts and recent_comments for calling recent scope on posts and comments.
Scenario: Fetch specific user’s (@user) recent posts which has recent comments added by admin user
I was trying to chain the recent_comments scope directly on recent_comments for the user,
filtered_posts = @user.recent_posts
.recent_comments
.where(comments: { user_id: admin.id })
but, recent_comments scope is defined on Post, not on a collection of posts, and @user.recent_posts returns a collection, so this will not work!
Then I found solution below, where scope can be applied directly on class!
filtered_posts = @user.recent_posts
.includes(comments: :user)
.where(comments: { user_id: admin.id })
.merge(Comment.recent)
Here:
- recent_posts is scope defined for getting last 2 days posts
- The includes method joins the tables and loads the comments association and, within comments, also loads the associated user
- Filters the comments to include only those created by the specified admin user.
- Applies merge the recent scope to the comments. You can see merge query here
.merge(Comment.recent)recent scope applied on Comment class which is merged with recent_posts- We can also use
joinsinstead ofincludesto ensure that the filtering happens at the database level.
In this way, we can chain the scopes, optimise our queries and increase the application performance!!
