Kapittel 3 · 3B · Forelesning 4 · Lærebok 3.8, 4.1

SQL — joins og subqueries

Sett tabeller sammen, og still spørringer inni andre spørringer. Her er det subtile semantiske valg som avgjør om svaret er riktig.

01 · Hvorfor JOIN

Skjul kartesian-produktet bak en intensjon

Tradisjonelt SQL kombinerer tabeller med FROM r, s WHERE r.k = s.k. Det fungerer, men det blander join-betingelsen sammen med radfilter-betingelsen i WHERE. JOIN ... ON skiller dem og gjør spørringen mer lesbar — og er nødvendig for ytre joins.

Gammel stil
SELECT i.name, t.course_id
FROM instructor i, teaches t
WHERE i.ID = t.ID
  AND i.dept_name = 'CS';
Eksplisitt JOIN
SELECT i.name, t.course_id
FROM instructor i
INNER JOIN teaches t ON i.ID = t.ID
WHERE i.dept_name = 'CS';

Begge gir samme resultat. Den nederste skiller hva som er en match-betingelse (i ON) fra et radfilter (i WHERE). Det blir kritisk for outer joins.

02 · INNER vs OUTER

Fire typer JOIN, fire ulike svar

INNER JOIN
bare match
LEFT JOIN
alt fra venstre
RIGHT JOIN
alt fra høyre
FULL OUTER
alt fra begge

Konkret eksempel

Tabellen instructor har 3 ansatte; teaches har 4 oppføringer der én ansatt mangler i instructor:

instructor (V — venstre)
IDname
10101Srinivasan
22222Einstein
33333Gold
teaches (H — høyre)
IDcourse_id
10101CS-101
10101CS-315
22222PHY-101
99999EE-181
INNER JOIN — bare match i.IDname t.IDcourse_id 10101Srinivasan10101CS-101 10101Srinivasan10101CS-315 22222Einstein22222PHY-101 Gold (V) og 99999/EE-181 (H) forsvinner — ingen match 3 rader i.IDname t.IDcourse_id 10101Srinivasan10101CS-101 10101Srinivasan10101CS-315 22222Einstein22222PHY-101 33333Gold NULLNULL 4 rader (alle 3 fra venstre + matchene) i.IDname t.IDcourse_id 10101Srinivasan10101CS-101 10101Srinivasan10101CS-315 22222Einstein22222PHY-101 NULLNULL99999EE-181 4 rader (alle 4 fra høyre + matchene) i.IDname t.IDcourse_id 10101Srinivasan10101CS-101 10101Srinivasan10101CS-315 22222Einstein22222PHY-101 33333Gold NULLNULL NULLNULL99999EE-181 5 rader (matche + ikke-matche fra venstre + ikke-matche fra høyre)
Klikk gjennom hver join-type for å se hvordan resultatet endrer seg. NULL-radene er der join-typen «utvider» med rader som ikke matcher.
Steg 1 / 4
Når velger jeg hvilken JOIN?
  • INNER JOIN — du bare bryr deg om de radene som matcher. Default.
  • LEFT JOIN — du må vise alle rader fra «hovedtabellen», selv om det ikke finnes noen tilhørende. F.eks. «alle ansatte og hvor mange kurs de underviser» (også de uten kurs).
  • RIGHT JOIN — sjelden i praksis. Kan alltid omskrives som LEFT JOIN ved å bytte tabell-rekkefølge.
  • FULL OUTER JOIN — symmetrisk: når du må fange ikke-matchende fra begge sider. Bra ved data-avstemming.
  • CROSS JOIN — eksplisitt kartesisk produkt. Bruk når du virkelig vil ha alle kombinasjoner (f.eks. dato × ressurs for tomme rapportceller).
Sjekkpunkt · Vanskelig
«Finn ansatte uten kurs» (Gold i eksemplet over). Hvilken spørring fungerer?
LEFT JOIN beholder ansatte uten kurs som rader med NULL fra teaches. WHERE t.ID IS NULL plukker akkurat dem. Mønsteret kalles anti-join.
03 · ON · USING · NATURAL

