Chapter 5 of 18

Aggregate Functions

Calculate counts, sums, averages, and more across rows of data using COUNT, SUM, AVG, MIN, MAX, and related functions.

Meritshot7 min read
SQLAggregateCOUNTSUMAVGMINMAX
All SQL Chapters

What Are Aggregate Functions?

Aggregate functions collapse multiple rows into a single value. Instead of returning one row per employee, they return one number that summarises all (or a filtered set of) employees.

-- Without aggregate — returns 8 rows
SELECT salary FROM employees;

-- With aggregate — returns 1 row
SELECT AVG(salary) FROM employees;

The five core aggregate functions:

FunctionWhat it computes
COUNT()Number of rows / non-NULL values
SUM()Total of numeric values
AVG()Arithmetic mean
MIN()Smallest value
MAX()Largest value

Sample Data

CREATE TABLE employees (
    id         INTEGER PRIMARY KEY,
    name       TEXT    NOT NULL,
    department TEXT    NOT NULL,
    salary     REAL    NOT NULL,
    hire_date  TEXT    NOT NULL,
    bonus      REAL    -- nullable
);

INSERT INTO employees VALUES
(1, 'Priya Sharma',  'Finance',    75000, '2022-03-15', 8000),
(2, 'Raj Patel',     'Technology', 92000, '2021-07-01', 12000),
(3, 'Meera Singh',   'Marketing',  68000, '2023-01-10', NULL),
(4, 'Arjun Nair',    'Finance',    81000, '2020-11-22', 9500),
(5, 'Sunita Rao',    'Technology', 88000, '2022-09-05', 11000),
(6, 'Dev Kumar',     'Marketing',  72000, '2021-04-18', 5000),
(7, 'Kiran Mehta',   'Finance',    95000, '2019-06-30', 15000),
(8, 'Ananya Das',    'Technology', 63000, '2024-02-14', NULL);

COUNT

COUNT counts rows. It comes in three forms:

-- COUNT(*) — counts all rows, including those with NULLs
SELECT COUNT(*) AS total_employees FROM employees;
-- Result: 8

-- COUNT(column) — counts non-NULL values in that column
SELECT COUNT(bonus) AS employees_with_bonus FROM employees;
-- Result: 6 (Meera and Ananya have NULL bonus)

-- COUNT(DISTINCT column) — counts unique non-NULL values
SELECT COUNT(DISTINCT department) AS num_departments FROM employees;
-- Result: 3

When to Use Each Form

FormUse When
COUNT(*)Counting total rows (including NULLs)
COUNT(col)Counting how many rows have a value
COUNT(DISTINCT col)Counting unique values

SUM

SUM adds up all numeric values in a column. It ignores NULL values:

-- Total salary bill
SELECT SUM(salary) AS total_salary_bill FROM employees;
-- Result: 634000

-- Total bonuses paid (NULLs excluded automatically)
SELECT SUM(bonus) AS total_bonuses FROM employees;
-- Result: 60500 (only 6 employees have bonuses)

-- Total compensation (salary + bonus, treating NULL bonus as 0)
SELECT SUM(salary + COALESCE(bonus, 0)) AS total_compensation
FROM employees;

AVG

AVG computes the arithmetic mean. It also ignores NULLs:

-- Average salary
SELECT ROUND(AVG(salary), 2) AS avg_salary FROM employees;
-- Result: 79250.00

-- Average bonus — ONLY of those who have one
SELECT ROUND(AVG(bonus), 2) AS avg_bonus FROM employees;
-- Result: 10083.33  (6 employees, not 8)

-- Average including non-bonus employees (treat NULL as 0)
SELECT ROUND(AVG(COALESCE(bonus, 0)), 2) AS avg_bonus_all
FROM employees;
-- Result: 7562.50  (60500 / 8)

Key distinction: AVG(bonus) computes the average over 6 rows (ignoring NULLs). AVG(COALESCE(bonus, 0)) computes over all 8 rows. Choose based on your business question.

MIN and MAX

MIN and MAX find the smallest and largest values. They work on numbers, text (alphabetical), and dates:

-- Salary range
SELECT
    MIN(salary) AS lowest_salary,
    MAX(salary) AS highest_salary,
    MAX(salary) - MIN(salary) AS salary_range
FROM employees;

Result:

lowest_salary | highest_salary | salary_range
--------------+----------------+-------------
63000         | 95000          | 32000
-- Earliest and latest hire dates
SELECT
    MIN(hire_date) AS oldest_hire,
    MAX(hire_date) AS newest_hire
FROM employees;

-- Alphabetically first and last employee name
SELECT
    MIN(name) AS first_alpha,
    MAX(name) AS last_alpha
FROM employees;

Multiple Aggregates in One Query

You can use multiple aggregate functions in a single SELECT:

SELECT
    COUNT(*)                       AS headcount,
    COUNT(DISTINCT department)     AS departments,
    SUM(salary)                    AS payroll,
    ROUND(AVG(salary), 0)          AS avg_salary,
    MIN(salary)                    AS min_salary,
    MAX(salary)                    AS max_salary,
    MAX(salary) - MIN(salary)      AS range
FROM employees;

Result:

headcount | departments | payroll | avg_salary | min_salary | max_salary | range
----------+-------------+---------+------------+------------+------------+-------
8         | 3           | 634000  | 79250      | 63000      | 95000      | 32000

