# ETL

## Vad det är

ETL, kort för extract, transform, load, alltså extrahera, transformera och ladda, är arbetet med att
kopiera data ut ur de system som skapar dem, ge dem den form och betydelse som ett annat system
förväntar sig och skriva in dem där, oftast i ett analysdatalager. En nära variant, ELT, behåller
de tre stegen men laddar först rådata och transformerar dem inuti målsystemet, och många team
skriver ETL/ELT för att täcka båda.

Varje bokstav är ett eget jobb. Extraheringen läser från en källa: en hel tabell, en exportfil, ett
API, eller bara de rader som har ändrats sedan förra körningen, som hittas genom en tidsstämpel i
`updated_at` eller genom change data capture (CDC), som läser databasens egen förteckning över varje
ändring. Transformeringen är där data rensas och stäms av: text görs om till datum och tal, enheter
och tidszoner görs enhetliga, dubbletter tas bort, koder slås upp mot referenstabeller, och
resultatet formas om till den modell som målet använder, t.ex. fakta- och dimensionstabeller.
Laddningen skriver resultatet, antingen som en massinsättning i en tom tabell eller som en
sammanslagning som uppdaterar de rader som redan finns och lägger till de nya.

Ordningen mellan de två sista stegen avgör var arbetet körs. Klassisk ETL transformerar på en
separat server innan något når målet, vilket var rimligt när lagring och beräkningskraft i
analysdatalagret var knappa och dyra. ELT lägger rådata i mellanlagringstabeller, så kallade
staging-tabeller, inuti målet och kör transformeringen där i SQL. Råkopian finns kvar, så när en
regel ändras kan transformeringen köras om över historiken utan att den behöver extraheras en gång
till. Priset är att allt som källan skickade, även kolumner som ingen bad om, nu bor i
analysdatalagret och måste förvaltas där.

PostgreSQL täcker varje steg med egna verktyg. `COPY` flyttar rader mellan en tabell och en fil i
text-, CSV- eller binärformat och är den snabbaste vägen in. Att läsa en fil på servern kräver
superanvändarrättigheter eller rollen `pg_read_server_files`, medan psql-kommandot `\copy` i stället
läser filen på klienten. `INSERT ... ON CONFLICT` och `MERGE` fogar in nya rader i befintliga.
Logisk avkodning gör om transaktionsloggen, write-ahead log, till en ström av radändringar för CDC,
och en procedur i PL/pgSQL kan hålla transformeringen nära de data den ändrar.

## Varför det spelar roll

De flesta frågor som är värda att ställa till en verksamhet behöver data från mer än ett system.
Ordrar ligger i en databas, betalningar i en export från en leverantör, kunduppgifter i ett
CRM-system. Tills något flyttar dem till ett och samma ställe under ett och samma regelverk sätts
varje svar ihop för hand, och handen ändras med personen som gör jobbet. ETL är det där något, och
ett analysdatalager är bara så korrekt som de laddningar som fyller det.

Transformeringssteget är också där en verksamhet skriver ned vad dess data betyder. Vilka rader som
räknas som en försäljning, vilken dag en betalning vid midnatt hör till, vilken post som vinner när
två kunder matchar: avgjort en gång i laddningen gäller svaret för varje rapport längre ned i
kedjan. Lämnas frågan åt varje rapport glider svaren isär, och två dashboards visar två olika
summor.

Hur en laddning extraherar avgör hur den växer. En full omladdning kopierar allt varje gång. Den är
enkel och rättar sig själv, eftersom gårdagens misstag skrivs över i natt, men kostnaden växer med
den totala datamängden. En inkrementell laddning flyttar bara det som har ändrats, så kostnaden
växer i stället med ändringstakten. Den måste komma ihåg var förra körningen slutade, märka rader
som har raderats i källan och ta emot rader som kommer sent med gamla tidsstämplar. De flesta
system börjar med fulla omladdningar och byter tabell för tabell när natten inte längre räcker till.

