Eloquent Aggregate Filtering with selectRaw and havingRaw

📖 2 minutes read

When you need to filter records based on aggregate calculations—like “find all products with more than 5 reviews”—you might reach for a subquery. But Eloquent’s selectRaw and havingRaw combo offers a cleaner, more performant alternative.

The Pattern

Instead of a subquery, combine aggregate calculation with filtering in a single query:

Product::joinRelationship('reviews')
    ->select('products.*')
    ->selectRaw('count(reviews.id) as review_count')
    ->groupBy('products.id')
    ->havingRaw('review_count > 5')
    ->orderByDesc('reviews.id')
    ->take(50)
    ->pluck('review_count', 'products.id');

This query:

  1. Joins the relationship (using a package like kirschbaum-development/eloquent-power-joins)
  2. Selects all product columns
  3. Adds a calculated column via selectRaw
  4. Groups by product ID
  5. Filters on the aggregate using havingRaw
  6. Sorts and limits results

Why This Works Better Than Subqueries

Readability: The query reads top-to-bottom like natural language: “join reviews, count them, group by product, keep only products with count > 5”

Performance: Single query execution instead of a nested subquery that MySQL has to materialize separately

Flexibility: Easy to add multiple aggregates (count, sum, avg) and filter on any of them

Understanding HAVING vs WHERE

The key difference:

  • WHERE filters rows before grouping (operates on individual records)
  • HAVING filters after grouping (operates on aggregate results)

You can’t use WHERE review_count > 5 because review_count doesn’t exist yet—it’s calculated during aggregation. That’s where havingRaw comes in.

The havingRaw Caveat

Note that we’re using havingRaw, not having. Laravel’s having method expects specific arguments and doesn’t support this pattern cleanly. With havingRaw, you write the HAVING clause exactly as you would in SQL:

->havingRaw('review_count > 5')
->havingRaw('SUM(amount) > 1000')
->havingRaw('AVG(rating) >= 4.0')

Combining Multiple Aggregates

You can add multiple calculated columns and filter on any of them:

Product::joinRelationship('reviews')
    ->select('products.*')
    ->selectRaw('count(reviews.id) as review_count')
    ->selectRaw('avg(reviews.rating) as avg_rating')
    ->groupBy('products.id')
    ->havingRaw('review_count > 5')
    ->havingRaw('avg_rating >= 4.0')
    ->get();

This finds products with at least 6 reviews AND an average rating of 4.0 or higher—all in one query.

When to Use This Pattern

Reach for selectRaw + havingRaw when you need to:

  • Filter on counts, sums, averages, or other aggregates
  • Include the aggregate value in your results (useful for display or sorting)
  • Avoid subquery complexity while keeping queries readable

It’s a powerful pattern that keeps your Eloquent queries expressive while generating efficient SQL.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *