Opbygning af Medallion-arkitekturer i Databricks: Hvad der faktisk virker

Ethvert team, der tager Databricks i brug, står til sidst over for det samme spørgsmål: hvad er den rigtige måde at bygge en Bronze-Silver-Gold pipeline på? Der er mindst et halvt dusin legitime tilgange, hver med sine egne afvejninger, og dokumentationen sætter dem sjældent side om side.

Så jeg implementerede den samme pipeline på syv forskellige måder -- samme CSV-data, samme skema, samme Kimball star schema i enden -- og sammenlignede resultaterne. Hele projektet er deployet som et Databricks Asset Bundle, og koden ligger i dette repo.

Dette indlæg gennemgår hver enkelt tilgang, fra den mest manuelle til den mest deklarative. Hvis du bare vil have anbefalingen, kan du springe til slutningen -- men rejsen er det værd.


Testcasen

Datasættet er bevidst lille: kunder, produkter, ordrer og ordrelinjer, fordelt på to batches. Batch 1 er den indledende indlæsning. Batch 2 introducerer en SCD2-trigger -- to kunder ændrer deres oplysninger, én ny kunde dukker op, og der er en ny ordre.

Gold-laget er en klassisk Kimball dimensionel model: dim_customer (med SCD2-historik), dim_product, dim_date og fact_order_line. Når alle batches er behandlet, skal hver tilgang producere præcis 8 kunderækker (5 aktuelle + 2 historiske + 1 ny), 5 produkter, 91 datoer (hele månederne jan-mar 2024) og 11 fakta-rækker.

Hvis tallene ikke stemmer, er der noget galt med implementeringen. Den begrænsning holder sammenligningen ærlig.

1. Python notebooks -- den manuelle baseline

Kode: bronze.py, silver.py, gold.py

Det er her, de fleste teams starter. Tre PySpark notebooks -- én per lag -- der gør alt manuelt. Bronze læser CSV-filer og tagger dem med metadata. Silver deduplikerer (seneste batch vinder via row_number over et window), caster typer og standardiserer tekst. Gold bygger den dimensionelle model.

window = (Window
    .partitionBy("customer_id")
    .orderBy(F.col("_batch_id").desc()))

silver_customers = (bronze_customers
    .withColumn("rn", F.row_number().over(window))
    .where("rn = 1"))

SCD2-logikken i Gold er den mest smertefulde del. Man ender med manuelt at sammenligne batch-snapshots, bygge historiske rækker og sy valid_from / valid_to / is_current sammen. Det virker, men det er omstændeligt og skrøbeligt.

Fordelen er fuld kontrol. Du kan gøre hvad du vil. Ulempen er, at du er nødt til at gøre alt selv -- inklusiv ting, som platformen kan håndtere for dig.

2. SQL notebooks med COPY INTO

Kode: bronze.sql, silver.sql, gold.sql

Samme arkitektur, men alt er SQL. Bronze bruger COPY INTO i stedet for spark.read. Silver er et CREATE OR REPLACE TABLE AS SELECT med CTE-baseret deduplikering. Gold bygger SCD2 med LEFT ANTI JOIN og ændringsdetektion -- stadig manuelt, men nok mere læsbart, hvis dit team tænker i SQL.

COPY INTO bronze_orders
FROM '/Volumes/.../orders'
    FILEFORMAT = CSV
FORMAT_OPTIONS ('header' = 'true');

For SQL-tunge teams føles denne tilgang naturlig. Code reviews er nemmere. Men det grundlæggende problem er det samme: orkestrering er stadig opgave-for-opgave, inkrementel behandling er manuel, og SCD2 kræver den samme omhyggelige merge-logik.

3. Materialized Views + Streaming Tables

Kode: setup.sql, scd2_merge.sql

Dette er det første skridt mod deklarativ tænkning. I stedet for at skrive imperativ ETL fortæller du Databricks, hvad tabellen skal se ud, og den finder ud af, hvordan den kommer derhen.

CREATE OR REFRESH STREAMING TABLE bronze_orders AS
SELECT *
FROM STREAM read_files('/Volumes/.../orders', format => 'csv');

CREATE OR REPLACE MATERIALIZED VIEW silver_orders AS
SELECT ...
FROM bronze_orders;

Bronze-tabeller bliver til streaming tables (Auto Loader under motorhjelmen -- den holder styr på, hvilke filer der er behandlet). Silver-tabeller bliver til materialized views, som Databricks opdaterer automatisk. SQL'en er bemærkelsesværdigt kort og ren.

Hagen? SCD2. Materialized views understøtter ikke slowly changing dimensions nativt. Du har brug for en separat MERGE-notebook som workaround, hvilket bryder det rene deklarative mønster. Hvis dit use case ikke kræver SCD2, er denne tilgang svær at slå for enkelhed. Hvis det gør, ender du med at blande paradigmer.

4. dbt-core på Databricks

Kode: src/dbt_project

dbt bringer en anden filosofi. I stedet for at tænke i pipelines tænker du i modeller -- SQL SELECT-statements, der definerer, hvad en tabel skal indeholde. dbt håndterer afhængighedsrækkefølge, test og dokumentation.

SCD2 håndteres via dbt snapshots, som sammenligner rækketilstande mellem kørsler:

{`{% snapshot snap_dim_customer %}
{{ config(strategy='check', unique_key='customer_id',
          check_cols=['email','address','city','country','segment']) }}
SELECT * FROM {{ ref('silver_customers') }}
{% endsnapshot %}`}

