Reader Stacks

Laravel Eloquent Aggregate Functions: count, sum, avg, min, max, and groupBy

count() returns just a number, while groupBy()+get() returns full grouped result sets — the two approaches solve genuinely different problems and shouldn't be reached for interchangeably.

Eloquent's aggregate methods — count(), sum(), avg(), min(), max() — run the calculation directly in the database rather than loading every row into PHP, which matters both for performance and for correctness on a large table.

count()

$totalOrders = Order::count();
$pendingOrders = Order::where('status', 'pending')->count();

sum()

$totalRevenue = Order::where('status', 'completed')->sum('total');

avg()

$averageOrderValue = Order::where('status', 'completed')->avg('total');

min() and max()

$cheapestPrice = Product::min('price');
$mostExpensivePrice = Product::max('price');

Why these run in the database, not in PHP

Each of these methods generates a SQL aggregate function (COUNT(), SUM(), and so on) rather than fetching every matching row and calculating the result in PHP — critical on a large table, since fetching millions of rows just to sum one column would be dramatically slower and consume far more memory than letting the database engine compute it directly.

groupBy() with an aggregate: totals per category

$revenueByCategory = Order::join('products', 'orders.product_id', '=', 'products.id')
    ->selectRaw('products.category_id, SUM(orders.total) as total_revenue')
    ->groupBy('products.category_id')
    ->get();

groupBy() combined with a raw SUM() in selectRaw() is what returns one row per category with its own aggregate total — unlike the single-value methods above (sum(), count()), which collapse the entire result set into one number.

Filtering grouped results with having()

$popularCategories = Order::join('products', 'orders.product_id', '=', 'products.id')
    ->selectRaw('products.category_id, COUNT(*) as order_count')
    ->groupBy('products.category_id')
    ->having('order_count', '>', 100)
    ->get();

having() filters on the aggregated value itself (order_count) — this is the essential distinction from where(), which can only filter on actual column values before aggregation happens, not on a computed aggregate result.

Combining multiple aggregates in one query

$stats = Order::selectRaw('
    COUNT(*) as total_orders,
    SUM(total) as total_revenue,
    AVG(total) as average_order_value,
    MAX(total) as largest_order
')->where('status', 'completed')->first();

echo $stats->total_orders;
echo $stats->total_revenue;

Combining several aggregates into a single selectRaw() call, rather than running four separate queries, computes every statistic in one database round-trip — a meaningful efficiency gain for a dashboard or reporting page that needs several related numbers at once.

limit(), for capping result size directly in the query

$topProducts = Product::orderBy('sales_count', 'desc')->limit(10)->get();

limit() (aliased as take()) caps the number of rows the database actually returns — distinct from pagination, this is useful for a fixed-size "top 10" style list where no page-through UI is needed at all.