ktg-plugin-marketplace/plugins/ms-ai-architect/skills/ms-ai-engineering/references/data-engineering/etl-vs-elt-ai.md
Kjell Tore Guttormsen 6a7632146e feat(ms-ai-architect): add plugin to open marketplace (v1.5.0 baseline)
Initial addition of ms-ai-architect plugin to the open-source marketplace.
Private content excluded: orchestrator/ (Linear tooling), docs/utredning/
(client investigation), generated test reports and PDF export script.
skill-gen tooling moved from orchestrator/ to scripts/skill-gen/.

Security scan: WARNING (risk 20/100) — no secrets, no injection found.
False positive fixed: added gitleaks:allow to Python variable reference
in output-validation-grounding-verification.md line 109.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 17:17:17 +02:00

16 KiB

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

# 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-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

# 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

{
    "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

# 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
# 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

# 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


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.