Workflowet er lidt usædvanligt. Fordi dbt snapshots fanger ændringer mellem kørsler (ikke mellem batches), kører bundlet en to-faset proces: først indlæs batch 1, snapshot, derefter indlæs batch 2, snapshot igen, og så byg gold. valid_from / valid_to-værdierne udledes fra _batch_id i stedet for snapshot-timestamps, da vi har brug for deterministiske forretningsdatoer, ikke kørselstidspunkter.

Den primære fordel ved dbt er organisatorisk. Det håndhæver en projektstruktur, ref-baserede afhængigheder og en testkultur (not_null, unique, relationships). Hvis dit team allerede bruger dbt, eller I værdsætter portable SQL-kompetencer, er dette et stærkt valg.

Afvejningen: du tilføjer et værktøj. Der er et projekt at vedligeholde (dbt_project.yml, profiles.yml, schema.yml), og dbt's verdensmodel mapper ikke perfekt til streaming- eller pipeline-native features i Databricks. Hvis du vil prøve det, er dbt-databricks-adapteren startpunktet.

5. Delta Live Tables -- den klassiske syntaks

Kode: pipeline.sql

Delta Live Tables (DLT) var Databricks' første rigtige forsøg på deklarative pipelines. Det introducerede nogle genuint nyttige idéer: indbyggede data quality expectations, nativ SCD2 via APPLY CHANGES og en managed runtime, der håndterer inkrementel behandling for dig.

CREATE STREAMING LIVE TABLE bronze_customers AS
SELECT * FROM STREAM read_files('/Volumes/.../customers', format => 'csv');

APPLY CHANGES INTO LIVE.dim_customer
FROM STREAM(LIVE.bronze_customers)
KEYS (customer_id)
STORED AS SCD TYPE 2;

SCD2 er højdepunktet. To linjer erstatter snesevis af linjer med manuel merge-logik. Runtimen producerer automatisk __START_AT / __END_AT-kolonner (lidt anderledes navngivning end valid_from / valid_to-konventionen, men semantisk ækvivalent).

Så hvorfor ikke stoppe her? Fordi Databricks siden har omdøbt og moderniseret denne syntaks. DLT-nøgleord som CREATE STREAMING LIVE TABLE og APPLY CHANGES INTO virker stadig, men de er legacy. Nye projekter bør bruge den aktuelle navngivning.

6. Declarative Pipelines (Lakeflow) -- den anbefalede tilgang

Kode: SQL-version, Python-version

Dette er den moderne evolution af DLT -- nu kaldet Lakeflow Declarative Pipelines. Samme runtime, samme kapabiliteter, men med opdateret syntaks, der er i overensstemmelse med standard SQL-konventioner:

CREATE OR REFRESH STREAMING TABLE bronze_customers AS
SELECT * FROM STREAM read_files('/Volumes/.../customers', format => 'csv');

CREATE FLOW scd2_dim_customer AS AUTO CDC INTO dim_customer
FROM STREAM(bronze_customers)
KEYS (customer_id)
STORED AS SCD TYPE 2;

SQL-versionen læser som det, den gør. Python-versionen bruger @dlt.table-dekoratorer og create_auto_cdc_flow() -- samme logik, forskellig syntaks afhængigt af dit teams præference.

Det, der får denne tilgang til at skille sig ud:

  • Inkrementel som standard. Streaming tables holder styr på, hvad der er behandlet. Du skriver ikke checkpoint-logik.
  • Nativ SCD2. AUTO CDC INTO håndterer historiksporing. Ingen manuel merge, ingen snapshot-workaround.
  • Indbygget datakvalitet. CONSTRAINT ... EXPECT ... ON VIOLATION DROP ROW validerer data inline.
  • Én pipeline-definition. Hele Bronze-Silver-Gold-flowet ligger i én fil. Databricks håndterer afhængighedsopløsning og udførelsesrækkefølge.
  • Mindre kode samlet set. SQL-pipelinen er én fil, der erstatter tre notebooks med manuel logik.

Afvejningerne er reelle, men håndterbare. Pipeline-syntaksen har sin egen semantik, som du skal lære. Debugging foregår i pipeline-runtimen, ikke i en notebook, du kan steppe igennem. Og __START_AT / __END_AT-kolonnenavnene adskiller sig fra valid_from / valid_to-konventionen (småt, men værd at dokumentere for dit team).

For de fleste teams, der bygger nye medallion-pipelines på Databricks, er det her, jeg ville starte.

Så hvilken skal du bruge?

Det afhænger af dit team og din situation, men her er et praktisk beslutningsframework:

Start med Declarative Pipelines, hvis du bygger noget nyt på Databricks. De giver dig den bedste balance mellem læsbarhed, indbygget SCD2-understøttelse, datakvalitetstjek og inkrementel behandling -- med mindst mulig custom kode.

Brug dbt, hvis dit team allerede er investeret i dbt-økosystemet, eller hvis portabilitet på tværs af platforme er vigtigt. dbt's test- og dokumentationskultur er en reel fordel, selv om det tilføjer projektoverhoved.

Brug Python eller SQL notebooks til prototyper, eksperimenter, eller når du har brug for fuld kontrol over behandlingslogikken. De er hurtige at starte og nemme at forstå, men de akkumulerer hurtigt kompleksitet i produktion.

Brug Materialized Views + Streaming Tables til kompakte, SQL-only pipelines, hvor SCD2 ikke er et hårdt krav. Når det virker, er det den reneste løsning.

Undgå klassisk DLT-syntaks til nye projekter. Den kører stadig fint, men der er ingen grund til at starte med forældede nøgleord, når de moderne ækvivalenter gør det samme.

Den fulde kildekode ligger på github.com/romaklimenko/databricks-medallion. Hvis du vil dykke ned i en specifik tilgang, har README'en detaljerede noter om hver enkelt.