Perché Delta Lake in un lakehouse

Un data lake basato su Azure Data Lake Storage Gen2 salva file Parquet economici e scalabili, ma da solo non garantisce transazioni: due job che scrivono contemporaneamente possono corrompere una tabella, e un lettore può vedere dati parziali. Delta Lake aggiunge sopra Parquet uno strato transazionale che porta garanzie ACID direttamente sull’object storage. È il formato di riferimento del lakehouse in Azure Databricks, nelle Spark pool di Azure Synapse e nei Lakehouse di Microsoft Fabric (dove è il formato nativo di OneLake).

Il transaction log _delta_log

Il cuore di Delta è la cartella _delta_log affiancata ai file di dati. Contiene una sequenza ordinata di file JSON (uno per commit) più checkpoint Parquet periodici. Ogni commit è un’operazione atomica che registra quali file Parquet vengono aggiunti (add) o rimossi logicamente (remove), insieme a statistiche e metadati dello schema.

Punti chiave da capire per l’esame:

  • I file Parquet non vengono mai modificati in place: un update o un delete produce nuovi file e marca i vecchi come rimossi nel log. Questa è la base sia dell’isolamento sia del time travel.
  • La versione corrente della tabella è la somma degli add/remove ricostruita dal log, non lo stato fisico della cartella.
  • Delta usa optimistic concurrency control: due writer procedono senza lock, e al momento del commit chi arriva secondo verifica se le sue letture sono ancora valide. Se c’è conflitto sui file, la transazione fallisce e va ritentata. Funziona bene con molte scritture disgiunte, meno con hot spot sugli stessi dati.

Time travel: audit e rollback

Poiché le versioni precedenti restano descritte nel log (e i relativi file esistono ancora), puoi interrogare la tabella nel passato:

SELECT * FROM sales VERSION AS OF 12;
SELECT * FROM sales TIMESTAMP AS OF '2026-07-01T00:00:00';

Casi d’uso tipici:

  • Audit e riproducibilità: rieseguire un report esattamente sui dati di una certa data.
  • Rollback: ripristinare una tabella dopo una scrittura errata con RESTORE TABLE ... TO VERSION AS OF n.
  • Debug di pipeline: confrontare due versioni per capire cosa è cambiato.

Usa DESCRIBE HISTORY per elencare versioni, timestamp e operazione (MERGE, DELETE, OPTIMIZE…).

MERGE INTO: upsert e slowly changing dimension

MERGE INTO applica in un’unica transazione atomica insert, update e delete confrontando una tabella target con una sorgente:

MERGE INTO dim_customer t
USING staging_customer s ON t.customer_id = s.customer_id
WHEN MATCHED AND s.op = 'D' THEN DELETE
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *;

È lo strumento canonico per:

  • Upsert di batch incrementali o output di stream (idempotenti).
  • SCD Type 1 (sovrascrittura) tramite WHEN MATCHED THEN UPDATE.
  • SCD Type 2 (storicizzazione): si combinano più clausole per chiudere la riga corrente (settando end_date/is_current = false) e inserire la nuova versione.

OPTIMIZE, Z-ORDER e VACUUM

Le scritture frequenti e lo streaming generano il problema degli small files: migliaia di file piccoli degradano le letture perché Spark paga overhead per ognuno.

  • OPTIMIZE compatta i file piccoli in file più grandi (bin-packing), riducendo il numero di oggetti da aprire.
  • ZORDER BY (colonna) riorganizza fisicamente i dati co-localizzando valori simili, così il data skipping basato sulle statistiche del log salta interi file. Scegli come chiave Z-ORDER le colonne ad alta cardinalità usate spesso nei filtri (es. customer_id), non quelle di partizionamento.
  • VACUUM elimina fisicamente i file non più referenziati e più vecchi della retention (default 7 giorni). Serve a liberare storage, ma è l’operazione più insidiosa.

La trappola: VACUUM rimuove i file che il time travel richiederebbe. Dopo un VACUUM con retention di 7 giorni non puoi più interrogare versioni più vecchie di 7 giorni, perché i loro Parquet non esistono più. Impostare retention troppo bassa (o forzare RETAIN 0 HOURS) distrugge la cronologia e può corrompere lettori/scritture concorrenti ancora attivi.

Trappole tipiche d’esame

  • Scenario: dopo un VACUUM un VERSION AS OF di 30 giorni fa fallisce. → La retention di VACUUM ha eliminato i file storici; il time travel funziona solo entro il periodo di retention, non è illimitato.
  • Scenario: letture lente su una tabella Delta alimentata da micro-batch di streaming. → È il problema degli small files: esegui OPTIMIZE (e Z-ORDER sulle colonne di filtro), non aumentare le partizioni.
  • Scenario: carico incrementale che deve aggiornare le righe esistenti e inserire le nuove in modo idempotente. → Usa MERGE INTO, non un DELETE seguito da INSERT.
  • Scenario: serve mantenere lo storico dei cambi di attributo di una dimensione. → SCD Type 2 con MERGE INTO multi-clausola (chiudi la riga corrente, inserisci la nuova); Type 1 sovrascrive e perde lo storico.
  • Scenario: due job Spark scrivono sulla stessa tabella e uno fallisce al commit. → È l’optimistic concurrency di Delta: la transazione in conflitto va semplicemente ritentata; le garanzie ACID hanno impedito la corruzione.