Den löpande kostnaden är att laddningar är kod som körs utan tillsyn, oftast på natten, mot källor
som ändras utan förvarning. De behöver loggning som säger vad som kördes och hur många rader som
flyttades, larm när en körning stannar, och framför allt en egenskap: en körning som upprepas med
samma indata ska lämna målet exakt som en enda körning skulle ha gjort. En laddning med den
egenskapen kan köras om efter vilket avbrott som helst utan att någon först behöver undersöka läget.

## Så ser det ut i praktiken

Mer än 600 pipelines, en migrering som kördes en enda gång och sex laddningar genom ett VPN: samma
tre steg återkommer här i mycket olika skala.

I flera PostgreSQL-miljöer för utveckling och produktion
[skrevs mer än 600 ETL/ELT-pipelines i PL/pgSQL](/sv/portfolio/engineered-600-pl-pgsql-based-etl-elt-pipelines-54/)
för att ersätta fragmenterade datapipelines som försenade affärskritisk rapportering. Villkor för
primärnycklar och främmande nycklar skyddade referensintegriteten under transformeringarna, unika
och sammansatta index snabbade upp JOIN-operationerna, och undantagshantering med
transaktionsstyrning rullade tillbaka en körning när något gick snett. Inkrementella laddningar
genom CDC och deltan baserade på tidsstämplar minskade bearbetningstiden med över 60 %, och varje
körning loggades och granskades. Den genomsnittliga körtiden sjönk med 35 %, genomströmningen
ökade med mer än 50 %, och över 90 % av transformeringsfelen eliminerades genom bättre tillämpning
av villkor och valideringslogik.

En migrering är ETL som körs en gång. Att flytta en kartapplikation från Microsoft SQL Server till
PostgreSQL med PostGIS innebar att föra
[över 50 miljoner spatiala poster genom ETL-pipelines](/sv/portfolio/improved-geographical-map-application-performance-by-10x-through-6/)
vars transformeringssteg räknade om varje koordinat till referenssystemet EPSG:4326. På den nya
sidan fanns index av typerna GiST och SP-GiST och en finjusterad konfiguration. Migreringen som
helhet gav tio gånger bättre prestanda för spatiala databasfrågor och 65 % lägre processorbelastning
i backend, och i genomsnitt kom svaren på under 250 millisekunder, mot 2,5 sekunder före flytten.

Över en nätverksgräns visar
[sex ETL/ELT-pipelines](/sv/portfolio/designed-implemented-and-administered-6-etl-elt-pipelines-8/)
varje bokstav i namnet i arbete. Extraheringen bestod av schemalagda nedladdningar av komprimerade
arkiv från en server som bara gick att nå genom ett IPsec-VPN, där skript i Bash och Python
sorterade filerna i Parquet, CSV och .bak efter typ och schema, och säkerhetskopiorna i .bak
återställdes till en lokal MSSQL-instans. Data lästes in i staging-scheman i MSSQL och
PostgreSQL med bcp, psql eller SSIS beroende på formatet, rensades, normaliserades och berikades av
procedurer i PL/pgSQL och T-SQL, komprimerades sedan med gzip och fördes via en bucket i Google
Cloud Storage till Google BigQuery. Arbete som hade tagit mer än tre timmar för hand gick på under
20 minuter från början till slut, och synkroniseringen gick från veckovis till daglig.

## Var det går fel

**En omkörning laddar samma rader två gånger.** Ett jobb gör vanliga `INSERT`, stannar halvvägs på
ett nätverksfel, och schemaläggaren kör det igen från början. Rader som kom in första gången finns
nu två gånger, och varje summa som bygger på dem blir för hög. Ge målet en nyckel som identifierar
en rad i verksamhetens termer och foga in batchen med `INSERT ... ON CONFLICT DO UPDATE`, som
PostgreSQL dokumenterar som en atomisk infogning eller uppdatering även vid hög samtidighet, eller
med `MERGE`. En hake finns: en enskild sats med `ON CONFLICT DO UPDATE` vägrar ändra samma rad två
gånger och ger ett kardinalitetsfel, så en batch med upprepade nycklar måste först reduceras till
en rad per nyckel.

