In SQL Aggregate Functions you turned a whole table into one number, such as the total salary bill. Often you need that number for each something: staff per department, customers per state, orders per customer. SQL GROUP BY does this by splitting rows into groups and running the aggregate once per group. HAVING then lets you keep only the groups you care about.
How SQL GROUP BY works
Picture the database sorting rows into buckets. Every row with the same value in the GROUP BY column lands in the same bucket. Each bucket then becomes one row in the result, and aggregate functions such as COUNT or AVG are calculated inside each bucket.
SELECT Department,
COUNT(*) AS Staff,
ROUND(AVG(Salary)) AS AvgSalary
FROM Employees
GROUP BY Department;Returns 5 rows, one per department. Development has 3 staff with an average of 75000; Sales has 2 with 61500.
Eight employee rows became five result rows – one per department. Without GROUP BY, the same COUNT(*) would have returned a single row saying 8.
The golden rule
Every column in the SELECT list must either be in the GROUP BY clause or be inside an aggregate function. In the query above, Department is grouped, and Salary only appears inside AVG(). If you added FirstName, which name should the database show for a group of three developers? There is no single answer.
Watch out: SQLite does not enforce this rule – it quietly picks a value from one row of the group. MySQL 5.7 and later reject such queries by default (the ONLY_FULL_GROUP_BY SQL mode), and so do PostgreSQL and SQL Server. Follow the rule even in SQLite so your queries work everywhere.
Grouping with WHERE and ORDER BY
GROUP BY sits after WHERE and before ORDER BY. WHERE removes rows before they are grouped, and ORDER BY can sort by the aggregate result. Here we count Indian customers per state and show the busiest states first:
SELECT State, COUNT(*) AS Customers
FROM Customers
WHERE Country = 'India'
GROUP BY State
ORDER BY Customers DESC, State;Returns 7 rows: Tamil Nadu 3, Karnataka 2, then five states with 1 customer each.
You can group by more than one column, such as GROUP BY Country, State. Each different combination then forms its own group. You can also group by an expression. Because our dates are text, SQLite’s strftime('%Y-%m', OrderDate) extracts the month, so GROUP BY strftime('%Y-%m', OrderDate) gives orders per month. In MySQL you would use DATE_FORMAT(OrderDate, '%Y-%m').
Filtering groups with HAVING
Suppose you want only customers who have placed more than one order. You cannot write WHERE COUNT(*) > 1, because WHERE runs before the groups exist – at that point there is nothing to count. HAVING is the filter that runs after grouping, so it can use aggregate results.
SELECT CustomerID, COUNT(*) AS OrderCount
FROM Orders
GROUP BY CustomerID
HAVING COUNT(*) > 1;Returns 2 rows: customer 1 and customer 3, each with 2 orders.
Showing CustomerID numbers is not very friendly. In SQL Joins you will combine this with the Customers table to show names instead.
WHERE and HAVING together
The two filters often work side by side. The next query looks only at products under ₹300 (WHERE), groups them by category, and keeps categories that still have at least three such products (HAVING).
SELECT CategoryID,
COUNT(*) AS Products,
MAX(Price) AS Dearest
FROM Products
WHERE Price < 300
GROUP BY CategoryID
HAVING COUNT(*) >= 3
ORDER BY CategoryID;Returns 3 rows: categories 1 (Beverages), 2 (Snacks) and 5 (Spices).
| WHERE | HAVING | |
|---|---|---|
| Filters | Individual rows | Whole groups |
| Runs | Before grouping | After grouping |
| Can use aggregates? | No | Yes |
| Example | WHERE Price < 300 | HAVING COUNT(*) >= 3 |
Tip: If a condition does not need an aggregate, put it in WHERE. Removing rows early means less work for the grouping step. SQLite and MySQL also let you use a column alias in HAVING (for example HAVING OrderCount > 1), but SQL Server does not, so repeating the expression is the portable choice.
The order SQL actually runs a query
You write SELECT first, but the database processes the clauses in a different order. Knowing this order explains most GROUP BY errors:
FROM– pick the table (and joins)WHERE– remove rowsGROUP BY– form groupsHAVING– remove groupsSELECT– calculate the output columnsORDER BY– sortLIMIT– cut down the rows
This is why a SELECT alias cannot normally be used in WHERE: when WHERE runs, the alias does not exist yet. ORDER BY comes after SELECT, so aliases work there. Next, learn to combine tables in SQL Joins.
