Rails Scopes: How To Write Better Queries
Learn how Rails scopes make Active Record queries cleaner, reusable, and easier to maintain with practical examples.
• 11 min read
• 11 min read
Learn how Rails scopes make Active Record queries cleaner, reusable, and easier to maintain with practical examples.
• 11 min read
• 11 min read
If you have spent any time writing Active Record queries, you have probably repeated the same where clause in more than one place. Rails scopes exist to fix exactly that problem. A Rails scope lets you wrap a common query into a reusable, named method on your model, so instead of copying conditions across controllers and services, you call one clean method that reads like plain English.
This article walks through what scopes are, how to define them, how to pass arguments, how to chain them together, and how they interact with the rest of Active Record. We will also look at default_scope, why it is risky, and how scopes relate to actual query performance.
A scope is a class method on an Active Record model that returns an Active Record relation. Scopes give a name to a query condition so you can reuse it anywhere the model is available.
Instead of writing this every time you need published posts:
Post.where(published: true)
You define a scope once:
class Post < ApplicationRecord
scope :published, -> { where(published: true) }
end
And then call it like this:
Post.published
The behavior is identical, but the intent is much clearer. Anyone reading Post.published immediately understands what the query does, without having to know the underlying column or condition.
Scopes are defined using the scope class method, followed by a name and a lambda that returns a query.
class Order < ApplicationRecord
scope :pending, -> { where(status: "pending") }
scope :shipped, -> { where(status: "shipped") }
scope :recent, -> { order(created_at: :desc) }
end
A few rules to keep in mind:
Once defined, scopes are called just like any other class method:
Order.pending
Order.shipped
Order.recent
Here are a few practical scopes you might use on a User model in a typical Rails application.
class User < ApplicationRecord
scope :active, -> { where(active: true) }
scope :admins, -> { where(role: "admin") }
scope :verified, -> { where.not(verified_at: nil) }
end
These read naturally in application code:
User.active.admins
User.verified.count
Each of these could be written inline with where, but naming them turns repeated logic into a single, testable, reusable unit.
Scopes are not limited to static conditions. You can pass arguments into a scope by defining the lambda with parameters.
class Order < ApplicationRecord
scope :by_status, ->(status) { where(status: status) }
scope :placed_after, ->(date) { where("created_at > ?", date) }
end
Usage looks like this:
Order.by_status("shipped")
Order.placed_after(1.week.ago)
This is one of the most useful features of scopes, since it lets you keep flexible query logic in the model instead of scattering conditions across controllers and services.
You can also give arguments default values, which is helpful for optional filters:
class Product < ApplicationRecord
scope :in_stock, ->(min_quantity = 1) { where("quantity >= ?", min_quantity) }
end
Because scopes return Active Record relations, they can be chained together freely, along with regular query methods.
Order.pending.recent
Product.in_stock.where(category: "electronics")
User.active.admins.order(:email)
Each call in the chain narrows the query further. Rails combines the conditions into a single SQL statement, so chaining scopes does not create multiple database round trips. This is the same behavior you already rely on when chaining where and order manually, scopes just give each step a readable name.
Scopes are not a separate system from Active Record query methods. Internally, a scope's lambda is just calling the same methods you would call directly, such as where, order, joins, includes, and left_joins. That means scopes can use any of these methods, and they combine naturally with calls made outside the scope.
class Post < ApplicationRecord
belongs_to :user
scope :published, -> { where(published: true) }
scope :by_author, ->(user_id) { where(user_id: user_id) }
scope :with_author, -> { joins(:user) }
end
You can then combine scopes with query methods written outside the model:
Post.published.joins(:user).where(users: { active: true })
Post.with_author.order("users.name")
Scopes work the same way with left_joins when you want matching records even when an association is missing, which matters if you are filtering on the parent table but still want posts without an associated record to appear. For a deeper comparison of when to use joins versus left_joins, see Joins vs Left Joins in Rails: What's the Difference.
class Post < ApplicationRecord
scope :with_optional_author, -> { left_joins(:user) }
end
Post.with_optional_author.where(users: { id: nil })
Scopes also compose cleanly with includes for eager loading, which helps avoid N+1 queries when you loop over associated records:
class Post < ApplicationRecord
scope :with_comments_loaded, -> { includes(:comments) }
end
Post.published.with_comments_loaded.each do |post|
post.comments.each { |comment| puts comment.body }
end
This detail matters more than it seems. Because a scope returns an Active Record relation rather than an array, the query is not actually executed against the database until you call a method that forces evaluation, such as .to_a, .each, .count, or rendering the records in a view.
relation = Post.published
relation = relation.recent
relation = relation.limit(10)
# No SQL has run yet
posts = relation.to_a
# SQL runs here
This laziness is what allows scopes to be chained without extra database hits. Each additional scope or query method modifies the relation in memory, and Active Record builds a single SQL statement once the relation is finally evaluated.
A scope is really just syntactic sugar over a class method that returns a relation. In fact, you can rewrite any scope as a class method with self.:
class Order < ApplicationRecord
def self.pending
where(status: "pending")
end
end
This is functionally equivalent to:
scope :pending, -> { where(status: "pending") }
So why choose one over the other?
Scopes are best for short, simple, single-purpose query conditions. Class methods are better when the logic needs branching, multiple steps, error handling, or anything beyond a single chainable expression.
Use a class method instead of a scope when:
class Order < ApplicationRecord
def self.summary_for(user)
return {} unless user.present?
orders = where(user_id: user.id)
{
total: orders.count,
pending: orders.where(status: "pending").count,
revenue: orders.sum(:total_cents)
}
end
end
Trying to force logic like this into a scope lambda usually makes the code harder to read, not easier.
When a scope accepts user input, treat it the same way you would treat any other query input. Always use parameterized conditions instead of interpolating values directly into SQL strings.
# Safe
scope :by_email, ->(email) { where(email: email) }
scope :placed_after, ->(date) { where("created_at > ?", date) }
# Unsafe, avoid this
scope :by_email_unsafe, ->(email) { where("email = '#{email}'") }
The unsafe version is vulnerable to SQL injection if the input ever comes from a user-controlled source, such as a search form or an API parameter. Stick to hash conditions or parameterized query strings with ? placeholders, and Active Record will handle escaping for you.
If a scope needs to conditionally skip a filter, guard it inside the lambda rather than calling the scope conditionally from outside:
scope :filter_by_status, ->(status) { where(status: status) if status.present? }
A few mistakes come up often when developers start using scopes:
Forgetting the lambda. A scope must be defined with a lambda, not a plain block or a bare value evaluated once at load time.
# Wrong, evaluated once when the class loads
scope :recent, order(created_at: :desc)
# Correct
scope :recent, -> { order(created_at: :desc) }
Returning nil instead of a relation. If the lambda's logic can return nil, calling further scope methods on it will raise an error. Guard with all when a condition might not apply.
scope :by_status, ->(status) { status.present? ? where(status: status) : all }
Naming collisions. Naming a scope the same as an existing column, association, or Active Record method causes confusing bugs. Avoid names like scope :order on a model that already has an order association.
Overloading a single scope with too much logic. If a scope needs several conditionals and branches, it is a sign you should use a class method instead.
Scopes work best when each one does exactly one thing. A scope named active_verified_admins_in_region is really three or four scopes glued together, and it becomes hard to reuse or test in isolation.
Prefer small, composable scopes that read well when chained:
User.active.verified.admins.in_region("northeast")
over a single scope that tries to do everything at once. Small scopes are easier to test individually, easier to reuse across different parts of the app, and easier for another developer to understand without reading the implementation.
default_scope applies a condition to every query on a model automatically, without needing to call anything explicitly.
class Post < ApplicationRecord
default_scope { where(deleted_at: nil) }
end
With this in place, Post.all, Post.find, and every other query on Post will automatically exclude soft-deleted records.
default_scope sounds convenient, but it causes real problems in practice:
unscoped, joins, and other scopes if you are not careful, sometimes producing unexpected WHERE clauses that are hard to trace.For soft deletion specifically, an explicit scope like Post.not_deleted is usually safer and more predictable than default_scope, because it makes the filtering visible at the call site instead of hidden inside the model.
If a model does use default_scope, you can bypass it with unscoped:
Post.unscoped.find(params[:id])
Or remove it for a single query chain using unscope:
Post.unscope(where: :deleted_at)
Because of the risks above, most Rails teams prefer named scopes such as Post.active or Post.not_deleted over default_scope, and reserve default_scope for narrow, well-understood cases like multi-tenancy setups where every query genuinely must be scoped to a tenant.
Because scopes are just model methods, they are reusable anywhere the model is referenced, not only in controllers.
# Controller
def index
@posts = Post.published.recent
end
# Service object
class PublishedPostsReport
def call
Post.published.recent.limit(10)
end
end
# Background job
class CleanupExpiredOrdersJob < ApplicationJob
def perform
Order.by_status("pending").placed_after(30.days.ago).find_each(&:cancel!)
end
end
This is one of the strongest arguments for defining scopes in the first place. The query condition is defined once on the model, and every layer of the application, controllers, services, background jobs, and console scripts, can reuse the exact same logic without duplicating it.
Scopes do not make a query faster by themselves. A scope is only a named shortcut for a query, so the resulting SQL and the database's ability to execute it efficiently still depend entirely on your schema and indexes.
scope :by_status, ->(status) { where(status: status) }
If status is not indexed, Order.by_status("pending") will run a slow query on a large table regardless of whether it is called as a scope or written out as where(status: "pending") directly. Wrapping a query in a scope changes nothing about the SQL generated or the execution plan. For guidance on making the underlying queries efficient, see How to Optimize Active Record Queries in Rails and Database Indexes in Rails: A Practical Guide.
When you want to confirm exactly what SQL a scope produces, call .to_sql on the relation instead of evaluating it.
Order.pending.recent.to_sql
# => SELECT "orders".* FROM "orders" WHERE "orders"."status" = 'pending' ORDER BY "orders"."created_at" DESC
You can also use explain to see the database's query plan, which is useful for confirming whether an index is actually being used.
Order.pending.recent.explain
This is the same debugging workflow you would use for any Active Record query, scopes included, since a scope is ultimately just Active Record building SQL on your behalf.
Rails scopes are a small feature with a large effect on code quality. They turn repeated query conditions into named, chainable, reusable methods, and they compose naturally with the rest of Active Record, including where, order, joins, left_joins, and includes. Use scopes for short, focused conditions, switch to class methods when the logic grows more complex, and remember that scopes describe intent, they do not change how the database executes the underlying SQL. Combined with sensible indexing, scopes are one of the easiest ways to make an Active Record codebase more readable and maintainable.