In Rails, ActiveRecord is one of the things that makes the framework so enjoyable to use. It reads almost like natural language and lets you write database logic in a clean, Ruby-style way.
But every line of Active Record still turns into real SQL behind the scenes. Every SQL query comes with a fixed cost:
- Network latency between your app and the database (even on localhost)
- Query parsing and planning time inside PostgreSQL
- Serialization of results back to Ruby
- Context switching inside your Rails process
By reducing the number of SQL queries, you minimize these fixed costs: fewer round-trips, less time spent parsing and deserializing, and fewer blocking waits inside your Ruby threads, which directly improves your app’s performance.
1. Aggregate Metrics
A common pattern is calculating both sum and count separately:
// language: ruby # Before pipeline_sum = PipelineRun.sum(:time_to_results_finalized) pipeline_count = PipelineRun.count
Each call triggers a separate query. You can combine both metrics in a single SQL scan:
// language: ruby
# After
pipeline_sum, pipeline_count = PipelineRun.pick(
Arel.sql("SUM(time_to_results_finalized)"),
Arel.sql("COUNT(*)")
)✅ Only 1 query
✅ Postgres calculates both in the same scan
2. Counting Conditional Subsets
If you’re counting filtered subsets, don’t run one query per condition.
// language: ruby # Before active_count = User.where(active: true).count inactive_count = User.where(active: false).count
Use Postgres FILTER to compute both in a single pass:
// language: ruby
# After
active_count, inactive_count = User.pick(Arel.sql("
COUNT(*) FILTER (WHERE active = TRUE),
COUNT(*) FILTER (WHERE active = FALSE)
"))
✅ Only 1 query
✅ Postgres calculates both in the same scan
We can use it with more complex conditions:
// language: ruby
User.pick(Arel.sql("
COUNT(*) FILTER (WHERE last_login_at > NOW() - INTERVAL '30 days') AS recent_users,
COUNT(*) FILTER (WHERE confirmed = FALSE) AS unconfirmed_users
"))3. Using .group for Grouped Metrics
Grouping is another easy win. We often loop over values and count them separately:
// language: ruby
# Before
statuses = {}
["draft", "published"].each do |s|
statuses[s] = Post.where(status: s).count
end
Rails can do it all in one query:
// language: ruby
# After
statuses = Post.group(:status).count
# => { "draft" => 12, "published" => 42 }
✅ Only 1 query
✅ Returns a hash directly
✅ Scales linearly even with many groups
4. Loading Metadata & Associated Totals Together
When showing project stats, we often query separately:
// language: ruby # Before project = Project.find(1) tasks_count = project.tasks.count members_count = project.members.count
Three queries total. You can aggregate directly with joins:
// language: ruby
# After
project = Project
.left_joins(:tasks, :members)
.where(id: 1)
.select("projects.*,
COUNT(DISTINCT tasks.id) AS tasks_count,
COUNT(DISTINCT members.id) AS members_count")
.group("projects.id")
.first
✅ Only 1 query
✅ You can access
Conclusion
Rails makes it easy to express queries cleanly, but small inefficiencies add up fast. When you notice multiple .sum , .count , or .where calls in a row, ask yourself:
"Can this be done in one SQL pass?"
"Can this be done in one SQL pass?"