SQL — grunderna
Kunna skriva SELECT med filter, join och gruppering mot en relationsdatabas.
Förkunskaper
Intuition
SQL är ett språk där du beskriver vad du vill ha, inte hur det ska hämtas. Databasen räknar ut hur.
SELECT klass, avg(poang) AS medel
FROM resultat
WHERE poang IS NOT NULL
GROUP BY klass
HAVING count(*) >= 5
ORDER BY medel DESC
LIMIT 10;
Läs uppifrån och ner, men tänk i den ordning databasen kör:
| Ordning | Nyckelord | Gör |
|---|---|---|
| 1 | FROM / JOIN | vilka tabeller |
| 2 | WHERE | filtrera rader |
| 3 | GROUP BY | slå ihop till grupper |
| 4 | HAVING | filtrera grupper |
| 5 | SELECT | välj kolumner |
| 6 | ORDER BY | sortera |
| 7 | LIMIT | begränsa |
Den ordningen förklarar den vanligaste nybörjarfrågan: varför kan jag inte använda WHERE count(*) > 5? Därför att WHERE körs innan grupperingen finns. Grupper filtreras med HAVING.
Formellt
JOIN-typerna:
| Typ | Behåller |
|---|---|
INNER JOIN | bara rader som matchar i båda |
LEFT JOIN | alla från vänstra, NULL där högra saknas |
RIGHT JOIN | tvärtom |
FULL OUTER JOIN | alla från båda |
Använd LEFT JOIN när du vill behålla alla i vänstertabellen — till exempel alla elever, även de som inte gjort något prov. Ett INNER JOIN skulle tyst kasta bort dem, och det är lätt att inte märka.
NULL beter sig annorlunda än du tror. NULL betyder «okänt», inte «tomt»:
| Uttryck | Resultat |
|---|---|
NULL = NULL | NULL (inte sant!) |
NULL <> 5 | NULL |
x IS NULL | sant/falskt — rätt sätt |
count(*) | räknar alla rader |
count(kolumn) | räknar rader där kolumnen inte är NULL |
avg(kolumn) | hoppar över NULL |
Skillnaden mellan count(*) och count(kolumn) är en klassisk källa till fel siffror i rapporter.
SQL-injektion. Bygg aldrig frågor med stränginterpolation:
cur.execute(f"SELECT * FROM elev WHERE namn = '{namn}'") # ALDRIG
cur.execute("SELECT * FROM elev WHERE namn = %s", (namn,)) # rätt
Med parametrar skickas värdet separat från frågan, och databasen kan aldrig tolka det som kod. Det är inte en optimering utan den enda korrekta metoden — och det gäller även när värdet «kommer inifrån systemet», eftersom det sällan förblir så.
När SQL och när Pandas? Filtrera och aggregera i databasen — den är byggd för det och du slipper flytta data. Hämta hem det som blir kvar och gör det sista i Pandas. Att hämta en miljon rader för att räkna ett medelvärde är nästan alltid fel väg.
Kod
-- Tabeller
CREATE TABLE elev (id int primary key, namn text, klass text);
CREATE TABLE prov (id int primary key, elev_id int references elev(id),
amne text, poang int, datum date);
-- Alla elever med sitt snitt — även de utan prov (LEFT JOIN)
SELECT e.namn,
e.klass,
count(p.id) AS antal_prov,
round(avg(p.poang), 1) AS snitt
FROM elev e
LEFT JOIN prov p ON p.elev_id = e.id
GROUP BY e.id, e.namn, e.klass
ORDER BY snitt DESC NULLS LAST;
-- Klasser med minst 5 prov och snitt över 70
SELECT e.klass, count(*) AS n, round(avg(p.poang), 1) AS snitt
FROM prov p JOIN elev e ON e.id = p.elev_id
WHERE p.poang IS NOT NULL -- filtrerar RADER
GROUP BY e.klass
HAVING count(*) >= 5 AND avg(p.poang) > 70 -- filtrerar GRUPPER
ORDER BY snitt DESC;
-- count(*) mot count(kolumn): olika svar när det finns NULL
SELECT count(*) AS alla_rader, count(poang) AS med_poang FROM prov;
# Från Python — alltid med parametrar
import psycopg
with psycopg.connect(dsn) as conn, conn.cursor() as cur:
cur.execute(
"""SELECT e.klass, round(avg(p.poang), 1)
FROM prov p JOIN elev e ON e.id = p.elev_id
WHERE p.datum >= %s
GROUP BY e.klass
HAVING count(*) >= %s""",
("2026-01-01", 5),
)
for klass, snitt in cur.fetchall():
print(klass, snitt)
Behärskning innebär
- Skriver SELECT med WHERE och ORDER BY
- Använder JOIN mellan tabeller
- Grupperar med GROUP BY och HAVING
Logga in för att göra övningarna och bygga upp din behärskning.
Källor
- PostgreSQL — dokumentation — PostgreSQL-licens (BSD-liknande)
- Wikipedia — SQL (CC BY-SA 4.0) — CC BY-SA 4.0
- OWASP — SQL Injection Prevention Cheat Sheet — CC BY-SA 4.0