DataLens

Aggregate functions (COUNT, SUM, AVG)

Turn many rows into one answer: how many, how much, on average, the smallest and the largest.

Level: Intermediate · 9 min read

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

The idea

Most business questions aren't "show me the rows", they're "give me a number": how many orders did we get, what's our total revenue, what's the average salary? Aggregate functions answer them. They take a whole column of values and squash it into a single value.

Analogy

An aggregate is the status bar at the bottom of Excel when you select a column: Count, Sum, Average. SQL gives you the same numbers, but on millions of rows and inside a query you can save and rerun.

The five you'll use daily

FunctionReturns
COUNT(*)number of rows
SUM(col)total of the values
AVG(col)average (mean) of the values
MIN(col)smallest value
MAX(col)largest value
SELECT COUNT(*) AS products,
       MIN(price) AS cheapest,
       MAX(price) AS priciest,
       AVG(price) AS avg_price
FROM products;
 products | cheapest | priciest | avg_price
----------+----------+----------+---------------------
       15 |    38000 |   320000 | 131066.666666666667
(1 row)

Fifteen rows went in, one row came out. That's what aggregates do.

The average has a lot of decimals. ROUND fixes that, as you saw in the expressions module:

SELECT ROUND(AVG(price)) AS avg_price
FROM products;
 avg_price
-----------
    131067
(1 row)

MIN and MAX work on text (alphabetical) and dates (earliest/latest) too:

SELECT MIN(order_date) AS first_order,
       MAX(order_date) AS last_order
FROM orders;
 first_order | last_order
-------------+------------
 2024-01-03  | 2024-06-29
(1 row)

Aggregating a calculation

The revenue of an order line is quantity * unit_price. Put the calculation inside SUM, and it's computed per row, then added up:

SELECT SUM(quantity) AS units_sold,
       SUM(quantity * unit_price) AS revenue
FROM order_items;
 units_sold | revenue
------------+----------
        117 | 17502000
(1 row)

Aggregates with WHERE

Usually you don't want to count everything. "How many orders did we get?" is less useful than "how many orders actually went through?", because cancelled orders didn't bring in any money. You can combine an aggregate with WHERE. The WHERE runs first, throwing away the rows that don't match, and the aggregate only sees what's left:

SELECT COUNT(*) AS completed_orders
FROM orders
WHERE status = 'completed';
 completed_orders
------------------
               26
(1 row)

The three COUNTs

"How many customers do we have?", "how many have given us an email?" and "how many cities are they in?" all sound like counting questions, and they are, but they each need a slightly different COUNT. Mixing them up is a classic mistake, so here are all three side by side:

SELECT COUNT(*) AS customers,
       COUNT(email) AS with_email,
       COUNT(DISTINCT city) AS cities
FROM customers;
 customers | with_email | cities
-----------+------------+--------
        12 |         10 |      8
(1 row)
Key idea

All aggregates except COUNT(*) ignore NULLs. That's usually what you want, but be aware of it: AVG(stock) is the average of the 14 known stock values, not of all 15 products.

What you can't do (yet)

Try to show a product name next to the highest price:

SELECT name, MAX(price)
FROM products;
ERROR: column "products.name" must appear in the GROUP BY clause or be used in an aggregate function

It sounds reasonable, but think about it from the database's side: MAX(price) squashes 15 rows into 1, while name still has 15 values. Which name should go on that single row? SQL refuses to guess. The error mentions GROUP BY, which is the next module: it lets you aggregate per group, such as the highest price per category. (To find which product is the most expensive, you already know a simpler way: ORDER BY price DESC LIMIT 1.)

Empty input

When no rows match, COUNT returns 0, but SUM, AVG, MIN and MAX return NULL, because there's nothing to add up:

SELECT COUNT(*) AS n, SUM(salary) AS total
FROM employees
WHERE department = 'Legal';
 n | total
---+-------
 0 |  NULL
(1 row)

Remember this when a dashboard shows an empty cell instead of 0: there were simply no rows.

Why this matters for clean code

Doing the maths in the database means one query, one number, the same for everyone. Downloading every row to Excel to add them up is slow and invites copy-paste mistakes. Name every aggregate (AS revenue, AS completed_orders), and pick the right COUNT deliberately, so a reader knows exactly what's being counted.

Recap

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

Next: GROUP BY and HAVING →