Ho passato qualche settimana a costruire un riferimento piccolo ma eseguibile per
ingestion-più-trasformazione su Databricks, e la cosa più utile che ho imparato non è stata
architetturale — è stata che due degli strumenti coinvolti hanno quasi lo stesso nome, e quella
collisione non è solo una scocciatura di marketing. Salta fuori nel tuo sys.path.
Quindi, prima di tutto, la disambiguazione, perché tutto il post ci si appoggia:
dlt(minuscolo) è dlthub, una libreria Python open-source che estrae dalle sorgenti e carica nelle destinazioni.pip install dlt.DLT/ Delta Live Tables è un prodotto Databricks per pipeline dichiarative, rinominato nel 2026 in Lakeflow Spark Declarative Pipelines.
Questo post parla del primo. Il secondo non l’ho usato di proposito. Qui il layer di
trasformazione è dbt, non Delta Live Tables. La forma che alla fine mi è
piaciuta è noiosa e disaccoppiata: dlt deposita le tabelle grezze in uno schema Unity Catalog, dbt
legge quello schema e ci costruisce sopra le mart, e l’unica cosa che li collega è il nome di uno
schema. Il codice completo è su GitHub:
AndreaBozzo/dlt-dbt-databricks.
L’architettura in una riga
dlt estrae da una sorgente (qui una REST API o Postgres) e carica le tabelle grezze in uno schema
Unity Catalog → dbt legge quello schema come source e costruisce staging → intermediate → marts, tutto su un SQL warehouse Databricks.

Non c’è handoff in memoria, nessun file condiviso, nessuna colla di orchestrazione che debba restare viva perché il contratto regga. Il confine è uno schema Unity Catalog. È quella singola decisione a rendere indipendenti i due strumenti.
Layer esterno: dlt come tessuto di ingestion
dlt possiede un solo schema — <catalog>.raw per default, più un Volume di staging gestito nello stesso catalog per i COPY INTO bulk — e non scrive altro. L’esempio
di punta è una sorgente REST dichiarativa con una relazione parent→child (i post, poi i commenti di
ogni post), caricata con merge così che le riesecuzioni facciano upsert invece di duplicare:
| |
Le cose che rendono dlt un buon layer esterno sono quelle poco appariscenti:
- Python-first. Si legge come normale codice client con qualche decoratore, non come una lotta con un sistema distribuito per scaricare del JSON. La long tail di SaaS di nicchia, endpoint REST interni e «il vendor ci ha dato questa strana API» è esattamente dove brilla e dove i connettori gestiti non arrivano.
- Stateful ma noioso. Cursori incrementali, inferenza dello schema e semantica di merge sono
dichiarativi. Gli esempi avanzati nel repo mostrano
write_disposition="merge"+dlt.sources.incremental(...)che compilano in unMERGE INTOidempotente, e contratti di schema (evolve/freeze) che emettono veri vincoliPRIMARY KEY/FOREIGN KEYsulle tabelle Databricks. - Poliglotta. La stessa pipeline gira in locale, in CI o dentro un job Databricks. C’è anche una
sorgente Postgres (
sql_database) e una variante con table-format Iceberg — Databricks è una destinazione tra le tante, non il centro di gravità.
Quando dlt ha finito, hai tabelle Delta in uno schema Unity Catalog governato — non in un Hive
metastore, non in un bucket non governato. È la precondizione su cui si appoggia tutto il resto del
design.
Il contratto: uno schema Unity Catalog, niente di più
Questa è la parte su cui vale la pena essere precisi, perché è tutto il senso dell’accoppiare i due strumenti.
- dlt possiede
raw. Ogni run carica in<catalog>.<DLT_DATASET_NAME>(defaultraw), accanto alle sue tabelle di servizio (_dlt_loads,_dlt_pipeline_state,_dlt_version). Lasciale stare. - dbt legge
rawcomesource, mai come tabella hardcoded. Unsources.ymldichiara lo schema, e i modelli di staging selezionano da{{ source('raw', 'rest_posts') }}. Il trucco furbo: lo schema della source si risolve da{{ env_var('DLT_DATASET_NAME', 'raw') }}— la stessa env var che leggono le pipeline dlt — così rinominare lo schema di atterraggio non può disallineare i due lati. - dbt possiede
analytics. I modelli si materializzano in<catalog>.<DATABRICKS_SCHEMA>. Niente riscrive mai dentroraw.
Poiché il confine è uno schema e non un processo, i due strumenti restano completamente disaccoppiati. Puoi eseguirli su schedule diverse, da macchine diverse, o l’uno senza l’altro, e il contratto regge comunque. dbt non sa né gli importa che un progetto Python alimenti le sue source; dlt non sa che qualcosa le sta leggendo.

