Grouping Aggregations
GROUP BY splits the rows into groups and runs the aggregates once per group. Every column in the SELECT that isn't aggregated must be in the GROUP BY.
Journeyman · T-SQL · SQL · Updated
After this lesson you can
- Aggregate per group with GROUP BY, by one column or several
- Explain why every non-aggregated column in the SELECT must be in the GROUP BY
Key takeaways
GROUP BYsorts the rows into groups and runs the aggregates once per group, so each group becomes one row.- Every column in the
SELECTmust be aggregated or in theGROUP BY. A group is one row, and SQL Server won't guess which value to show. - Grouping by several columns, such as
GROUP BY Country, City, makes one group for each combination. - In a query,
GROUP BYgoes afterWHEREand beforeORDER BY. - To group by an expression, repeat it, as in
GROUP BY YEAR(OrderDate): theSELECT's alias doesn't exist yet when theGROUP BYruns.
An aggregate on its own answers one question about all the rows. To ask how many products are in category 1, you'd filter with WHERE CategoryId = 1. For category 2, another query. For all eight categories, eight queries.
GROUP BY answers them all at once.
One row per group
SELECT CategoryId
, COUNT(*) AS Products
, AVG(UnitPrice) AS AveragePrice
FROM Products
GROUP BY CategoryId
ORDER BY CategoryId;
| CategoryId | Products | AveragePrice |
|---|---|---|
| 1 | 12 | 37.9791 |
| 2 | 12 | 23.0625 |
| 3 | 13 | 25.16 |
| 4 | 10 | 28.73 |
| 5 | 7 | 20.25 |
| 6 | 6 | 54.0066 |
| 7 | 5 | 32.37 |
| 8 | 12 | 20.6825 |
GROUP BY CategoryId sorts the products into one pile per category. Then COUNT and AVG run once per pile, and each pile becomes one row.
GROUP BY goes after WHERE and before ORDER BY. (Category names arrive in the next chapter, Joins in T-SQL.)
The GROUP BY rule
Every column in the SELECT must either be aggregated or be in the GROUP BY. Break the rule, and SQL Server refuses:
SELECT CategoryId, ProductName, COUNT(*) AS Products
FROM Products
GROUP BY CategoryId;
Msg 8120, Level 16, State 1
Column 'Products.ProductName' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
It makes sense: category 1 is one row in the result, but it has twelve products. Which ProductName should that row show? SQL Server won't guess. Either aggregate it (MAX(ProductName), COUNT(ProductName)) or group by it too.
Grouping by more than one column
List several columns, and there's one group for each combination. How many customers in each city of each country?
SELECT Country, City, COUNT(*) AS Customers
FROM Customers
GROUP BY Country, City
ORDER BY Customers DESC, Country, City;
| Country | City | Customers |
|---|---|---|
| UK | London | 6 |
| Mexico | México D.F. | 5 |
| Brazil | Sao Paulo | 4 |
| Argentina | Buenos Aires | 3 |
| Brazil | Rio de Janeiro | 3 |
| Spain | Madrid | 3 |
(The first 6 of 69 rows.)
Grouping by a calculation
You can group by an expression, such as the year of a date. How many orders in each year?
SELECT YEAR(OrderDate) AS OrderYear, COUNT(*) AS Orders
FROM Orders
GROUP BY YEAR(OrderDate)
ORDER BY OrderYear;
| OrderYear | Orders |
|---|---|
| 2016 | 152 |
| 2017 | 408 |
| 2018 | 270 |
The GROUP BY repeats the expression, YEAR(OrderDate): SQL Server reads the GROUP BY before the SELECT, so the alias OrderYear doesn't exist there yet. (ORDER BY runs last, so it can use the alias.)
Check yourself
2 questions to try on the lesson page.
So what
GROUP BY turns one aggregate into a whole report: a row per category, per city, per year. Group by what you want a row for, aggregate everything else, and the report writes itself.