Query builder GROUP BY, HAVING, COUNT in Laravel

Stefan Bogdanescu

Founder & Senior Architect · 2026-06-29

Laravel Company
# Mastering Aggregation: GROUP BY, HAVING, and COUNT in the Laravel Query Builder As developers working with relational databases, mastering SQL concepts like `GROUP BY` and `HAVING` is fundamental. When migrating these complex queries into an ORM or Query Builder like Laravel's Eloquent or the fluent Query Builder, it’s easy to run into syntax errors that look like a mismatch between your code and the resulting SQL. This post dives into the specific challenge you encountered—translating a query involving grouping and filtering aggregates (`COUNT`) in Laravel 6—and shows you exactly what went wrong and how to fix it correctly. ## The SQL Foundation: GROUP BY vs. HAVING Before we look at the Laravel implementation, let’s quickly review the core concepts from the original SQL query: ```sql SELECT * FROM feedback GROUP BY noTicket having count(`status`) < 2 ``` 1. **`GROUP BY noTicket`**: This clause aggregates rows that have the same value in the `noTicket` column into a single summary row. All other selected columns must either be part of the `GROUP BY` clause or be an aggregate function (like `COUNT()`, `SUM()`, `AVG()`). 2. **`HAVING count(status) < 2`**: The `HAVING` clause filters the results *after* the grouping has occurred. It is used to filter based on the results of aggregate functions. ## Why Your Code Failed: The Non-Grouped Column Error Your attempt to translate this directly into the Laravel Query Builder resulted in an error: ``` SQLSTATE[42000]: Syntax error or access violation: 1055 'sifora.feedback.idFeedback' isn't in GROUP BY ``` The error message is crystal clear: when you use `GROUP BY noTicket`, the database only knows how to group by that column. Since your original query used `SELECT *`, it implicitly tried to select every column, including `idFeedback`. Because `idFeedback` was not listed in the `GROUP BY` clause, the SQL engine rejected the query, as it doesn't know which `idFeedback` value to return for a group containing multiple rows. The issue isn't with how you used `groupBy()` or `having()`, but rather with what you selected (`*`) in combination with the grouping instruction. ## The Correct Laravel Implementation To fix this, we must explicitly select only the columns we are grouping by and the aggregate results we need. We should stop using `SELECT *` when performing complex aggregations. Here is the correct way to express your logic using the Laravel Query Builder: ```php use Illuminate\Support\Facades\DB; $results = DB::table('feedback') ->select('noTicket', DB::raw('count(status) as status_count')) // Select only what we need ->groupBy('noTicket') // Group by the ticket number ->having('status_count', '<', 2) // Filter the groups based on the count ->get(); ``` ### Breakdown of the Fix: 1. **Explicit Selection (`select(...)`):** Instead of `SELECT *`, we explicitly select `'noTicket'` and use a raw expression (`DB::raw('count(status) as status_count')`) to calculate the required aggregate value. 2. **Grouping (`groupBy()`):** We correctly apply `groupBy('noTicket')`. 3. **Filtering Aggregates (`having()`):** The `having` clause now correctly references the alias we created in the `select` statement (`status_count`). This approach ensures that every column returned is either explicitly grouped or derived from an aggregate function, satisfying standard SQL rules and allowing Laravel to successfully build the complex query. This pattern is crucial when moving beyond simple CRUD operations and dealing with data aggregation, a skill essential for building robust applications on platforms like Laravel. ## Conclusion: Best Practices for Complex Queries When working with Laravel's Query Builder, always remember that you are translating structured SQL logic