Layer interno: dbt, staging → marts
Il lato trasformazione è dbt ordinario, e il repo ci fa passare due dataset distinti.
Il percorso alimentato da dlt. Lo staging è una pulizia 1:1 delle tabelle atterrate — rinomina e
cast, nessuna logica di business, dato che dlt ha già normalizzato camelCase → snake_case in ingresso:
| |
Un layer analitico vero e disordinato. I modelli più interessanti stanno sopra
samples.healthverity.claims_sample_synthetic — un dataset sintetico di sinistri sanitari (~410k
righe, una riga per service line di sinistro) presente in ogni workspace Databricks. È un buon
stress test perché i dati grezzi sono tutti stringhe, ~57% di line_charge è NULL, e qualche
migliaio di righe è negativo (storni di sinistro). Lo staging fa cast e marca questo; un modello
intermedio aggrega il dettaglio-riga a una riga per sinistro; la mart risponde a una vera domanda di
business — importi addebitati vs. riconosciuti per segmento di payer:
| |
Questa è l’onesta divisione del lavoro: dlt sta vicino a dove vivono HTTP e JSON; dbt sta vicino a dove vivono SQL, test, surrogate key e strategie di merge incrementale. A nessuno dei due strumenti viene chiesto di fare il lavoro dell’altro.

Cosa si è rotto davvero
L’intro lo prometteva, quindi ecco quelle vere — nessuna architetturale, tutte del tipo che scopri solo eseguendo.
I due dlt collidono su sys.path. Databricks serverless include un modulo dlt integrato —
quello Lakeflow/DLT — il cui import hook mette in ombra il dlt di dlthub. import dlt in un notebook
può risolversi nel pacchetto sbagliato. La fix nel repo è un brutto piccolo shim che rimuove
temporaneamente il primo finder di sys.meta_path così vince il pacchetto giusto:
| |
Se hai mai dubitato che la collisione di nomi sia un problema reale e non solo una nota a piè di pagina
pedante: sono sei righe di chirurgia su meta_path in codice di produzione.
Serverless non ha sempre un SQL warehouse per la destinazione di dlt. La destinazione Databricks di
dlt vuole un warehouse e un volume di staging. Dentro un job serverless quel percorso non è sempre
disponibile, così la pipeline REST ha un fallback --load-mode spark che fa semplicemente
spark.saveAsTable. Il punto: quel fallback deve emettere esattamente le stesse colonne snake_case
più _dlt_load_id che emetterebbe dlt, altrimenti i modelli di staging di dbt si rompono a valle.
Due percorsi di codice, un solo contratto di schema da rispettare.
Tenere lo schema della source di dbt sincronizzato con il dataset name di dlt è la modalità di fallimento silenziosa — puntali a schemi diversi e dbt costruisce allegramente nulla. Risolverli entrambi da un’unica env var è ciò che rende il problema inesistente.
Orchestrazione: prima locale, poi un Bundle
Per le demo c’è un piccolo runner locale (~50 righe) che chiama dlt, poi dbt build, in un solo
processo — abbastanza per dimostrare l’handoff. Per la produzione il repo fornisce un Databricks
Asset Bundle: un Job, due task, dbt_build subordinato al successo di dlt_ingest, con
infrastruttura versionata e deployabile invece di uno script.
| |

C’è anche un preflight offline doctor.py che controlla le env var, fa il parse del progetto dbt ed
esegue bundle validate senza toccare il warehouse — così un nuovo contributor può capire se il
repo è cablato correttamente prima di spendere un centesimo di compute. Ogni esempio è validato contro
un workspace reale; niente demo morte.
Perché un solo Unity Catalog conta comunque
Anche se il layer interno è dbt e non una pipeline nativa Databricks, far terminare tutto in Unity Catalog è ciò che ripaga lo split:
- Il controllo accessi è su catalog e schema, non su bucket a caso — analisti, ML engineer e service principal vedono tutti gli stessi permessi.
- Il lineage attraversa dalle mart di dbt indietro attraverso lo staging fino alle tabelle
rawatterrate da dlt — e avanti verso qualunque cosa le legga — un solo grafo, entrambi gli strumenti. - Una sola superficie di governance. Tabelle, view, funzioni e qualunque modello tu registri vivono
tutti sotto lo stesso namespace
catalog.schema— un solo posto per permessi, audit e lineage, invece di storie separate per dati, codice e ML.

È questo il vero motivo per mandare l’output di dlt dritto in UC invece di parcheggiarlo da qualche parte e reimportarlo dopo: trasforma una libreria di ingestion in un cittadino di prima classe di un lakehouse governato, senza scrivere una riga di Spark.
Una checklist, mappata sul repo
- Configura Unity Catalog. Un catalog con uno schema
raw(dlt) e uno schemaanalytics(dbt), e un SQL warehouse attivo. (make doctorcontrolla il resto.) - Configura dlt.
dlt[databricks], punta la destinazione al tuo catalog, modella ogni sorgente comerest_api_source/@dlt.resourceconmerge+ cursori incrementali dove ha senso. - Rendi lo schema il contratto. Risolvi lo schema
sourcedi dbt e ilDLT_DATASET_NAMEdi dlt da un’unica env var. Non lasciare che dlt scriva fuori daraw. - Costruisci dbt sopra.
staging → intermediate → martsleggendorawcome source, materializzando inanalytics, con test sulle chiavi. - Promuovi l’orchestrazione. Parti dal runner locale; spedisci l’Asset Bundle quando lo vuoi in produzione.
Alla fine hai un confine di catalog, due strumenti che fanno i lavori in cui sono davvero bravi, e un posto chiaro dove appendere la governance. Non è l’unico modo di usare Databricks — ma ha smesso di sembrare prodotti che si sovrappongono e ha iniziato a sembrare un reticolo dove ogni pezzo fa esattamente la quantità di lavoro in cui è bravo. E no, niente di tutto questo è «DLT».
