INNER, LEFT, RIGHT, and FULL joinsPlannedSelf joins and many-to-many dataPlannedSubqueries and correlated subqueriesPlannedUNION, INTERSECT, and EXCEPTPlanned
Designing reliable schemas
CREATE TABLE and schema designPlannedConstraints and data integrityPlannedNormalization and intentional denormalizationPlannedViews and materialized viewsPlanned
Writing and protecting data
INSERT, UPDATE, and DELETEPlannedUpserts and conflict handlingPlannedTransactions and savepointsPlannedIsolation, locks, and concurrencyPlanned
Performance and administration
Indexes and access pathsPlannedEXPLAIN and query plansPlannedQuery tuning patternsPlannedBackups, restores, and migrationsPlanned
Security and production workflow
Users, roles, and least privilegePlannedPreventing SQL injectionPlannedStored procedures, functions, and triggersPlannedProduction data projectPlanned
Reports, aggregation, and analytics · Lesson 10 130 min
GROUP BY and HAVING
An aggregate without grouping gives one answer for the whole filtered table. GROUP BY makes one answer per group. WHERE decides which source rows enter; HAVING decides which completed groups remain.
Think in rows, groups, then results
RowsFilter source records with WHERE.
GroupsChoose the dimensions that define one output row.
Group filterApply HAVING to aggregate results.
AuditCheck group totals against the original rows.
SOURCE8 orders5 paid · 2 pending · 1 refunded
The data is the same as the aggregate functions lesson.
GROUPGROUP BY status3 groups
Rows sharing a status contribute to one result.
RESULT3 summary rowsCOUNT(*) per status
Each status has its own count and total.
One aggregate result versus grouped results
The first query returns one row: eight orders and 12,000 known cents. It has no grouping key. Adding GROUP BY status creates three result rows instead. A grouped result describes a category, not an individual order.
SQL BROWSER RUNNER
Start with one summary
See the ungrouped baseline before partitioning rows.
Edit the query, predict the rows it will return, then run it.
The grouping key defines result granularity
Grouping by customer_id makes one row per customer. The four customer counts are 3, 2, 1, and 2. COUNT(amount_cents) can be smaller than COUNT(*) within a group because pending amounts are NULL. Select the grouping key and aggregates, not an unrelated raw column whose value would be ambiguous.
SQL BROWSER RUNNER
Summarize each customer
Compare row counts, known amounts, and totals per customer.
Edit the query, predict the rows it will return, then run it.
WHERE filters rows before grouping
A paid-customer report should first remove pending and refunded rows. Then GROUP BY customer_id builds four paid-customer summaries. Filtering a status in WHERE changes the rows used by every aggregate, not merely which result rows are displayed.
SQL BROWSER RUNNER
Build paid-customer summaries
Filter to paid source rows, then group by customer.
Edit the query, predict the rows it will return, then run it.
HAVING filters completed groups
To keep only customers with at least two paid orders, use HAVING COUNT(*) >= 2 after grouping. Only customer 10 qualifies. WHERE COUNT(*) >= 2 is invalid because WHERE runs before the count exists. Prefer WHERE for ordinary source-row conditions; HAVING is for group-level conditions.
SQL BROWSER RUNNER
Keep repeat paid customers
Filter source rows with WHERE and summary groups with HAVING.
Edit the query, predict the rows it will return, then run it.
HAVING can also test a group total. With a 3,000-cent paid threshold, customers 10 and 12 qualify. Repeating SUM(amount_cents) in HAVING is portable across SQL engines; some engines allow aliases there, but not all do.
SQL BROWSER RUNNER
Filter by a grouped total
Keep customers whose paid orders total at least 3,000 cents.
Edit the query, predict the rows it will return, then run it.
Multiple keys make smaller groups
GROUP BY customer_id, status makes a separate group for each customer–status combination. Customer 10 therefore has a paid group and a pending group. Grouping by a unique order ID would make every group one order and defeat this report's purpose.
SQL BROWSER RUNNER
Group by customer and status
Inspect how two dimensions change result granularity.
Edit the query, predict the rows it will return, then run it.
NULL is a group key, even though it is not a value
Grouping by amount_cents puts the two unknown pending amounts into one NULL group. That group's COUNT(*) is two, while COUNT(amount_cents) would be zero. A zero-amount order forms a different group.
SQL BROWSER RUNNER
Inspect NULL and zero groups
See the separate groups for unknown and zero amounts.
Edit the query, predict the rows it will return, then run it.
Common grouped-report mistakes
WHERE COUNT(*) > 1
Wrong phase
Use HAVING for a count calculated per group.
Unrelated raw column in SELECT
Ambiguous value
Select grouping keys or aggregate expressions; do not rely on a random row's value.
GROUP BY order_id
Wrong granularity
A unique key makes one group per order, not one per customer.
HAVING status = 'paid'
Late population filter
Use WHERE status = 'paid' before aggregating paid-order metrics.
Independent lab: rank repeat paid customers
Build a September leaderboard with customer ID, paid-order count, paid cents, and average paid cents. Include only paid orders in the half-open September date window, then keep customers with at least two paid orders. The starter returns customer 10: two paid orders, 4,000 cents, and a 2,000-cent average. Lower the HAVING threshold to one and predict the new result order.
SQL BROWSER RUNNER
Audit a repeat-customer leaderboard
Combine WHERE, GROUP BY, HAVING, and deterministic ORDER BY.
Edit the query, predict the rows it will return, then run it.
Lesson review
WHERE filters individual rows, GROUP BY partitions the survivors, aggregate functions measure each group, and HAVING filters those summaries. Choose grouping keys to match the exact entity represented by each result row.
I can predict the number of groups from the grouping keys.
I can separate source-row filters from aggregate filters.
I can explain NULL and zero within grouped counts.
I can keep a grouped SELECT unambiguous and portable.
I can audit a leaderboard against its source rows.
KNOWLEDGE CHECK
Check grouped-report reasoning
Answer all ten questions, then rerun any example whose group size or filter stage surprised you.