Introduction
Active Record provides powerful methods for grouping data and calculating aggregates like counts, sums, and averages. These map directly to SQL GROUP BY, HAVING, and aggregate functions, letting you compute statistics without loading records into Ruby.
Key Concepts
- Aggregate methods:
count,sum,average,minimum,maximum-- each translates to the corresponding SQL function. group: Groups records by one or more columns, returning a hash of results.having: Filters groups after aggregation (likewherebut for grouped results).pick: Returns a single row's values as an array, useful for fetching one aggregate result.
Real World Context
Dashboards, analytics pages, and reporting features all rely on aggregations. Computing monthly revenue, counting orders by status, or finding the top-selling category are everyday tasks. Doing these in SQL via Active Record is orders of magnitude faster than loading all records into Ruby and computing in memory.
Deep Dive
Basic Aggregations
rubyArticle.count # Total articles Order.sum(:total) # Sum of all totals Product.average(:price) # Returns BigDecimal Product.minimum(:price) Product.maximum(:price)
These execute a single SQL query each and return a scalar value.
Grouping Data
rubyArticle.group(:category_id).count # => {1 => 10, 2 => 5, 3 => 8} Order.group(:status).sum(:total) # => {"pending" => 1500, "completed" => 8500} # Multiple groupings Order.group(:status, :payment_method).count # => {["completed", "credit_card"] => 50, ["completed", "paypal"] => 30}
The result is a hash where keys are the grouped values and values are the aggregate results.
Having Clause
Filter groups after aggregation:
rubyProduct.group(:category_id) .having("COUNT(*) > ?", 10) .count Article.group(:author_id) .having("AVG(rating) > ?", 4.0) .average(:rating)
having is to group what where is to the full result set. It filters after the aggregation.
Select with Calculations
rubyauthors = Author.select( "authors.*, COUNT(articles.id) as articles_count, AVG(articles.rating) as avg_rating" ).joins(:articles) .group("authors.id") authors.each do |author| puts "#{author.name}: #{author.articles_count} articles" end
Custom select with aggregate functions lets you compute multiple metrics in a single query.
Distinct
rubyArticle.distinct.count(:author_id) # Number of unique authors Article.distinct.pluck(:category_id) # Unique category IDs
Common Pitfalls
- Grouping without aggregation -- Calling
group(:status)without an aggregate method returns raw grouped relations. Always pairgroupwithcount,sum, etc. - Mixing group and non-group columns -- In strict SQL mode, selecting columns not in the GROUP BY clause raises an error. Only select grouped or aggregated columns.
- Using Ruby for aggregation --
Order.all.map(&:total).sumloads every record. UseOrder.sum(:total)instead.
Best Practices
- Do math in SQL -- Aggregations in SQL are faster and use less memory than loading records into Ruby.
- Use
havingfor group filters -- Do not filter aggregated results in Ruby; push the filter to the database. - Add database indexes on grouped columns -- If you frequently group by
statusorcategory_id, index those columns.
Summary
- Active Record provides
count,sum,average,minimum, andmaximumas direct SQL aggregate wrappers. groupreturns a hash of grouped results; pair it with an aggregate method.havingfilters groups after aggregation.- Always compute aggregations in SQL, not Ruby, for performance.
- Use
distinctto count or pluck unique values.
Code Examples
# Count orders by status
Order.group(:status).count
# => {"pending" => 15, "completed" => 85, "cancelled" => 5}
# Monthly revenue
Order.where(status: "completed")
.group("DATE_TRUNC('month', created_at)")
.sum(:total)
# Categories with more than 10 products
Product.group(:category_id)
.having("COUNT(*) > ?", 10)
.count