Databasebegreber for udviklere: tabeller, nøgler, indekser, transaktioner og normalisering

Databasebegreber for udviklere: tabeller, nøgler, indekser, transaktioner og normalisering

Overblik: de vigtigste databasebegreber for udviklere

Som udvikler bruger du hele tiden databaser, også selv om du “bare lige” skriver lidt SQL til et sideprojekt. De vigtigste begreber at have styr på er: tabeller, nøgler, relationer, indekser, transaktioner og normalisering. Når du forstår, hvordan de hænger sammen, kan du designe en database, der både er korrekt, til at vedligeholde og til at køre hurtigt nok.

BegrebKort forklaring
DatabaseStruktureret samling af data, som et DBMS styrer adgang til
TabelSamling af rækker (poster) med samme type data, fx kunder
NøgleKolonne(r), der identificerer rækker entydigt og forbinder tabeller
IndeksEkstra datastruktur, der gør opslag hurtigere
TransaktionPakke af ændringer, der enten sker helt eller slet ikke
NormaliseringMetode til at strukturere tabeller så data er konsistente og ikke unødigt duplikeret

En god mental model er:

  • data → tabelstruktur → nøgler → relationer → performance (indekser) → sikre ændringer (transaktioner).

Hvad er en database, og hvad betyder normalisering?

En database er en struktureret samling af data, styret af et databasesystem (DBMS), hvor du kan oprette, læse, opdatere og slette data med fx SQL. Normalisering er et sæt designregler, der hjælper dig med at dele data op i tabeller på en måde, der minimerer redundans og sikrer dataintegritet.

I praksis er en database ofte en relationel database: data ligger i tabeller med rækker og kolonner, og relationer mellem tabellerne styres med nøgler og JOINs. SQL er sproget, du bruger til CRUD-operationer (Create, Read, Update, Delete).

Et DBMS (fx PostgreSQL, MySQL, SQL Server) sørger for:

  • lagring og indeksering af data
  • adgangskontrol og sikkerhed
  • transaktioner og backup
  • optimering af forespørgsler

Normalisering er designfasen: her beslutter du, hvilke tabeller du har, hvilke kolonner de indeholder, og hvordan de hænger sammen. Målet er:

  • mindre redundans (undgå at gemme det samme stykke info mange steder)
  • bedre dataintegritet (undgå modsigende data)
  • nemmere vedligeholdelse (ændringer ét sted, ikke ti steder)

De fleste produktionssystemer lander typisk omkring 3NF (tredje normalform), fordi det er et fornuftigt kompromis mellem orden i strukturen og performance. Vil du se, hvordan SQL-siden af det ser ud, kan du fx øve SELECT i praksis i denne guide til SQL SELECT.

Hvad er en tabel, en række og en kolonne?

I en relationel database er en tabel en samling af rækker, der beskriver den samme type “ting”, fx kunder. Hver række er én post (én kunde), og hver kolonne er en attribut, der beskriver noget ved den post, fx navn eller email.

Typisk ser det sådan ud:

customer_idnameemail
1Ana Hansenana@example.com
2Jonas Madsenjonas@example.com
  • Tabel: customers repræsenterer entiteten “kunde”.
  • Række: hver kunde (Ana, Jonas) er én række.
  • Kolonne: name, email osv. er attributter ved en kunde.

En god tommelfingerregel er: én tabel pr. “type ting” i din model. Én tabel for kunder, én for produkter, én for ordrer osv. Hvis du begynder at lægge flere uafhængige koncepter i den samme tabel, er det typisk et tegn på, at du ikke har normaliseret nok.

Schemaet (schema = beskrivelsen af tabeller, kolonner, nøgler og relationer) er den logiske model, som SQL-forespørgslerne arbejder imod. Når du senere joiner tabeller, som i øvelserne i denne artikel om SQL JOINs, er det tabelstrukturen, du faktisk udnytter.

Hvad er primærnøgle, kandidatnøgle og fremmednøgle?

En primærnøgle er den kolonne (eller kombination af kolonner), der entydigt identificerer en række i en tabel. En kandidatnøgle er en mulig primærnøgle, som også kunne bruges til at identificere rækker entydigt. En fremmednøgle er en kolonne, der peger på en primærnøgle i en anden tabel og skaber relationen mellem dem.