Tre måter å skrive match-betingelsen på

ON — vilkårlig predikat

Mest fleksibel. Kolonnene fra begge tabellene blir med i resultatet:

SELECT *
FROM course c JOIN department d
  ON c.dept_name = d.dept_name AND d.budget > 1000000;

USING(col) — match på navngitt kolonne

Kort form når kolonnenavnene er like. Kolonnen som matches dukker opp én gang i resultatet — ikke to.

SELECT *
FROM course JOIN department USING (dept_name);

NATURAL JOIN — implisitt USING på alle felles kolonner

Kompakt — og farlig. SQL leter etter alle kolonner som har samme navn, og bruker dem alle som match. En liten skjemaendring kan stille endre semantikken.

SELECT *
FROM course NATURAL JOIN department;
-- Hvis noen senere legger til en kolonne med samme navn
-- i begge tabeller, blir spørringen subtilt feil.
Råd

Bruk ON som default. USING er ok for kjapp ad-hoc-bruk. NATURAL JOIN bør unngås i produksjonskode — den er for skjør.

04 · Self-joins

Når en tabell må joines mot seg selv

Klassisk eksempel: hierarkier. employee(id, name, manager_id), der manager_id peker på en annen rad i samme tabell.

-- Liste opp alle ansatte sammen med navnet på lederen sin
SELECT e.name AS ansatt,
       m.name AS leder
FROM employee e
LEFT JOIN employee m ON e.manager_id = m.id;

Aliaser brukes — uten dem ville id være tvetydig. Tenk på e og m som to «kopier» av samme tabell.

Et annet eksempel: «Lønninger som er høyere enn minst én Biology-ansatt»

SELECT DISTINCT T.name
FROM instructor T, instructor S
WHERE T.salary > S.salary
  AND S.dept_name = 'Biology';

Dette er den klassiske formuleringen i Silberschatz. DISTINCT er nødvendig fordi T kan matche flere S-rader.

05 · Subqueries

Spørring inni en spørring

En subquery er en SELECT skrevet inne i en annen spørring. Den kan stå i WHERE, FROM, eller SELECT.

1. Skalar-subquery — én verdi

-- Snittlønn over alle
SELECT name, salary
FROM instructor
WHERE salary > (SELECT AVG(salary) FROM instructor);

2. Tabell-subquery i FROM (derived table)

SELECT dept_name, max_salary
FROM (
  SELECT dept_name, MAX(salary) AS max_salary
  FROM instructor
  GROUP BY dept_name
) AS per_dept
WHERE max_salary > 90000;

3. Subquery i SELECT (inline)

SELECT dept_name,
       (SELECT AVG(salary)
        FROM instructor i
        WHERE i.dept_name = d.dept_name) AS snitt
FROM department d;

Subqueryen kjører én gang per rad i den ytre spørringen — det gjør den korrelert. Mer om det rett under.

06 · EXISTS · IN · ANY · ALL

Operatorer på subquery-resultater

OperatorTestNULL-trygg?
x IN (subq)Finnes x i resultatet?Nei — NULL i subq lager problemer for NOT IN
EXISTS (subq)Returnerer subq minst én rad?Ja — opererer på antall, ikke verdier
x = ANY (subq)Lik minst én verdiSom IN
x > ALL (subq)Større enn samtligeTrygg, returnerer false hvis NULL
x > ANY (subq)Større enn minst énOK

Tre formuleringer av samme spørsmål

«Finn navn på ansatte som underviser i minst ett emne»:

IN
SELECT name
FROM instructor
WHERE ID IN
  (SELECT ID FROM teaches);
EXISTS (korrelert)
SELECT name
FROM instructor i
WHERE EXISTS (
  SELECT 1 FROM teaches t
  WHERE t.ID = i.ID);
NOT IN-fellen

