COUNT, SUM, AVG, GROUP BY, HAVING — the mental model that confuses beginners, explained from scratch.
F-4 — Aggregation — Summarising Your Data
Who this module is for: You completed F-3 and can filter and sort individual rows. Now you need to answer questions about groups of rows — "how many products are in each category?", "what is the average price?", "which categories have more than 5 products?". This module covers aggregation — the most conceptually tricky part of SQL for beginners.
The Problem Aggregation Solves
So far, every query has returned rows from the table — one row of output per row of input (or fewer after filtering). Aggregation is different: it collapses multiple rows into a single summary value.
Aggregate Functions
These functions take a column of values and return a single summary value:
When you use an aggregate function without GROUP BY, it collapses the entire table into a single row:
GROUP BY — The Mental Model
GROUP BY is where most beginners get confused. The mental model:
- PostgreSQL divides all your rows into groups based on the column(s) you specify
- Each group is collapsed into a single output row
- You can then apply aggregate functions to each group separately
Visualise what happens:
The Golden Rule of GROUP BY
Every column in your SELECT list must either be in the GROUP BY clause OR be wrapped in an aggregate function.
This error trips up beginners constantly. The reason: if you group by category, and there are 3 rows in the "peripherals" group, which name value should PostgreSQL return? There are three of them. It cannot pick one arbitrarily — so it forces you to either group by name too (making each row unique) or use an aggregate.
Multiple columns in GROUP BY
When you GROUP BY multiple columns, each unique combination of those columns becomes one output row.
Common Aggregation Patterns
HAVING — Filtering Groups
WHERE filters individual rows before grouping. HAVING filters groups after aggregation.
The difference between WHERE and HAVING:
The Execution Order
Understanding the order PostgreSQL executes a query helps predict how WHERE vs HAVING behaves:
This is why you cannot use a column alias from SELECT in a WHERE clause — WHERE runs before SELECT:
COUNT(*) vs COUNT(column) vs COUNT(DISTINCT column)
These three are different:
COALESCE and NULLIF in Aggregations
NULL values are silently ignored by aggregate functions — but this can surprise you:
Practical Exercise: Sales Analysis
Create a sales table and practice aggregation:
Now answer these questions:
Summary
| Concept | What it does |
|---|---|
COUNT(*) | Count all rows |
COUNT(col) | Count non-NULL values in column |
COUNT(DISTINCT col) | Count unique non-NULL values |
SUM(col) | Sum all values |
AVG(col) | Average of all non-NULL values |
MIN(col) | Smallest value |
MAX(col) | Largest value |
GROUP BY col | Collapse rows with same col value into one group |
HAVING condition | Filter groups after aggregation |
WHERE vs HAVING | WHERE filters rows before grouping; HAVING filters groups after |
COALESCE(val, default) | Replace NULL with a default |
NULLIF(a, b) | Return NULL if a equals b |
The rule that saves beginners from errors: every non-aggregated column in SELECT must be in GROUP BY.
Module F-5 covers joins — the feature that makes relational databases powerful by connecting data spread across multiple tables.
Next: F-5 — Connecting Tables — Joins Demystified →
Question 1: A junior engineer writes the following SQL query to find the average price of products in each category, along with the name of one product from that category:
This query results in an error: ERROR: column "products.name" must appear in the GROUP BY clause or be used in an aggregate function. As a Senior Principal Software Engineer, what is the most accurate explanation for this error, highlighting the underlying architectural principle?
- A) The
namecolumn cannot be included becauseAVG(price)is an aggregate, and you cannot mix aggregated and non-aggregated columns in theSELECTlist. - B) PostgreSQL cannot arbitrarily choose a single
namevalue for a group that contains multiple products, thus enforcing the "Golden Rule" that non-aggregatedSELECTcolumns must be part of theGROUP BYclause. - C) The
namecolumn is aTEXTtype, andGROUP BYclauses only support numeric or date types for grouping, leading to a type mismatch error. - D) The
AVG()function implicitly requires all otherSELECTlist columns to also be aggregated, whichnameis not.
Reveal Answer
Correct Answer: B
B is correct because when you GROUP BY category, there can be multiple name values within a single category group. PostgreSQL cannot decide which name to display for that aggregated category row. The "Golden Rule" of GROUP BY dictates that any column in the SELECT list that is not part of an aggregate function must also be included in the GROUP BY clause to ensure a one-to-one mapping between the grouped output row and the non-aggregated column. Option A is incorrect because you can mix aggregated and non-aggregated columns, as long as the non-aggregated ones are in the GROUP BY. Option C is incorrect; GROUP BY works with TEXT types. Option D is incorrect; AVG() does not implicitly require other columns to be aggregated; it's about the non-aggregated columns needing to be part of the grouping key.
Question 2: A database administrator needs to identify product categories that meet two criteria:
- They must contain at least 5 products.
- Among those products, only products with a
pricegreater than $50 should be considered for the count.
Which of the following SQL queries correctly implements these requirements, considering the PostgreSQL query execution order?
- A) SELECT category, COUNT() AS product_count FROM products HAVING COUNT() > 4 AND price > 50 GROUP BY category;
- B) SELECT category, COUNT() AS product_count FROM products WHERE price > 50 GROUP BY category HAVING COUNT() > 4;
- C) SELECT category, COUNT() AS product_count FROM products GROUP BY category WHERE price > 50 HAVING COUNT() > 4;
- D) SELECT category, COUNT() AS product_count FROM products WHERE COUNT() > 4 GROUP BY category HAVING price > 50;
Reveal Answer
Correct Answer: B
B is correct. The requirement "only products with a price greater than $50 should be considered for the count" implies a row-level filter that must occur before the aggregation. This is handled by the WHERE price > 50 clause. The requirement "They must contain at least 5 products" (after the price filter) is a group-level filter, which must occur after the GROUP BY and aggregation. This is handled by HAVING COUNT(*) > 4. Option A is incorrect because price > 50 is a row-level condition and cannot be used in HAVING. Option C is incorrect because WHERE must come before GROUP BY in the query execution order. Option D is incorrect because COUNT(*) is an aggregate and cannot be used in WHERE, and price is a row-level column that cannot be used in HAVING without being aggregated. The correct execution order is FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY -> LIMIT.
Question 3: Consider a products table with the following data, including a NULL value for description for 'Mystery Box' and a NULL for category for 'Uncategorized Item':
| name | category | description | price |
|---|---|---|---|
| Keyboard | peripherals | Mechanical | 129.99 |
| Mouse | peripherals | Wireless | 49.99 |
| Monitor Stand | accessories | Ergonomic | 39.99 |
| USB-C Hub | accessories | 7-in-1 | 64.99 |
| Laptop Sleeve | accessories | Neoprene | 24.99 |
| Mystery Box | peripherals | NULL | 19.99 |
| Uncategorized Item | NULL | Generic | 5.00 |
A developer runs the following three queries:
SELECT COUNT(*) FROM products;SELECT COUNT(description) FROM products;SELECT COUNT(DISTINCT category) FROM products;
What will be the respective outputs for these queries, and what is the key implication for data quality and reporting in a production environment?
- A) 1: 7, 2: 6, 3: 2. Implication:
COUNT(column)andCOUNT(DISTINCT column)silently ignoreNULLvalues, which can lead to undercounting ifNULLs represent valid, but unspecified, data points. - B) 1: 7, 2: 7, 3: 3. Implication: All
COUNTfunctions treatNULLs as distinct values, which can inflate counts of unique items. - C) 1: 5, 2: 5, 3: 2. Implication:
COUNTfunctions only consider rows with complete data, potentially hiding incomplete records. - D) 1: 7, 2: 6, 3: 3. Implication:
COUNT(*)includesNULLs, whileCOUNT(column)andCOUNT(DISTINCT column)countNULLs as a single distinct value.
Reveal Answer
Correct Answer: A
A is correct.
COUNT(*)counts all rows, including those withNULLs in any column. There are 7 rows in total, soCOUNT(*)returns 7.COUNT(description)counts only rows where thedescriptioncolumn is notNULL. The 'Mystery Box' has aNULLdescription, so it's excluded. This results in 6.COUNT(DISTINCT category)counts unique non-NULL values in thecategorycolumn. The categories are 'peripherals', 'accessories', andNULL.NULLis ignored byCOUNT(DISTINCT), so only 'peripherals' and 'accessories' are counted, resulting in 2. The key implication is thatCOUNT(column)andCOUNT(DISTINCT column)silently ignoreNULLvalues. In a production environment, this behavior is crucial to understand because ifNULLrepresents a meaningful "unknown" or "not applicable" state, these counts might underrepresent the true number of items or unique categories ifNULLs should have been considered. Developers must explicitly handleNULLs (e.g., withCOALESCE) if they need them to be included in counts or treated as distinct values. Options B, C, and D misrepresent howNULLs are handled by one or more of theCOUNTfunctions.
Discussion
0Join the discussion