Aggregates with WHERE

WHERE filters rows before aggregation:

-- Stats for Finance department only
SELECT
    COUNT(*)          AS headcount,
    SUM(salary)       AS payroll,
    ROUND(AVG(salary), 0) AS avg_salary
FROM employees
WHERE department = 'Finance';
headcount | payroll | avg_salary
----------+---------+-----------
3         | 251000  | 83667
-- Stats for employees hired after 2021
SELECT
    COUNT(*)          AS headcount,
    ROUND(AVG(salary), 0) AS avg_salary
FROM employees
WHERE hire_date > '2021-12-31';

Aggregate Functions and NULL

All aggregate functions (except COUNT(*)) ignore NULL values:

-- Only 6 employees have bonuses; SUM, AVG, MIN, MAX ignore the 2 NULLs
SELECT
    COUNT(*)     AS total_rows,      -- 8
    COUNT(bonus) AS has_bonus,       -- 6
    SUM(bonus)   AS sum_bonus,       -- 60500 (6 employees)
    AVG(bonus)   AS avg_bonus        -- 10083.33 (60500 / 6)
FROM employees;

This is often the right behaviour — but always be clear about whether NULLs should be treated as zero or excluded.

String Aggregation

Some databases let you concatenate strings across rows:

-- PostgreSQL / SQLite 3.44+
SELECT STRING_AGG(name, ', ' ORDER BY name) AS all_employees
FROM employees;

-- MySQL
SELECT GROUP_CONCAT(name ORDER BY name SEPARATOR ', ') AS all_employees
FROM employees;

Result: Ananya Das, Arjun Nair, Dev Kumar, Kiran Mehta, Meera Singh, Priya Sharma, Raj Patel, Sunita Rao

Practical Examples

Example 1: Company-Wide Compensation Summary

SELECT
    COUNT(*)                           AS total_employees,
    ROUND(SUM(salary), 0)              AS total_payroll,
    ROUND(AVG(salary), 0)              AS avg_salary,
    MIN(salary)                        AS min_salary,
    MAX(salary)                        AS max_salary,
    ROUND(SUM(COALESCE(bonus, 0)), 0)  AS total_bonuses
FROM employees;

Example 2: Technology Department Metrics

SELECT
    COUNT(*)                 AS headcount,
    ROUND(AVG(salary), 0)   AS avg_salary,
    MIN(hire_date)           AS first_hire,
    MAX(hire_date)           AS latest_hire
FROM employees
WHERE department = 'Technology';

Example 3: Above-Average Earners Count

-- Count employees earning above the average salary
SELECT COUNT(*) AS above_avg_earners
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);

Example 4: Bonus Statistics

SELECT
    COUNT(bonus)                           AS employees_with_bonus,
    COUNT(*) - COUNT(bonus)                AS employees_without_bonus,
    ROUND(AVG(bonus), 0)                   AS avg_bonus_for_recipients,
    ROUND(AVG(COALESCE(bonus, 0)), 0)      AS avg_bonus_all_employees,
    MAX(bonus)                             AS highest_bonus
FROM employees;

Common Mistakes

1. Mixing aggregate and non-aggregate columns

-- Wrong — name is not in GROUP BY and not aggregated
SELECT name, AVG(salary)
FROM employees;
-- Error in PostgreSQL/MySQL (SQLite may return an arbitrary name)

-- Correct — either group by name or use only aggregates
SELECT AVG(salary) FROM employees;
-- OR
SELECT name, salary FROM employees;

2. Forgetting that AVG ignores NULLs

-- If you want to include NULLs as 0:
SELECT AVG(COALESCE(bonus, 0)) FROM employees;

-- If you want to count only non-NULL values:
SELECT AVG(bonus) FROM employees;  -- already ignores NULLs

3. Using WHERE to filter aggregates

-- Wrong — cannot filter on aggregate result with WHERE
SELECT department, AVG(salary)
FROM employees
WHERE AVG(salary) > 80000;  -- Error!

-- Correct — use HAVING (covered in the next chapter)
SELECT department, AVG(salary)
FROM employees
GROUP BY department
HAVING AVG(salary) > 80000;

Practice Exercises

  1. Find the total payroll cost for the Marketing department.
  2. Calculate the average, minimum, and maximum salary for the entire company.
  3. Count how many employees were hired after 2022.
  4. Find the highest salary among employees who have a bonus (bonus IS NOT NULL).
  5. What is the difference between COUNT(*) and COUNT(bonus) for the sample data? Calculate both.

Summary

In this chapter you learned:

  • COUNT(*) — counts all rows; COUNT(col) — counts non-NULL values
  • SUM(col) — total; ignores NULLs
  • AVG(col) — mean; ignores NULLs (denominator = non-NULL row count)
  • MIN(col) / MAX(col) — extremes; work on numbers, text, dates
  • All aggregate functions except COUNT(*) ignore NULL values
  • Combine multiple aggregates in one SELECT
  • Use WHERE to filter rows before aggregation
  • Use COALESCE(col, 0) when you want NULLs treated as zero
  • Cannot mix non-aggregated columns with aggregates without GROUP BY
  • Cannot filter on aggregate results with WHERE — use HAVING instead

Next up: GROUP BY & HAVING — run aggregate functions on groups of rows and filter those groups.