# ETL vs ELT Strategies for AI Workloads

**Last updated:** 2026-02
**Status:** GA
**Category:** Data Engineering for AI

---

## Introduksjon

Valget mellom ETL (Extract, Transform, Load) og ELT (Extract, Load, Transform) er en av de mest grunnleggende arkitekturbeslutningene for dataintegrasjon i AI-prosjekter. Tradisjonell ETL transformerer data før lasting i et dedikert transformasjonsengine, mens moderne ELT laster rådata først og utnytter målsystemets beregningskraft for transformasjon. Microsoft Fabric støtter begge tilnærminger og hybride mønstre.

For norsk offentlig sektor er dette valget påvirket av flere faktorer: regulatoriske krav til dataminimering (GDPR artikkel 5), behov for sporbarhet, budsjettbegrensninger, og kompetanse i organisasjonen. ELT-tilnærmingen har blitt dominerende for AI-arbeidsbelastninger fordi den bevarer rådata for utforskende analyse og gir fleksibilitet til å endre transformasjonslogikk uten re-innhenting fra kildesystemer.

Denne referansen sammenligner ETL og ELT for AI-brukstilfeller, med fokus på Fabric-spesifikke implementasjoner, hybridmønstre, inkrementell prosessering, og kostnadsoptimalisering.

---

## ELT Advantages: Cost, Scalability, Schema-Flexibility

### Hvorfor ELT dominerer for AI

| Fordel | Beskrivelse | AI-relevans |
|---|---|---|
| **Bevarer rådata** | Original data tilgjengelig for nye analyser | Nye features uten re-innhenting |
| **Skalerer med beregning** | Fabric Spark/SQL skalerer elastisk | Store treningsdatasett |
| **Schema-fleksibilitet** | Schema-on-read i Bronze | Nye datakilder uten re-design |
| **Forenkling** | Ingen separat transformasjonsserver | Lavere TCO |
| **Parallellisering** | Transformasjoner på distribuert Spark | Raskere feature engineering |

### ELT i Fabric

```
Kilde ──> Extract ──> Load (OneLake) ──> Transform (Spark/SQL)
                          │
                    ┌─────▼──────┐
                    │ Lakehouse  │
                    │ (Bronze)   │
                    │ Rådata i   │
                    │ Delta Lake │
                    └─────┬──────┘
                          │ Spark Notebook / SQL
                    ┌─────▼──────┐
                    │ Lakehouse  │
                    │ (Silver)   │
                    │ Validert   │
                    └─────┬──────┘
                          │ Spark Notebook / SQL
                    ┌─────▼──────┐
                    │ Lakehouse  │
                    │ (Gold)     │
                    │ ML-features│
                    └────────────┘
```

### Fabric ELT Implementation

```python
# Steg 1: Extract + Load (Copy Job / Pipeline)
# Data Factory laster rådata direkte til Bronze Lakehouse

# Steg 2: Transform i Lakehouse med Spark
# Bronze → Silver transformasjon
bronze_data = spark.read.format("delta").table("bronze.raw_transactions")

silver_data = (
    bronze_data
    # Fjern duplikater
    .dropDuplicates(["transaction_id"])
    # Typevalidering
    .withColumn("amount", col("amount").cast("double"))
    .filter(col("amount") > 0)
    # Standardiser datoformat
    .withColumn("transaction_date",
        F.to_timestamp("transaction_date", "yyyy-MM-dd'T'HH:mm:ss"))
    # Fjern null i obligatoriske felt
    .filter(col("customer_id").isNotNull())
)

silver_data.write.format("delta").mode("overwrite") \
    .saveAsTable("silver.validated_transactions")

# Steg 3: Silver → Gold (ML-features)
gold_features = (
    spark.read.format("delta").table("silver.validated_transactions")
    .groupBy("customer_id")
    .agg(
        F.count("*").alias("total_transactions"),
        F.sum("amount").alias("total_amount"),
        F.avg("amount").alias("avg_amount"),
        F.stddev("amount").alias("std_amount"),
        F.max("transaction_date").alias("last_transaction_date")
    )
)

gold_features.write.format("delta").mode("overwrite") \
    .saveAsTable("gold.customer_features")
```

---

