🚧 Under construction — VoTechWorks is being built. A free tech program — complimentary, as our commitment to get America working.
voTechWorksGrade 13 · a vocational technology learning center, by urbanicity
newSchoolWorks.com › VoTechWorks › Technology & Design › Data Science & Analytics › SQL and PostgreSQL for Data Analytics › Lesson 3
TEC-DATA85-04 · Lesson 3 · 11 clock hours · practice

Filtering, Aggregation, and Grouping

Objective. Students write queries using GROUP BY, HAVING, and aggregate functions (COUNT, SUM, AVG, MIN, MAX) to produce summary statistics from multi-row datasets.

⚠ Pending professional review

This lesson, its worked examples and its practice questions has not yet been verified by an industry-certified professional or by a certification or approval body. It is a study draft and may contain errors. Do not use it as instructions for work on a job site or on live equipment. Always follow your instructor, your supervising professional, and current official references and regulations.

VoTechWorks program approval and accreditation are still in progress. Credentials are issued only by the certifying bodies named in each program, after their own exams.

Why Aggregation Matters in Data Analytics

Raw tables store one fact per row. A table of library checkouts might have hundreds of thousands of rows, each recording a single transaction. Decision-makers rarely need every row; they need summaries: How many books did each branch lend last month? Which patrons check out the most items? Which branches are underperforming? SQL's aggregation tools let you collapse many rows into meaningful summary numbers without ever leaving the database.

Aggregate Functions

PostgreSQL (and standard SQL) provides five core aggregate functions you will use constantly:

These functions appear in the SELECT list. On their own, without GROUP BY, they collapse the entire result set into a single summary row:

```sql

SELECT COUNT(*) AS total_checkouts,

AVG(days_borrowed) AS avg_days

FROM checkouts;

```

GROUP BY: Splitting Results into Groups

When you add GROUP BY, PostgreSQL divides the rows into subsets and applies the aggregate function to each subset independently. Every column in the SELECT list must either appear inside an aggregate function or be listed in the GROUP BY clause — otherwise the query will raise an error, because the engine would not know which row's value to display for that column.

```sql

SELECT branch_name,

COUNT(*) AS total_checkouts

FROM checkouts

GROUP BY branch_name

ORDER BY total_checkouts DESC;

```

This returns one row per branch with the total checkouts for that branch.

WHERE vs. HAVING: A Critical Distinction

This is the most common source of errors for new SQL writers, and it appears on certification exams:

Mnemonic: WHERE comes Where the raw data lives; HAVING comes Hafter (after) aggregation.

```sql

-- Correct: filter raw rows with WHERE, then group

SELECT branch_name, COUNT(*) AS total

FROM checkouts

WHERE checkout_year = 2023

GROUP BY branch_name

HAVING COUNT(*) > 500;

```

The WHERE clause removes rows from years other than 2023 before any grouping occurs. The HAVING clause then keeps only branches whose 2023 count exceeds 500. Swapping HAVING for WHERE here would cause an error because WHERE cannot evaluate COUNT(*).

Query Execution Order

Understanding the logical order in which PostgreSQL processes clauses prevents many mistakes:

1. FROM — identify the table(s)

2. WHERE — filter rows

3. GROUP BY — form groups

4. HAVING — filter groups

5. SELECT — compute output columns and aliases

6. ORDER BY — sort results

7. LIMIT — restrict row count

Because SELECT is evaluated at step 5, column aliases created in SELECT are not available in WHERE or HAVING in standard SQL. In PostgreSQL, ORDER BY can reference SELECT aliases, but HAVING generally cannot.

Combining Filters and Aggregates

You can use WHERE and HAVING together in the same query. A realistic example: find branches that served more than 200 patrons in the first quarter of 2024:

```sql

SELECT branch_name,

COUNT(DISTINCT patron_id) AS unique_patrons

FROM checkouts

WHERE checkout_date BETWEEN '2024-01-01' AND '2024-03-31'

GROUP BY branch_name

HAVING COUNT(DISTINCT patron_id) > 200

ORDER BY unique_patrons DESC;

```

COUNT(DISTINCT patron_id) counts each patron only once per branch even if they checked out multiple items.

Readability and Professional Standards

In professional settings, readable SQL is as important as correct SQL. Follow these conventions:

Data Integrity and Safety Notes

Data analytics work does not involve physical hazards in the traditional sense, but it carries its own professional safety obligations:

Summary

Aggregate functions (COUNT, SUM, AVG, MIN, MAX) collapse multiple rows into single summary values. GROUP BY splits the data into subsets before aggregation. WHERE filters raw rows before grouping; HAVING filters groups after aggregation. Keeping the logical execution order in mind and writing readable, well-commented SQL are foundational professional skills that every credential exam will test.

Key terms8

Aggregate functionA SQL function that computes a single summary value from a set of rows; the five core functions are COUNT, SUM, AVG, MIN, and MAX.
GROUP BYA SQL clause that divides query results into subsets based on one or more columns, applying aggregate functions to each subset independently.
HAVINGA SQL clause that filters groups produced by GROUP BY; it is evaluated after aggregation and can reference aggregate functions, unlike WHERE.
WHEREA SQL clause that filters individual rows before any grouping or aggregation occurs; it cannot reference aggregate functions.
COUNT(DISTINCT column)A variant of COUNT that counts only unique non-NULL values in a column, eliminating duplicates from the total.
Query execution orderThe logical sequence in which a SQL engine processes clauses: FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT.
AliasA temporary name assigned to a column or table in a query using the AS keyword, improving readability of output without changing the underlying data.
AVGAn aggregate function that returns the arithmetic mean of non-NULL numeric values in a column across all rows in a group.

