SQLab Hub
Lesson 23 · Aggregate Functions
Lesson 23 · Functions

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.

COUNT · SUM · AVG · MAX · MIN NULL handling ▶ Runnable Playground examples 🧪 6 practice exercises 📝 25-question quiz ⏱ ~25 min

What Are Aggregate Functions?

One query, five summaries of the 120 employees' salaries:

SQLite · runs in the Playground
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

Five aggregates over all employees
employeestotal_salaryavg_salarylowesthighest
1201492450112437146044204449
✓ 1 row · real output from the Playground database
FunctionReturnsWorks on
COUNT(*)Number of rowsAny rows
COUNT(column)Number of non-NULL valuesAny type
SUM(column)TotalNumbers
AVG(column)Average (mean)Numbers
MIN(column) / MAX(column)Smallest / largest valueNumbers, 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:

SQLite · runs in the Playground
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

Three kinds of COUNT on orders
all_ordersorders_with_shipped_datedistinct_customersdistinct_statuses
2001031146
✓ 1 row · real output from the Playground database
FormCountsHere
COUNT(*)Every row200
COUNT(shipped_date)Rows where shipped_date is not NULL103
COUNT(DISTINCT user_id)Different non-NULL values114 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:

SQLite · runs in the Playground
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

Revenue and average order value of delivered orders
delivered_ordersrevenueavg_order_value
82210776.342570.44
✓ 1 row · real output from the Playground database

WHERE runs before the aggregates, so only the delivered orders are added up. Rounding with ROUND keeps long decimals out of reports.

Integer averages. In SQL Server, AVG of an integer column returns an integer, so the average of 1 and 2 is 1. Cast first: 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:

SQLite · runs in the Playground
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 and latest order, smallest and largest total
first_orderlatest_ordersmallest_orderlargest_order
2022-01-062024-12-2429.174997.15
✓ 1 row · real output from the Playground database
SQLite · runs in the Playground
SELECT MIN(name) AS first_alphabetically,
       MAX(name) AS last_alphabetically
FROM products;

▶ Run this query in the SQL Playground

MIN and MAX on product names
first_alphabeticallylast_alphabetically
2-Drawer Filing CabinetZero to One (Thiel)
✓ 1 row · real output from the Playground database

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:

SQLite · runs in the Playground
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

Aggregates over a column with NULLs
peoplebonuses_recordedtotalavg_recordedavg_per_person
53230007666.674600
✓ 1 row · real output from the Playground database

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:

SQLite · runs 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

Aggregates when no row matches
ordersrevenuelargestrevenue_or_zero
0NULLNULL0
✓ 1 row · real output from the Playground database

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:

SQLite · runs in the Playground
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

Headcount and salary per department
departmentemployeesavg_salarytop_salary
Customer Success15134140203999
Design10133965194543
Engineering11131028199029
Operations13126879203467
HR12124151191037
Marketing13124111204449
Finance10121471196032
Product9119153188273
Sales11116893195759
Legal16112866192220
✓ 10 rows · real output from the Playground database

Filter rows with WHERE before grouping, and filter groups with HAVING after it. Departments with at least 12 employees:

SQLite · runs in the Playground
SELECT department, COUNT(*) AS employees
FROM employees
GROUP BY department
HAVING COUNT(*) >= 12
ORDER BY employees DESC;

▶ Run this query in the SQL Playground

Departments with 12 or more employees
departmentemployees
Legal16
Customer Success15
Operations13
Marketing13
HR12
✓ 5 rows · real output from the Playground database

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:

SQLite · runs in the Playground
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

Several conditional totals in one pass
ordersdeliveredlostdelivered_revenue
2008246210776.34
✓ 1 row · real output from the Playground database

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:

SQLite · runs in the Playground
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

First names per department in one string
departmentfirst_names
DesignJason, Amy, Paul, Ashley, Ashley, Joshua, Maria, Michael, Robert, Sandra
ProductSandra, Susan, Barbara, Steven, Joshua, Matthew, Kevin, Donald, Michael
✓ 2 rows · real output from the Playground database

Differences Between Databases

BehaviourSQLitePostgreSQLMySQLSQL Server
AVG of integersDecimalDecimalDecimalInteger (cast first)
SUM of textConverts, often 0ErrorConverts, often 0Error
Non-grouped column in SELECTAllowed, picks a rowErrorError with ONLY_FULL_GROUP_BY (default)Error
Join stringsGROUP_CONCATSTRING_AGGGROUP_CONCATSTRING_AGG
Filtered aggregateFILTER (WHERE ...)FILTER (WHERE ...)CASE onlyCASE only

