# Databasenormalisering

## Hva det er

Databasenormalisering er praksisen med å legge opp en relasjonsdatabase slik at
hvert faktum lagres ett sted, i tabellen det beskriver, og alt annet viser til
det med nøkkelen. Mange sier også normalisering av databaser, eller bare
normalisering når sammenhengen gjør det klart. En kundes adresse står i én rad i
en kundetabell; en ordre har bare kundens nøkkel, og en join setter de to sammen
når en spørring trenger begge.

Både idéen og ordet kommer fra Edgar F. Codd, som foreslo relasjonsmodellen i
Communications of the ACM i juni 1970. I den artikkelen betydde normalisering
ett bestemt steg: å bli kvitt relasjoner som inneholder andre relasjoner, for
eksempel en ansattpost med sin egen liste over tidligere stillinger og sin egen
liste over barn. Codds fremgangsmåte kopierer forelderens primærnøkkel inn i
hver nøstet gruppe og løfter gruppen ut i en egen relasjon, med forelderens
nøkkel pluss det som identifiserer en rad innenfor gruppen som nøkkel. Det som
står igjen, er på normalform, i dag kjent som første normalform: hver verdi i
hver kolonne er én enkel verdi.

Codd definerte senere strengere normalformer, blant dem den som Ronald Fagins
artikkel fra 1977 kaller Codds forbedrede tredje normalform eller
Boyce–Codd-normalform. Fagins artikkel legger til en fjerde. Den bygger på en ny
type avhengighet mellom kolonner, den flerverdige avhengigheten, som omfatter
den velkjente funksjonelle avhengigheten som et spesialtilfelle, og Fagin viste
at den fjerde formen er strengt sterkere enn Boyce–Codd-normalform.

De strengere formene nås ved å dele opp. En tabell erstattes av to eller flere
av sine projeksjoner, tabeller med noen av kolonnene, valgt slik at en join av
dem gir nøyaktig originalen tilbake. Fagins artikkel viser at den garantien
holder for hans egen form: enhver tabell kan deles opp i tabeller på fjerde
normalform uten at informasjon går tapt, fordi en join av dem gjenskaper
originalen. Denormalisering er det motsatte: å lagre en kopi av et faktum et
annet sted med vilje, som regel for å spare en join.

## Hvorfor det er viktig

En tabell som lagrer det samme faktumet mange ganger, inviterer til tre slags
problemer. Tenk deg én rad per ansatt og prosjekt, der den ansattes kontor står
på hver rad. Når noen bytter kontor, må flere rader få den nye verdien, og
glemmer man én, motsier databasen seg selv. Når en ansatts siste prosjekt
avsluttes og raden slettes, forsvinner opplysningen om hvor vedkommende sitter.
Og en nyansatt som ikke er med på noe prosjekt, har ingen plass å føre opp
kontoret sitt. Dette er oppdaterings-, slette- og innsettingsanomaliene, og
deles tabellen i ansatt–prosjekt og ansatt–kontor, forsvinner alle tre.

I et teams daglige arbeid dukker disse anomaliene opp som feil. En faktura viser
den gamle adressen fordi én kopi ble glemt. En rapport teller en kunde to ganger
fordi navnet ble skrevet på to måter. Et normalisert skjema gjør slike feil
vanskelige å skrive i utgangspunktet, og når primærnøkler, fremmednøkler og
CHECK-skranker er deklarert, avviser databasen dårlige data selv i stedet for å
stole på at all applikasjonskoden husker reglene.

Endringer blir også billigere. Et skjema der hver tabell beskriver én ting, tar
som regel imot nye typer data med mindre ombygging: det er en mindre migrering å
legge til en kolonne i den ene tabellen som eier et faktum, enn å finne hver
tabell som har kopiert det.

Prisen er joins. En normalisert database besvarer de fleste spørsmål ved å joine
tabellene sammen igjen, og hver join er arbeid for serveren. Med indekser på
kolonnene det joines på, er det arbeidet som regel lite, og derfor bør en målt
treg spørring og ikke en magefølelse avgjøre når et faktum kopieres tilbake.

## Slik ser det ut i praksis

En liten nettbutikk starter med én flat tabell, lastet inn fra et
regnearkuttrekk:

```sql
CREATE TABLE order_flat (
    order_no       integer,
    ordered_on     date,
    customer_email text,
    customer_name  text,
    customer_city  text,
    sku            text,
    product_name   text,
    unit_price     numeric,
    quantity       integer,
    status         text
);
```

Hver rad er én linje i én ordre, så en kundes navn og by gjentas på hver linje i
hver ordre, og et produkts navn gjentas på hver linje som selger det. En kunde
som flytter, har dusinvis av rader å oppdatere. Sletter man den siste linjen som
solgte et produkt, glemmer tabellen at produktet finnes. Statusen er fritekst,
så `shipped`, `Shipped` og `shipd` kan alle dukke opp.

