06 - SQL Aggregation & Grouping
1497 words
7 min read
Visual companion
Python
Type and operator map
Python Week 1: the first filter for runtime behavior
View
Revision summary
What this note is really saying
Short form
# 06 - SQL Aggregation & Grouping ## 🎯 Learning Objectives After reading this topic, you will be able to: - Use aggregate functions (COUNT, SUM, AVG, MIN, MAX) correctly - Write GROUP BY clauses to group rows for aggregation - Filter groups with the HAVING clause - Understand NULL behavior in aggregations - Explain...

06 - SQL Aggregation & Grouping
🎯 Learning Objectives
After reading this topic, you will be able to:
- Use aggregate functions (COUNT, SUM, AVG, MIN, MAX) correctly
- Write GROUP BY clauses to group rows for aggregation
- Filter groups with the HAVING clause
- Understand NULL behavior in aggregations
- Explain 3-valued logic (TRUE, FALSE, UNKNOWN)
📋 Prerequisites
- 05 - SQL Queries — SELECT, WHERE, ORDER BY
- Understanding of NULL values
📖 Core Content
6.1 Intuition: From Individual Rows to Summaries
Sometimes you don't want individual rows — you want summaries:
- What's the average salary in each department?
- How many students are in each course?
- What's the total budget across all departments? Aggregation answers these questions. Instead of returning one row per data item, it returns one row per group.
Why This Matters: In real-world analytics, you rarely look at raw data — you look at aggregated summaries. Dashboards, reports, and business intelligence all rely on aggregation.
6.2 Aggregate Functions
SQL provides five standard aggregate functions:
| Function | Returns | Notes |
|---|---|---|
COUNT(*) | Number of rows | Counts ALL rows including NULLs |
COUNT(column) | Number of non-NULL values | Ignores NULLs |
COUNT(DISTINCT column) | Number of unique non-NULL values | Ignores NULLs |
SUM(column) | Sum of values | Works only on numeric columns |
AVG(column) | Average of values | Works only on numeric columns |
MIN(column) | Minimum value | Works on numeric, string, date |
MAX(column) | Maximum value | Works on numeric, string, date |
Basic Examples
sql-- Total number of instructors SELECT COUNT(*) FROM instructor; -- Result: 12 -- Number of instructors who have a non-NULL salary SELECT COUNT(salary) FROM instructor; -- 12 (if all have salaries) -- Number of distinct departments SELECT COUNT(DISTINCT dept_name) FROM instructor; -- Result: 7 -- Average salary SELECT AVG(salary) FROM instructor; -- Result: ~75500 -- Maximum salary SELECT MAX(salary) FROM instructor; -- Result: 95000
6.3 GROUP BY: Grouping Rows
The power of aggregation comes with GROUP BY — it partitions rows into groups and applies the aggregate to each group:
sqlSELECT dept_name, AVG(salary) AS avg_salary FROM instructor GROUP BY dept_name;
Execution order: FROM → WHERE → GROUP BY → SELECT → ORDER BY
How it works:
(Diagram)
Result:
| dept_name | avg_salary |
|---|---|
| Comp. Sci. | 83000.00 |
| Finance | 85000.00 |
| Music | 40000.00 |
| Physics | 91000.00 |
| History | 61000.00 |
| Biology | 72000.00 |
| Elec. Eng. | 80000.00 |
Critical Rule: SELECT columns must be in GROUP BY or aggregated
sql-- ERROR: name is neither in GROUP BY nor aggregated SELECT dept_name, name, AVG(salary) FROM instructor GROUP BY dept_name;
For each salary group, which
name should appear? There are multiple! The DBMS won't guess.sql-- CORRECT SELECT dept_name, AVG(salary) FROM instructor GROUP BY dept_name;
Exception: MySQL/SQLite allow this (returning the first value encountered), but PostgreSQL and standard SQL reject it. Write portable SQL by following the rule.
6.4 HAVING: Filtering Groups
WHERE filters rows before grouping. HAVING filters groups after aggregation.sqlSELECT dept_name, AVG(salary) AS avg_salary FROM instructor GROUP BY dept_name HAVING AVG(salary) > 70000;
Execution order: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY
| dept_name | avg_salary |
|---|---|
| Comp. Sci. | 83000.00 |
| Finance | 85000.00 |
| Physics | 91000.00 |
| Biology | 72000.00 |
| Elec. Eng. | 80000.00 |
WHERE vs. HAVING: When to Use Which
sql-- WHERE filters rows BEFORE grouping SELECT dept_name, AVG(salary) FROM instructor WHERE salary > 50000 -- excludes low salaries before averaging GROUP BY dept_name; -- HAVING filters groups AFTER grouping SELECT dept_name, AVG(salary) FROM instructor GROUP BY dept_name HAVING AVG(salary) > 50000; -- excludes depts with low avg salary
Rule of thumb:
- Filter on individual rows → WHERE
- Filter on aggregate results → HAVING
- You can use both in the same query
6.5 NULLs and 3-Valued Logic
NULL means "unknown" or "does not exist." It creates a 3-valued logic:
| TRUE | FALSE | UNKNOWN |
|---|
Truth Tables
AND:
| A | B | A AND B |
|---|---|---|
| TRUE | FALSE | FALSE |
| TRUE | UNKNOWN | UNKNOWN |
| FALSE | UNKNOWN | FALSE |
| UNKNOWN | UNKNOWN | UNKNOWN |
OR:
| A | B | A OR B |
|---|---|---|
| TRUE | UNKNOWN | TRUE |
| FALSE | UNKNOWN | UNKNOWN |
| UNKNOWN | UNKNOWN | UNKNOWN |
NOT:
| A | NOT A |
|---|---|
| TRUE | FALSE |
| FALSE | TRUE |
| UNKNOWN | UNKNOWN |
Key consequence:
WHERE clause treats UNKNOWN as FALSE. So WHERE salary > NULL returns no rows.NULL in Aggregations
sql-- All aggregate functions except COUNT(*) ignore NULLs SELECT AVG(salary) FROM instructor; -- average of only non-NULL salaries SELECT COUNT(*) FROM instructor; -- counts ALL rows SELECT COUNT(salary) FROM instructor; -- counts only rows with non-NULL salary
If all values are NULL, what does each function return?
COUNT(*)→ number of rows (not 0!)COUNT(column)→ 0SUM(column)→ NULLAVG(column)→ NULLMIN(column)→ NULLMAX(column)→ NULL
6.6 Worked Examples
Example 1: Department salary statistics
sqlSELECT dept_name, COUNT(*) AS num_instructors, AVG(salary) AS avg_salary, MIN(salary) AS min_salary, MAX(salary) AS max_salary FROM instructor GROUP BY dept_name;
| dept_name | num_instructors | avg_salary | min_salary | max_salary |
|---|---|---|---|---|
| Biology | 1 | 72000 | 72000 | 72000 |
| Comp. Sci. | 3 | 83000 | 65000 | 92000 |
| Elec. Eng. | 1 | 80000 | 80000 | 80000 |
| Finance | 2 | 85000 | 80000 | 90000 |
| History | 2 | 61000 | 60000 | 62000 |
| Music | 1 | 40000 | 40000 | 40000 |
| Physics | 2 | 91000 | 87000 | 95000 |
Example 2: Departments with at least 2 instructors
sqlSELECT dept_name, COUNT(*) AS count FROM instructor GROUP BY dept_name HAVING COUNT(*) >= 2;
| dept_name | count |
|---|---|
| Comp. Sci. | 3 |
| Finance | 2 |
| History | 2 |
| Physics | 2 |
Example 3: WHERE + GROUP BY + HAVING together
Find departments where the average salary of instructors earning > 60000 is above 80000:
sqlSELECT dept_name, AVG(salary) AS avg_high_salary FROM instructor WHERE salary > 60000 -- 1. Only consider high-salary instructors GROUP BY dept_name -- 2. Group by department HAVING AVG(salary) > 80000; -- 3. Only departments with avg > 80000
📐 Key Formulas / Concepts
| Concept | Description |
|---|---|
| Aggregate functions | COUNT, SUM, AVG, MIN, MAX — summarize multiple rows into one value |
| GROUP BY | Partitions rows into groups; one result row per group |
| HAVING | Filters groups after aggregation (like WHERE for groups) |
| NULL in aggregation | Ignored by all aggregates except COUNT(*) |
| 3-valued logic | TRUE, FALSE, UNKNOWN; UNKNOWN treated as FALSE in WHERE |
⚠️ Common Pitfalls
Pitfall 1: Using column not in GROUP BY in SELECT
The Mistake:
SELECT dept_name, name, AVG(salary) FROM instructor GROUP BY dept_name
Why It's Wrong: name is neither in GROUP BY nor aggregated. For each department group, there are multiple names — which one should appear?
Correct: Either add name to GROUP BY (if you want per-name groups) or remove it from SELECT.Pitfall 2: Using HAVING without GROUP BY
The Mistake:
SELECT AVG(salary) FROM instructor HAVING AVG(salary) > 70000
Why It's Wrong: It's valid SQL (means: average salary > 70000? return one row), but often misused. Without GROUP BY, the whole table is one group.
Better: Understand that HAVING without GROUP BY treats the entire result as a single group.Pitfall 3: Confusing WHERE and HAVING
The Mistake: Using WHERE to filter on aggregated values:
sqlSELECT dept_name, AVG(salary) FROM instructor WHERE AVG(salary) > 70000 GROUP BY dept_name;
Why It's Wrong: WHERE operates on individual rows, but
AVG(salary) is not known until after grouping.
Correct: Use HAVING for aggregate conditions:sqlSELECT dept_name, AVG(salary) FROM instructor GROUP BY dept_name HAVING AVG(salary) > 70000;
📝 Practice Questions
Q1. Write a query to find the number of instructors in each department.
AnswersqlSELECT dept_name, COUNT(*) AS num_instructors FROM instructor GROUP BY dept_name;
Q2. Find the average salary of instructors in departments with at least 2 instructors.
AnswersqlSELECT dept_name, AVG(salary) AS avg_salary FROM instructor GROUP BY dept_name HAVING COUNT(*) >= 2;
Q3. What is the difference between COUNT(*) and COUNT(column)?
Answer
COUNT(*)counts all rows in the group (including rows with NULL values)COUNT(column)counts only non-NULL values in that columnIf a group has 10 rows but columnsalaryhas 2 NULLs,COUNT(*) = 10butCOUNT(salary) = 8.
Q4. What does SUM(salary) return if all salaries are NULL?
AnswerSUM(salary)returns NULL, not 0. All aggregate functions except COUNT(*) return NULL when operating on all-NULL values.COUNT(*)would return the number of rows.COUNT(salary)would return 0.
Q5. Write a query to find departments whose total budget (sum of instructor salaries) exceeds 200000.
AnswersqlSELECT dept_name, SUM(salary) AS total_salary FROM instructor GROUP BY dept_name HAVING SUM(salary) > 200000;
Q6. Explain the difference between WHERE and HAVING in a GROUP BY query.
Answer
- WHERE: Filters rows before grouping. Conditions can reference individual columns but NOT aggregate functions.
- HAVING: Filters groups after aggregation. Conditions CAN reference aggregate functions.
Execution order: FROM → WHERE → GROUP BY → HAVING → SELECT
Q7. How does NULL behave in AND/OR/NOT operations?
Answer
- NULL AND TRUE = UNKNOWN
- NULL AND FALSE = FALSE (short-circuit!)
- NULL OR TRUE = TRUE (short-circuit!)
- NULL OR FALSE = UNKNOWN
- NOT NULL = UNKNOWN
WHERE clause treats UNKNOWN as FALSE, so the row is not included.
Q8. Find the minimum and maximum salary for each department, ordered by department name.
AnswersqlSELECT dept_name, MIN(salary) AS min_salary, MAX(salary) AS max_salary FROM instructor GROUP BY dept_name ORDER BY dept_name;
🔗 Cross-References
- Next Topic: 07 - Advanced SQL
- Previous Topic: 05 - SQL Queries
- Related: BSMS2001 (BDM) — Business reporting, analytics queries
- Textbook: Silberschatz, Korth, Sudarshan — Chapter 3 (SQL), Chapter 4 (Intermediate SQL) Join Discord Previous05 - SQL QueriesNext07 - Advanced SQL