Skip to content

Lesson 4: Aggregate Functions

In Lesson 3, we sorted results with ORDER BY and LIMIT to get the top N rows. This time, we learn aggregate functions that summarize multiple rows into a single number, such as "how many total customers are there" or "what is the average order amount."

Already familiar?

If you already know COUNT, COUNT(DISTINCT), SUM, AVG, MIN, and MAX, skip ahead to Lesson 5: GROUP BY.

flowchart LR T["Multiple\nRows"] --> A["Aggregate\nCOUNT / SUM\nAVG / MIN / MAX"] --> R["Single\nValue"]

Concept: Aggregate functions summarize multiple rows into a single value.

COUNT

COUNT(*) counts the total number of rows in the result. COUNT(column) counts the number of non-NULL values in that column.

-- Total number of customers
SELECT COUNT(*) AS total_customers
FROM customers;

Result:

total_customers
5230
-- Compare customer counts by birth date registration
SELECT
    COUNT(*)           AS total_customers,
    COUNT(birth_date)  AS with_birth_date,
    COUNT(*) - COUNT(birth_date) AS missing_birth_date
FROM customers;

Result:

total_customers with_birth_date missing_birth_date
5230 4492 738

COUNT(DISTINCT) -- Unique Value Count

COUNT(DISTINCT column) counts the number of values after removing duplicates. Use it when you want to know "how many distinct types exist."

-- Number of unique customers who have placed orders
SELECT
    COUNT(*)                    AS total_orders,
    COUNT(DISTINCT customer_id) AS unique_customers
FROM orders;

Result:

total_orders unique_customers
37557 2839

There are 34,908 orders, but only 4,985 unique customers have placed orders, since one customer can place multiple orders.

-- Number of brands TechShop carries
SELECT COUNT(DISTINCT brand) AS brand_count
FROM products;

SUM

SUM calculates the total of a numeric column. NULL values are ignored.

-- Total revenue from completed orders
SELECT SUM(total_amount) AS total_revenue
FROM orders
WHERE status IN ('delivered', 'confirmed');

Result:

total_revenue
34582358414.0
-- Total points held by active customers
SELECT SUM(point_balance) AS total_points_outstanding
FROM customers
WHERE is_active = 1;

Result:

total_points_outstanding
337459019

AVG

AVG returns the arithmetic mean, excluding NULL values from the calculation.

-- Average price and average stock of active products
SELECT
    AVG(price)     AS avg_price,
    AVG(stock_qty) AS avg_stock
FROM products
WHERE is_active = 1;

Result:

avg_price avg_stock
659594.495412844 272.408256880734
-- Average order amount excluding cancellations and returns
SELECT AVG(total_amount) AS avg_order_value
FROM orders
WHERE status NOT IN ('cancelled', 'returned');

Result:

avg_order_value
1010677.9067462009

ROUND -- Rounding

AVG results can have many decimal places. Use ROUND(value, digits) to round to the desired number of decimal places.

-- Average review rating rounded to 1 decimal place
SELECT
    AVG(rating)          AS avg_raw,
    ROUND(AVG(rating), 1) AS avg_rounded
FROM reviews;

Result:

avg_raw avg_rounded
3.904984788205008 3.9
-- Average product price rounded to the nearest whole number
SELECT ROUND(AVG(price), 0) AS avg_price
FROM products;
avg_price
665405

Integer Division Caution

In SQLite, integer / integer = integer. The decimal part is truncated:

-- Problem: trying to calculate the review rate...
SELECT
    COUNT(*)                    AS total_orders,
    (SELECT COUNT(*) FROM reviews) AS total_reviews,
    (SELECT COUNT(*) FROM reviews) / COUNT(*) AS review_rate
FROM orders;
total_orders total_reviews review_rate
34908 7945 0

7945 / 34908 = 0.2275... but because of integer division, it becomes 0. The fix:

-- Fix: multiply by a float (1.0)
SELECT ROUND((SELECT COUNT(*) FROM reviews) * 1.0 / COUNT(*) * 100, 1) AS review_rate_pct
FROM orders;
review_rate_pct
22.8

What about MySQL/PostgreSQL?