Worked examples4

1. Write a query that returns each branch name and its total number of checkout transactions, ordered from most to fewest checkouts, using the checkouts table.
  1. Identify the grouping column: branch_name. Each branch should appear once in the output.
  2. Choose the aggregate function: COUNT(*) counts every row in each group, giving the total transactions per branch.
  3. Write the SELECT list: include branch_name (must appear in GROUP BY) and COUNT(*) aliased as total_checkouts.
  4. Add GROUP BY branch_name so PostgreSQL forms one group per branch.
  5. Add ORDER BY total_checkouts DESC to rank branches from most to fewest.
Answer: SELECT branch_name, COUNT(*) AS total_checkouts FROM checkouts GROUP BY branch_name ORDER BY total_checkouts DESC;
2. Find every branch whose average number of days_borrowed per checkout exceeds 14, but only considering checkouts made during the year 2023.
  1. Use WHERE checkout_year = 2023 to remove rows from other years before any grouping — WHERE operates on raw rows.
  2. Add GROUP BY branch_name to form one group per branch from the filtered rows.
  3. Use AVG(days_borrowed) to compute the mean loan duration for each branch group.
  4. Use HAVING AVG(days_borrowed) > 14 to keep only groups whose average exceeds 14. HAVING is required here because the condition references an aggregate result.
  5. Select branch_name and AVG(days_borrowed) aliased as avg_days in the SELECT list.
Answer: SELECT branch_name, AVG(days_borrowed) AS avg_days FROM checkouts WHERE checkout_year = 2023 GROUP BY branch_name HAVING AVG(days_borrowed) > 14 ORDER BY avg_days DESC;
3. Count the number of distinct patrons served by each branch. A patron who visited the same branch three times should be counted only once for that branch.
  1. Recognize that COUNT(*) would count all rows, including repeat visits by the same patron, which is not what is asked.
  2. Use COUNT(DISTINCT patron_id) instead, which counts each patron_id value only once per group regardless of how many rows contain it.
  3. Group by branch_name so each branch produces one output row.
  4. Alias the result as unique_patrons for clarity.
Answer: SELECT branch_name, COUNT(DISTINCT patron_id) AS unique_patrons FROM checkouts GROUP BY branch_name ORDER BY unique_patrons DESC;
4. A student writes the following query intending to find branches with more than 100 checkouts in 2023, but it produces an error. Identify and fix it. SELECT branch_name, COUNT(*) AS total FROM checkouts WHERE COUNT(*) > 100 AND checkout_year = 2023 GROUP BY branch_name;
  1. Locate the error: COUNT(*) appears inside a WHERE clause. WHERE is processed before aggregation, so aggregate functions are not yet available there — this causes a syntax error in PostgreSQL.
  2. The filter on checkout_year = 2023 is a row-level filter and correctly belongs in WHERE.
  3. Move COUNT(*) > 100 out of WHERE and into a HAVING clause, which is evaluated after GROUP BY and can reference aggregate functions.
  4. Keep checkout_year = 2023 in the WHERE clause since it filters individual rows, not groups.
Answer: SELECT branch_name, COUNT(*) AS total FROM checkouts WHERE checkout_year = 2023 GROUP BY branch_name HAVING COUNT(*) > 100;
In class: Demonstration and guided notes: instructor live-codes progressively complex GROUP BY queries against the library dataset, narrating each clause; students follow along and annotate printed SQL syntax cards. · Error-correction exercise: students receive five intentionally broken queries (wrong GROUP BY columns, HAVING vs. WHERE confusion) and must identify and fix each error in psql. · Mini-project: students write a four-query analytical memo answering questions such as total checkouts by branch, average checkouts per patron, and branches whose monthly average exceeds a threshold, then share findings with a partner for peer review.
Materials: Library-checkouts PostgreSQL database from Lesson 2, SQL syntax reference cards (instructor-prepared), peer-review rubric
Check: Partner peer review scored with rubric (query correctness, readability, and correct use of HAVING vs. WHERE); both partners must earn 80% to log the activity complete; instructor spot-checks three pairs.

Practice check8

Answer each question, then check it. Nothing is recorded; this is your own practice.

Data Analysis and Queries
1. A data analyst needs to find the total number of checkout transactions for each branch. Which SQL clause is required to produce one summary row per branch?
Data Analysis and Queries
2. Which of the following queries correctly returns only branches where the average loan duration exceeds 10 days?
Database Concepts
3. In what order does PostgreSQL logically process the following clauses: WHERE, GROUP BY, HAVING, SELECT?
Data Analysis and Queries
4. A table called sales has columns: rep_id, region, and sale_amount. Which query correctly returns the total sale amount per region?
Data Analysis and Queries
5. An analyst wants to count each patron only once per branch, even if the patron checked out multiple items. Which function should they use?
Data Analysis and Queries
6. Which aggregate function returns the largest value in a column within each group?
Data Analysis and Queries
7. A student writes: SELECT branch_name, COUNT(*) AS total FROM checkouts GROUP BY branch_name, patron_id; The student expected one row per branch but gets far more rows. What is the most likely cause?
Data Governance and Ethics
8. Which of the following best describes a professional data safety practice when working with query results that include patron names and IDs?
Record this lesson on my transcript. Sign in with your key and enrollment email to record lessons (enroll free if you have no key). This page stays open to read and practise.