# Partisjonering

## Hva det er

Partisjonering, også kalt tabellpartisjonering, lagrer radene i én logisk tabell i flere
vanlige tabeller, der hver tar vare på radene med en nøkkel innenfor sine egne grenser,
mens spørringer og skriving fortsatt bruker det ene navnet de alltid har brukt.
PostgreSQL, som denne siden tar utgangspunkt i, kaller sin innebygde variant deklarativ
partisjonering: tabellen deklareres som partisjonert, med en metode og en
partisjonsnøkkel, og hver partisjon opprettes som en egen tabell.

Den partisjonerte tabellen har ingen rader selv. En innsetting rutes til partisjonen med
grenser som passer nøkkelen, og en oppdatering som endrer nøkkelen, flytter raden til
partisjonen der den nå hører hjemme. Partisjonene deler kolonnene til foreldretabellen,
men kan ha egne indekser, egne lagringsinnstillinger og eget tabellområde (tablespace),
og en partisjon kan selv partisjoneres videre.

Det finnes tre metoder. Intervallpartisjonering (range) gir hver partisjon et spenn av
verdier, som regel datoer; den nedre grensen er med og den øvre ikke, så med spennene
1–10 og 10–20 hører verdien 10 til det andre. Listepartisjonering (list) navngir
verdiene hver partisjon tar imot, f.eks. et sett landkoder. Hash-partisjonering gir hver
partisjon en modulus og en rest, og en rad havner der hashen av nøkkelen gir nettopp den
resten, noe som fordeler radene jevnt når det ikke finnes noe naturlig intervall å
skjære langs. En standardpartisjon (default) kan fange opp verdiene som ikke passer noe
annet sted, men bare ved intervall- og listepartisjonering.

Partisjonering skjer inne i én databaseserver. Å dele dataene på separate servere, hver
med sin egen kopi av databaseskjemaet, er sharding, og det er et mye større steg. SQL
Server og BigQuery har egne former for partisjonering, med egen syntaks, og tankene
nedenfor gjelder også der kommandoene er andre.

## Hvorfor det er viktig

De fleste store tabeller vokser i den ene enden. Ordrer, måleravlesninger, logger og
hendelser kommer inn hver dag, blir liggende i måneder eller år, og de fleste
spørringene gjelder den nyeste delen. Uten partisjonering arbeider hver spørring mot én
tabell og ett sett indekser som bare fortsetter å vokse.

Med en partisjonsnøkkel som passer spørringene, kan planleggeren bevise at de fleste
partisjonene ikke kan inneholde noen rad som stemmer, og utelate dem helt fra planen.
PostgreSQL kaller dette partition pruning, beskjæring av partisjoner. En rapport om
forrige uke leser forrige ukes partisjon og ingenting annet. Dokumentasjonen nevner også
en gevinst som er mindre opplagt: fordi partisjonene overtar jobben til de øverste
nivåene i et indekstre, er det større sjanse for at de mest brukte delene av indeksene
får plass i minnet.

Aller størst er endringen når gamle data skal bort. Å slette en måned med rader fra én
enkelt tabell er en lang `DELETE` som etterlater døde rader `VACUUM` må rydde opp i. Med
én partisjon per måned blir den samme oppbevaringsregelen én `DROP TABLE` eller én
`DETACH PARTITION`, som dokumentasjonen kaller langt raskere enn en masseoperasjon, og
som unngår kostnaden ved `VACUUM` helt. En frakoblet partisjon er fortsatt en vanlig
tabell, så det kan tas sikkerhetskopi av den, eller den kan oppsummeres, før den
slettes, og partisjoner som sjelden leses, kan ligge på billigere og tregere lagring.

Gevinsten har en pris, og dokumentasjonen sier rett ut når den lønner seg. Hver
partisjon gir planleggingen mer arbeid, og fordelene er normalt bare verdt det når
tabellen ellers ville vært svært stor. Tommelfingerregelen er at tabellen bør være
større enn det fysiske minnet på databaseserveren. Én ting endrer partisjonering heller
ikke: en rad som mange transaksjoner slåss om, er fortsatt én rad i én partisjon, bak én
lås.

## Slik ser det ut i praksis

Et notat om å skalere en database før den shardes, har et avsnitt om å
[partisjonere tabellene rundt den hete raden](/nb/notes/optimise-before-you-shard/).
Tabellen `orders` der er partisjonert per måned på `created_at`, med primærnøkkelen
`(id, created_at)`, fordi nøkkelen må inneholde partisjonskolonnen. En spørring som
angir et datointervall, leser bare partisjonene innenfor det, denne månedens indekser
holder seg små nok til å ligge i minnet, og oppbevaringsregelen blir én enkelt
`DETACH PARTITION` i stedet for en `DELETE` av millioner av rader. Notatet er like
tydelig på grensen: partisjonering gjør ikke raden det kjempes om, raskere, og en
spørring som ikke angir nøkkelen, leser hver eneste partisjon.

