SQL Aggregations โ GROUP BY & HAVING
Summarize millions of rows into a few meaningful numbers: counting, summing, averaging, and grouping โ the heart of every business report.
Learning Objectives
- Summarize data with
COUNT,SUM,AVG,MIN, andMAX. - Explain the difference between
COUNT(*),COUNT(column), andCOUNT(DISTINCT column). - Group rows into categories using
GROUP BY. - Avoid the classic "non-aggregate column" GROUP BY error.
- Filter grouped results with
HAVINGand explain how it differs fromWHERE. - Understand how aggregate functions treat
NULLvalues.
0. The Sample Tables
We continue with customers and add a small transactions table for the HAVING examples.
| customer_id | customer_name | city | account_type | balance | join_date |
|---|---|---|---|---|---|
| 1 | Aarav Sharma | Mumbai | Savings | 25000 | 2023-01-15 |
| 2 | Priya Verma | Delhi | Current | 120000 | 2022-06-30 |
| 3 | Rahul Mehta | Mumbai | Savings | 8000 | 2024-03-10 |
| 4 | Sneha Iyer | Bengaluru | Salary | 95000 | 2021-11-22 |
| 5 | Vikram Singh | Delhi | Current | 45000 | 2023-09-05 |
| 6 | Ananya Das | Chennai | Savings | 15000 | 2022-02-14 |
| txn_id | customer_id | amount | txn_date |
|---|---|---|---|
| 101 | 1 | 500 | 2024-01-10 |
| 102 | 1 | 1200 | 2024-02-15 |
| 103 | 2 | 800 | 2024-01-20 |
| 104 | 3 | 300 | 2024-03-05 |
| 105 | 1 | 950 | 2024-04-01 |
| 106 | 4 | 1500 | 2024-05-12 |
1. Aggregate Functions โ an Overview
Aggregate functions collapse many rows into a single summary value. They answer "how many?", "how much in total?", "what's the average?".
| Function | What it returns | Example |
|---|---|---|
COUNT() | number of rows / values | COUNT(*) |
SUM() | total of a numeric column | SUM(balance) |
AVG() | average of a numeric column | AVG(balance) |
MIN() | smallest value | MIN(balance) |
MAX() | largest value | MAX(balance) |
-- How many customers in total?
SELECT COUNT(*) AS total_customers
FROM customers;
-- What is the total balance across all customers?
SELECT SUM(balance) AS total_balance
FROM customers;
-- What is the average balance?
SELECT AVG(balance) AS avg_balance
FROM customers;
2. COUNT(*) vs COUNT(column) vs COUNT(DISTINCT)
This is one of the most common interview traps in SQL. The three forms count different things:
| Expression | What it counts |
|---|---|
COUNT(*) | all rows, including rows with NULLs |
COUNT(column) | only rows where that column is not NULL |
COUNT(DISTINCT column) | only unique, non-NULL values |
-- Count all customers vs distinct cities
SELECT COUNT(*) AS total_rows, -- 6
COUNT(city) AS non_null_cities, -- 6 (no NULLs here)
COUNT(DISTINCT city) AS unique_cities -- 4 (Mumbai, Delhi, Bengaluru, Chennai)
FROM customers;
city column had some NULLs, COUNT(city) would
be lower than COUNT(*). This is the #1 source of "off-by-a-few" count bugs.
3. GROUP BY โ Summarize by Category
GROUP BY splits the table into groups (one per unique value) and runs the
aggregate function within each group. It's the SQL equivalent of an Excel Pivot Table.
-- Total balance per city
SELECT city, SUM(balance) AS total_balance
FROM customers
GROUP BY city
ORDER BY total_balance DESC;
-- Number of customers per account type
SELECT account_type, COUNT(*) AS customer_count
FROM customers
GROUP BY account_type;
GROUP BY requests, and they make up a large share of daily analyst work.
4. The GROUP BY Trap (Read This Carefully)
The golden rule: every non-aggregate column in your SELECT must also appear in GROUP BY. Violating this causes an error in PostgreSQL/SQL Server/modern MySQL โ or worse, silently wrong results in older MySQL.
-- โ WRONG: 'customer_name' is selected but not in GROUP BY
SELECT customer_id, customer_name, SUM(balance)
FROM customers
GROUP BY customer_id;
-- โ
CORRECT: every non-aggregate column is grouped
SELECT customer_id, customer_name, SUM(balance) AS total_balance
FROM customers
GROUP BY customer_id, customer_name;
5. HAVING vs WHERE
The single most asked aggregation question: WHERE filters rows before grouping; HAVING filters groups after grouping.
-- Cities where the total balance is above 40,000
SELECT city, SUM(balance) AS total_balance
FROM customers
GROUP BY city
HAVING SUM(balance) > 40000;
Why can't we use WHERE SUM(balance) > 40000? Because SUM doesn't exist yet when WHERE runs โ aggregation happens after WHERE.
A full example: customers with more than one transaction
SELECT customer_id, COUNT(*) AS txn_count
FROM transactions
WHERE amount > 100 -- row filter (before grouping)
GROUP BY customer_id
HAVING COUNT(*) > 1 -- group filter (after grouping)
ORDER BY txn_count DESC;
This returns customer 1 (3 transactions). Note how WHERE and HAVING work at different stages.
WHERE COUNT(*) > 5 is always invalid. Conversely,
putting a simple row filter in HAVING (like HAVING city = 'Mumbai') is legal
but inefficient โ filter as early as possible with WHERE.
6. MIN, MAX, and How Aggregates Treat NULL
MIN and MAX
-- Smallest and largest balance per account type
SELECT account_type, MIN(balance) AS min_balance, MAX(balance) AS max_balance
FROM customers
GROUP BY account_type;
How NULL affects each aggregate
| Function | NULL handling |
|---|---|
COUNT(*) | counts NULL rows |
COUNT(column) | ignores NULLs |
SUM(column) | ignores NULLs (treats them as not present) |
AVG(column) | ignores NULLs (divides by non-NULL count only) |
MIN / MAX | ignore NULLs |
AVG ignores NULLs entirely. If you compute
AVG(salary + bonus) and some bonuses are NULL, those rows are dropped from the
average โ biasing it upward. Use AVG(salary + COALESCE(bonus, 0)) when "no bonus"
should count as zero.
7. Interview Questions (with Model Answers)
These are the aggregation questions most commonly asked to Data Analyst freshers. Self-test: answer each before revealing.
IQ1. What is the difference between COUNT(*), COUNT(column), and COUNT(DISTINCT column)?
Why they ask: It's the single most common aggregation trap โ and the #1 source of count bugs.
Model answer: "COUNT(*) counts every row including NULLs. COUNT(column)
counts only non-NULL values in that column. COUNT(DISTINCT column) counts unique non-NULL values."
IQ2. What is GROUP BY used for?
Why they ask: A core conceptual check โ do you understand grouping vs. filtering?
Model answer: "GROUP BY summarizes data by category. It splits rows into groups based
on one or more columns and runs aggregate functions โ like SUM or COUNT โ within each group.
It's the SQL equivalent of a Pivot Table in Excel."
IQ3. What is the difference between WHERE and HAVING?
Why they ask: Arguably the most-asked SQL interview question of all.
Model answer: "WHERE filters rows before aggregation, while
HAVING filters groups after aggregation. So WHERE works on individual rows,
and HAVING works on aggregated results like SUM or COUNT."
IQ4. What is wrong with SELECT city, customer_name, SUM(balance) ... GROUP BY city?
Why they ask: Tests the GROUP BY golden rule.
Model answer: "customer_name is selected but not in GROUP BY. Every
non-aggregate column in the SELECT must be in GROUP BY. On PostgreSQL/SQL Server this errors; on legacy
MySQL it silently returns an arbitrary name โ a correctness bug."
IQ5. How do you find customers who have placed more than 5 orders?
Why they ask: The classic "group + HAVING" business question.
Model answer: "I'd group orders by customer and filter with HAVING:
SELECT customer_id, COUNT(*) FROM orders GROUP BY customer_id HAVING COUNT(*) > 5."
IQ6. How do SUM, AVG, MIN, and MAX handle NULL values?
Why they ask: NULL behavior affects real numbers in reports.
Model answer: "They all ignore NULLs. SUM adds only non-NULL values, AVG divides by
the non-NULL count, and MIN/MAX skip NULLs. COUNT(*) is the exception โ it still counts rows with NULLs."
IQ7. Why might AVG of an integer column return a truncated (wrong) result?
Why they ask: A subtle, practical correctness detail.
Model answer: "In some databases, AVG of an integer column returns integer division
and drops the decimal. To get an accurate average, I'd cast to a decimal first โ
e.g. AVG(balance::numeric) in PostgreSQL or AVG(CAST(balance AS DECIMAL))."
IQ8. How do you find duplicate values in a column?
Why they ask: Data-quality checks are a staple of analyst interviews.
Model answer: "I'd group by the column and filter for counts greater than one:
SELECT email, COUNT(*) FROM customers GROUP BY email HAVING COUNT(*) > 1."
ROLLUP and CUBE generate subtotals and
grand totals; a "trimmed average" ignores outliers by removing the top/bottom 5%. Knowing these
concepts exists โ even if you can't write them from memory โ impresses in a senior round.
Hands-On Project: Summarize the Customers
Using the customers and transactions tables, write a query for each task.
Steps
- Count the total number of customers.
- Count the number of distinct cities.
- Find the total balance for each
account_type. - Find the average balance for each city, rounded to 0 decimals.
- List cities whose average balance is above 50,000.
- Find customers who made more than one transaction (from
transactions).
View Solution / Walkthrough
-- 1. Total customers
SELECT COUNT(*) AS total FROM customers; -- 6
-- 2. Distinct cities
SELECT COUNT(DISTINCT city) AS unique_cities FROM customers; -- 4
-- 3. Total balance per account type
SELECT account_type, SUM(balance) AS total_balance
FROM customers GROUP BY account_type;
-- Savings 48000 | Current 165000 | Salary 95000
-- 4. Average balance per city (rounded)
SELECT city, ROUND(AVG(balance), 0) AS avg_balance
FROM customers GROUP BY city;
-- 5. Cities with average balance above 50,000
SELECT city, AVG(balance) AS avg_balance
FROM customers GROUP BY city
HAVING AVG(balance) > 50000;
-- Delhi (82500), Bengaluru (95000)
-- 6. Customers with more than one transaction
SELECT customer_id, COUNT(*) AS txn_count
FROM transactions GROUP BY customer_id
HAVING COUNT(*) > 1;
-- customer 1 (3 transactions)
Key Takeaways
COUNT(*) counts all rows; COUNT(column) skips NULLs; COUNT(DISTINCT ...) counts uniques.
GROUP BY summarizes by category โ the SQL version of a Pivot Table.
Every non-aggregate column in SELECT must appear in GROUP BY.
WHERE filters rows before grouping; HAVING filters groups after.
Aggregates ignore NULLs โ and AVG of integers can silently truncate. Cast to decimal.
Objective Questions โ Test Your Understanding
Q1. Which expression counts only non-NULL values in a specific column?
Q2. Which clause filters groups after aggregation?
Q3. In a GROUP BY query, what must be true of non-aggregate columns in the SELECT?
Q4. What does COUNT(*) count?
Q5. Which query lists cities that have more than one customer?