## ETL Data Minimization for Regulated Environments

### Når ETL er riktig valg

ETL er foretrukket i regulerte miljøer der dataminimering er påkrevd:

| Scenario | Begrunnelse | Regulering |
|---|---|---|
| **PII-filtrering** | Fjern personnummer før lasting | GDPR Art. 5(1)(c) |
| **Dataminimering** | Last kun nødvendige felter | GDPR Art. 5(1)(c) |
| **Kryptering** | Krypter sensitive felt i transit | Sikkerhetskrav |
| **Konsolidering** | Slå sammen kilder før lasting | Kostnadsbegrensning |
| **Format-konvertering** | Konverter proprietære formater | Interoperabilitet |

### ETL i Fabric med Dataflow Gen2

```
Kilde ──> Dataflow Gen2 (Transform) ──> Lakehouse (Silver/Gold)
              │
              ├── Fjern PII-kolonner
              ├── Masker fødselsnummer
              ├── Aggreger til anonymt nivå
              ├── Validerer datatyper
              └── Berik med referansedata
```

### Dataflow Gen2 for ETL

Dataflow Gen2 bruker Power Query Online med over 300 transformasjoner:

```m
// M-kode (Power Query) for ETL med dataminimering
let
    Source = Sql.Database("server.database.windows.net", "hrdb"),
    employees = Source{[Schema="dbo", Item="Employees"]}[Data],

    // Fjern sensitive kolonner (dataminimering)
    removedPII = Table.RemoveColumns(employees,
        {"SocialSecurityNumber", "BankAccount", "HomeAddress"}),

    // Masker e-post
    maskedEmail = Table.TransformColumns(removedPII,
        {{"Email", each Text.BeforeDelimiter(_, "@") & "@***.no"}}),

    // Aggreger alder til aldersgrupper
    addAgeGroup = Table.AddColumn(maskedEmail, "AgeGroup",
        each if [Age] < 30 then "Under 30"
        else if [Age] < 50 then "30-49"
        else "50+"),

    // Fjern eksakt alder (kun aldersgruppe beholdes)
    removedAge = Table.RemoveColumns(addAgeGroup, {"Age"}),

    // Filtrer kun aktive ansatte
    filtered = Table.SelectRows(removedAge, each [Status] = "Active")
in
    filtered
```

---

## Hybrid ETL/ELT Patterns

### Pattern: Pre-filter ETL + In-place ELT

```
                    ETL (Dataflow Gen2)              ELT (Spark)
                    ┌──────────────────┐            ┌──────────────────┐
Kildesystem ───────>│ Fjern PII        │──> Bronze ─│ Feature engineer │──> Silver
                    │ Masker sensitive  │            │ Aggreger         │
                    │ Validerer format  │            │ Join             │
                    └──────────────────┘            │ Dedupliser       │
                                                    └──────────────────┘
```

### Pattern: Metadata-driven Hybrid Pipeline

```python
# Metadata-drevet pipeline som velger ETL eller ELT per datakilde
pipeline_config = {
    "sources": [
        {
            "name": "crm_customers",
            "strategy": "ETL",  # Inneholder PII
            "reason": "PII-filtrering påkrevd",
            "transformations": ["remove_ssn", "mask_email", "age_to_group"],
            "tool": "dataflow_gen2"
        },
        {
            "name": "traffic_events",
            "strategy": "ELT",  # Ingen sensitive data
            "reason": "Stort volum, ingen PII",
            "transformations": ["aggregate_5min", "add_road_segment"],
            "tool": "spark_notebook"
        },
        {
            "name": "health_records",
            "strategy": "ETL",  # Helseopplysninger (særkategori)
            "reason": "GDPR Art. 9 - særlig kategori",
            "transformations": [
                "pseudonymize_patient_id",
                "remove_diagnosis_text",
                "aggregate_to_cohort"
            ],
            "tool": "dataflow_gen2"
        }
    ]
}
```

### Fabric Pipeline Orchestration