BegrebDefinitionTypisk brugTypisk fejl
Primærnøgle (PK) Unik identifikator for en række customer_id i customers Ingen PK, eller en PK der kan ændre sig (fx email)
Kandidatnøgle Enhver kolonne/kolonne-sæt, der kunne være PK email kan være kandidatnøgle, hvis den er unik Flere kandidatnøgler, men ingen valgt som tydelig PK
Fremmednøgle (FK) Kolonne, der refererer til PK i en anden tabel customer_id i orders peger på customers Relationer håndteres kun i kode, ikke som FK-constraint

Eksempel i et lille ordresystem:

  • customers(customer_id PK, name, email ...)
  • orders(order_id PK, customer_id FK, order_date ...)
  • order_items(order_id FK, product_id FK, quantity ...) (her kan PK være en sammensat nøgle (order_id, product_id)).

Fremmednøgler sikrer referentiel integritet: databasen kan forhindre, at du gemmer en ordre med et customer_id, der ikke findes, eller at du sletter en kunde, som stadig har ordrer, uden at tage stilling til konsekvensen.

En klassiker er at springe constraints over “for at gøre udvikling nemmere”. Det gør typisk kun fejlene sværere at opdage. Hvis du senere skal lave schema-ændringer i produktion, er stærke nøgler og constraints din ven. Vil du se mere om sikre ændringer, er denne guide til databaseændringer uden svedige håndflader værd at læse.

Hvad er 1NF, 2NF, 3NF og BCNF?

Normalformer er trin i, hvor “rent” din tabelstruktur er. 1NF kræver, at hver celle har én værdi og ingen gentagne grupper. 2NF fjerner afhængigheder af kun en del af primærnøglen. 3NF fjerner transitive afhængigheder, hvor ikke-nøgler afhænger af andre ikke-nøgler. BCNF er en strengere version af 3NF, hvor alle funktionelle afhængigheder udgår fra en kandidatnøgle.

NormalformHovedregelSymptom hvis du mangler den
1NF Atomiske værdier, ingen gentagne grupper Komma-separerede værdier, phone1/phone2-kolonner
2NF Ingen delvise afhængigheder på en sammensat nøgle Kolonner, der afhænger af kun halvdelen af PK
3NF Ingen transitive afhængigheder Kolonner, der afhænger af andre ikke-nøgle-kolonner
BCNF For hver funktionel afhængighed er venstresiden en kandidatnøgle Mere subtile afhængigheder i komplekse modeller

Tre klassiske anomalier, normalisering hjælper dig af med:

  • Update anomaly: du skal opdatere den samme oplysning mange steder, fx kundens adresse i 10 rækker, og glemmer én.
  • Insert anomaly: du kan ikke indsætte en ny ting uden at opfinde data til noget andet, fx et produkt uden en ordre.
  • Delete anomaly: du mister mere data end forventet, fx sletter sidste ordre på en kunde og mister samtidig kundens kontaktinfo.

I praksis er 3NF ofte et godt mål for OLTP-systemer (systemer med mange små transaktioner, fx webapps). BCNF dukker typisk op i mere teoretiske eller komplekse modeller. Hvis du har styr på 1NF-3NF og kan spotte anomalier, er du allerede langt foran mange reelle produktionsdatabaser.

Hvordan normaliserer man en tabel trin for trin?

Du normaliserer ved systematisk at splitte en “rodet” tabel op i flere tabeller, så hver tabel repræsenterer én entitet, og hver kolonne kun afhænger af sin primærnøgle. Det betyder typisk at fjerne gentagne grupper, trække uafhængige ting ud i egne tabeller og forbinde dem med nøgler.

Mini-case: rå ordredata

Forestil dig, at du har en rå orders-tabel fra et lille webshop-prototype:

order_idcustomer_namecustomer_email product_namesproduct_pricesorder_total
1Ana Hansenana@example.com “T-shirt, Kop”“199, 99”298

Problemer:

  • Flere produkter i én celle (product_names, product_prices) bryder 1NF.
  • Kunde-info gentages for hver ordre.
  • order_total kan blive inkonsistent med priserne.

