Implementing group_by and having in Laravel using Eloquent

Stefan Bogdanescu

Founder & Senior Architect · 2026-06-29

Laravel Company

Mastering Aggregation in Laravel: Solving the group_by and having Puzzle

As a senior developer working with Laravel, you frequently encounter scenarios where combining joins, aggregations (SUM, COUNT), grouping (GROUP BY), and filtering (HAVING) feels like navigating a maze. The specific issue you are facing—where an expression works fine without having but returns zero results when the condition is added—is a classic SQL pitfall that often trips up developers moving from simple CRUD operations to complex data manipulation.

Let’s dive into your scenario, analyze why this happens, and implement the correct solution using Eloquent and the Query Builder.

The Scenario Breakdown

You are trying to find orders that have outstanding quantities based on their related deliveries.

Data Model:

  • orders: id, qty
  • deliveries: id, qty, order_id

Goal: Display orders where the total quantity delivered is less than the ordered quantity (i.e., the balance is greater than zero).

Your attempted query structure:

php
Order::left_join('deliveries', 'orders.id', '=', 'deliveries.order_id')
    ->select(array('orders.*'), DB::raw('orders.qty - IFNULL(sum(deliveries.qty), 0) AS balance'))
    ->group_by('order_id')
    ->having('balance', '>', 0) // This is where the failure occurs
    ->get();

The Diagnosis: Why HAVING Fails

The reason your query returns no results when you add the having clause, despite the calculation working correctly without it, lies in the order of operations and how SQL handles grouping and filtering.

When you use a LEFT JOIN, if an order has no corresponding deliveries, the columns from the deliveries table will be NULL. Your aggregation function (SUM(deliveries.qty)) correctly handles these NULLs by treating them as 0 (thanks to IFNULL), resulting in a calculated balance.

The problem arises when you apply GROUP BY followed by HAVING. The HAVING clause filters the groups created by the GROUP BY. If an order exists but has no matching deliveries, the aggregation result is based on that single row. However, if the combination of the join and grouping results in a set where the calculated aggregate doesn't meet the criteria, zero rows are returned.

The most common reason for this specific failure is subtle: In some SQL dialects or when dealing with complex aggregations across LEFT JOINs, filtering based on an aggregated value that involves conditions from the joined table can interact unexpectedly with the grouping logic, especially if you aren't careful about which columns are included in the GROUP BY.

The Correct Implementation: Leveraging Subqueries for Clarity

While it is possible to force this using raw expressions, the most robust and readable way to handle complex conditional aggregations in Laravel is often to separate the calculation into a subquery or a nested query. This isolates the aggregation step from the final filtering step, making debugging much easier.

Here is the recommended approach:

Step 1: Calculate the Delivery Sums Separately (Subquery)

First, calculate the total delivered quantity for each order.

php
$orderTotals = DB::table('orders')
    ->select('id', 'qty')
    ->leftJoin('deliveries', 'orders.id', '=', 'deliveries.order_id')
    ->groupBy('orders.id')
    ->selectRaw('orders.id, orders.qty, IFNULL(SUM(deliveries.qty), 0) AS delivered_qty')
    ->toSql();

Step 2: Join and Filter the Results

Now, use this result set to join back to the main orders table and apply the final filter.

php
$incompleteOrders = DB::table('orders')