Aggregate Functions: MAX, MIN, AVG, SUM, COUNT
Quick answer Aggregate functions squeeze a whole column into one value, and every one of them except COUNT(*) silently skips NULL — which is exactly where the board sets its trap.
Every SELECT you have written so far gives one output row for every row that matched. An aggregate function breaks that rule: it takes a whole column of values and returns a single value. Five are on your syllabus — MAX, MIN, AVG, SUM, COUNT.
Two tables run through this entire chapter. Build them once and every query in every section will work.
CREATE DATABASE company;
USE company;
CREATE TABLE dept (
deptno INT PRIMARY KEY,
dname VARCHAR(20),
city VARCHAR(20)
);
INSERT INTO dept VALUES
(10, 'Sales', 'Mumbai'),
(20, 'Technical', 'Bengaluru'),
(30, 'HR', 'Delhi');
CREATE TABLE emp (
empno INT PRIMARY KEY,
ename VARCHAR(20),
deptno INT,
salary DECIMAL(10,2),
bonus DECIMAL(10,2),
doj DATE
);
INSERT INTO emp VALUES
(101, 'Ananya Iyer', 20, 85000, 12000, '2019-06-10'),
(102, 'Rohit Sharma', 10, 62000, NULL, '2020-01-15'),
(103, 'Meera Nair', 20, 91000, 15000, '2018-03-01'),
(104, 'Vikram Singh', 30, 47000, 5000, '2021-07-20'),
(105, 'Priya Menon', 10, 62000, 8000, '2020-11-05'),
(106, 'Arjun Desai', 20, 120000, NULL, '2017-02-11'),
(107, 'Kavya Reddy', 30, 54000, 6000, '2022-08-30'),
(108, 'Imran Khan', NULL, 40000, 4000, '2023-04-02');This is what EMP actually holds. Two gaps are deliberate: Rohit and Arjun have no bonus recorded, and Imran has no department assigned. Almost every mark students lose in this unit comes from those two NULLs.
| empno | ename | deptno | salary | bonus | doj |
|---|---|---|---|---|---|
| 101 | Ananya Iyer | 20 | 85000.00 | 12000.00 | 2019-06-10 |
| 102 | Rohit Sharma | 10 | 62000.00 | NULL | 2020-01-15 |
| 103 | Meera Nair | 20 | 91000.00 | 15000.00 | 2018-03-01 |
| 104 | Vikram Singh | 30 | 47000.00 | 5000.00 | 2021-07-20 |
| 105 | Priya Menon | 10 | 62000.00 | 8000.00 | 2020-11-05 |
| 106 | Arjun Desai | 20 | 120000.00 | NULL | 2017-02-11 |
| 107 | Kavya Reddy | 30 | 54000.00 | 6000.00 | 2022-08-30 |
| 108 | Imran Khan | NULL | 40000.00 | 4000.00 | 2023-04-02 |
And DEPT:
| deptno | dname | city |
|---|---|---|
| 10 | Sales | Mumbai |
| 20 | Technical | Bengaluru |
| 30 | HR | Delhi |
Worked example — all five functions in one query.
SELECT MAX(salary) AS highest,
MIN(salary) AS lowest,
SUM(salary) AS total,
AVG(salary) AS average,
COUNT(*) AS employees
FROM emp;Real output:
| highest | lowest | total | average | employees |
|---|---|---|---|---|
| 120000.00 | 40000.00 | 561000.00 | 70125.000000 | 8 |
Eight rows went in, one row came out. That is the whole idea. Notice AVG came back as 70125.000000 — MySQL widens the result of AVG, so do not be surprised by the trailing zeros.
The NULL rule — learn this sentence. COUNT(*) counts rows. Every other aggregate, including COUNT(column), looks only at NON-NULL values. Here is the proof on our own table.
SELECT COUNT(*) AS rows_in_table,
COUNT(bonus) AS bonus_values,
COUNT(deptno) AS deptno_values,
COUNT(DISTINCT salary) AS distinct_salaries,
COUNT(DISTINCT deptno) AS distinct_depts
FROM emp;| rows_in_table | bonus_values | deptno_values | distinct_salaries | distinct_depts |
|---|---|---|---|---|
| 8 | 6 | 7 | 7 | 3 |
Read that row slowly. COUNT(*) says 8 because there are eight rows. COUNT(bonus) says 6 because two bonuses are NULL. COUNT(deptno) says 7 because one department is NULL. COUNT(DISTINCT salary) says 7 because 62000 appears twice (Rohit and Priya). Same table, four different numbers — and the examiner will hand you exactly one of them.
Why AVG is not what students expect. If NULLs are skipped, they are skipped in the divisor too. AVG is the sum divided by the non-null count, never by the row count.
SELECT SUM(bonus) AS sum_bonus,
COUNT(bonus) AS non_null_count,
AVG(bonus) AS avg_bonus,
SUM(bonus)/COUNT(bonus) AS sum_over_nonnull,
SUM(bonus)/COUNT(*) AS sum_over_all_rows
FROM emp;| sum_bonus | non_null_count | avg_bonus | sum_over_nonnull | sum_over_all_rows |
|---|---|---|---|---|
| 50000.00 | 6 | 8333.333333 | 8333.333333 | 6250.000000 |
AVG(bonus) equals 8333.333333, which is 50000/6. It is not 6250, which is 50000/8. If a question asks for the average bonus per employee including those who got none, AVG is the wrong tool — you must write SUM(bonus)/COUNT(*) yourself.
An all-NULL set gives NULL, not zero. Rohit and Arjun are the only two employees here with no bonus:
SELECT SUM(bonus) AS s, AVG(bonus) AS a, COUNT(bonus) AS c, COUNT(*) AS r
FROM emp WHERE empno IN (102,106);| s | a | c | r |
|---|---|---|---|
| NULL | NULL | 0 | 2 |
SUM and AVG return NULL, COUNT(bonus) returns 0, COUNT(*) returns 2. COUNT is the only aggregate that can never come back NULL.
NULLs also escape your WHERE clause. This surprises everyone:
| Query | Result |
|---|---|
| SELECT COUNT(*) FROM emp WHERE bonus > 5000; | 4 |
| SELECT COUNT(*) FROM emp WHERE bonus <= 5000; | 2 |
| SELECT COUNT(*) FROM emp; | 8 |
4 + 2 = 6, not 8. The two NULL-bonus rows fail both conditions, because any comparison with NULL is UNKNOWN, not true and not false. To catch them you need WHERE bonus IS NULL.
MAX and MIN are not only for numbers. On text they mean alphabetical order; on dates, latest and earliest.
SELECT MAX(ename) AS last_name_alpha, MIN(doj) AS earliest_joining FROM emp;| last_name_alpha | earliest_joining |
|---|---|
| Vikram Singh | 2017-02-11 |
The query you must never write. An aggregate returns one value, so it cannot sit beside an ordinary column that has eight values:
SELECT ename, MAX(salary) FROM emp;MySQL 8 refuses it outright:
ERROR 1140 (42000): In aggregated query without GROUP BY, expression #1 of SELECT
list contains nonaggregated column 'company.emp.ename'; this is incompatible with
sql_mode=only_full_group_byMySQL names the column in full, database.table.column, so company in that message is simply the database you are working in — you will see your own database's name there.
To get the name of the highest-paid employee you must ask twice. First find the maximum:
SELECT MAX(salary) AS highest FROM emp;| highest |
|---|
| 120000.00 |
Then look up the row that carries that value:
SELECT ename, salary FROM emp WHERE salary = 120000;| ename | salary |
|---|---|
| Arjun Desai | 120000.00 |
Two ordinary queries, one after the other. That is all your syllabus asks for.
- COUNT(*) counts rows and never skips anything; COUNT(column) counts only NON-NULL values in that column. On our EMP table the two answers are 8 and 6.
- Every aggregate except COUNT(*) ignores NULL. AVG(bonus) = 50000/6 = 8333.333333, not 50000/8 = 6250.
- SUM and AVG over an entirely NULL set return NULL, but COUNT returns 0. COUNT is the only aggregate that never yields NULL.
- NULL rows fail both bonus > 5000 and bonus <= 5000, so the two counts do not add up to the table size. Use IS NULL to find them.
- You cannot mix a plain column with an aggregate when there is no GROUP BY — MySQL raises ERROR 1140 (only_full_group_by). With a GROUP BY present, the same mistake gives ERROR 1055 instead.