Trin 1: opfyld 1NF (atomiske værdier)

Første skridt er at gøre rækkerne “flade” og atomiske. Vi deler ordrer ud på flere rækker, én pr. ordrelinje:

order_idcustomer_namecustomer_email product_nameunit_pricequantity
1Ana Hansenana@example.com T-shirt1991
1Ana Hansenana@example.com Kop991

Nu har vi atomiske værdier, men vi gentager stadig kunde-info og produktnavne.

Trin 2: identificér entiteter og nøgler

Vi kan se mindst tre entiteter:

  • Kunde: navn, email, adresse osv.
  • Ordre: dato, status, tilhører en kunde.
  • Produkt: navn, pris, kategori osv.

Vi opretter derfor tabeller:

  • customers(customer_id PK, name, email, ...)
  • orders(order_id PK, customer_id FK, order_date, ...)
  • products(product_id PK, name, unit_price, ...)
  • order_items(order_id FK, product_id FK, quantity)

Trin 3: flyt data ud i de rigtige tabeller (2NF og 3NF)

Nu fordeler vi kolonnerne:

  • Kundedata flyttes til customers. Ordrer peger kun på customer_id.
  • Produktdata flyttes til products. Ordrelinjer peger kun på product_id.
  • Ordrelinjer ligger i order_items med quantity og referencer til ordre og produkt.
  • Afledte værdier som ordre-total kan beregnes via SUM over order_items og products.unit_price.

Resultatet er en struktur i 3NF:

  • Ingen gentagne grupper (1NF).
  • I sammensatte nøgler (fx order_items(order_id, product_id)) afhænger alle kolonner af hele nøglen (2NF).
  • Ikke-nøgler afhænger ikke af andre ikke-nøgler (3NF). Fx afhænger kundens email af customer_id, ikke af order_id.

Nu kan du nemt ændre kundens email ét sted og være sikker på, at alle fremtidige ordrer bruger den nye. Og du kan lave SQL-joins mellem tabellerne, som vist i artiklen om JOIN-øvelser.

Hvad er et indeks, og hvordan hænger det sammen med normalisering?

Et indeks er en ekstra datastruktur, databasen opretter for at kunne finde rækker hurtigere uden at scanne hele tabellen. Normalisering handler om, hvordan dine data er struktureret logisk, mens indeksering handler om, hvordan forespørgslerne fysisk udføres effektivt. De løser forskellige problemer, men spiller sammen.

Når du laver en SELECT med WHERE, JOIN eller ORDER BY, skal databasen finde de relevante rækker. Uden indeks må den ofte lave et “full table scan” og kigge på alle rækker. Med et indeks på fx customer_email kan den slå direkte op.

Typiske steder, du overvejer indeks:

  • Kolonner, du joiner på, fx orders.customer_id.
  • Kolonner, du ofte filtrerer på, fx email, status, created_at.
  • Kolonner, du sorterer eller paginerer på.

Indekser har en pris:

  • De tager ekstra diskplads.
  • Indsættelser, opdateringer og sletninger bliver dyrere, fordi indekserne også skal opdateres.

Derfor er det sjældent en god idé bare at indeksere “alt”. Start med de vigtigste queries, se på deres eksekveringsplaner og tilføj målrettede indekser. I mange databaser kan du bruge EXPLAIN til at se, om et indeks faktisk bliver brugt.

Et godt designmønster er:

  1. Normalisér data til et fornuftigt niveau (typisk 3NF).
  2. Identificér de vigtigste forespørgsler.
  3. Tilføj indekser på de kolonner, der optræder i JOINs og filtre.
  4. Mål. Fjern eller justér indekser, der ikke hjælper.

Vil du dykke ned i performance-siden, er der en guide om SQL-indeks vs. brute force og en artikel om, hvorfor din SQL ikke er langsom, den er bare ærlig.

Hvad er transaktioner, COMMIT, ROLLBACK og ACID?

En transaktion er en pakke af database-ændringer, der enten gennemføres fuldt ud eller rulles helt tilbage. ACID beskriver de garantier, transaktioner kan give: Atomicity, Consistency, Isolation og Durability. Du bruger COMMIT til at gemme ændringerne og ROLLBACK til at annullere dem, hvis noget går galt.

