# Partitionering

## Hvad det er

Partitionering, også kaldet tabelpartitionering, gemmer rækkerne fra én logisk
tabel i flere almindelige tabeller, der hver rummer de rækker, hvis nøgle falder
inden for dens egne grænser, mens databaseforespørgsler og skrivninger fortsat
bruger det ene navn, de altid har brugt. PostgreSQL, som denne side følger,
kalder sin indbyggede form deklarativ partitionering, på engelsk declarative
partitioning: tabellen erklæres som partitioneret med en metode og en
partitionsnøgle, og hver partition oprettes som en tabel for sig.

Den partitionerede tabel rummer selv ingen rækker. En indsættelse sendes videre
til den partition, hvis grænser passer til nøglen, og en opdatering, der ændrer
nøglen, flytter rækken til den partition, hvor den nu hører hjemme. Alle
partitioner har forældretabellens kolonner, men hver kan have sine egne indeks,
lagringsindstillinger og sit eget tablespace, og en partition kan selv
partitioneres igen.

Der er tre metoder. Range-partitionering giver hver partition et interval af
værdier, oftest datoer; den nedre grænse er med, og den øvre er ikke, så med
intervallerne 1–10 og 10–20 hører værdien 10 til det andet. List-partitionering
opregner de værdier, hver partition tager imod, for eksempel en gruppe
landekoder. Hash-partitionering giver hver partition et modulus og en rest, og
en række havner der, hvor hashen af dens nøgle giver netop den rest, hvilket
fordeler rækkerne jævnt, når der ikke er et naturligt interval at skære efter.
En standardpartition, på engelsk default partition, kan fange de værdier, der
ikke passer i nogen anden, ved range og list, men ikke ved hash.

Partitionering foregår inden for én databaseserver. At fordele data over
separate servere, hver med sin egen kopi af skemaet, er sharding, og det er et
langt større skridt. SQL Server og BigQuery har deres egne former for
partitionering med deres egen syntaks, og idéerne nedenfor gælder også der, selv
hvor kommandoerne er andre.

## Hvorfor det betyder noget

De fleste store tabeller vokser i den ene ende. Ordrer, målinger, logs og
hændelser kommer ind hver dag, bliver liggende i måneder eller år, og de fleste
forespørgsler spørger til den nyeste del. Uden partitionering arbejder hver
forespørgsel mod én tabel og ét sæt indeks, der bliver ved med at vokse.

Med en partitionsnøgle, der passer til forespørgslerne, kan planneren bevise, at
de fleste partitioner umuligt kan rumme en matchende række, og lade dem helt ude
af planen. PostgreSQL kalder det partition pruning. En rapport over sidste uge
læser sidste uges partition og intet andet. Dokumentationen nævner også en
mindre oplagt gevinst: fordi partitionerne overtager rollen fra de øverste
niveauer i et indekstræ, er der større chance for, at de mest brugte dele af
indeksene kan ligge i hukommelsen.

Mest ændrer det sig, når gamle data skal væk. At slette en måneds rækker fra én
samlet tabel er en lang `DELETE`, der efterlader døde rækker, som `VACUUM` skal
rydde op efter. Med én partition pr. måned er den samme oprydning én
`DROP TABLE` eller én `DETACH PARTITION`, som dokumentationen kalder langt
hurtigere end en masseoperation, og som helt undgår arbejdet med `VACUUM`. En
frakoblet partition er stadig en almindelig tabel, så den kan sikkerhedskopieres
eller opsummeres, før den slettes, og partitioner, der sjældent læses, kan ligge
på billigere og langsommere lager.

Gevinsten har en pris, og dokumentationen siger ligeud, hvornår den betaler sig.
Hver partition giver planlægningen mere arbejde, og fordelene er normalt kun
umagen værd, når tabellen ellers ville være meget stor; tommelfingerreglen er,
at tabellen bør være større end databaseserverens fysiske hukommelse. Én ting
rører partitionering heller ikke ved: en enkelt række, som mange transaktioner
kæmper om, er stadig én række i én partition bag én lås.

## Sådan ser det ud i praksis

En note om at skalere en database, før den shardes, har et afsnit om at
[partitionere tabellerne omkring den varme række](/da/notes/optimise-before-you-shard/).
Dens `orders`-tabel er partitioneret pr. måned på `created_at`, med primærnøglen
`(id, created_at)`, fordi nøglen skal indeholde partitionskolonnen. En
forespørgsel, der nævner et datointerval, læser kun partitionerne inden for det,
denne måneds indekser forbliver små nok til at ligge i hukommelsen, og oprydning
bliver én `DETACH PARTITION` i stedet for en `DELETE` af millioner af rækker.
Noten siger også ligeud, hvor grænsen går: partitionering gør ikke den omstridte
række hurtigere, og en forespørgsel, der ikke nævner nøglen, læser hver eneste
partition.

Et PostgreSQL-projekt for en applikation med brugerkonti, tenant-oplysninger,
geografiske data og dagligt elforbrug
[delte eltabellen op i tidsintervaller](/da/portfolio/accelerated-postgresql-performance-by-10x-via-strategic-indexing-15/)
pr. dag og måned og brugte hash-partitionering til de geografiske data. Det
skete sammen med GiST- og composite-indeks og omskrevne forespørgsler, og det
samlede resultat var svartider, der blev 10 gange bedre, og et ressourceforbrug,
der faldt med 40 %.

