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 denormaliseraExempel
LäsprestandaUndvik dyra joins på heta frågor
RapporteringFörberäknade aggregat för dashboards
SkalbarhetDistribuerade system där joins är omöjliga
CacheMaterialiserade 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

NormalformKravProblem det löserÅtgärd
1NFAtomära värden, unik primärnyckelListor och kommaseparerade värdenDela upp till separata rader/tabeller
2NF1NF + inga partiella beroendenKolumner som bara beror på del av sammansatt nyckelSeparera entiteter till egna tabeller
3NF2NF + inga transitiva beroendenKolumner som beror på en annan icke-nyckelkolumnExtrahera beroende data till egen tabell
BCNF3NF + alla determinanter är supernycklarÖverlappande kandidatnycklarDela 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