Normalisert får hvert faktum ett hjem:

```sql
CREATE TABLE customers (
    customer_id integer PRIMARY KEY,
    email       text NOT NULL UNIQUE,
    name        text NOT NULL,
    city        text NOT NULL
);

CREATE TABLE products (
    sku        text PRIMARY KEY,
    name       text NOT NULL,
    list_price numeric NOT NULL CHECK (list_price >= 0)
);

CREATE TABLE order_status (
    status_id smallint PRIMARY KEY,
    label     text NOT NULL UNIQUE
);

CREATE TABLE orders (
    order_id    integer PRIMARY KEY,
    customer_id integer NOT NULL REFERENCES customers ON DELETE RESTRICT,
    status_id   smallint NOT NULL REFERENCES order_status,
    ordered_on  date NOT NULL
);

CREATE TABLE order_lines (
    order_id   integer REFERENCES orders ON DELETE CASCADE,
    sku        text NOT NULL REFERENCES products,
    quantity   integer NOT NULL CHECK (quantity > 0),
    unit_price numeric NOT NULL CHECK (unit_price >= 0),
    PRIMARY KEY (order_id, sku)
);
```

En kundes by er nå lagret én gang, og en flytting er en oppdatering av én rad.
Statusnavnene ligger i en liten oppslagstabell med en `smallint` som nøkkel, så
hver ordre bærer et kort tall i stedet for en gjentatt tekst, og en feilstavet
status kan ikke slippe inn: fremmednøkkelen avviser enhver `status_id` som
`order_status` ikke har.

`unit_price` blir stående på `order_lines`, selv om `products` allerede har en
pris. Det er ingen forglemmelse. Prisen kunden betalte, er et faktum om salget,
ikke om produktet, og den skal ikke endre seg når katalogen gjør det.
Normalisering spør hvor hvert faktum hører hjemme, og dette hører til linjen.

De to `ON DELETE`-leddene sier hva en sletting betyr. Sletter man en ordre,
følger linjene med, for en linje betyr ingenting uten ordren sin. Å slette en
kunde som har ordrer, feiler, fordi butikken må ta vare på de ordrene.
PostgreSQLs standard, `NO ACTION`, avviser også den slettingen; `RESTRICT` gjør
intensjonen tydelig, og i motsetning til `NO ACTION` kan den ikke utsettes til
slutten av transaksjonen.

PostgreSQL lager en indeks for hver primærnøkkel, men ikke for den refererende
siden av en fremmednøkkel. Dokumentasjonen foreslår å legge til en, fordi
sletting eller oppdatering av en referert rad må finne hver rad som peker på
den:

```sql
CREATE INDEX ON orders (customer_id);
CREATE INDEX ON order_lines (sku);
```

`order_lines.order_id` trenger ikke noe ekstra: den er den første kolonnen i
tabellens primærnøkkel.

Til slutt gir en visning (et view) den flate formen tilbake til dem som vil ha
den:

```sql
CREATE VIEW order_report AS
SELECT o.order_id, o.ordered_on, c.email, c.name AS customer_name, c.city,
       l.sku, p.name AS product_name, l.unit_price, l.quantity, s.label
FROM   orders o
JOIN   customers c    USING (customer_id)
JOIN   order_status s USING (status_id)
JOIN   order_lines l  USING (order_id)
JOIN   products p     USING (sku);
```

Rapporter kan lese `order_report` omtrent som de leste den gamle tabellen, mens
hvert faktum bak den er lagret én gang. Innføringen i PostgreSQL-dokumentasjonen
argumenterer likt for visninger generelt: en visning gir en spørring et navn som
kan brukes som en tabell, og lar tabellene under endre seg bak et grensesnitt
som står fast.

De samme reglene gjelder i langt større skala. Selskapets
[maritime datamodell](/nb/portfolio/modeled-348-tables-across-34-postgres-schemas/)
er lagt opp som normaliserte PostgreSQL-skjemaer organisert etter domene; i
oktober 2026 oppga siden den til 348 tabeller fordelt på 34 skjemaer. Små,
stabile mengder som statuser, typer og kategorier ble
`SMALLINT`-oppslagstabeller, som holder radene kompakte og join-operasjonene
billige, og ekte fremmednøkler og skranker holder relasjonene sammen, så
integriteten er databasens jobb og ikke noe applikasjonen må huske.

## Hvor det går galt