Common Aggregate Mistakes

Six errors and how to avoid them.

1. An aggregate in WHERE

❌ Error
SQL
SELECT department
FROM employees
WHERE AVG(salary) > 130000
GROUP BY department;
✅ HAVING or a subquery
SQL
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(*)

❌ Skips NULLs
SQL
SELECT COUNT(shipped_date) AS orders
FROM orders;   -- 103
✅ Counts every row
SQL
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

❌ Error in most databases
SQL
SELECT department, first_name, AVG(salary)
FROM employees
GROUP BY department;
✅ Group by it or aggregate it
SQL
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

❌ NULL on an empty period
SQL
SELECT SUM(total) FROM orders
WHERE order_date >= '2025-01-01';
✅ COALESCE to 0
SQL
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

❌ Gives small groups too much weight
SQL
SELECT AVG(dept_avg)
FROM (SELECT AVG(salary) AS dept_avg
      FROM employees GROUP BY department) t;
✅ Average the rows
SQL
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

❌ Counts rows
SQL
SELECT DISTINCT COUNT(user_id)
FROM orders;   -- 200
✅ Counts unique values
SQL
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

QuestionExpression
How many rows?COUNT(*)
How many have a value?COUNT(col)
How many different values?COUNT(DISTINCT col)
Total / averageSUM(col) / AVG(col)
Earliest / latest / smallest / largestMIN(col) / MAX(col)
Zero instead of NULLCOALESCE(SUM(col), 0)
Count matching rowsSUM(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.

  1. 1Easy Find the number of products, the average price and the price of the cheapest and most expensive product. Table: products

    Show solution
    SQLite · runs in the Playground
    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.

  2. 2Easy How many users are there, and how many different cities and countries do they live in? Table: users

    Show solution
    SQLite · runs in the Playground
    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.

  3. 3Medium For each rating from 1 to 5, show the number of reviews and the average helpful votes. Table: reviews

    Show solution
    SQLite · runs in the Playground
    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.

  4. 4Medium Show the units sold and revenue per product category, highest revenue first. Table: order_items, products

    Show solution
    SQLite · runs in the Playground
    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.

  5. 5Medium Show the departments whose lowest salary is above 50,000. Table: employees

    Show solution
    SQLite · runs in the Playground
    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.

  6. 6Hard For each payment method, show the number of payments, the total amount and the share of payments that succeeded. Table: payments

    Show solution
    SQLite · runs in the Playground
    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

Question 1 of 25

What do aggregate functions do?

Question 2 of 25

What does COUNT(*) count?

Question 3 of 25

What does COUNT(column) count?

Question 4 of 25

What does SELECT COUNT(DISTINCT department_id) FROM employees answer?

Question 5 of 25

What does SUM(total) return when no rows match?

Question 6 of 25

Does AVG include NULL values?

Question 7 of 25

Can MAX and MIN be used on non-numeric columns?

Question 8 of 25

Can aggregate functions be used without GROUP BY?

Question 9 of 25

What does SELECT department, AVG(salary) FROM employees GROUP BY department return?

Question 10 of 25

What is HAVING for?

Question 11 of 25

Why can WHERE not filter on AVG(salary) > 50000?

Question 12 of 25

Which runs first, WHERE or HAVING?

Question 13 of 25

How many rows does SELECT COUNT(*), SUM(salary), AVG(salary), MAX(salary), MIN(salary) FROM employees return?

Question 14 of 25

If one employee has a NULL salary, how do COUNT(salary) and COUNT(*) compare?

Question 15 of 25

What does MAX(order_date) usually answer?

Question 16 of 25

What does MIN(order_date) usually answer?

Question 17 of 25

Which aggregate is used most often?

Question 18 of 25

What does SUM return over a column that is entirely NULL?

Question 19 of 25

Which clause comes right before HAVING?

Question 20 of 25

Can WHERE and HAVING be used in the same query?

Question 21 of 25

What does SELECT status, COUNT(*) FROM orders GROUP BY status HAVING COUNT(*) > 25 return?

Question 22 of 25

Does COUNT(DISTINCT column) ignore NULLs?

Question 23 of 25

Which aggregates do not make sense on a text column such as a name?

Question 24 of 25

Why learn aggregates before window functions?

Question 25 of 25

How do you check whether an aggregate query uses an index well?

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.