0%
Reading Settings
Font Size
18px
Line Height
1.5
Letter Spacing
0.01em
Font Family
Table of contents
    blog cover

    Reducing ActiveRecord Queries to Speed Up Your Requests

    Software Engineer
    Software Engineer
    Ruby on Rails
    Ruby on Rails
    published 2025-10-06 09:52:56 +0700 · 2 mins read
    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

    For simple requests, the delay might only be a few milliseconds. But in production, when a single request fires dozens of queries, that overhead can quickly add up to 100–200 ms. Reducing the number of SQL queries doesn’t just make your code cleaner, it 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, developers 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 project.tasks_count directly

    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?"

    Related blogs