Perché partizionare un data lake
In Azure Data Lake Storage Gen2 i dati non vivono in tabelle con indici, ma in file (tipicamente Parquet o Delta) organizzati in cartelle. Il partizionamento fisico consiste nel distribuire questi file in una folder hierarchy che riflette una o più colonne, così che i motori di query leggano solo le cartelle rilevanti anziché scansionare l’intero dataset. È la leva di performance più economica del lake: riduce l’I/O prima ancora che il compute entri in gioco.
La convenzione standard è lo stile Hive-style partitioning con coppie chiave=valore nel path:
/sales/year=2026/month=07/day=17/part-0001.parquet
Questo formato è auto-descrittivo: motori come Azure Synapse (Spark e serverless SQL), Microsoft Fabric e Databricks riconoscono le colonne year, month, day direttamente dai nomi delle cartelle e le espongono come colonne virtuali della tabella.
Partizionare per data o per chiave di business
La scelta della chiave dipende dai pattern di accesso, non dalla struttura del dato.
- Partizionamento temporale (
year/month/day): ideale quando le query filtrano quasi sempre per intervallo di date e quando l’ingestion è incrementale. Semplifica retention e archiviazione (si elimina un’intera cartellayear=2023/). - Partizionamento per chiave di business (es.
region=,tenant_id=,product_category=): conviene quando le query filtrano stabilmente su quella dimensione e la cardinalità è bassa e bilanciata. Untenant_idcon pochi tenant grandi funziona; uncustomer_idcon milioni di valori è un antipattern.
Si possono combinare (es. region=/year=/month=), ma ogni livello aggiuntivo moltiplica il numero di partizioni.
Partition pruning: come i motori lo sfruttano
Il partition pruning (o partition elimination) è il meccanismo per cui il query engine, leggendo un predicato come WHERE year = 2026 AND month = 07, scarta a priori tutte le cartelle non corrispondenti e ne evita l’apertura. Funziona solo se il filtro insiste sulla colonna di partizionamento: una query che filtra su una colonna non partizionata (o che applica una funzione alla colonna partizione) forza una full scan.
Per questo la chiave di partizione deve rispecchiare i predicati più frequenti nel workload reale.
Fisico sul lake vs logico nel motore
Sono due piani distinti da non confondere:
- Partizionamento fisico: la disposizione dei file in cartelle su ADLS Gen2. È persistente, condiviso da tutti i motori e determina quanti file vengono aperti.
- Partizionamento logico: la ripartizione in memoria che un motore applica a runtime, ad esempio le partizioni Spark controllate da
repartition()/spark.sql.shuffle.partitions, o la distribuzione di una tabella in un dedicated SQL pool (HASH,ROUND_ROBIN,REPLICATE). Vive per la durata del job e serve a parallelizzare compute e shuffle, non a ridurre l’I/O di lettura dal lake.
Ottimizzare un layer senza l’altro è un errore ricorrente: un buon partizionamento fisico non salva un JOIN con distribuzione logica sbagliata, e viceversa.
Over-partitioning e small files problem
Partizionare troppo genera il small files problem: tantissime cartelle contenenti file di pochi KB. Poiché ogni file comporta overhead di apertura, listing dei metadati e task Spark dedicato, si finisce per rallentare il sistema pur avendo “ottimizzato”. La regola pratica è puntare a file da circa 128 MB–1 GB per partizione.
Quando NON partizionare
- Dataset piccoli (pochi GB): la scansione completa è già veloce; partizionare aggiunge solo overhead.
- Chiavi ad alta cardinalità (ID utente, timestamp al secondo): esplodono le partizioni.
- Ingestion frequente a micro-batch: ogni batch deposita file minuscoli in molte partizioni. In questi casi si preferisce Delta Lake con comando OPTIMIZE (bin-packing / compaction) e Z-Ordering per il data skipping basato su statistiche, invece di aggiungere livelli di cartelle.
Trappole tipiche d’esame
- Query lente perché filtrano su una colonna diversa dalla partizione → ripartizionare fisicamente sulla colonna dei predicati più frequenti per abilitare il partition pruning; non basta aggiungere compute.
- Migliaia di file piccoli dopo streaming/micro-batch → non aumentare le partizioni: eseguire OPTIMIZE/compaction su Delta e riportare i file verso circa 128 MB+.
- Partizionare per una chiave ad altissima cardinalità (es.
customer_id) → risposta corretta: scegliere una chiave a bassa cardinalità (es.dateoregion), eventualmente con Z-Order/data skipping sulla colonna ad alta cardinalità. - Confondere il tuning del dedicated SQL pool con il layout del lake →
HASH/ROUND_ROBIN/REPLICATEsono distribuzione logica nel motore; il partizionamento fisico in cartelle è cosa distinta. - Dataset di pochi GB “da partizionare per performance” → spesso la risposta giusta è non partizionare e affidarsi al columnar pruning di Parquet.