**Lister pakket inn i én kolonne.** En `tags`-kolonne med `red,large,sale`,
eller en telefonkolonne med to numre, bryter første normalform. Databasen kan
ikke kontrollere de enkelte verdiene, la en fremmednøkkel peke på dem eller
joine på dem, og hver spørring må dele opp teksten først. Gi lista en egen
tabell med én rad per verdi og forelderens nøkkel pluss verdien som nøkkel:
Codds fremgangsmåte fra 1970.

**Historikk som normaliseres bort.** Prisen på en ordrelinje eller
leveringsadressen på en ordre ser ut som en dublett av det produkt- og
kundetabellene har. Det er den ikke: den registrerer hva som gjaldt da ordren
ble lagt inn. Bytt den ut med en nøkkel til den gjeldende katalogen, så endrer
hver gammel faktura seg neste gang en pris gjør det. Lagre fakta om en hendelse
på hendelsen.

**Fremmednøkler uten indekser.** Den refererte siden av en fremmednøkkel har
alltid en indeks, fordi den må være en primærnøkkel, en unik skranke eller en
unik indeks. Den refererende siden har det ikke, så å slette en kunde eller
endre et produkts SKU betyr å lete gjennom hele den refererende tabellen etter
rader som peker dit. Indekser de refererende kolonnene som vist over. PostgreSQL
overlater det til deg fordi det ikke alltid trengs.

**Kaskader som sletter bilag.** `ON DELETE CASCADE` fra kunder til fakturaer
betyr at én uforsiktig sletting av en kunde i det stille fjerner data
virksomheten må oppbevare. La bare slettinger gå i kaskade til rader som ikke
betyr noe uten forelderen, som ordrelinjer, og bruk `RESTRICT`, eller
standarden, alle andre steder.

**Et normalisert design som ingenting håndhever.** Tabeller som er laget for
hånd eller lastet inn fra regneark, kommer ofte uten primærnøkkel og uten
fremmednøkler, så ingenting stopper en duplikatrad eller en ordre til en kunde
som ikke finnes. PostgreSQL krever ikke primærnøkkel, selv om dokumentasjonen
påpeker at relasjonsteorien krever en i hver tabell. Deklarer nøklene, så
avviser databasen den dårlige raden med en feilmelding som nevner skranken ved
navn.

**Lagrede summer som sklir.** En `order_total`-kolonne på `orders`, holdt ved
siden av linjene den summerer, er en denormalisering, og hver kodevei som endrer
en linje, må oppdatere den også. En kodevei som glemmer det, etterlater en feil
sum uten å utløse noen feil. Regn ut summen i en spørring eller en visning.
Viser det seg å være for tregt, så behold kopien, men vedlikehold den ett sted,
i den samme transaksjonen som endrer linjene.

**Denormalisering før måling.** Joins på indekserte nøkler er billige, og å
kopiere kolonner for å unngå dem bytter en liten, kjent kostnad mot anomaliene
som er beskrevet ovenfor. Les spørringsplanen først. Når lesingen virkelig er
for treg, kan en materialisert visning eller en egen rapportdatabase holde den
flate formen, mens kildetabellene beholder én kopi av hvert faktum. Rapportering
er der denormalisering hører hjemme med vilje, som i stjerneskjemaet i et
datavarehus.

**For mye oppsplitting.** Normalisering ber om ett hjem for hvert faktum, ikke
om flest mulig tabeller. Å dele en tabell der kolonnene allerede avhenger av
nøkkelen og ikke av noe annet, gir bare flere joins og flere tabellnavn som alle
må lære seg. Et skjema som trenger et dusin joins for å vise én ordre, er ikke
lettere å jobbe med enn et flatt. Stopp når hvert faktum har ett hjem.

## Les mer

- [A Relational Model of Data for Large Shared Data Banks](https://doi.org/10.1145/362384.362685)
  — Codds artikkel fra 1970 i Communications of the ACM, som innførte
  relasjonsmodellen, normalform og normalisering.
- [Multivalued Dependencies and a New Normal Form](https://doi.org/10.1145/320557.320571)
  — Fagins artikkel fra 1977, som definerer den flerverdige avhengigheten og
  fjerde normalform og viser at enhver tabell kan deles opp på fjerde normalform
  uten at informasjon går tapt.
- [Constraints](https://www.postgresql.org/docs/current/ddl-constraints.html) —
  PostgreSQL om primærnøkler, fremmednøkler, CHECK-skranker og
  `ON DELETE`-handlingene, og hvorfor refererende kolonner er verdt å indeksere.
- [Views](https://www.postgresql.org/docs/current/tutorial-views.html) —
  innføringen i PostgreSQL-dokumentasjonen om visninger som et stabilt
  grensesnitt over tabeller med skiftende struktur.

<https://engineer.company/nb/glossary/normalization/>
