A foreign key points to a valid row in another table.
One station can have many observations.
A join builds a virtual table
SELECT s.station_id, s.name, o.date, o.precipFROM observations AS oJOIN stations AS sON o.station_id = s.station_id;
ON states how rows correspond.
Aliases keep multi-table queries readable
FROM observations AS oJOIN stations AS sON o.station_id = s.station_id
Then qualify columns:
s.nameo.dateo.precip
Aliases shorten names; they do not create new tables.
INNER JOIN keeps matches
SELECT s.name, o.date, o.precipFROM observations AS oINNERJOIN stations AS sON o.station_id = s.station_id;
Only rows satisfying the ON condition survive.
Writing JOIN alone means INNER JOIN.
Filters apply to the joined rows
SELECT s.name, o.date, o.precipFROM observations AS oJOIN stations AS sON o.station_id = s.station_idWHERE s.state ='PA'AND o.date=DATE'2025-09-01'AND o.precip ISNOTNULL;
Columns from either table can appear in WHERE.
Checkpoint 1: combine facts
Return station ID, station name, date, and precipitation for Pennsylvania observations on September 1, 2025. Exclude unknown precipitation and show the wettest first.
Use aliases s and o.
Checkpoint 1: one solution
SELECT s.station_id, s.name, o.date, o.precipFROM observations AS oJOIN stations AS sON o.station_id = s.station_idWHERE s.state ='PA'AND o.date=DATE'2025-09-01'AND o.precip ISNOTNULLORDERBY o.precip DESC, s.name;
The result grain follows the many side
Without the date filter:
SELECT s.station_id, s.name, o.dateFROM stations AS sJOIN observations AS oON s.station_id = o.station_id;
One station appears once for every matching observation.
That is expected multiplication—not automatically an error.
Count before trusting a join
SELECTCOUNT(*) FROM stations;SELECTCOUNT(*)FROM stations AS sJOIN observations AS oON s.station_id = o.station_id;
If the row count changes, explain why.
Never assume a join preserves one row per input row.
LEFT JOIN preserves the left table
SELECT s.station_id, s.name, o.precipFROM stations AS sLEFTJOIN observations AS oON s.station_id = o.station_idAND o.date=DATE'2025-09-01'WHERE s.state ='PA';
Every Pennsylvania station remains, even without a match.
Unmatched right-side fields become NULL
station_id name precip----------- ------------------- ------USW...001 PITTSBURGH ASOS 12.4USW...002 SOME STATION NULL
The second row may mean no observation row—or a row with missing precipitation.
Those are different data-quality states.
Checkpoint 2: preserve stations
Return every Pennsylvania station and its precipitation on September 1, 2025, if an observation exists. Keep stations with no observation.
Then adapt the query to show only stations with no observation row on that date.
Checkpoint 2: preserve every station
SELECT s.station_id, s.name, o.precipFROM stations AS sLEFTJOIN observations AS oON s.station_id = o.station_idAND o.date=DATE'2025-09-01'WHERE s.state ='PA'ORDERBY s.name;
Checkpoint 2: find no matching row
SELECT s.station_id, s.nameFROM stations AS sLEFTJOIN observations AS oON s.station_id = o.station_idAND o.date=DATE'2025-09-01'WHERE s.state ='PA'AND o.station_id ISNULLORDERBY s.name;
Test a non-nullable right-side key to identify no match.
A right-table filter can undo a left join
FROM stations AS sLEFTJOIN observations AS oON s.station_id = o.station_idWHERE o.date=DATE'2025-09-01'
Unmatched rows have o.date = NULL, so WHERE removes them.
The query behaves like an inner join for that condition.
Put match rules in ON
FROM stations AS sLEFTJOIN observations AS oON s.station_id = o.station_idAND o.date=DATE'2025-09-01'WHERE s.state ='PA'
ON: which right-side rows count as matches?
WHERE: which completed result rows should remain?
Checkpoint 3: repair silent data loss
This query claims to retain every Pennsylvania station. Repair it, then explain what must be unique for the result to contain at most one row per station.
SELECT s.station_id, s.name, o.precipFROM stations AS sLEFTJOIN observations AS oON s.station_id = o.station_idWHERE s.state ='PA'AND o.date=DATE'2025-09-01';
Checkpoint 3: repaired
SELECT s.station_id, s.name, o.precipFROM stations AS sLEFTJOIN observations AS oON s.station_id = o.station_idAND o.date=DATE'2025-09-01'WHERE s.state ='PA';
Required design rule:
UNIQUE (station_id, date)
Many-to-many joins need a bridge
A storm can affect many stations. A station can be affected by many storms.
One row represents one storm–station–date relationship.
Diagnose a suspicious join
Before accepting a result, ask:
What does one row represent now?
Which table is preserved?
Can either key repeat?
Did a WHERE condition remove unmatched rows?
Did the row count change as expected?
Project transfer
Join two project tables. Diagnose loss and multiplication.
Include:
relationship, keys, and grains,
pre-join and post-join counts,
one unmatched-row diagnostic.
Assignment 1: finish with evidence
Before submitting tomorrow:
run every query from a clean connection,
include a small result sample,
state the grain beside each query,
explain one denominator or join risk,
keep today’s checkpoint attempts in the file.
The pattern to keep
name each table's grain → identify the keys → choose which table must survive → write the match condition → count the result → inspect unmatched rows → explain multiplication or loss
Next: design a schema that makes correct joins easier.