November 2 · Build a complicated answer in readable steps
MaDS Databases & SQL
One question will organize today
Which station-days deserve review because their records are missing, contradictory, or unusually wet?
The challenge is not one new keyword. It is composing a trustworthy answer.
Course source and adaptation
This course follows and adapts Alex Reinhart’s MADS Computing course. Today’s subquery concepts follow his Advanced SQL chapter; CASE, CTE, and set-operation material extends that backbone using current PostgreSQL behavior and the Fall 2026 weather database.
By the end of class
You should be able to:
encode decision rules with CASE,
use scalar, IN, and EXISTS subqueries,
break a query into named CTEs,
combine compatible results with set operations,
test every step before composing the final query.
Complex queries contain smaller questions
Our review queue requires:
Which rows violate simple quality rules?
What is normal precipitation for each station?
Which days are far above that baseline?
How do we combine the reasons without hiding duplicates?
Order rules from most specific or urgent to least.
Always include an ELSE unless NULL is the intended fallback.
Checkpoint 1: classify records
For observations on September 1, label each row as missing precipitation, temperature conflict, no precipitation, or measured. Put quality problems first.
Return station ID, date, precipitation, temperatures, and review_band.
SELECT s.station_id, s.nameFROM stations AS sWHEREEXISTS (SELECT1FROM observations AS oWHERE o.station_id = s.station_idAND o.date>=DATE'2025-01-01'AND o.date<DATE'2026-01-01');
The inner query is correlated with the current station row.
NOT EXISTS expresses missing relationships
SELECT s.station_id, s.nameFROM stations AS sWHERENOTEXISTS (SELECT1FROM observations AS oWHERE o.station_id = s.station_id);
This directly asks for stations with no matching observation.
Be careful with NOT IN
WHERE station_id NOTIN (SELECT station_id FROM observations)
If the subquery can contain NULL, the comparison may become unknown for every row.
Prefer NOT EXISTS for an anti-join unless null behavior is proven safe.
A CTE gives a step a name
WITH station_baseline AS (SELECT station_id, AVG(precip) AS mean_precipFROM observationsWHEREdate>=DATE'2025-01-01'ANDdate<DATE'2026-01-01'AND precip ISNOTNULLGROUPBY station_id)SELECT*FROM station_baseline;
A CTE exists only for this statement.
Multiple CTEs create a readable pipeline
station_baseline ↓scored_days ↓join station names ↓final review queue
Each step should have a clear grain and be runnable on its own while developing.
CTEs are not automatic performance fences
Current PostgreSQL can fold a side-effect-free, one-use CTE into the parent query.
It may materialize a CTE used multiple times.
Use CTEs first for clarity. Use EXPLAIN before making performance claims.
WITH station_baseline AS (SELECT station_id, AVG(precip) AS mean_precipFROM observationsWHEREdate>=DATE'2025-01-01'ANDdate<DATE'2026-01-01'AND precip ISNOTNULLGROUPBY station_id)SELECT*FROM station_baseline;
Result grain: one row per station.
Step 2: score individual days
, scored_days AS (SELECT o.station_id, o.date, o.precip, b.mean_precip,CASEWHEN b.mean_precip >0AND o.precip >=3* b.mean_precip THEN'3x baseline'ELSE'not flagged'ENDAS review_reasonFROM observations AS oJOIN station_baseline AS b USING (station_id)WHERE o.precip ISNOTNULL)
Result grain returns to one row per station-day.
Checkpoint 3: assemble the queue
Finish the two-CTE query. Attach station names, retain only days at least three times their station baseline, and show the largest precipitation values first.
Before running the whole query, run each CTE’s body separately.
Checkpoint 3: final query
WITH station_baseline AS (...),scored_days AS (...)SELECT s.station_id, s.name, d.date, d.precip,ROUND(d.mean_precip::numeric, 1) AS mean_precip, d.review_reasonFROM scored_days AS dJOIN stations AS s USING (station_id)WHERE d.review_reason ='3x baseline'ORDERBY d.precip DESCLIMIT25;
Set operations stack compatible results
query_aUNIONALLquery_b
Both queries need the same number and order of columns, with compatible types.
UNION ALL keeps duplicates; UNION removes identical result rows.
One station-day can appear twice when it needs two kinds of review.
Other set questions have direct operators
Question
Operator
either result
UNION
both results
INTERSECT
in the first but not second
EXCEPT
Use ALL when multiplicity matters and deduplication is not intended.
Project transfer
Rewrite one complicated project query as two or three named steps.
For every step, note its purpose, row grain, a standalone test, and the assumption most likely to fail.
Homework starts here
Save the project-transfer query as the first draft of your query library entry.
Add one adversarial test:
a missing value,
a zero baseline,
a station with no matching row, or
a row satisfying multiple review reasons.
The pattern to keep
split the question → test each piece → name intermediate results → state every grain → compose the pieces → preserve or remove duplicates deliberately → test an edge case
Next: calculate across related rows without collapsing them.