```sql
INSERT INTO meter_reading (meter_id, read_at, kwh)
SELECT DISTINCT ON (meter_id, read_at) meter_id, read_at, kwh
FROM staging_meter_reading
ORDER BY meter_id, read_at, received_at DESC
ON CONFLICT (meter_id, read_at)
DO UPDATE SET kwh = EXCLUDED.kwh;
```

**En massladdning stannar nära slutet och lämnar disken fullare än förut.** `COPY FROM` skriver
rader till tabellen allteftersom. När kommandot stannar på en dålig rad blir de raderna kvar i
raderat tillstånd, osynliga för databasfrågor men kvar på disken tills `VACUUM` återvinner dem, och
ett avbrott sent i en stor fil kan slösa mycket utrymme. PostgreSQL 17 lade till `ON_ERROR ignore`,
som hoppar över rader i text- eller CSV-indata vars värden inte går att konvertera till kolumnens
typ, och PostgreSQL 18 lade till `REJECT_LIMIT` för att begränsa hur många rader som får hoppas
över. Det täcker bara typkonvertering; en rad som bryter mot ett villkor stoppar fortfarande
kommandot. Att först ladda in i en staging-tabell, kontrollera den där och flytta de bra raderna
till den riktiga tabellen i en enda transaktion håller en dålig fil borta från de data som folk
läser.

**Change data capture fyller disken.** Logisk avkodning läser ändringar genom en replikeringsslot,
och en slot behåller varje del av transaktionsloggen som dess konsument fortfarande behöver, oavsett
om en konsument är ansluten eller inte. När läsaren stannar hopar sig loggen tills disken är full,
och i extrema fall stänger servern av sig själv för att förhindra wraparound av transaktions-id.
Dokumentationen för PostgreSQL är tydlig: en slot som inte längre behövs ska tas bort. Efter en
krasch kan en slot också skicka nyliga ändringar en gång till, så konsumenten måste tåla
dubbletter, alltså samma krav på säkra omkörningar som ovan.

**Morgonens första rapport är långsam efter en stor laddning.** Planeraren väljer hur en
databasfråga ska köras utifrån statistik om varje tabell, och en massladdning ändrar datan under
statistiken. Autovacuum kan uppdatera statistiken på egen hand, men inte alltid innan den första
rapporten körs. Att köra `ANALYZE` som laddningens sista steg löser det. För mycket stora
laddningar till en befintlig tabell tillägger guiden från PostgreSQL om att fylla en databas att det
kan löna sig att ta bort index och främmande nycklar och skapa dem igen efteråt, eftersom det går
snabbare att bygga dem i ett svep än att uppdatera dem rad för rad. Under tiden kontrolleras
villkoren inte, så ett unikt index förtjänar en extra tanke innan det tas bort.

## Läs mer

- [COPY](https://www.postgresql.org/docs/current/sql-copy.html) — kommandot för massladdning, dess
  format och inställningar, och vad en misslyckad laddning lämnar efter sig.
- [Populating a Database](https://www.postgresql.org/docs/current/populate.html) — råden från
  PostgreSQL för stora laddningar, från en enda transaktion och `COPY` till att bygga om index och
  köra `ANALYZE` efteråt.
- [MERGE](https://www.postgresql.org/docs/current/sql-merge.html) — infogning, uppdatering och
  borttagning i en och samma sats, styrd av hur varje rad i källan matchar målet.
- [INSERT](https://www.postgresql.org/docs/current/sql-insert.html) — satsdelen `ON CONFLICT`, dess
  atomiska upsert och regeln mot att röra samma rad två gånger.
- [Logical Decoding Concepts](https://www.postgresql.org/docs/current/logicaldecoding-explanation.html)
  — hur ändringar läses ur transaktionsloggen och varför en oanvänd replikeringsslot måste bort.

<https://engineer.company/sv/glossary/etl/>