Partitionering kan også bruges i flere databasemotorer side om side. Med
[over 100 databaser på tværs af PostgreSQL, SQL Server og BigQuery](/da/portfolio/architected-created-and-managed-100-postgresql-ms-sql-12/),
primært med GIS-data og tidsseriedata, indgik tidsseriepartitionering i
skemadesignet til PostgreSQL og SQL Server, mens BigQuery brugte
tidspartitionerede og clustrede tabeller. Sammen med spatial indeksering,
materialized views og caching faldt svartiderne for de data med 40–60 %.

## Hvor det går galt

Partitionering flytter noget af arbejdet fra forespørgselstid til designtid, og
de fleste problemer kommer af en designbeslutning, der blev truffet uden
forespørgslerne for øje.

**Indsættelser begynder at fejle den første dag i en ny måned.** Ingen partition
dækker det nye interval, og PostgreSQL afviser en indsættelse i
forældretabellen, der ikke passer til nogen eksisterende partition. Opret
partitionerne i god tid fra et planlagt job, der genererer DDL'en, som
dokumentationen foreslår, og send en alarm, når den nyeste partition nærmer sig
sin slutning:

```sql
CREATE TABLE orders_2026_11
  PARTITION OF orders
  FOR VALUES FROM ('2026-11-01')
              TO ('2026-12-01');
```

En standardpartition stopper fejlene, men så havner rækkerne for alle udækkede
måneder sammen i den ene tabel, og når en ny partition senere tilkobles, scannes
standardpartitionen under en `ACCESS EXCLUSIVE`-lås, medmindre en
`CHECK`-constraint på standardpartitionen allerede udelukker det nye interval.
Behandl en standardpartition med rækker i som en alarm.

**Forespørgsler er langsommere efter partitioneringen end før.** De almindelige
forespørgsler filtrerer ikke på partitionsnøglen, så hver af dem læser alle
partitioner og deres indeks. En anden årsag er antallet af partitioner:
dokumentationen siger, at planneren klarer op til et par tusind ret godt,
forudsat at typiske forespørgsler skærer alle undtagen nogle få fra, og at
planlægningstid og hukommelsesforbrug vokser derudover. Vælg nøglen ud fra de
`WHERE`-betingelser, programmet allerede skriver, tjek med `EXPLAIN`, at kun de
forventede partitioner optræder, og hold antallet i et rimeligt forhold. Én
partition pr. kunde er en typisk fælde, fordi antallet vokser med forretningen;
dokumentationen foreslår i stedet hash-partitionering med et rimeligt antal
partitioner.

**En primærnøgle eller en unik constraint lader sig ikke oprette.** På en
partitioneret tabel skal en unik nøgle eller primærnøgle indeholde alle
partitionsnøglens kolonner, og partitionsnøglen må ikke indeholde udtryk eller
funktionskald. Hver partitions indeks kan kun håndhæve unikhed inden for sin
egen partition. Tilføj nøglekolonnen til constrainten, som notens `orders`-tabel
gør med `(id, created_at)`.

**En indeksbygning på forældretabellen blokerer skrivninger længe.** Et
almindeligt `CREATE INDEX` på forældretabellen spærrer for skrivninger, indtil
hver partition er indekseret, og `CONCURRENTLY` virker pr. partition og ikke på
forældretabellen. Opret indekset med `ON ONLY` på forældretabellen, så det
markeres som ugyldigt, byg indekset på hver partition med `CONCURRENTLY`, og
tilkobl hvert af dem med `ALTER INDEX ... ATTACH PARTITION`. Når alle
partitionernes indeks er tilkoblet, bliver forældreindekset gyldigt af sig selv.

**En eksisterende stor tabel lader sig ikke lægge om på stedet.** PostgreSQL gør
ikke en almindelig tabel til en partitioneret. Opret den nye partitionerede
tabel, og tilkobl den gamle tabel som en af dens partitioner. Tilføj før
tilkoblingen en `CHECK`-constraint, der svarer til partitionens grænser, så
tilkoblingen ikke scanner hele tabellen, mens den holder en
`ACCESS EXCLUSIVE`-lås på den.

## Læs mere

- [Table Partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html)
  — PostgreSQL-kapitlet: de tre metoder, pruning, vedligeholdelse,
  begrænsninger og best practices.
- [CREATE TABLE](https://www.postgresql.org/docs/current/sql-createtable.html)
  — `PARTITION BY`, `PARTITION OF`, partitionsgrænser og standardpartitionen.
- [ALTER TABLE](https://www.postgresql.org/docs/current/sql-altertable.html)
  — `ATTACH PARTITION` og `DETACH PARTITION`, og hvad hver af dem låser.
- [CREATE INDEX](https://www.postgresql.org/docs/current/sql-createindex.html)
  — indeks på partitionerede tabeller, `ON ONLY` og bygning med `CONCURRENTLY`.

<https://engineer.company/da/glossary/partitioning/>
