Session 04 ยท Phase 1: Foundations & SQL

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.

โฑ ~2 hrs ๐Ÿ“š Core content ๐ŸŽฏ High priority

Learning Objectives

0. The Sample Tables

We continue with customers and add a small transactions table for the HAVING examples.

customer_idcustomer_namecityaccount_typebalancejoin_date
1Aarav SharmaMumbaiSavings250002023-01-15
2Priya VermaDelhiCurrent1200002022-06-30
3Rahul MehtaMumbaiSavings80002024-03-10
4Sneha IyerBengaluruSalary950002021-11-22
5Vikram SinghDelhiCurrent450002023-09-05
6Ananya DasChennaiSavings150002022-02-14
txn_idcustomer_idamounttxn_date
10115002024-01-10
102112002024-02-15
10328002024-01-20
10433002024-03-05
10519502024-04-01
106415002024-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?".

FunctionWhat it returnsExample
COUNT()number of rows / valuesCOUNT(*)
SUM()total of a numeric columnSUM(balance)
AVG()average of a numeric columnAVG(balance)
MIN()smallest valueMIN(balance)
MAX()largest valueMAX(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:

ExpressionWhat 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;
โš ๏ธ
Warning: If the 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;
๐ŸŒ
Real World: "Give me sales by region", "customers by segment", "orders by month" โ€” these are all 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;
โš ๏ธ
Why it matters: With legacy MySQL settings, the wrong query still runs and returns an arbitrary name for each customer_id โ€” a silent correctness bug that's very hard to catch. Interviewers specifically check whether you know this.

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.

๐Ÿ“
Note: 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

FunctionNULL 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 / MAXignore NULLs
โš ๏ธ
Watch out for AVG: 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.
๐Ÿ“‹ Stable content โ€” Reviewed: August 2026

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."

๐Ÿ’ก
Good to know (advanced): 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

  1. Count the total number of customers.
  2. Count the number of distinct cities.
  3. Find the total balance for each account_type.
  4. Find the average balance for each city, rounded to 0 decimals.
  5. List cities whose average balance is above 50,000.
  6. 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

1

COUNT(*) counts all rows; COUNT(column) skips NULLs; COUNT(DISTINCT ...) counts uniques.

2

GROUP BY summarizes by category โ€” the SQL version of a Pivot Table.

3

Every non-aggregate column in SELECT must appear in GROUP BY.

4

WHERE filters rows before grouping; HAVING filters groups after.

5

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?