Table of Contents
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:
- Joins the relationship (using a package like
kirschbaum-development/eloquent-power-joins) - Selects all product columns
- Adds a calculated column via
selectRaw - Groups by product ID
- Filters on the aggregate using
havingRaw - 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:
WHEREfilters rows before grouping (operates on individual records)HAVINGfilters 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.
Leave a Reply