ACID kort forklaret

  • Atomicity: Alt eller intet. Enten lykkes hele transaktionen, eller også sker der ingenting.
  • Consistency: Efter transaktionen er data stadig i en gyldig tilstand ifølge dine regler og constraints.
  • Isolation: Parallelle transaktioner ser ikke hinandens halvfærdige data.
  • Durability: Når du har committet, overlever ændringerne også nedbrud som databasen er konfigureret til.

Eksempel: opret en ordre med ordrelinjer

Forestil dig et flow, hvor du skal:

  1. Oprette en række i orders.
  2. Oprette flere rækker i order_items.
  3. Opdatere lageret i products.

Hvis noget går galt midt i, vil du ikke ende med en halv ordre eller negativ lagerbeholdning. Derfor pakker du det i en transaktion, fx (pseudo-SQL):

BEGIN;
INSERT INTO orders …;
INSERT INTO order_items …;
UPDATE products SET stock = stock – …;
COMMIT;

Hvis opdateringen af lager fejler, laver du i stedet:

ROLLBACK;

Dermed er ingen af ændringerne blevet permanente.

Isolation bliver vigtig, når mange brugere skriver til databasen samtidig. Her skal du vælge et passende isolationsniveau, så du undgår mærkelige race conditions, men stadig har performance. Hvis du har oplevet dubletter eller underlige retrys i et API, har du rørt ved det her område. Der er et godt eksempel på konsekvenserne i artiklen om et uskyldigt retry loop, der væltede et API.

Hvornår skal man normalisere, og hvornår skal man denormalisere?

Som udgangspunkt normaliserer du til cirka 3NF og stopper der. Du denormaliserer først bevidst, når du kan måle, at en konkret læse- eller performancecase ikke kan løses fornuftigt med indekser og almindelig optimering, og når du er klar til at håndtere den ekstra kompleksitet.

Beslutningsmatrix

SituationAnbefalet strategiBegrundelse
Almindelig webapp / OLTP Normalisér til 3NF Mange små læse-/skriveoperationer, krav om konsistens og enkel vedligeholdelse
Tung rapportering på tværs af mange tabeller Normaliseret kernedata + evt. rapporttabeller Behold “sandheden” normaliseret, byg læsevenlige views/denormaliserede tabeller til rapporter
Hot path API-endepunkt med ekstrem læsebelastning Selektiv denormalisering Gem præ-aggregerede eller sammenfoldede data, hvis joins er en flaskehals
System med meget skrivetrafik Fortsat normalisering, forsigtig med ekstra indekser Undgå unødig redundans, da writes skal opdatere mange felter og indekser

Når du denormaliserer, kopierer du bevidst data ud i flere tabeller eller kolonner for at gøre bestemte queries hurtigere. Typiske mønstre:

  • Gem ordre-total direkte på orders i stedet for at SUM’e hver gang.
  • Gem et ofte brugt felt både på orders og customers for at undgå et join i hot paths.

Men hver gang du gør det, skal du overveje:

  • Hvordan holder jeg felterne i sync?
  • Hvad sker der ved schema-ændringer?
  • Er det her problem i virkeligheden et indeks- eller cache-problem?

Ofte er det klogere at starte med bedre indekser, caching og generel query-optimering, før du ændrer på selve datamodellen. Artiklerne om caching-faldgruber og rate limiting i praksis giver et godt billede af, hvor meget du kan gøre uden at ødelægge en pæn model.

Hvilke fejl laver udviklere typisk i database-design?