MySQL and PostgreSQL preserve decimal places even in integer division, so this issue does not occur. This is a SQLite-specific caveat.


MIN and MAX

MIN and MAX find the smallest and largest values in a column.

-- Lowest and highest prices among active products
SELECT
    MIN(price) AS cheapest,
    MAX(price) AS most_expensive
FROM products
WHERE is_active = 1;

Result:

cheapest most_expensive
18500.0 5481100.0
-- First and most recent order dates
SELECT
    MIN(ordered_at) AS first_order,
    MAX(ordered_at) AS latest_order
FROM orders;

Result:

first_order latest_order
2016-01-09 10:20:06 2025-12-31 22:25:39

Using Multiple Aggregate Functions Together

You can use multiple aggregate functions in a single SELECT.

-- TechShop review statistics summary
SELECT
    COUNT(*)                    AS total_reviews,
    AVG(rating)                 AS avg_rating,
    MIN(rating)                 AS lowest_rating,
    MAX(rating)                 AS highest_rating,
    SUM(CASE WHEN rating = 5 THEN 1 ELSE 0 END) AS five_star_count
FROM reviews;

Result:

total_reviews avg_rating lowest_rating highest_rating five_star_count
8546 3.904984788205008 1 5 3433

Aggregate Functions and NULL

Aggregate functions silently ignore NULL values. This is an important behavior:

-- About 15% of customers have NULL birth_date
SELECT
    COUNT(*)           AS total,
    COUNT(birth_date)  AS with_birth,
    AVG(CASE
        WHEN birth_date IS NOT NULL
        THEN 2025 - CAST(SUBSTR(birth_date, 1, 4) AS INTEGER)
    END) AS avg_age
FROM customers;
total with_birth avg_age
5230 4450 39.2
  • COUNT(*) = 5,230 (all rows including NULL)
  • COUNT(birth_date) = 4,450 (excluding NULL)
  • AVG = calculated only for the 4,450 with data (780 with NULL are excluded)

NULL can skew results

AVG(age based on birth_date) is the average only for people who entered their birth date. It may differ from the actual average age of all customers. When aggregating columns with many NULLs, always compare COUNT(*) and COUNT(column) to check the NULL ratio.


Summary

Function Description Example
COUNT(*) Total row count (includes NULL) SELECT COUNT(*) FROM orders
COUNT(column) Row count excluding NULL COUNT(birth_date)
COUNT(DISTINCT column) Unique value count COUNT(DISTINCT customer_id)
SUM(column) Sum (ignores NULL) SUM(total_amount)
AVG(column) Average (ignores NULL) AVG(price)
ROUND(value, N) Round to N decimal places ROUND(AVG(price), 0)
MIN(column) Minimum value MIN(price)
MAX(column) Maximum value MAX(price)
Integer division SQLite: integer / integer = integer Multiply by * 1.0 to convert to float
NULL handling Aggregate functions ignore NULL Compare COUNT(*) and COUNT(column)

Lesson Review Problems

These are simple problems to immediately check the concepts learned in this lesson. For comprehensive practice combining multiple concepts, see the Practice Problems section.

Practice Problems

Problem 1

Calculate the average review rating from the reviews table, rounded to 2 decimal places. Use the alias avg_rating.

Answer
SELECT ROUND(AVG(rating), 2) AS avg_rating
FROM reviews;

Result (example):

avg_rating
3.9

Problem 2

Calculate the total revenue (sum of total_amount) from completed orders (status is 'delivered' or 'confirmed') in the orders table. Use the alias total_revenue.

Answer
SELECT SUM(total_amount) AS total_revenue
FROM orders
WHERE status IN ('delivered', 'confirmed');

Result (example):

total_revenue
34582358414.0

Problem 3

From the customers table, find the total number of customers and the number of customers with a registered birth_date. Use the aliases total_customers and with_birth_date.

Answer
SELECT
    COUNT(*)          AS total_customers,
    COUNT(birth_date) AS with_birth_date
FROM customers;

Result (example):

total_customers with_birth_date
5230 4492

Problem 4

Count the number of currently active products at TechShop and calculate their total inventory value (sum of price * stock_qty).

Answer
SELECT
    COUNT(*)                AS active_product_count,
    SUM(price * stock_qty)  AS total_inventory_value
