GROUP BY, HAVING and Aggregate Functions
Codevity Admin · 10/24/2026
Codevity Admin · 10/24/2026
Aggregate functions reduce many rows to a single value, and lets you do that once per group. Together they answer questions such as "how many orders per customer?" or "what is the average salary per department?".
The examples use the table from the practice database, with the columns , , , and .
| Function | Returns |
|---|---|
| number of rows | |
| number of rows where the column is not NULL | |
| number of different non-NULL values | |
| total | |
| average of the non-NULL values | |
| / | smallest / largest value |
All of them except ignore . For values , returns 15, not 10, because the row is skipped rather than counted as zero.
Each distinct becomes one output row, and employees whose is form one group of their own. The rule to remember: every column in must either be inside an aggregate function or be listed in . Selecting here would be an error, because a department has many names and SQL cannot pick one.
You can group by several columns, and each distinct combination becomes a group:
Queries are logically evaluated in this order, which is not the order you write them:
Because runs before grouping, it cannot use an aggregate. Use for conditions on aggregates:
This ignores rows with no salary, keeps only departments with at least five people, and sorts the survivors. Put every condition you can in : it removes rows early, which is faster than grouping them first.
A expression inside an aggregate counts or sums only some rows. It is the portable way to turn rows into columns (a "pivot"), and it works in every database:
GROUP BYemployeesidnamedepartment_idsalaryhired_onCOUNT(*)COUNT(column)COUNT(DISTINCT column)SUM(column)AVG(column)MIN(column)MAX(column)COUNT(*)NULL10, 20, NULLAVGNULLdepartment_iddepartment_idNULLSELECTGROUP BYnameFROMJOINWHEREGROUP BYHAVINGSELECTORDER BYLIMITWHEREHAVINGWHERECASEWHERE COUNT(*) > 5HAVINGCOUNT(column)NULLAVGNULLAVG(COALESCE(bonus, 0))SELECT department_id,
COUNT(*) AS headcount,
AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id;-- SQL Server and MySQL: YEAR(hired_on). PostgreSQL: EXTRACT(YEAR FROM hired_on).
SELECT department_id, YEAR(hired_on) AS hire_year, COUNT(*)
FROM employees
GROUP BY department_id, YEAR(hired_on);SELECT department_id, AVG(salary) AS avg_salary
FROM employees
WHERE salary > 0
GROUP BY department_id
HAVING COUNT(*) >= 5
ORDER BY avg_salary DESC;SELECT department_id,
COUNT(*) AS total,
SUM(CASE WHEN salary >= 60000 THEN 1 ELSE 0 END) AS high_earners,
SUM(CASE WHEN salary < 60000 THEN 1 ELSE 0 END) AS others
FROM employees
GROUP BY department_id;