Normalisering
Normalisering är processen att strukturera en relationsdatabas för att minska dataredundans och förbättra dataintegritet.
Motivation — varför normalisera?
Utan normalisering riskerar du:
- Upprepad data — samma information lagras på flera ställen
- Uppdateringsanomalier — du måste ändra på flera ställen vid en uppdatering
- Insertionsanomalier — du kan inte lägga till data utan att skapa ofullständiga rader
- Deletionsanomalier — du förlorar information när du tar bort en rad
1NF — Första normalformen
En tabell är i 1NF om varje kolumn innehåller atomära värden (ett värde per cell).
Regler:
- Varje cell innehåller ett enda värde
- Alla värden i en kolumn är av samma typ
- Varje rad är unikt identifierbar (primärnyckel)
Problemet
users
┌─────────┬────────┬─────────────────────┐
│ user_id │ name │ phones │
├─────────┼────────┼─────────────────────┤
│ 1 │ Anna │ 070-111, 070-222 │ ← Flera värden i en cell
│ 2 │ Bo │ 073-333 │
└─────────┴────────┴─────────────────────┘
Kolumnen phones bryter mot 1NF — den innehåller en kommaseparerad lista i en enda cell.
Lösningen
Flytta ut telefonnumren till en egen tabell med en rad per nummer.
erDiagram USERS { int user_id PK string name } USER_PHONES { int id PK int user_id FK string phone } USERS ||--o{ USER_PHONES : "har"
CREATE TABLE users (
user_id INT PRIMARY KEY,
name VARCHAR(100)
);
CREATE TABLE user_phones (
id INT PRIMARY KEY,
user_id INT REFERENCES users(user_id),
phone VARCHAR(20)
);
-- Varje telefonnummer är nu sin egen rad
INSERT INTO user_phones VALUES (1, 1, '070-111');
INSERT INTO user_phones VALUES (2, 1, '070-222');
2NF — Andra normalformen
En tabell är i 2NF om den är i 1NF och varje icke-nyckelkolumn är fullt funktionellt beroende av hela primärnyckeln (inget partiellt beroende).
Partiellt beroende uppstår bara när primärnyckeln är sammansatt (flera kolumner).
Problemet
order_details har sammansatt primärnyckel (order_id, product_id), men product_name beror bara på product_id — inte på hela nyckeln.
graph LR
subgraph PK["Sammansatt primärnyckel"]
oid([order_id])
pid([product_id])
end
oid --> qty([quantity])
pid --> qty
pid --> pname([product_name partiellt])
style PK fill:#f0f4ff,stroke:#9999bb
style pname fill:#ffe0e0,stroke:#cc6666
style qty fill:#e0ffe0,stroke:#66aa66
-- Dåligt — product_name beror bara på product_id
CREATE TABLE order_details (
order_id INT,
product_id INT,
quantity INT,
product_name VARCHAR(100), -- Beror INTE på order_id
PRIMARY KEY (order_id, product_id)
);
Lösningen
Separera entiteterna: beställningsrader i en tabell, produktinformation i en annan.
erDiagram ORDERS { int order_id PK date created_at } ORDER_ITEMS { int order_id PK "FK" int product_id PK "FK" int quantity } PRODUCTS { int id PK string name decimal price } ORDERS ||--o{ ORDER_ITEMS : "innehåller" PRODUCTS ||--o{ ORDER_ITEMS : "ingår i"
-- Bra — separerade tabeller
CREATE TABLE order_items (
order_id INT REFERENCES orders(order_id),
product_id INT REFERENCES products(id),
quantity INT,
PRIMARY KEY (order_id, product_id)
);
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(100),
price DECIMAL(10,2)
);
3NF — Tredje normalformen
En tabell är i 3NF om den är i 2NF och inga icke-nyckelkolumner är transitivt beroende av primärnyckeln.
Transitivt beroende: A → B → C
A är primärnyckeln, C beror på B, inte direkt på A.
Problemet
tasks har ett enkelt primärnyckel id, men category_name beror på category_id (inte på id) och user_name beror på user_id (inte på id).
flowchart LR id([id]) title([title]) cid([category_id]) uid([user_id]) cname([category_name]) uname([user_name]) id --> title id --> cid id --> uid cid --> cname uid --> uname id -.-> cname id -.-> uname
-- Dåligt — transitiva beroenden
CREATE TABLE tasks (
id INT PRIMARY KEY,
title VARCHAR(100),
category_id INT,
category_name VARCHAR(50), -- Beror på category_id, inte på id
user_id INT,
user_name VARCHAR(100) -- Beror på user_id, inte på id
);
Lösningen
Extrahera varje transitivt beroende till sin egen tabell.
erDiagram TASKS { int id PK string title int category_id FK int user_id FK } CATEGORIES { int id PK string name } USERS { int id PK string name string email } TASKS }o--|| CATEGORIES : "tillhör" TASKS }o--|| USERS : "ägs av"
-- Bra — separerade tabeller
CREATE TABLE tasks (
id INT PRIMARY KEY,
title VARCHAR(100),
category_id INT REFERENCES categories(id),
user_id INT REFERENCES users(id)
);
CREATE TABLE categories (
id INT PRIMARY KEY,
name VARCHAR(50)
);
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100)
);
BCNF — Boyce-Codd Normalform
En tabell är i BCNF om den är i 3NF och varje determinant (vänsterled i ett funktionellt beroende) är en supernyckel.
BCNF är en striktare version av 3NF. De flesta tabeller i 3NF är också i BCNF — men inte alla.
Problemet
Antaganden:
- En student kan gå många kurser
- En kurs kan ha flera lärare
- En lärare undervisar bara en kurs
Det ger oss dessa beroenden:
graph LR sk(["student_id + course_id"]) teacher([teacher]) course(["course_id"]) sk --> teacher teacher --> course sk -. "teacher är inte supernyckel" .-> course style teacher fill:#ffe0e0,stroke:#c66 style course fill:#fff3cd,stroke:#c96
teacher bestämmer course_id, men teacher är inte en supernyckel — vilket bryter mot BCNF.
-- 3NF men INTE BCNF
-- Beroenden: (student_id, course_id) → teacher
-- teacher → course_id
CREATE TABLE enrollment (
student_id INT,
course_id INT,
teacher VARCHAR(100),
PRIMARY KEY (student_id, course_id)
);
Lösningen
Dela upp tabellen så att teacher → course_id hamnar i en separat tabell med teacher som nyckel.
erDiagram STUDENTS { int student_id PK string name } ENROLLMENT { int student_id PK "FK" int course_id PK "FK" } COURSE_TEACHER { int course_id PK string teacher } COURSES { int id PK string title } STUDENTS ||--o{ ENROLLMENT : "deltar i" ENROLLMENT }o--|| COURSE_TEACHER : "via" COURSE_TEACHER ||--|| COURSES : "kopplas till"
-- BCNF — alla determinanter är supernycklar
CREATE TABLE enrollment (
student_id INT REFERENCES students(student_id),
course_id INT REFERENCES course_teacher(course_id),
PRIMARY KEY (student_id, course_id)
);
CREATE TABLE course_teacher (
course_id INT PRIMARY KEY REFERENCES courses(id),
teacher VARCHAR(100)
);
Denormalisering — när bryta mot reglerna?
Normalisering är inte alltid rätt val. Denormalisering innebär att medvetet lägga till redundans för att förbättra läsprestanda.
| Skäl att denormalisera | Exempel |
|---|---|
| Läsprestanda | Undvik dyra joins på heta frågor |
| Rapportering | Förberäknade aggregat för dashboards |
| Skalbarhet | Distribuerade system där joins är omöjliga |
| Cache | Materialiserade vyer |
erDiagram ORDERS { int order_id PK int customer_id FK date created_at } ORDER_ITEMS { int order_id FK int product_id FK int quantity decimal unit_price } ORDER_SUMMARY { int order_id PK string customer_name "redundant" decimal total "förberäknat" int item_count "förberäknat" } ORDERS ||--o{ ORDER_ITEMS : "innehåller" ORDERS ||--|| ORDER_SUMMARY : "sammanfattas i"
-- Denormaliserad tabell för snabb åtkomst utan joins
CREATE TABLE order_summary (
order_id INT PRIMARY KEY,
customer_name VARCHAR(100), -- Redundans — finns i customers
total DECIMAL(10,2), -- Förberäknat — finns i order_items
item_count INT -- Förberäknat — finns i order_items
);
Regel: Normalisera för skrivprestanda och dataintegritet. Denormalisera för läsprestanda — men mät alltid först.
Sammanfattning
| Normalform | Krav | Problem det löser | Åtgärd |
|---|---|---|---|
| 1NF | Atomära värden, unik primärnyckel | Listor och kommaseparerade värden | Dela upp till separata rader/tabeller |
| 2NF | 1NF + inga partiella beroenden | Kolumner som bara beror på del av sammansatt nyckel | Separera entiteter till egna tabeller |
| 3NF | 2NF + inga transitiva beroenden | Kolumner som beror på en annan icke-nyckelkolumn | Extrahera beroende data till egen tabell |
| BCNF | 3NF + alla determinanter är supernycklar | Överlappande kandidatnycklar | Dela upp vid icke-supernyckel-determinanter |
graph TD raw["Onormaliserad tabell\n(redundans, anomalier)"] nf1["1NF\nAtomära värden"] nf2["2NF\nInget partiellt beroende"] nf3["3NF\nInget transitivt beroende"] bcnf["BCNF\nAlla determinanter är supernycklar"] raw -->|"Dela upp\nlistor"| nf1 nf1 -->|"Separera\nentiteter"| nf2 nf2 -->|"Extrahera\nberoenden"| nf3 nf3 -->|"Hantera\növerlappande nycklar"| bcnf style raw fill:#ffe0e0,stroke:#c66 style nf1 fill:#fff3cd,stroke:#c96 style nf2 fill:#d4edda,stroke:#5a8 style nf3 fill:#cce5ff,stroke:#46c style bcnf fill:#e2d9f3,stroke:#86c
Av Victor Hernandez från Bytebase.se