Module F-4·18 min read

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.

text

Aggregate Functions

These functions take a column of values and return a single summary value:

sql

When you use an aggregate function without GROUP BY, it collapses the entire table into a single row:

sql

GROUP BY — The Mental Model

GROUP BY is where most beginners get confused. The mental model:

  1. PostgreSQL divides all your rows into groups based on the column(s) you specify
  2. Each group is collapsed into a single output row
  3. You can then apply aggregate functions to each group separately
sql

Visualise what happens:

text

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.

sql

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

sql

When you GROUP BY multiple columns, each unique combination of those columns becomes one output row.


Common Aggregation Patterns

sql

HAVING — Filtering Groups

WHERE filters individual rows before grouping. HAVING filters groups after aggregation.

sql

The difference between WHERE and HAVING:

sql

The Execution Order

Understanding the order PostgreSQL executes a query helps predict how WHERE vs HAVING behaves:

text

This is why you cannot use a column alias from SELECT in a WHERE clause — WHERE runs before SELECT:

sql

COUNT(*) vs COUNT(column) vs COUNT(DISTINCT column)

These three are different:

sql

COALESCE and NULLIF in Aggregations

NULL values are silently ignored by aggregate functions — but this can surprise you:

sql

Practical Exercise: Sales Analysis

Create a sales table and practice aggregation:

sql

Now answer these questions:

sql

Summary

ConceptWhat 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 colCollapse rows with same col value into one group
HAVING conditionFilter groups after aggregation
WHERE vs HAVINGWHERE 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 →


Knowledge Check

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:

sql

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 name column cannot be included because AVG(price) is an aggregate, and you cannot mix aggregated and non-aggregated columns in the SELECT list.
  • B) PostgreSQL cannot arbitrarily choose a single name value for a group that contains multiple products, thus enforcing the "Golden Rule" that non-aggregated SELECT columns must be part of the GROUP BY clause.
  • C) The name column is a TEXT type, and GROUP BY clauses only support numeric or date types for grouping, leading to a type mismatch error.
  • D) The AVG() function implicitly requires all other SELECT list columns to also be aggregated, which name is 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:

  1. They must contain at least 5 products.
  2. Among those products, only products with a price greater 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':

namecategorydescriptionprice
KeyboardperipheralsMechanical129.99
MouseperipheralsWireless49.99
Monitor StandaccessoriesErgonomic39.99
USB-C Hubaccessories7-in-164.99
Laptop SleeveaccessoriesNeoprene24.99
Mystery BoxperipheralsNULL19.99
Uncategorized ItemNULLGeneric5.00

A developer runs the following three queries:

  1. SELECT COUNT(*) FROM products;
  2. SELECT COUNT(description) FROM products;
  3. 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) and COUNT(DISTINCT column) silently ignore NULL values, which can lead to undercounting if NULLs represent valid, but unspecified, data points.
  • B) 1: 7, 2: 7, 3: 3. Implication: All COUNT functions treat NULLs as distinct values, which can inflate counts of unique items.
  • C) 1: 5, 2: 5, 3: 2. Implication: COUNT functions only consider rows with complete data, potentially hiding incomplete records.
  • D) 1: 7, 2: 6, 3: 3. Implication: COUNT(*) includes NULLs, while COUNT(column) and COUNT(DISTINCT column) count NULLs as a single distinct value.
Reveal Answer

Correct Answer: A

A is correct.

  1. COUNT(*) counts all rows, including those with NULLs in any column. There are 7 rows in total, so COUNT(*) returns 7.
  2. COUNT(description) counts only rows where the description column is not NULL. The 'Mystery Box' has a NULL description, so it's excluded. This results in 6.
  3. COUNT(DISTINCT category) counts unique non-NULL values in the category column. The categories are 'peripherals', 'accessories', and NULL. NULL is ignored by COUNT(DISTINCT), so only 'peripherals' and 'accessories' are counted, resulting in 2. The key implication is that COUNT(column) and COUNT(DISTINCT column) silently ignore NULL values. In a production environment, this behavior is crucial to understand because if NULL represents a meaningful "unknown" or "not applicable" state, these counts might underrepresent the true number of items or unique categories if NULLs should have been considered. Developers must explicitly handle NULLs (e.g., with COALESCE) if they need them to be included in counts or treated as distinct values. Options B, C, and D misrepresent how NULLs are handled by one or more of the COUNT functions.

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.