Et ytelsesprosjekt i PostgreSQL for en applikasjon med brukerkontoer, leietakere,
geografiske data og daglig strømforbruk
[delte strømtabellen inn i tidsintervaller](/nb/portfolio/accelerated-postgresql-performance-by-10x-via-strategic-indexing-15/)
per dag og måned, og brukte hash-partisjonering for de geografiske dataene. Det kom
sammen med GiST-indekser, sammensatte indekser og omskrevne spørringer, og samlet ble
svartidene for spørringer 10 ganger bedre, mens ressursbruken falt med 40 %.

En tredje side handler om
[mer enn 100 databaser i PostgreSQL, SQL Server og BigQuery](/nb/portfolio/architected-created-and-managed-100-postgresql-ms-sql-12/),
mye av det med geografiske data og tidsseriedata. Tidsseriepartisjonering var en del av
skjemadesignet i PostgreSQL og SQL Server, og i BigQuery ble det brukt tidspartisjonerte
og klyngede tabeller. Sammen med romlige indekser, materialiserte visninger og caching
falt svartidene for spørringer mot disse dataene med 40–60 %.

## Hvor det går galt

Partisjonering flytter en del av arbeidet fra det øyeblikket spørringen kjøres, til det
øyeblikket tabellen designes, og de fleste problemene kommer av en designbeslutning som
ble tatt uten at spørringene var i syne.

**Innsettinger begynner å feile første dag i en ny måned.** Ingen partisjon dekker det
nye intervallet, og PostgreSQL avviser en innsetting i foreldretabellen som ikke hører
hjemme i noen eksisterende partisjon. Opprett partisjonene på forhånd, fra en planlagt
jobb som genererer DDL-en, slik dokumentasjonen foreslår, og varsle når den nyeste
partisjonen nærmer seg slutten:

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

En standardpartisjon stopper feilene, men da havner radene for hver måned som ikke er
dekket, sammen i den ene tabellen, og å koble til en ny partisjon senere skanner
standardpartisjonen under en `ACCESS EXCLUSIVE`-lås, med mindre en `CHECK`-begrensning
på standardpartisjonen allerede utelukker det nye intervallet. Behandle hver rad
som havner i standardpartisjonen, som en alarm.

**Spørringene er tregere etter partisjoneringen enn før.** De vanlige spørringene
filtrerer ikke på partisjonsnøkkelen, så hver av dem leser alle partisjonene og
indeksene deres. En annen årsak er antallet partisjoner: dokumentasjonen sier at
planleggeren takler opptil noen få tusen godt, forutsatt at typiske spørringer beskjærer
bort alle unntatt noen få, og at planleggingstid og minnebruk vokser utover det. Velg
nøkkelen ut fra `WHERE`-leddene applikasjonen allerede skriver, sjekk med `EXPLAIN` at
bare de ventede partisjonene dukker opp, og hold antallet i rimelige proporsjoner. Én
partisjon per kunde er en typisk felle, siden antallet vokser med virksomheten;
dokumentasjonen foreslår heller hash-partisjonering med et fornuftig antall partisjoner.

**En primærnøkkel eller unik begrensning lar seg ikke opprette.** På en partisjonert
tabell må en unik nøkkel eller primærnøkkel inneholde hver kolonne i partisjonsnøkkelen,
og partisjonsnøkkelen kan ikke inneholde uttrykk eller funksjonskall. Indeksen i hver
partisjon kan bare håndheve unikhet innenfor den partisjonen. Legg nøkkelkolonnen til
begrensningen, slik notatets `orders`-tabell gjør med `(id, created_at)`.

**Å bygge en indeks på foreldretabellen blokkerer skriving lenge.** En vanlig
`CREATE INDEX` på foreldretabellen stenger ute skriving til hver partisjon er indeksert,
og valget `CONCURRENTLY` virker per partisjon, ikke på foreldretabellen. Opprett
indeksen med `ON ONLY` på foreldretabellen, som lar den stå merket som ugyldig, bygg
indeksen på hver partisjon med `CONCURRENTLY`, og koble hver av dem til med
`ALTER INDEX ... ATTACH PARTITION`. Når indeksen i hver partisjon er koblet til, blir
indeksen på foreldretabellen gyldig av seg selv.

**En eksisterende stor tabell kan ikke gjøres om der den står.** PostgreSQL gjør ikke en
vanlig tabell om til en partisjonert. Opprett den nye partisjonerte tabellen, og koble
den gamle tabellen til som én av partisjonene. Før tilkoblingen legger du til en
`CHECK`-begrensning som stemmer med partisjonens grenser, så tilkoblingen ikke skanner
hele tabellen mens den holder en `ACCESS EXCLUSIVE`-lås på den.

## Les mer

- [Table Partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html) —
  kapitlet i PostgreSQL-dokumentasjonen: de tre metodene, beskjæring, vedlikehold,
  begrensninger og beste praksis.
- [CREATE TABLE](https://www.postgresql.org/docs/current/sql-createtable.html) —
  `PARTITION BY`, `PARTITION OF`, partisjonsgrenser og standardpartisjonen.
- [ALTER TABLE](https://www.postgresql.org/docs/current/sql-altertable.html) —
  `ATTACH PARTITION` og `DETACH PARTITION`, med hvilken lås hver av dem tar.
- [CREATE INDEX](https://www.postgresql.org/docs/current/sql-createindex.html) —
  indekser på partisjonerte tabeller, `ON ONLY` og bygging med `CONCURRENTLY`.

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