Hvis subqueryen returnerer minst én NULL, vil x NOT IN (subq) alltid gi UNKNOWN — og dermed ingen rader. Bytt til NOT EXISTS hvis NULL er en mulighet.

-- Trygg variant: ansatte som IKKE underviser noe
SELECT name
FROM instructor i
WHERE NOT EXISTS (
  SELECT 1 FROM teaches t WHERE t.ID = i.ID
);
Sjekkpunkt · Vanskelig
Tabellen t1(x) = {1, 2, 3} og t2(y) = {2, NULL}. Hvor mange rader returnerer SELECT x FROM t1 WHERE x NOT IN (SELECT y FROM t2);?
x NOT IN (2, NULL) gir UNKNOWN for alle x ≠ 2 (fordi vi ikke kan utelukke at x = NULL), og FALSE for x = 2. Ingen rader passerer. Dette er den klassiske NOT IN-fella.
07 · Korrelerte subqueries

Når den indre refererer til den ytre

En korrelert subquery refererer til en kolonne fra den ytre spørringen. Det betyr (logisk) at den må evalueres på nytt for hver ytre rad.

-- For hver avdeling: ansatte med over-snittlig lønn for deres avdeling
SELECT name, dept_name, salary
FROM instructor i
WHERE salary > (
  SELECT AVG(salary)
  FROM instructor i2
  WHERE i2.dept_name = i.dept_name   -- ← korrelasjon
);

Optimalisatoren prøver ofte å skrive om dette til en join + GROUP BY for ytelse, men logisk modell er «for hver rad i, regn ut en avdelingssnitt og sammenlign».

Klassisk: division med NOT EXISTS NOT EXISTS

Spørsmålet «hvilke studenter har tatt alle emner i CS-avdelingen?» kan ikke uttrykkes med vanlig join. Den klassiske SQL-løsningen er dobbel negasjon:

SELECT s.ID
FROM student s
WHERE NOT EXISTS (
  SELECT c.course_id
  FROM course c
  WHERE c.dept_name = 'CS'
    AND NOT EXISTS (
      SELECT 1
      FROM takes t
      WHERE t.ID = s.ID
        AND t.course_id = c.course_id
    )
);

Lest naturlig: «det finnes ikke et CS-emne som studenten ikke har tatt». Dobbel negasjon gir for-alle-kvantor-en SQL ikke har som primitiv.

08 · WITH (CTE)

Common Table Expressions

En WITH-klausul lar deg navngi en subquery og bruke den senere — som en lokal view. Bra for lesbarhet og når samme delspørring brukes flere ganger.

WITH dept_avg AS (
  SELECT dept_name, AVG(salary) AS snitt
  FROM instructor
  GROUP BY dept_name
)
SELECT i.name, i.dept_name, i.salary, d.snitt
FROM instructor i
JOIN dept_avg d USING (dept_name)
WHERE i.salary > d.snitt;

Du kan også definere flere CTE-er etter hverandre, og en CTE kan referere til en tidligere CTE i samme WITH.

Viktig kobling

Den rekursive versjonen av CTE — WITH RECURSIVE — er det som lar SQL uttrykke transitive lukninger som forfedre, naborelasjoner, og graf-traversering. Det dekkes i 3D · Prosedyrer, triggere, rekursjon.

Oppsummering

Det du skal kunne

  1. Velge riktig JOIN-type ut fra hvilke rader som må bevares.
  2. Forklare hva LEFT JOIN ON … WHERE …IS NULL gjør (anti-join).
  3. Skille ON, USING og NATURAL — og vite hvorfor NATURAL er skjør.
  4. Skrive en self-join og forklare hvorfor aliaser er obligatoriske.
  5. Velge mellom IN, EXISTS, ANY, ALL — og kjenne NOT IN-fella med NULL.
  6. Identifisere en korrelert subquery og argumentere for når den kan skrives om til join.
  7. Bruke WITH til å gjøre komplekse spørringer leselige.

Klar for views, transaksjoner og integritet? Gå til 3C →