SQL Tutorial › Functions & Grouping › Aggregate Functions

SQL Aggregate Functions

BeginnerLesson 23 of 6911 min read9 queries run on MySQL 8.4

Aggregate functions turn many rows into one value: how many orders, total revenue, average salary, the cheapest product. They are the building blocks of every report and dashboard, and the start of GROUP BY, which applies them per group.

Advertisement
In this lesson
  • Count rows with COUNT(*), COUNT(column) and COUNT(DISTINCT ...)
  • Total and average with SUM and AVG
  • Find extremes with MIN and MAX
  • How aggregates treat NULL, and joining values with GROUP_CONCAT

The five core aggregates

FunctionReturnsSkips NULL?
COUNT(*)number of rowscounts every row
COUNT(col)number of non-NULL valuesyes
SUM(col)totalyes
AVG(col)average (sum ÷ count of non-NULL)yes
MIN(col) / MAX(col)smallest / largest valueyes

Sample data

employees · 12 rows (some columns)
emp_idfirst_namedept_idsalarycommissionhire_date
100MeeraNULL250000NULL2015-01-05
101Asha1125000NULL2018-04-12
102Rahul198000NULL2019-07-01
103Priya172000NULL2021-01-18
104Karan172000NULL2022-08-29
105Vikram2105000120002017-11-20
106Neha25400060002022-03-14
107Arjun24800045002023-06-05
108Kavya388000NULL2020-09-09
109Rohan345000NULL2024-01-22
110Sneha478000NULL2019-02-25
111Amit125000NULL2025-06-02

COUNT

SQL
SELECT COUNT(*)                AS employees,       COUNT(dept_id)          AS with_department,       COUNT(commission)       AS with_commission,       COUNT(DISTINCT dept_id) AS departmentsFROM employees;
ResultMySQL 8.4
employeeswith_departmentwith_commissiondepartments
121134
1 row in set
How this query works, step by step
  1. COUNT(*) counts all 12 rows, NULLs or not.
  2. COUNT(dept_id) skips the CEO, whose dept_id is NULL: 11.
  3. COUNT(commission) counts the 3 sales staff.
  4. COUNT(DISTINCT dept_id) counts different departments: 4.

SUM and AVG

MySQL
SELECT SUM(salary)            AS payroll,       AVG(salary)            AS avg_salary,       ROUND(AVG(salary), 0)  AS avg_roundedFROM employees;
ResultMySQL 8.4
payrollavg_salaryavg_rounded
106000088333.333388333
1 row in set
How this query works, step by step
  1. SUM(salary) adds the 12 salaries: 1,060,000.
  2. AVG(salary) divides that by 12. MySQL returns an exact decimal with four places: 88333.3333.
  3. ROUND(..., 0) rounds to a whole number for display.
Advertisement

MIN and MAX

MIN and MAX work on numbers, text (alphabetical) and dates:

SQL
SELECT MIN(salary)     AS lowest_salary,       MAX(salary)     AS highest_salary,       MIN(hire_date)  AS first_hire,       MAX(hire_date)  AS latest_hire,       MIN(first_name) AS first_alphabeticallyFROM employees;
ResultMySQL 8.4
lowest_salaryhighest_salaryfirst_hirelatest_hirefirst_alphabetically
250002500002015-01-052025-06-02Amit
1 row in set
Note

MAX(salary) tells you the highest salary, not who earns it. To get the whole row, use ORDER BY salary DESC LIMIT 1, a subquery, or a window function.

Aggregates with WHERE

WHERE filters rows before they are aggregated:

SQL
SELECT COUNT(*) AS engineers, SUM(salary) AS engineering_payroll, MAX(salary) AS top_salaryFROM employeesWHERE dept_id = 1;
ResultMySQL 8.4
engineersengineering_payrolltop_salary
5392000125000
1 row in set
How this query works, step by step
  1. WHERE dept_id = 1 keeps the 5 Engineering employees.
  2. The aggregates run on those 5 rows only.

Aggregates on expressions

Total revenue from all order lines, quantity × price:

