Lær JOIN, GROUP BY, HAVING og subqueries for komplekse dataspørringer.
SQL – avanserte spørringer og sammenkoblinger
Hittil har vi jobbet med data fra én tabell om gangen. Men den virkelige kraften i relasjonsdatabaser ligger i muligheten til å koble sammen data fra flere tabeller. Tenk deg at du vil se elevnavn sammen med fagnanvn og karakterer – den informasjonen er fordelt over tre tabeller (elever, fag og karakterer). For å sette dette sammen trenger vi JOIN.
I tillegg skal vi lære å beregne statistikk med aggregeringsfunksjoner (telle, summere, finne gjennomsnitt), gruppere data med GROUP BY, filtrere grupper med HAVING, og bruke underspørringer for å løse komplekse dataspørsmål.
Disse teknikkene er det som gjør SQL til et kraftig analyseverktøy. Med dem kan du svare på spørsmål som «Hva er gjennomsnittskarakteren per fag?», «Hvilke elever har karakter over gjennomsnittet?» og «Hvilken klasse har flest elever med toppkarakter?».
En JOIN kobler rader fra to tabeller basert på en felles kolonne. Den vanligste formen er INNER JOIN, som returnerer bare rader der det finnes en match i begge tabellene.
Syntaks for INNER JOIN
SELECT kolonne1, kolonne2, ...
FROM tabell1
INNER JOIN tabell2 ON tabell1.kolonne = tabell2.kolonne;Eksempel: Elever med klassenavn
I stedet for å se bare klasse_id (som er et tall), vil vi se klassenavnet:
SELECT elever.fornavn, elever.etternavn, klasser.klassenavn
FROM elever
INNER JOIN klasser ON elever.klasse_id = klasser.klasse_id;Resultat:
| fornavn | etternavn | klassenavn |
|---|---|---|
| Emma | Hansen | 10A |
| Oliver | Johansen | 10A |
| Sofie | Berg | 10A |
| Nora | Olsen | 10B |
| Jakob | Larsen | 10B |
| Noah | Andreassen | 10B |
| Liam | Dahl | 10C |
| Ella | Nilsen | 10C |
Tabellaliaser
For å gjøre spørringene kortere kan vi gi tabellene aliaser:
SELECT e.fornavn, e.etternavn, k.klassenavn
FROM elever e
INNER JOIN klasser k ON e.klasse_id = k.klasse_id;Her er e et alias for elever og k et alias for klasser.La oss koble elever, karakterer og fag for å se en komplett karakteroversikt med navn:
SELECT
e.fornavn,
e.etternavn,
f.fagnavn,
k.karakter,
k.termin
FROM karakterer k
INNER JOIN elever e ON k.elev_id = e.elev_id
INNER JOIN fag f ON k.fag_id = f.fag_id
ORDER BY e.etternavn, f.fagnavn;Resultat (utdrag):
| fornavn | etternavn | fagnavn | karakter | termin |
|---|---|---|---|---|
| Noah | Andreassen | Matematikk | 3 | H2024 |
| Noah | Andreassen | Norsk | 2 | H2024 |
| Sofie | Berg | Matematikk | 5 | H2024 |
| Sofie | Berg | Naturfag | 4 | H2024 |
| Sofie | Berg | Norsk | 5 | H2024 |
| ... | ... | ... | ... | ... |
Her kobler vi tre tabeller:
karakterer med elever (via elevid), og karakterer med fag (via fagid). Resultatet gir oss lesbare navn i stedet for ID-nummer.Det finnes flere typer JOIN:
INNER JOIN – Returnerer bare rader der det finnes match i begge tabellene. Rader uten match utelates.
LEFT JOIN (LEFT OUTER JOIN) – Returnerer alle rader fra venstre tabell, og matchende rader fra høyre tabell. Hvis ingen match finnes, fylles høyre side med NULL.
RIGHT JOIN (RIGHT OUTER JOIN) – Returnerer alle rader fra høyre tabell, og matchende rader fra venstre. Motsatt av LEFT JOIN. (Merk: RIGHT JOIN støttes ikke i SQLite, men kan simuleres ved å bytte tabellrekkefølgen med LEFT JOIN.)
FULL OUTER JOIN – Returnerer alle rader fra begge tabellene, med NULL der det ikke er match. (Støttes heller ikke direkte i SQLite.)
INNER JOIN viser bare elever som har karakterer. Med LEFT JOIN får vi med alle elever, selv de som mangler karakterer:
SELECT e.fornavn, e.etternavn, f.fagnavn, k.karakter
FROM elever e
LEFT JOIN karakterer k ON e.elev_id = k.elev_id
LEFT JOIN fag f ON k.fag_id = f.fag_id
ORDER BY e.etternavn;Hvis en elev ikke har noen karakterer, vises NULL i kolonnene fagnavn og karakter:
| fornavn | etternavn | fagnavn | karakter |
|---|---|---|---|
| Emma | Hansen | Matematikk | 5 |
| Emma | Hansen | Norsk | 4 |
| Emma | Hansen | Naturfag | 5 |
| Maja | Strand | NULL | NULL |
Her ser vi at Maja Strand (som vi satte inn tidligere) ikke har noen karakterer ennå. Med INNER JOIN ville Maja vært usynlig i resultatet.
Tommelfingerregel: Bruk INNER JOIN når du bare vil ha rader som matcher i begge tabeller. Bruk LEFT JOIN når du vil beholde alle rader fra den «venstre» tabellen, uansett om de har matchende rader i den andre.
Aggregeringsfunksjoner beregner én verdi basert på mange rader. De vanligste er:
| Funksjon | Beskrivelse | Eksempel |
|---|---|---|
COUNT() | Teller antall rader | COUNT(*) eller COUNT(kolonne) |
SUM() | Summerer verdier | SUM(karakter) |
AVG() | Beregner gjennomsnitt | AVG(karakter) |
MIN() | Finner laveste verdi | MIN(karakter) |
MAX() | Finner høyeste verdi | MAX(karakter) |
Eksempler
-- Antall elever i databasen
SELECT COUNT(*) AS antall_elever FROM elever;Resultat: 8-- Gjennomsnittskarakter for alle
SELECT AVG(karakter) AS gjennomsnitt FROM karakterer;Resultat: 4.0 (ca.)-- Høyeste og laveste karakter
SELECT MAX(karakter) AS hoyeste, MIN(karakter) AS laveste FROM karakterer;Resultat: hoyeste = 6, laveste = 2
-- Antall karakterer registrert
SELECT COUNT(*) AS antall_karakterer FROM karakterer;Legg merke til AS – det gir kolonnen i resultatet et forståelig navn (alias).GROUP BY grupperer rader med like verdier slik at aggregeringsfunksjoner beregnes per gruppe.Eksempel: Antall elever per klasse
SELECT klasse_id, COUNT(*) AS antall_elever
FROM elever
GROUP BY klasse_id;Resultat:
| klasseid | antallelever |
|---|---|
| 1 | 3 |
| 2 | 3 |
| 3 | 2 |
Eksempel: Gjennomsnittskarakter per fag
SELECT f.fagnavn, AVG(k.karakter) AS snittkarakter
FROM karakterer k
INNER JOIN fag f ON k.fag_id = f.fag_id
GROUP BY f.fagnavn;Resultat:| fagnavn | snittkarakter |
|---|---|
| Matematikk | 4.0 |
| Naturfag | 4.67 |
| Norsk | 3.75 |
Eksempel: Antall fag per elev
SELECT e.fornavn, e.etternavn, COUNT(*) AS antall_fag
FROM karakterer k
INNER JOIN elever e ON k.elev_id = e.elev_id
GROUP BY e.elev_id, e.fornavn, e.etternavn;Resultat:
| fornavn | etternavn | antall_fag |
|---|---|---|
| Emma | Hansen | 3 |
| Oliver | Johansen | 3 |
| Nora | Olsen | 3 |
| Jakob | Larsen | 2 |
| ... | ... | ... |
HAVING filtrerer grupper etter at GROUP BY har gjort sin jobb. Det er til grupper det WHERE er til individuelle rader.Viktig forskjell:
- WHERE filtrerer rader før gruppering
- HAVING filtrerer grupper etter gruppering
Eksempel: Fag med gjennomsnittskarakter over 4
SELECT f.fagnavn, AVG(k.karakter) AS snittkarakter
FROM karakterer k
INNER JOIN fag f ON k.fag_id = f.fag_id
GROUP BY f.fagnavn
HAVING AVG(k.karakter) > 4;Resultat:
| fagnavn | snittkarakter |
|---|---|
| Naturfag | 4.67 |
Eksempel: Elever med mer enn 2 fag
SELECT e.fornavn, e.etternavn, COUNT(*) AS antall_fag
FROM karakterer k
INNER JOIN elever e ON k.elev_id = e.elev_id
GROUP BY e.elev_id, e.fornavn, e.etternavn
HAVING COUNT(*) > 2;Kombinere WHERE og HAVING
-- Gjennomsnittskarakter per fag for termin H2024,
-- men bare fag der snittet er over 3.5
SELECT f.fagnavn, AVG(k.karakter) AS snittkarakter, COUNT(*) AS antall
FROM karakterer k
INNER JOIN fag f ON k.fag_id = f.fag_id
WHERE k.termin = 'H2024'
GROUP BY f.fagnavn
HAVING AVG(k.karakter) > 3.5;Her filtrerer WHERE først ut bare H2024-karakterer, deretter grupperer GROUP BY etter fag, og til slutt filtrerer HAVING bort grupper med snitt under 3.5.
En fullstendig SELECT-setning har klausulene i denne rekkefølgen:
SELECT kolonner -- 1. Velg kolonner
FROM tabell -- 2. Fra tabell(er)
JOIN tabell2 ON ... -- 3. Koble med andre tabeller
WHERE betingelse -- 4. Filtrer rader
GROUP BY kolonne -- 5. Grupper rader
HAVING betingelse -- 6. Filtrer grupper
ORDER BY kolonne -- 7. Sorter resultatet
LIMIT antall; -- 8. Begrens antall raderDenne rekkefølgen må alltid følges i SQL-koden. Databasen utfører dem derimot i en annen logisk rekkefølge: FROM/JOIN, deretter WHERE, GROUP BY, HAVING, SELECT, ORDER BY, og til slutt LIMIT.
En subquery (underspørring) er en SELECT-setning som er nestet inne i en annen spørring. Underspørringen kjøres først, og resultatet brukes av den ytre spørringen.
Subquery i WHERE
-- Finn alle elever med karakter høyere enn gjennomsnittet
SELECT e.fornavn, e.etternavn, k.karakter
FROM karakterer k
INNER JOIN elever e ON k.elev_id = e.elev_id
WHERE k.karakter > (SELECT AVG(karakter) FROM karakterer);Her beregner underspørringen først gjennomsnittskarakteren, og den ytre spørringen bruker dette resultatet for å filtrere.
Subquery med IN
-- Finn alle elever som har karakter 6 i et eller annet fag
SELECT fornavn, etternavn
FROM elever
WHERE elev_id IN (SELECT elev_id FROM karakterer WHERE karakter = 6);Underspørringen finner alle elev_id-er med karakter 6, og den ytre spørringen henter navnene til disse elevene.
Subquery i FROM (avledede tabeller)
-- Finn elevenes gjennomsnittskarakter og vis bare de med snitt over 4
SELECT fornavn, etternavn, snitt
FROM (
SELECT e.fornavn, e.etternavn, AVG(k.karakter) AS snitt
FROM karakterer k
INNER JOIN elever e ON k.elev_id = e.elev_id
GROUP BY e.elev_id, e.fornavn, e.etternavn
) AS elevsnitt
WHERE snitt > 4;Her lager underspørringen en midlertidig tabell (elevsnitt) med gjennomsnitt per elev, og den ytre spørringen filtrerer denne.
La oss bruke alle teknikkene vi har lært for å lage en omfattende analyse:
1. Karakteroversikt med klassenavn og fagnavn:
SELECT kl.klassenavn, e.fornavn, e.etternavn,
f.fagnavn, k.karakter
FROM karakterer k
INNER JOIN elever e ON k.elev_id = e.elev_id
INNER JOIN fag f ON k.fag_id = f.fag_id
INNER JOIN klasser kl ON e.klasse_id = kl.klasse_id
ORDER BY kl.klassenavn, e.etternavn, f.fagnavn;2. Beste elev per fag:
SELECT f.fagnavn, e.fornavn, e.etternavn, k.karakter
FROM karakterer k
INNER JOIN elever e ON k.elev_id = e.elev_id
INNER JOIN fag f ON k.fag_id = f.fag_id
WHERE k.karakter = (
SELECT MAX(k2.karakter)
FROM karakterer k2
WHERE k2.fag_id = k.fag_id
);3. Klassestatistikk:
SELECT kl.klassenavn,
COUNT(DISTINCT e.elev_id) AS antall_elever,
ROUND(AVG(k.karakter), 2) AS snittkarakter,
MIN(k.karakter) AS laveste,
MAX(k.karakter) AS hoyeste
FROM klasser kl
INNER JOIN elever e ON kl.klasse_id = e.klasse_id
INNER JOIN karakterer k ON e.elev_id = k.elev_id
GROUP BY kl.klassenavn
ORDER BY snittkarakter DESC;ROUND(AVG(k.karakter), 2) avrunder gjennomsnittet til 2 desimaler, og COUNT(DISTINCT e.elev_id) teller unike elever (ikke antall karakterrader).
Når du kobler flere tabeller, kan kolonnenavn bli tvetydige. For eksempel har både elever og karakterer en kolonne elev_id. Bruk tabellaliaser for å unngå forvirring:
-- Uten alias (langt og uoversiktlig):
SELECT elever.fornavn, karakterer.karakter
FROM elever INNER JOIN karakterer ON elever.elev_id = karakterer.elev_id;
-- Med alias (kort og ryddig):
SELECT e.fornavn, k.karakter
FROM elever e
INNER JOIN karakterer k ON e.elev_id = k.elev_id;Velg korte, meningsfulle aliaser: e for elever, k for karakterer, f for fag, kl for klasser osv.
Hva gjør en INNER JOIN?
Hvilken aggregeringsfunksjon beregner gjennomsnittet av verdier i en kolonne?
Hva er forskjellen mellom WHERE og HAVING?
Skriv SQL-spørringer for følgende oppgaver (bruk skoledatabasen):
a) Vis fornavn, etternavn og klassenavn for alle elever (bruk JOIN)
b) Beregn gjennomsnittskarakteren per elev (vis fornavn og gjennomsnitt)
c) Tell antall elever per klasse (vis klassenavn og antall)
Hva returnerer denne spørringen?
SELECT f.fagnavn, COUNT(*) AS antall
FROM karakterer k
INNER JOIN fag f ON k.fag_id = f.fag_id
GROUP BY f.fagnavn
HAVING COUNT(*) > 5;Skriv SQL-spørringer for følgende oppgaver:
a) Vis elevnavn, fagnavn og karakter for alle karakterer som er over gjennomsnittskarakteren (bruk subquery)
b) Finn alle elever som har fått karakter 6 i minst ett fag – vis navnet og faget de fikk 6 i
c) Vis klassestatistikk: klassenavn, antall elever, gjennomsnittskarakter (avrundet til 1 desimal), høyeste og laveste karakter – men bare for klasser med gjennomsnitt over 3.5
Hva er forskjellen mellom INNER JOIN og LEFT JOIN i denne situasjonen?
Du har 8 elever i elevtabellen, men bare 6 av dem har karakterer i karakterer-tabellen.
-- Spørring A:
SELECT e.fornavn FROM elever e INNER JOIN karakterer k ON e.elev_id = k.elev_id;
-- Spørring B:
SELECT e.fornavn FROM elever e LEFT JOIN karakterer k ON e.elev_id = k.elev_id;Du har nettbutikk-databasen fra kapittel 6.2. Skriv SQL-spørringer for:
a) Vis alle bestillinger med kundenavn (fornavn og etternavn), bestillingsdato og totalpris – sortert etter dato
b) Finn total omsetning (sum av alle totalpris) per kunde – vis bare kunder med total omsetning over 1000 kr
c) Finn den mest bestilte produktet (produktet som har flest bestillingslinjer). Vis produktnavn og antall ganger det er bestilt.
Oppsummering
I dette kapittelet har du lært:
- JOIN: kobler sammen data fra flere tabeller.
- Typer JOIN: INNER, LEFT, RIGHT og FULL.
- Aggregeringsfunksjoner: COUNT, SUM, AVG, MIN og MAX.
- GROUP BY og HAVING: grupperer og filtrerer grupper.
- Subqueries: underspoerringer for komplekse spørringer.
Noekkelbegreper
| Begrep | Forklaring |
|---|---|
| JOIN | Kobler data fra flere tabeller |
| GROUP BY | Grupperer rader for aggregering |
| Subquery | Spørring inne i en annen spørring |
Dette kapitlet er skrevet av Anthropics toppmodeller (Claude Opus og Claude Fable) og er foreløpig ikke manuelt gjennomgått — kvalitetskontrollen gjøres av uavhengige KI-agenter, og innmeldte feil rettes fortløpende. Funnet en feil? Meld fra, så retter vi den. Les mer om hvordan innholdet lages.