FROM products
WHERE is_active = 1;

Result (example):

active_product_count total_inventory_value
218 39328835500.0

Problem 5

Calculate the average, minimum, and maximum total_amount for orders that were not cancelled or returned. Use the aliases avg_order, min_order, and max_order.

Answer
SELECT
    AVG(total_amount) AS avg_order,
    MIN(total_amount) AS min_order,
    MAX(total_amount) AS max_order
FROM orders
WHERE status NOT IN ('cancelled', 'returned', 'return_requested');

Result (example):

avg_order min_order max_order
1002058.562654908 16876.0 50867500.0

Problem 6

From the products table, calculate the average price (avg_price, 0 decimal places), average cost (avg_cost, 0 decimal places), and average margin rate (avg_margin_pct, 1 decimal place) of active products in a single query. Margin rate = (price - cost_price) / price * 100, and you should compute the average of each product's margin rate.

Answer
SELECT
    ROUND(AVG(price), 0)                          AS avg_price,
    ROUND(AVG(cost_price), 0)                           AS avg_cost,
    ROUND(AVG((price - cost_price) / price * 100), 1)   AS avg_margin_pct
FROM products
WHERE is_active = 1;

Result (example):

avg_price avg_cost avg_margin_pct
659594.0 504305.0 23.9

Problem 7

From the products table, find the minimum price, maximum price, and price range for active products (is_active = 1). Use the aliases min_price, max_price, and price_range.

Answer
SELECT
    MIN(price)             AS min_price,
    MAX(price)             AS max_price,
    MAX(price) - MIN(price) AS price_range
FROM products
WHERE is_active = 1;

Result (example):

min_price max_price price_range
18500.0 5481100.0 5462600.0

Problem 8

From the order_items table, find the total row count, total quantity sum (quantity), average unit price (unit_price, 2 decimal places), and maximum quantity. Use the aliases total_items, total_qty, avg_unit_price, and max_qty.

Answer
SELECT
    COUNT(*)                    AS total_items,
    SUM(quantity)               AS total_qty,
    ROUND(AVG(unit_price), 2)   AS avg_unit_price,
    MAX(quantity)               AS max_qty
FROM order_items;

Result (example):

total_items total_qty avg_unit_price max_qty
91104 101162 394885.14 10

Problem 9

From the payments table, find the count, total amount, average amount (0 decimal places), and minimum/maximum amounts for completed payments (status = 'completed') in a single query. Use the aliases payment_count, total_amount, avg_amount, min_amount, and max_amount.

Answer
SELECT
    COUNT(*)              AS payment_count,
    SUM(amount)           AS total_amount,
    ROUND(AVG(amount), 0) AS avg_amount,
    MIN(amount)           AS min_amount,
    MAX(amount)           AS max_amount
FROM payments
WHERE status = 'completed';

Result (example):

payment_count total_amount avg_amount min_amount max_amount
34616 34682197764.0 1001912.0 16876.0 50867500.0

Problem 10

How many orders have a shipping note (notes), and what percentage of total orders is that? Return orders_with_notes, total_orders, and pct_with_notes (1 decimal place).

Answer
SELECT
    COUNT(CASE WHEN notes IS NOT NULL THEN 1 END)  AS orders_with_notes,
    COUNT(*)                                        AS total_orders,
    ROUND(
        100.0 * COUNT(CASE WHEN notes IS NOT NULL THEN 1 END) / COUNT(*),
        1
    ) AS pct_with_notes
FROM orders;

Result (example):

orders_with_notes total_orders pct_with_notes
13219 37557 35.2

Scoring Guide

Score Next Step
9-10 Move to Lesson 5: GROUP BY
7-8 Review the explanations for incorrect answers, then proceed to Lesson 5
5 or fewer Read this lesson again
3 or fewer Start over from Lesson 3: Sorting and Pagination

Problem Areas:

Area Problems
AVG / ROUND 1, 6
SUM 2, 4
COUNT / COUNT(col) 3
MIN / MAX 5, 7
Combined aggregates (COUNT, SUM, AVG, MAX) 8, 9
CASE + COUNT (NULL ratio) 10

Next: Lesson 5: GROUP BY and HAVING