SQL
SELECT SUM(oi.quantity * p.price) AS revenue,       SUM(oi.quantity)           AS units_sold,       COUNT(DISTINCT oi.order_id) AS ordersFROM order_items oiJOIN products p ON p.product_id = oi.product_id;
ResultMySQL 8.4
revenueunits_soldorders
722623810
1 row in set
How this query works, step by step
  1. The join adds each product’s price to its order lines.
  2. For every line MySQL calculates quantity * price, then SUM adds them all.
  3. COUNT(DISTINCT oi.order_id) counts orders, not lines: 16 lines belong to 10 orders.
Advertisement

Aggregates and NULL

MySQL
SELECT AVG(commission)              AS avg_of_sellers,       AVG(COALESCE(commission, 0)) AS avg_of_everyone,       SUM(commission)              AS totalFROM employees;
ResultMySQL 8.4
avg_of_sellersavg_of_everyonetotal
7500.00001875.000022500
1 row in set
How this query works, step by step
  1. AVG(commission) ignores the 9 NULLs: 22,500 ÷ 3 = 7,500.
  2. COALESCE(commission, 0) turns NULL into 0, so the average is over all 12 rows: 22,500 ÷ 12 = 1,875.
  3. Which answer is right depends on what NULL means in your data.

An aggregate over zero rows returns NULL (except COUNT, which returns 0):

SQL
SELECT COUNT(*) AS n, SUM(salary) AS total, MAX(salary) AS topFROM employeesWHERE dept_id = 5;
ResultMySQL 8.4
ntotaltop
0NULLNULL
1 row in set

GROUP_CONCAT: join values into one string

MySQL’s GROUP_CONCAT collects values into a comma-separated list:

MySQL
SELECT GROUP_CONCAT(first_name ORDER BY first_name SEPARATOR ', ') AS engineering_teamFROM employeesWHERE dept_id = 1;
ResultMySQL 8.4
engineering_team
Amit, Asha, Karan, Priya, Rahul
1 row in set

The standard SQL equivalent is STRING_AGG (PostgreSQL, SQL Server).

Mixing aggregates and plain columns

This query fails in MySQL 8.4, because first_name is not aggregated and there is no GROUP BY. Which of the 12 names should go next to the single maximum?

MySQL
SELECT first_name, MAX(salary)FROM employees;
ResultMySQL 8.4
ERROR 1140 (42000): In aggregated query without GROUP BY, expression #1 of SELECT list contains nonaggregated column 'company.employees.first_name'; this is incompatible with sql_mode=only_full_group_by
Tip

Either aggregate every selected column, or add the plain columns to a GROUP BY. That rule comes from the default ONLY_FULL_GROUP_BY mode.

Advertisement

Exercise: Product statistics

In one row show the number of products, the cheapest price, the most expensive price and the total stock.

Expected resultMySQL 8.4
productscheapestdearestunits_in_stock
919918999780
1 row in set
Hint 1

COUNT(*), MIN(price), MAX(price), SUM(stock).

Exercise: Delivered orders

How many orders have the status ‘Delivered’, and what was the first and last delivered order date?

Expected resultMySQL 8.4
deliveredfirst_datelast_date
72025-01-052025-06-01
1 row in set
Hint 1

Filter with WHERE, then use COUNT, MIN and MAX.

Quick quiz

1. What is the difference between COUNT(*) and COUNT(email)?

2. Values are 10, 20, NULL. What is AVG?

3. What does SUM(salary) return when no rows match?

4. Which function returns the latest date?

Advertisement

Aggregate functions FAQ

What are aggregate functions in SQL?

Functions that compute one value from many rows: COUNT, SUM, AVG, MIN and MAX, plus others such as GROUP_CONCAT in MySQL.

Does COUNT count NULL values?

COUNT(*) counts every row. COUNT(column) counts only non-NULL values of that column.

Can I use aggregate functions in a WHERE clause?

No. WHERE filters individual rows before aggregation. Use HAVING to filter on aggregates, or a subquery.

Why does AVG return so many decimal places in MySQL?

AVG of integers returns an exact DECIMAL with four extra digits. Wrap it in ROUND(AVG(col), 2) to format it.