SQL Aggregate Functions: COUNT, SUM, AVG, MIN and MAX
An aggregate function turns many rows into one value: how many, the total, the average, the smallest or the largest. On their own they summarise a whole table; with GROUP BY they give one summary per group, such as revenue per month or headcount per department.
What Are Aggregate Functions?
One query, five summaries of the 120 employees' salaries:
SELECT COUNT(*) AS employees,
SUM(salary) AS total_salary,
ROUND(AVG(salary)) AS avg_salary,
MIN(salary) AS lowest,
MAX(salary) AS highest
FROM employees;
▶ Run this query in the SQL Playground
| employees | total_salary | avg_salary | lowest | highest |
|---|---|---|---|---|
| 120 | 14924501 | 124371 | 46044 | 204449 |
| Function | Returns | Works on |
|---|---|---|
COUNT(*) | Number of rows | Any rows |
COUNT(column) | Number of non-NULL values | Any type |
SUM(column) | Total | Numbers |
AVG(column) | Average (mean) | Numbers |
MIN(column) / MAX(column) | Smallest / largest value | Numbers, text (alphabetical), dates |
Without GROUP BY, the whole result is one group, so the query returns exactly one row however many rows the table has.
COUNT(*), COUNT(column) and COUNT(DISTINCT)
The three forms of COUNT answer different questions. Orders that have not shipped have NULL in shipped_date, which shows the difference:
SELECT COUNT(*) AS all_orders,
COUNT(shipped_date) AS orders_with_shipped_date,
COUNT(DISTINCT user_id) AS distinct_customers,
COUNT(DISTINCT status) AS distinct_statuses
FROM orders;
▶ Run this query in the SQL Playground
| all_orders | orders_with_shipped_date | distinct_customers | distinct_statuses |
|---|---|---|---|
| 200 | 103 | 114 | 6 |
| Form | Counts | Here |
|---|---|---|
COUNT(*) | Every row | 200 |
COUNT(shipped_date) | Rows where shipped_date is not NULL | 103 |
COUNT(DISTINCT user_id) | Different non-NULL values | 114 customers placed the 200 orders |
COUNT(1) behaves exactly like COUNT(*); neither is faster in modern databases.
SUM and AVG
Revenue and average order value for delivered orders:
SELECT COUNT(*) AS delivered_orders,
ROUND(SUM(total), 2) AS revenue,
ROUND(AVG(total), 2) AS avg_order_value
FROM orders
WHERE status = 'delivered';
▶ Run this query in the SQL Playground
| delivered_orders | revenue | avg_order_value |
|---|---|---|
| 82 | 210776.34 | 2570.44 |
WHERE runs before the aggregates, so only the delivered orders are added up. Rounding with ROUND keeps long decimals out of reports.
AVG(CAST(x AS DECIMAL(10,2))). SQLite, PostgreSQL and MySQL return a decimal.MIN and MAX on Numbers, Dates and Text
MIN and MAX work on any type that can be sorted. On dates they give the first and last; on text, the first and last alphabetically:
SELECT MIN(order_date) AS first_order,
MAX(order_date) AS latest_order,
MIN(total) AS smallest_order,
MAX(total) AS largest_order
FROM orders;
▶ Run this query in the SQL Playground
| first_order | latest_order | smallest_order | largest_order |
|---|---|---|---|
| 2022-01-06 | 2024-12-24 | 29.17 | 4997.15 |
SELECT MIN(name) AS first_alphabetically,
MAX(name) AS last_alphabetically
FROM products;
▶ Run this query in the SQL Playground
| first_alphabetically | last_alphabetically |
|---|---|
| 2-Drawer Filing Cabinet | Zero to One (Thiel) |
MIN and MAX return a value, not the row it belongs to. To find which product is cheapest, use ORDER BY price LIMIT 1 or WHERE price = (SELECT MIN(price) FROM products).
How Aggregates Handle NULL
Every aggregate except COUNT(*) skips NULL values. That is usually what you want, but it changes averages. The sample rows below include a missing bonus:
WITH bonuses(name, bonus) AS (
VALUES ('Aditi', 15000), ('Rohan', NULL), ('Priya', 8000), ('Karan', NULL), ('Sneha', 0)
)
SELECT COUNT(*) AS people,
COUNT(bonus) AS bonuses_recorded,
SUM(bonus) AS total,
ROUND(AVG(bonus), 2) AS avg_recorded,
AVG(COALESCE(bonus, 0)) AS avg_per_person
FROM bonuses;
▶ Run this query in the SQL Playground
| people | bonuses_recorded | total | avg_recorded | avg_per_person |
|---|---|---|---|---|
| 5 | 3 | 23000 | 7666.67 | 4600 |
AVG divides by the 3 recorded values, not by all 5 people. If a missing bonus should count as zero, say so with COALESCE.
Aggregates over no rows
When nothing matches, COUNT returns 0 but SUM, AVG, MIN and MAX return NULL. There are no orders from 2025 in the Playground:
SELECT COUNT(*) AS orders,
SUM(total) AS revenue,
MAX(total) AS largest,
COALESCE(SUM(total), 0) AS revenue_or_zero
FROM orders
WHERE order_date >= '2025-01-01';
▶ Run this query in the SQL Playground
| orders | revenue | largest | revenue_or_zero |
|---|---|---|---|
| 0 | NULL | NULL | 0 |
Aggregates With GROUP BY and HAVING
GROUP BY splits the rows into groups and runs the aggregates once per group. Headcount and average salary per department:
SELECT department,
COUNT(*) AS employees,
ROUND(AVG(salary)) AS avg_salary,
MAX(salary) AS top_salary
FROM employees
GROUP BY department
ORDER BY avg_salary DESC;
▶ Run this query in the SQL Playground
| department | employees | avg_salary | top_salary |
|---|---|---|---|
| Customer Success | 15 | 134140 | 203999 |
| Design | 10 | 133965 | 194543 |
| Engineering | 11 | 131028 | 199029 |
| Operations | 13 | 126879 | 203467 |
| HR | 12 | 124151 | 191037 |
| Marketing | 13 | 124111 | 204449 |
| Finance | 10 | 121471 | 196032 |
| Product | 9 | 119153 | 188273 |
| Sales | 11 | 116893 | 195759 |
| Legal | 16 | 112866 | 192220 |
Filter rows with WHERE before grouping, and filter groups with HAVING after it. Departments with at least 12 employees:
SELECT department, COUNT(*) AS employees
FROM employees
GROUP BY department
HAVING COUNT(*) >= 12
ORDER BY employees DESC;
▶ Run this query in the SQL Playground
| department | employees |
|---|---|
| Legal | 16 |
| Customer Success | 15 |
| Operations | 13 |
| Marketing | 13 |
| HR | 12 |
WHERE cannot contain an aggregate because it runs before the groups exist. The GROUP BY and HAVING lessons go further.
Conditional Aggregates and GROUP_CONCAT
A CASE inside an aggregate counts or sums only the rows you choose, so one query can produce several figures side by side:
SELECT COUNT(*) AS orders,
SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) AS delivered,
SUM(CASE WHEN status IN ('cancelled', 'refunded') THEN 1 ELSE 0 END) AS lost,
ROUND(SUM(CASE WHEN status = 'delivered' THEN total ELSE 0 END), 2) AS delivered_revenue
FROM orders;
▶ Run this query in the SQL Playground
| orders | delivered | lost | delivered_revenue |
|---|---|---|---|
| 200 | 82 | 46 | 210776.34 |
Most databases also have an aggregate that joins text: GROUP_CONCAT in SQLite and MySQL, STRING_AGG in PostgreSQL and SQL Server, LISTAGG in Oracle:
SELECT department,
GROUP_CONCAT(first_name, ', ') AS first_names
FROM employees
WHERE department IN ('Design', 'Product')
GROUP BY department;
▶ Run this query in the SQL Playground
| department | first_names |
|---|---|
| Design | Jason, Amy, Paul, Ashley, Ashley, Joshua, Maria, Michael, Robert, Sandra |
| Product | Sandra, Susan, Barbara, Steven, Joshua, Matthew, Kevin, Donald, Michael |
Differences Between Databases
| Behaviour | SQLite | PostgreSQL | MySQL | SQL Server |
|---|---|---|---|---|
| AVG of integers | Decimal | Decimal | Decimal | Integer (cast first) |
| SUM of text | Converts, often 0 | Error | Converts, often 0 | Error |
| Non-grouped column in SELECT | Allowed, picks a row | Error | Error with ONLY_FULL_GROUP_BY (default) | Error |
| Join strings | GROUP_CONCAT | STRING_AGG | GROUP_CONCAT | STRING_AGG |
| Filtered aggregate | FILTER (WHERE ...) | FILTER (WHERE ...) | CASE only | CASE only |
Common Aggregate Mistakes
Six errors and how to avoid them.
1. An aggregate in WHERE
SELECT department
FROM employees
WHERE AVG(salary) > 130000
GROUP BY department;
SELECT department
FROM employees
GROUP BY department
HAVING AVG(salary) > 130000;
WHERE filters rows before aggregation; HAVING filters the groups afterwards.
2. COUNT(column) when you meant COUNT(*)
SELECT COUNT(shipped_date) AS orders
FROM orders; -- 103
SELECT COUNT(*) AS orders
FROM orders; -- 200
COUNT(column) only counts rows where the column has a value.
3. Selecting a column that is not grouped
SELECT department, first_name, AVG(salary)
FROM employees
GROUP BY department;
SELECT department, AVG(salary)
FROM employees
GROUP BY department;
A department has many first names; the database cannot know which one to show. SQLite picks one silently, which hides the bug.
4. Assuming SUM returns 0 for no rows
SELECT SUM(total) FROM orders
WHERE order_date >= '2025-01-01';
SELECT COALESCE(SUM(total), 0) FROM orders
WHERE order_date >= '2025-01-01';
SUM of no rows is NULL, which shows as a blank in reports.
5. Averaging averages
SELECT AVG(dept_avg)
FROM (SELECT AVG(salary) AS dept_avg
FROM employees GROUP BY department) t;
SELECT AVG(salary)
FROM employees;
The first query gives a 9-person department the same weight as a 16-person one. It answers "the average department", not "the average employee".
6. DISTINCT in the wrong place
SELECT DISTINCT COUNT(user_id)
FROM orders; -- 200
SELECT COUNT(DISTINCT user_id)
FROM orders; -- 114
DISTINCT outside the function removes duplicate result rows (there is only one); inside, it removes duplicate values before counting.
Aggregate Functions Cheat Sheet
| Question | Expression |
|---|---|
| How many rows? | COUNT(*) |
| How many have a value? | COUNT(col) |
| How many different values? | COUNT(DISTINCT col) |
| Total / average | SUM(col) / AVG(col) |
| Earliest / latest / smallest / largest | MIN(col) / MAX(col) |
| Zero instead of NULL | COALESCE(SUM(col), 0) |
| Count matching rows | SUM(CASE WHEN cond THEN 1 ELSE 0 END) |
| Per group | ... GROUP BY g; filter groups with HAVING |
Practice Aggregate Functions Queries
6 exercises on the real Playground tables.
Write each query yourself first. Every solution has been run against the Playground database.
-
1Easy Find the number of products, the average price and the price of the cheapest and most expensive product. Table:
productsShow solution
SELECT COUNT(*) AS products, ROUND(AVG(price), 2) AS avg_price, MIN(price) AS cheapest, MAX(price) AS most_expensive FROM products;▶ Run this query in the SQL Playground
Without GROUP BY, the whole table is one group.
-
2Easy How many users are there, and how many different cities and countries do they live in? Table:
usersShow solution
SELECT COUNT(*) AS users, COUNT(DISTINCT city) AS cities, COUNT(DISTINCT country) AS countries FROM users;▶ Run this query in the SQL Playground
COUNT(DISTINCT ...) counts each value once.
-
3Medium For each rating from 1 to 5, show the number of reviews and the average helpful votes. Table:
reviewsShow solution
SELECT rating, COUNT(*) AS reviews, ROUND(AVG(helpful_votes), 1) AS avg_helpful_votes FROM reviews GROUP BY rating ORDER BY rating DESC;▶ Run this query in the SQL Playground
One row per rating value.
-
4Medium Show the units sold and revenue per product category, highest revenue first. Table:
order_items, productsShow solution
SELECT p.category, SUM(oi.quantity) AS units, ROUND(SUM(oi.subtotal), 2) AS revenue FROM order_items oi JOIN products p ON p.id = oi.product_id GROUP BY p.category ORDER BY revenue DESC;▶ Run this query in the SQL Playground
Join first, then aggregate the joined rows per category.
-
5Medium Show the departments whose lowest salary is above 50,000. Table:
employeesShow solution
SELECT department, MIN(salary) AS lowest_salary FROM employees GROUP BY department HAVING MIN(salary) > 50000 ORDER BY lowest_salary DESC;▶ Run this query in the SQL Playground
A condition on an aggregate goes in HAVING.
-
6Hard For each payment method, show the number of payments, the total amount and the share of payments that succeeded. Table:
paymentsShow solution
SELECT method, COUNT(*) AS payments, ROUND(SUM(amount), 2) AS total_amount, ROUND(100.0 * SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) / COUNT(*), 1) AS success_pct FROM payments GROUP BY method ORDER BY payments DESC;▶ Run this query in the SQL Playground
A conditional SUM divided by COUNT(*) gives a percentage per group.
📝 Quiz — Aggregate Functions
Test your understanding · 0/25 answered
What do aggregate functions do?
COUNT, SUM, AVG, MIN and MAX.
What does COUNT(*) count?
All 200 orders, shipped or not.
What does COUNT(column) count?
COUNT(shipped_date) gives 103.
What does SELECT COUNT(DISTINCT department_id) FROM employees answer?
Each department id is counted once.
What does SUM(total) return when no rows match?
Wrap it in COALESCE(..., 0) if you need 0.
Does AVG include NULL values?
In the sample bonuses, AVG divides by 3, not 5.
Can MAX and MIN be used on non-numeric columns?
MAX(order_date) is the latest order.
Can aggregate functions be used without GROUP BY?
The result is a single row.
What does SELECT department, AVG(salary) FROM employees GROUP BY department return?
Ten rows in the Playground.
What is HAVING for?
HAVING COUNT(*) >= 12, for example.
Why can WHERE not filter on AVG(salary) > 50000?
Use HAVING.
Which runs first, WHERE or HAVING?
FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY.
How many rows does SELECT COUNT(*), SUM(salary), AVG(salary), MAX(salary), MIN(salary) FROM employees return?
All five values on one row.
If one employee has a NULL salary, how do COUNT(salary) and COUNT(*) compare?
By one.
What does MAX(order_date) usually answer?
In the Playground, 2024-12-24.
What does MIN(order_date) usually answer?
In the Playground, 2022-01-06.
Which aggregate is used most often?
Counting rows, customers and orders comes up constantly.
What does SUM return over a column that is entirely NULL?
There are no values to add.
Which clause comes right before HAVING?
HAVING filters the groups GROUP BY builds.
Can WHERE and HAVING be used in the same query?
They do different jobs.
What does SELECT status, COUNT(*) FROM orders GROUP BY status HAVING COUNT(*) > 25 return?
Groups that fail the HAVING condition are removed.
Does COUNT(DISTINCT column) ignore NULLs?
Like the other aggregates.
Which aggregates do not make sense on a text column such as a name?
PostgreSQL and SQL Server raise an error; SQLite and MySQL convert the text to a number, usually 0.
Why learn aggregates before window functions?
SUM(...) OVER (...) is the same SUM.
How do you check whether an aggregate query uses an index well?
MIN and MAX on an indexed column, for example, can read just one end of the index.
Aggregate Functions Interview Questions
Q1. What is the difference between COUNT(*), COUNT(column) and COUNT(DISTINCT column)?
COUNT(*) counts rows. COUNT(column) counts rows where the column is not NULL. COUNT(DISTINCT column) counts different non-NULL values. In the Playground orders table they give 200, 103 (shipped_date) and 114 (user_id).
Q2. How do aggregates treat NULL?
All except COUNT(*) ignore NULLs. AVG divides by the number of non-NULL values. Over no rows, COUNT returns 0 and the others return NULL.
Q3. What is the difference between WHERE and HAVING?
WHERE filters rows before grouping and cannot use aggregates. HAVING filters groups after aggregation and usually does.
Q4. Why is averaging averages wrong?
It weights every group equally regardless of size. The average of department averages is not the average salary unless every department is the same size.
Q5. How do you count several categories in one query?
Conditional aggregation: SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END), one expression per category.
Q6. Why does SELECT department, first_name, AVG(salary) ... GROUP BY department fail?
first_name is neither grouped nor aggregated, so there is no single value to show per department. Most databases reject it; SQLite returns an arbitrary name.
Frequently Asked Questions
What are aggregate functions in SQL?
Functions that take many rows and return one summary value: COUNT counts rows, SUM adds values, AVG averages them, and MIN and MAX return the smallest and largest value.
What is COUNT(*) in SQL?
COUNT(*) counts every row the query returns, whether or not any column contains NULL. It is the standard way to answer "how many rows are there?".
What is the difference between COUNT(*) and COUNT(column)?
COUNT(*) counts all rows. COUNT(column) counts only rows where that column is not NULL, so it can be smaller.
Does AVG ignore NULL values?
Yes. AVG, like SUM, MIN, MAX and COUNT(column), ignores NULLs, both in the total and in the number it divides by. Use AVG(COALESCE(col, 0)) if missing values should count as zero.
Can aggregate functions be used without GROUP BY?
Yes. Without GROUP BY the whole result is treated as one group and the query returns a single summary row, for example SELECT COUNT(*) FROM orders.
What is the difference between WHERE and HAVING?
WHERE filters individual rows before grouping and cannot use an aggregate. HAVING filters groups after aggregation, usually with a condition on an aggregate.
Which aggregate function is most commonly used?
COUNT, because counting rows, customers or orders is one of the most common questions asked of data.
Are aggregate functions important in SQL interviews?
Yes. COUNT(*) versus COUNT(column), WHERE versus HAVING, GROUP BY rules and NULL handling are among the most frequently asked topics.
Can you use multiple aggregate functions in one query?
Yes. A single SELECT can combine them, for example SELECT COUNT(*), SUM(salary), AVG(salary), MIN(salary), MAX(salary) FROM employees.
Do aggregate functions work on text columns?
COUNT, MIN and MAX do; MIN and MAX compare text alphabetically. SUM and AVG are for numbers: PostgreSQL and SQL Server raise an error on text, while SQLite and MySQL convert it, usually to 0.