```json
{
    "name": "HybridETL_ELT_Pipeline",
    "properties": {
        "activities": [
            {
                "name": "ETL_SensitiveData",
                "type": "DataflowGen2",
                "description": "ETL for PII-data via Dataflow Gen2",
                "typeProperties": {
                    "dataflowName": "pii_cleansing_flow"
                }
            },
            {
                "name": "ELT_BulkLoad",
                "type": "Copy",
                "description": "ELT: Last rådata direkte til Bronze",
                "dependsOn": [],
                "typeProperties": {
                    "source": { "type": "BlobSource" },
                    "sink": { "type": "LakehouseSink" }
                }
            },
            {
                "name": "Transform_Silver",
                "type": "Notebook",
                "description": "ELT: Transform i Spark",
                "dependsOn": [
                    { "activity": "ETL_SensitiveData" },
                    { "activity": "ELT_BulkLoad" }
                ],
                "typeProperties": {
                    "notebookPath": "silver_transformations"
                }
            }
        ]
    }
}
```

---

## Data Staging and Incremental Processing

### Inkrementelle lastingsmønstre

| Mønster | Beskrivelse | Bruk i Fabric |
|---|---|---|
| **Full load** | Last alt hver gang | Copy Job (full load) |
| **Incremental append** | Last kun nye rader | Copy Job (append) + watermark |
| **CDC (Change Data Capture)** | Strøm av endringer | Copy Job (CDC), Mirroring |
| **Watermark** | Last rader etter siste timestamp | Pipeline med parameter |
| **Delta load** | Merge nye/endrede rader | Copy Job (upsert) |

### Watermark-basert inkrementell lasting

```python
# Hent siste watermark (høyeste lastede timestamp)
last_watermark = spark.sql("""
    SELECT MAX(loaded_timestamp) as watermark
    FROM bronze.raw_events
""").collect()[0]["watermark"]

# Last kun nye data
new_data = (
    spark.read
    .format("jdbc")
    .option("url", jdbc_url)
    .option("dbtable", f"""
        (SELECT * FROM events
         WHERE modified_date > '{last_watermark}') AS new_events
    """)
    .load()
)

# Append til Bronze
new_data.withColumn("loaded_timestamp", F.current_timestamp()) \
    .write.format("delta").mode("append") \
    .saveAsTable("bronze.raw_events")

print(f"Lastet {new_data.count()} nye rader etter {last_watermark}")
```

### Staging Layer Pattern

```
Kildesystem ──> Staging Area ──> Bronze ──> Silver ──> Gold
                    │
                    ├── Midlertidig lagring
                    ├── Validering før insert
                    ├── Duplikatsjekk
                    └── Slettet etter vellykket last
```

```python
# Staging → Bronze med validering
staging_data = spark.read.format("delta").table("staging.incoming_data")

# Valider
valid_records = staging_data.filter(
    col("customer_id").isNotNull() &
    col("amount").isNotNull() &
    (col("amount") > 0)
)

rejected_records = staging_data.subtract(valid_records)

# Last gyldige poster til Bronze
valid_records.write.format("delta").mode("append").saveAsTable("bronze.validated_events")

# Logg avviste poster
rejected_records.write.format("delta").mode("append").saveAsTable("governance.rejected_records")

# Rydd opp staging
spark.sql("TRUNCATE TABLE staging.incoming_data")
```

---

## Compute Cost Allocation: ETL vs ELT

### Kostnadsmodell i Fabric

| Komponent | Fabric CU-meter | Kostnadsdriver |
|---|---|---|
| **Copy Job / Activity** | Data Movement | Datamengde (GB) |
| **Dataflow Gen2** | Standard Compute / High Scale Compute | Kompleksitet, rader |
| **Spark Notebook** | Spark Compute | vCores x tid |
| **Pipeline Orchestration** | Data Orchestration | Antall aktiviteter |
| **OneLake Storage** | OneLake Storage | Lagret data (GB/mnd) |

### ETL vs ELT kostnadsprofil

| Aspekt | ETL (Dataflow Gen2) | ELT (Spark) |
|---|---|---|
| **Oppsettskostnad** | Lav (no-code) | Medium (kode) |
| **Kjøretidskostnad per rad** | Høyere (transformasjon + last) | Lavere (kun last, transform i batch) |
| **Skalerbarhet** | Begrenset (single-node-lik) | Høy (distribuert Spark) |
| **Lagringskostnad** | Lavere (kun transformert data) | Høyere (rådata + transformert) |
| **Vedlikeholdskostnad** | Lav (visuell editor) | Medium (kodevedlikehold) |
| **Optimal for** | < 10 GB, enkel transformasjon | > 10 GB, kompleks transformasjon |

