Course 2 of 6 · f1db dataset
Avoid correctness traps, master conditional aggregates, and build reports you can trust.
GROUP BY collapses rows into groups and the evaluation order — WHERE → GROUP BY → HAVING → SELECT — determines which clauses can reference aggregate results. This module builds that mental model against F1 race data.
Book reference: Chapter 16 — Group By, Having, With, Union All
Free preview: GROUP BY and HAVING · Common Aggregate Functions
select extract(
'year' from
date_trunc('decade', date)
) as decade,
count(*) as races
from races
group by decade
order by decade;Race count grouped by decade across all of F1 history.
COUNT(col) excludes NULLs while COUNT(*) does not. COUNT DISTINCT carries hidden performance costs. This module traces every correctness trap with working examples on real datasets.
Book reference: Chapter 16 — Group By, Having, With, Union All
Free preview: GROUP BY and HAVING · Common Aggregate Functions · The FILTER Clause
select extract(year from races.date)
as season,
count(distinct raceid) as nb_races,
count(*) as nb_status,
count(*) filter(
where status = 'Accident'
) as accidents,
round(100.0
* count(*) filter(
where status = 'Accident'
)
/ count(*), 2) as pct
from results
join status using(statusid)
join races using(raceid)
group by season
order by accidents desc
limit 5;Top 5 seasons by accident count, with total results and accident percentage per season.
Window functions combined with GROUP BY enable multi-level summaries without running the query twice. Nested subqueries let you aggregate over aggregates cleanly.
Book reference: Chapter 16 — Group By, Having, With, Union All
Free preview: Common Aggregate Functions
with decade_points as (
select extract('year' from
date_trunc('decade', races.date)
)::int as decade,
drivers.driverid,
drivers.surname,
sum(results.points) as points
from results
join races using(raceid)
join drivers using(driverid)
group by decade,
drivers.driverid,
drivers.surname
),
decade_max as (
select decade, max(points) as top
from decade_points
group by decade
)
select dp.decade,
dp.surname as top_scorer,
dp.points
from decade_points dp
join decade_max dm using(decade)
where dp.points = dm.top
order by dp.decade;The highest-scoring F1 driver in each decade, with their total points for that decade.
GROUPING SETS computes multiple GROUP BY combinations in one scan. ROLLUP adds hierarchical subtotals; CUBE adds all cross-tabulations. GROUPING() distinguishes a NULL result from a missing row.
Book reference: Chapter 16 — Group By, Having, With, Union All
select drivers.surname as driver,
constructors.name as constructor,
sum(points) as points
from results
join races using(raceid)
join drivers using(driverid)
join constructors using(constructorid)
where drivers.surname in ('Prost', 'Senna')
group by rollup(
drivers.surname,
constructors.name
);Points for Prost and Senna by constructor, with per-driver subtotals and a grand total.
The FILTER clause computes conditional counts and sums in a single pass — far faster than multiple CASE expressions. Ordered-set aggregates (percentile_cont, mode) extend what GROUP BY can express.
Book reference: Chapter 16 — Group By, Having, With, Union All
Free preview: Common Aggregate Functions · The FILTER Clause
with season_results as (
select constructors.constructorid,
constructors.name as constructor,
results.points,
results.position,
results.milliseconds,
results.raceid
from results
join races using(raceid)
join constructors using(constructorid)
where races.year = 2017
)
select constructor,
count(*) as entries,
count(*) filter(
where position = 1) as wins,
count(*) filter(
where milliseconds is null
and position is null) as dnfs,
sum(points) as total_points
from season_results
group by constructorid, constructor
having count(*) >= 20
order by total_points desc;2017 F1 constructor standings with race entries, wins, DNFs, and total points for teams with 20+ entries.
HashAgg vs SortedAgg, spill to disk, and partial aggregation in parallel plans. This module explains when to pre-aggregate into a materialized view versus aggregating live at query time.
Book reference: Chapter 16 — Group By, Having, With, Union All
explain
select extract(
'year' from
date_trunc('decade', date)
) as decade,
count(*) as races
from races
group by decade;EXPLAIN output for a decade-level race count grouped by a computed decade expression.
Reliable reporting queries stay correct as data grows — no implicit column dependencies, no unstable GROUP BY expressions, behavior verified with realistic data volumes before going to production.
Book reference: Chapter 16 — Group By, Having, With, Union All
Free preview: Percentiles & Ordered-Set Aggregates
with seasons as (
select generate_series(2007, 2017)
as season
),
mclaren_wins as (
select races.year as season,
count(*) as wins
from results
join races using(raceid)
join constructors using(constructorid)
where constructors.name = 'McLaren'
and races.year between 2007 and 2017
and position = 1
group by races.year
)
select s.season,
coalesce(w.wins, 0) as wins
from seasons s
left join mclaren_wins w
using(season)
order by s.season;McLaren's win count for each year from 2007 to 2017, including seasons with zero wins.
MERGE, upsert patterns, and incremental aggregation maintain summary tables at scale. Trade-offs between query-time aggregation, periodic rollups, and streaming pre-aggregation.
Book reference: Chapter 16 — Group By, Having, With, Union All
insert into constructor_season_summary
(season, constructorid, name,
points, races)
select races.year,
constructors.constructorid,
constructors.name,
sum(results.points),
count(distinct results.raceid)
from results
join races using(raceid)
join constructors using(constructorid)
where races.year between 2010 and 2017
group by races.year,
constructors.constructorid,
constructors.name
on conflict (season, constructorid)
do update set
points = excluded.points,
races = excluded.races;Constructor season totals upserted into a summary table, safe to re-run for 2010–2017.
Core, Advanced, and Architect tiers — 48 modules across 6 PostgreSQL courses.