datarekha

Aggregates and GROUP BY

Counting, summing, averaging — the math that turns rows into answers — plus the WHERE-versus-HAVING distinction every beginner gets wrong.

7 min read Beginner SQL Lesson 4 of 27

What you'll learn

  • The five aggregates you'll use daily: COUNT, SUM, AVG, MIN, MAX
  • The three flavours of COUNT, and when each matters
  • GROUP BY on one column and on several
  • WHERE versus HAVING — what filters rows, what filters groups

Before you start

An aggregate function takes many rows and collapses them into a single number. SELECT AVG(price) FROM products returns one row, however many products there are. That is the whole idea — and GROUP BY is how you say “do that once per group” rather than once for the entire table.

We will work on this product table all lesson:

idnamecategoryprice
1PencilStationery2
2NotebookStationery6
3CoffeeGrocery9
4LaptopElectronics900
5CableElectronics12

The five you reach for

COUNT(*) counts rows, SUM totals, AVG averages, MIN and MAX take the extremes. When every column you select is an aggregate, the engine treats the whole table as one big group and hands back a single row:

SELECT COUNT(*) AS n,
       MIN(price) AS cheapest,
       MAX(price) AS dearest,
       SUM(price) AS catalog_value,
       AVG(price) AS avg_price
FROM   products;
ncheapestdearestcatalog_valueavg_price
52900929185.8

The three flavours of COUNT

This is the question interview panels open with. The three are not interchangeable the moment NULLs are involved. Suppose a tiny orders table where one product is unrecorded:

order_iduser_idproduct_id
174
27NULL
394
SELECT COUNT(*)                AS total_rows,
       COUNT(product_id)       AS known_products,
       COUNT(DISTINCT user_id) AS distinct_buyers
FROM   orders;
total_rowsknown_productsdistinct_buyers
322

COUNT(*) counts every row; COUNT(col) counts only rows where col is not NULL (so it drops order 2); COUNT(DISTINCT col) counts unique non-NULL values (users 7 and 9). Write COUNT(email) on a table where some users have no email and you will quietly under-count — a classic silent bug.

GROUP BY — one result per group

Add GROUP BY and the aggregate runs once for each distinct value of the grouping column:

SELECT category,
       COUNT(*)   AS n,
       AVG(price) AS avg_price
FROM   products
GROUP BY category
ORDER BY avg_price DESC;
categorynavg_price
Electronics2456
Grocery19
Stationery24

The one rule to remember: every column in SELECT must either appear in GROUP BY or sit inside an aggregate — otherwise the engine has many rows for that column and no way to pick one. (You can group by several columns too; each unique combination becomes one row.)

WHERE filters rows; HAVING filters groups

This is where people stumble. WHERE runs before grouping and filters individual rows; HAVING runs after grouping and filters whole groups. And you cannot put an aggregate in WHERE, because at that stage the aggregate has not been computed yet.

SELECT category, AVG(price) AS avg_price
FROM   products
GROUP BY category
HAVING AVG(price) > 10;
categoryavg_price
Electronics456

Only Electronics survives — Grocery (9) and Stationery (4) average below 10. Compare the two clauses as a pair: WHERE price > 10 keeps the pricey rows and then groups them; HAVING AVG(price) > 10 groups everything and then keeps the pricey groups. Different question, different answer. Prefer WHERE when the condition is about a row (it is faster — fewer rows to group); use HAVING only when the condition is about the group.

Practice

Quick check

0/3
Q1What is the difference between COUNT(*) and COUNT(email)?
Q2SELECT country, COUNT(*) FROM users WHERE COUNT(*) > 1 GROUP BY country — why does it fail?
Q3You want revenue per product, but only for products with at least 2 orders. Which structure is right?

Sign in to track your progress

Completed lessons, your XP, level, and streak save to your account — it's free and takes a few seconds.

Practice this in an interview

All questions
How can aggregating after a JOIN produce inflated (double-counted) totals, and how do you fix it?

A JOIN that fans out rows — one-to-many or many-to-many — causes the same source row to appear multiple times in the joined result set. Aggregating on that inflated set multiplies values, giving totals larger than the true sum.

What aggregate functions does SQL provide, and what are the subtle behaviours of MIN/MAX on non-numeric types?

SQL's standard aggregate functions are COUNT, SUM, AVG, MIN, and MAX. MIN and MAX work on any orderable type including strings and dates, using the type's collation or sort order, which surprises analysts who expect numeric-only semantics.

Can you GROUP BY a derived expression or a SELECT alias, and how does this differ across databases?

You can always GROUP BY a derived expression written directly. Whether you can reference a SELECT alias in GROUP BY depends on the database: MySQL and BigQuery allow it, while PostgreSQL and SQL Server do not because aliases are not resolved until after GROUP BY in the logical order.

How does GROUP BY behave with multiple columns, and what does each combination represent?

GROUP BY on multiple columns creates one group per unique combination of those column values. Each row in the result represents a distinct tuple, and every non-aggregated column in SELECT must appear in the GROUP BY list.

Why does SQL require every non-aggregated SELECT column to appear in GROUP BY?

Because after grouping, multiple source rows collapse into one output row. Any column not in the GROUP BY key could have different values across those collapsed rows, making a single deterministic output value impossible without an aggregate function.

What are ROLLUP, CUBE, and GROUPING SETS, and when would you choose each?

All three are extensions to GROUP BY that produce multiple levels of aggregation in a single query. ROLLUP produces hierarchical subtotals, CUBE produces all possible subtotal combinations, and GROUPING SETS lets you specify exactly which grouping combinations you want.

Related lessons

Explore further

Skip to content