De største fejl i database-design handler næsten altid om for meget redundans, uklare nøgler, manglende constraints eller overoptimering for tidligt. Heldigvis er de ret nemme at genkende, når du ved, hvad du skal kigge efter.

  • Ingen ordentlig primærnøgle
    Fx brug af navn som nøgle. Konsekvens: svært ved at referere til rækker, svært ved at rette stavefejl, og risiko for dubletter.
  • Manglende fremmednøgler
    Relationer håndteres kun i applikationskoden. Konsekvens: forældreløse rækker, inkonsistente relationer.
  • Kommaseparerede værdier i én kolonne
    Fx tags = "blue,large,cotton". Konsekvens: svært at søge effektivt og bryder 1NF.
  • For mange eller tilfældige indekser
    Alt får et indeks “for en sikkerheds skyld”. Konsekvens: langsommere writes og sværere tuning.
  • Uklar brug af NULL
    NULL betyder pludselig både “ukendt”, “ikke relevant” og “tom streng”. Konsekvens: uforudsigelige queries og bugs.
  • Overnormalisering
    Alt deles i bittesmå tabeller, også hvor der ikke er reelle afhængigheder at løse. Konsekvens: meget komplekse queries og mange joins uden reel gevinst.

Et godt værn mod de fleste af de her fejl er at:

  • tegne et enkelt ER-diagram
  • sikre primær- og fremmednøgler fra start
  • tænke igennem, hvordan data skal læses i virkeligheden
  • have et solidt migrationsflow, så du kan rette op senere uden kaos

Hvis du arbejder i et team, er artiklen om database-migrationer i teams et godt næste skridt. Den hjælper dig med at få schema-ændringer til at spille sammen med resten af udviklingsprocessen.

Brug en surrogatnøgle (autoincrement eller UUID) når du vil have en simpel, stabil primærnøgle uden forretningslogik. Naturlige nøgler kan bruges hvis en kolonne altid er unik og uforanderlig, men de kan skabe problemer ved ændringer eller når nøglen er sammensat. En god praksis er derfor ofte: primær surrogatnøgle + unikke constraints på de naturlige attributter.
Indekser de kolonner du ofte bruger i WHERE, JOIN og ORDER BY, især hvis de har høj kardinalitet. Overvej sammensatte indekser for hyppige kombinationer af kolonner, og husk at indekser øger skriveomkostninger og lagerforbrug. Brug EXPLAIN og slow query logs til at finde kandidater og fjern indeks, der sjældent bruges.
Denormaliser når læseperformance eller simple forespørgsler er vigtigere end minimal redundans, fx ved mange joins på store tabeller. Gode teknikker er tilføjelse af afledte kolonner, materialized views eller cache-lag, og sørg for et klart 'source of truth' plus opdateringsstrategi (transaktioner eller baggrundsjob). Dokumenter denormaliseringen og write-tests, så du undgår inkonsistens.
Kør migrationer i små, reversible trin: tilføj først nye nullable kolonner eller indeks, deploy kode der understøtter både gamle og nye felter, udfyld data og skift så til strengere constraints i et senere trin. Undgå nedskæringer ved peak-trafik, test på staging, tag backup før destructive changes og brug feature toggles ved behov.

Ida Balslev er den type ven, der pludselig dukker op i din messenger med et link til en lille web-app, hun lige har bygget for sjov – og bagefter gerne viser dig, hvordan du selv kan lave den. Hendes passion for kodning startede med en hjemmebygget hjemmeside til en hestestald og er langsomt vokset gennem aftener med tutorials, fejlmeldinger og små, hjemmelavede projekter.

På Codingclass.dk deler Ida den viden, hun selv manglede i starten: konkrete eksempler, tydelige forklaringer og ærlige historier om, hvad der typisk går galt første, anden og tredje gang. Hun elsker at tage et abstrakt begreb som fx "API" eller "asynkron JavaScript" og koge det ned til noget, du kan se, klikke på og lege med i browseren. For hende handler kodning ikke om at være perfekt, men om at turde prøve, bryde ting og bygge dem op igen.

Ida skriver især om webudvikling med HTML, CSS og JavaScript, små Python-scripts og grundlæggende koncepter som debugging, versionsstyring og struktur i din kode. Hun tænker altid i næste skridt: når du først forstår idéen, viser hun dig, hvordan du kan udvide det med en ekstra funktion, lidt pænere styling eller en smartere måde at tænke din kode på.

Gennem sine artikler på Codingclass.dk vil Ida gerne give dig følelsen af, at du ikke sidder alene med koden – men at der faktisk er en, der har kæmpet med de samme fejlmeddelelser og nu gerne vil vise dig en vej igennem dem, i et tempo hvor alle kan være med.

Send kommentar

You May Have Missed