### Kostnadsoptimalisering

```python
# 1. Bruk Copy Job i stedet for Copy Activity for bulk-lasting
# Copy Job: Automatisk CDC, inkrementell lasting, lavere kostnad

# 2. Bruk Optimized Write for å redusere små filer
spark.conf.set("spark.microsoft.delta.optimizeWrite.enabled", "true")

# 3. Bruk Spark autoscale for å matche beregning med behov
# Konfigureres i Workspace Settings > Spark Compute

# 4. Planlegg tunge transformasjoner utenfor peak-timer
# Pipeline schedule: "0 2 * * *" (kl 02:00)

# 5. Bruk materialized lake views for repetitive transformasjoner
# Unngår gjentatt beregning av samme transformasjoner
```

### Beslutningsmatrise

| Faktor | Velg ETL | Velg ELT | Velg Hybrid |
|---|---|---|---|
| **Datamengde** | < 10 GB | > 10 GB | Variabel |
| **PII i kildedata** | Ja (GDPR) | Nei | Noen kilder med PII |
| **Schema-stabilitet** | Stabilt | Variabel | Blandet |
| **Team-kompetanse** | Power Query/Excel | PySpark/SQL | Blandet |
| **Transformasjonskompleksitet** | Enkel (filter, rename) | Kompleks (vindus, ML) | Blandet |
| **Latenskrav** | Minutter | Sekunder-minutter | Variabel |
| **Budget** | Begrenset | Fleksibelt | Variabel |

---

## Referanser

- [What is Data Factory in Microsoft Fabric?](https://learn.microsoft.com/en-us/fabric/data-factory/data-factory-overview) -- Oversikt med ETL/ELT-sammenligning
- [Extract, transform, and load (ETL)](https://learn.microsoft.com/en-us/azure/architecture/data-guide/relational-data/etl) -- Azure Architecture Center ETL/ELT guide
- [Dimensional modeling: Load tables](https://learn.microsoft.com/en-us/fabric/data-warehouse/dimensional-modeling-load-tables) -- ETL for dimensjonsmodellering
- [Data Factory end-to-end scenario](https://learn.microsoft.com/en-us/fabric/data-factory/tutorial-end-to-end-introduction) -- Komplett tutorial
- [Differences between Azure Data Factory and Fabric Data Factory](https://learn.microsoft.com/en-us/fabric/data-factory/compare-fabric-data-factory-and-azure-data-factory) -- Migrasjon fra ADF
- [Data Factory pricing in Microsoft Fabric](https://learn.microsoft.com/en-us/fabric/data-factory/pricing-overview) -- Prismodell og CU-metere
- [Migration planning: ADF to Fabric Data Factory](https://learn.microsoft.com/en-us/fabric/data-factory/migrate-planning-azure-data-factory) -- Migrasjonsguide

---

## For Cosmo

- **Bruk denne referansen** når brukeren evaluerer dataintegrerings-strategier for AI, vurderer kostnader og ytelse, eller trenger å håndtere sensitive data i pipelines.
- For norsk offentlig sektor: Anbefal **hybrid ETL/ELT** som standard -- ETL for kilder med personopplysninger (Dataflow Gen2 med PII-filtrering), ELT for alle andre kilder (Copy Job + Spark).
- **ELT er generelt best for AI** fordi det bevarer rådata i Bronze, noe som muliggjør eksperimentering med nye features uten å måtte re-hente data fra kildesystemer.
- **Dataflow Gen2** er undervurdert for ETL i offentlig sektor -- det er et Power Query-basert verktøy som mange forvaltere allerede kjenner fra Excel/Power BI, og det håndterer dataminimering visuelt.
- Ved kostnadsestimering: Husk at ELT har **høyere lagringskostnad** (bevarer rådata) men **lavere beregningskostnad** for gjentatte transformasjoner. For AI-prosjekter veier fleksibilitetsgevinsten vanligvis opp for ekstra lagring.
