Parliamone
// tecnologie.data-partitioning

Data Partitioning

Strategie di partizionamento orizzontale e verticale, sharding, consistent hashing e partition evolution: analisi delle architetture per la distribuzione scalabile dei dati.

Data EngineeringSoftware Architecture

Executive summary

Quando un sistema informatico deve gestire volumi di dati che superano la capacità di un singolo server, diventa necessario suddividere le informazioni su più macchine in modo ordinato e prevedibile. Questa suddivisione, nota come partizionamento dei dati, determina la velocità con cui le interrogazioni ottengono risposte, la capacità del sistema di crescere senza interruzioni e la facilità con cui la struttura dei dati può adattarsi a esigenze che cambiano nel tempo. L'analisi che segue mostra che non esiste una strategia universale: la scelta tra le diverse tecniche dipende dal tipo di carico di lavoro, dalla distribuzione delle chiavi di accesso e dalla necessità di far evolvere lo schema di partizionamento senza riscrivere i dati esistenti. I sistemi più maturi combinano più approcci e automatizzano il ribilanciamento, riducendo l'intervento manuale e i rischi operativi.


Background

Il partizionamento dei dati rappresenta uno dei problemi fondamentali dell'ingegneria dei sistemi distribuiti. La necessità di suddividere un dataset su più nodi emerge quando le dimensioni dei dati, il throughput richiesto o i requisiti di disponibilità superano le capacità di una singola macchina. Il problema non è nuovo: le prime formalizzazioni risalgono agli anni ottanta con i lavori sul partizionamento verticale delle relazioni nei database relazionali, dove Navathe e Ra proposero algoritmi grafici per la decomposizione ottimale delle tabelle [1]. Tuttavia, la scala dei sistemi moderni, con dataset nell'ordine dei petabyte e milioni di richieste al secondo, ha trasformato il partizionamento da tecnica di ottimizzazione a requisito architetturale imprescindibile.

Il partizionamento opera su due dimensioni ortogonali. La dimensione orizzontale distribuisce le righe di una tabella su nodi diversi, in modo che ciascun nodo contenga un sottoinsieme delle tuple. La dimensione verticale distribuisce le colonne, in modo che attributi diversi della stessa entità risiedano su nodi o strutture di storage distinte. Entrambe le dimensioni possono essere combinate: Agrawal, Narasayya e Yang hanno dimostrato nel 2004 che l'integrazione del partizionamento verticale e orizzontale nella progettazione fisica automatizzata di database relazionali produce piani di esecuzione significativamente più efficienti rispetto all'applicazione isolata di una sola tecnica [2].

Il termine sharding, ampiamente diffuso nell'industria, identifica specificamente il partizionamento orizzontale distribuito su nodi fisicamente separati, ciascuno dei quali opera in modo autonomo per il sottoinsieme di dati assegnato. La distinzione tra partizionamento locale (all'interno di un singolo DBMS) e sharding distribuito è rilevante perché introduce complessità aggiuntive: coordinamento delle transazioni distribuite, routing delle query, gestione dei fallimenti parziali e ribilanciamento dinamico dei dati tra nodi [3].

L'evoluzione dei sistemi di storage ha aggiunto una terza prospettiva al problema. Nei data lake moderni basati su formati tabulari aperti come Apache Iceberg, il partizionamento non è più una proprietà fisica immutabile della tabella, ma un attributo dei metadati che può evolvere nel tempo senza riscrittura dei file esistenti [4]. Questa capacità, nota come partition evolution, rappresenta un cambio di paradigma rispetto ai sistemi tradizionali dove la modifica dello schema di partizionamento richiedeva la migrazione completa dei dati.


Partizionamento orizzontale e verticale: fondamenti e distinzioni

Partizionamento orizzontale

Il partizionamento orizzontale suddivide una relazione $R$ in frammenti $R_1, R_2, \ldots, R_n$ tali che $R = R_1 \cup R_2 \cup \ldots \cup R_n$ e $R_i \cap R_j = \emptyset$ per $i \neq j$. Ogni frammento contiene un sottoinsieme delle righe della relazione originale, con lo stesso schema. La funzione di partizionamento $p: K \rightarrow {1, 2, \ldots, n}$ mappa ogni valore della chiave di partizionamento $k \in K$ all'indice del frammento di destinazione.

Le tre strategie principali per il partizionamento orizzontale sono il partizionamento per range, per hash e per lista. Nel partizionamento per range, lo spazio delle chiavi viene suddiviso in intervalli contigui: il frammento $R_i$ contiene tutte le tuple con chiave $k$ tale che $l_i \leq k < u_i$, dove $l_i$ e $u_i$ sono i limiti inferiore e superiore dell'intervallo. PostgreSQL implementa questa strategia nella sua partitioning dichiarativa, dove una tabella partizionata per range su un campo data può generare automaticamente partizioni mensili o giornaliere [5]. Il vantaggio principale è il supporto efficiente per le range query; il limite è la sensibilità alla distribuzione non uniforme delle chiavi, che può generare partizioni sbilanciate (hot partition).

Il partizionamento per hash applica una funzione hash alla chiave e utilizza l'operazione modulo per determinare il frammento: $p(k) = h(k) \mod n$. Questa strategia garantisce una distribuzione uniforme delle chiavi indipendentemente dalla loro distribuzione originale, ma sacrifica la località: range query sulla chiave di partizionamento richiedono l'interrogazione di tutti i frammenti. Il partizionamento per lista assegna esplicitamente valori discreti della chiave a frammenti specifici ed è adatto a chiavi categoriche con cardinalità nota, come codici regionali o categorie di prodotto.

Partizionamento verticale

Il partizionamento verticale decompone una relazione in proiezioni: dato uno schema $R(A_1, A_2, \ldots, A_m)$, si producono frammenti $R_1(A_{i_1}, \ldots, A_{i_k}, PK)$ e $R_2(A_{j_1}, \ldots, A_{j_l}, PK)$ dove $PK$ è la chiave primaria replicata in ogni frammento per consentire la ricostruzione tramite join. L'obiettivo è separare attributi acceduti frequentemente insieme da quelli acceduti raramente, riducendo la quantità di dati letti per ogni query.

L'interesse per il partizionamento verticale ha conosciuto una rinascita con l'avvento dei database colonnari. Sistemi come Apache Parquet e Apache ORC applicano implicitamente un partizionamento verticale a livello di formato di storage: ogni colonna viene memorizzata e compressa indipendentemente, consentendo la lettura selettiva dei soli attributi necessari [17]. Questa architettura si è dimostrata particolarmente efficace per i carichi analitici (OLAP), dove le query tipicamente proiettano un sottoinsieme ristretto delle colonne su grandi volumi di righe. L'integrazione automatizzata del partizionamento verticale e orizzontale nella progettazione fisica, come proposto da Agrawal et al. [2], rimane un problema computazionalmente complesso: lo spazio di ricerca cresce esponenzialmente con il numero di attributi e il numero di frammenti, richiedendo euristiche e approcci basati su workload per ottenere soluzioni praticabili.


Strategie di sharding: hash, range e directory-based

La scelta della strategia di sharding determina le proprietà fondamentali del sistema distribuito: la distribuzione del carico, la tipologia di query supportate efficientemente e la complessità del ribilanciamento. Kleppmann identifica tre strategie principali, ciascuna con trade-off distinti [3].

Hash-based sharding

Nello sharding basato su hash, una funzione hash deterministica mappa la chiave di partizionamento a uno degli $n$ shard disponibili. La formula $\text{shard}(k) = h(k) \mod n$ è la formulazione più elementare. Il sistema TDSQL di Tencent, descritto in un articolo presentato a VLDB nel 2024, adotta questo schema come meccanismo predefinito: un campo specifico viene selezionato per il calcolo del modulo, garantendo che i dati siano distribuiti uniformemente sui dispositivi fisici [6].

Il problema critico dello sharding hash con modulo fisso è il ribilanciamento: quando il numero di nodi cambia da $n$ a $n+1$, la frazione di chiavi che deve essere riassegnata è approssimativamente $\frac{n}{n+1}$, cioè quasi tutte. Per un sistema con miliardi di record, questo implica una migrazione massiva. Kleppmann descrive tre approcci al ribilanciamento: il numero fisso di partizioni, dove si creano inizialmente molte più partizioni dei nodi (ad esempio 1000 partizioni su 10 nodi) e si spostano partizioni intere tra nodi; il partizionamento dinamico, dove le partizioni vengono suddivise quando superano una soglia dimensionale predefinita; e il numero fisso di partizioni per nodo, dove l'aggiunta di un nodo causa la suddivisione di alcune partizioni esistenti [3].

Range-based sharding

Lo sharding basato su range assegna intervalli contigui dello spazio delle chiavi a shard diversi. Google Spanner [7], descritto da Corbett et al. nel 2012, organizza i dati in directory che rappresentano la granularità minima di posizionamento e replicazione. Le directory vengono distribuite automaticamente tra i server in base al carico e alla località geografica, con il sistema che muove directory tra i Paxos group per bilanciare il carico e ridurre la latenza. CockroachDB adotta un approccio simile: lo spazio delle chiavi viene suddiviso in range contigui, ciascuno corrispondente a un Raft group, con split automatico quando un range supera i 512 MiB e merge quando due range adiacenti scendono sotto i 128 MiB [8].

Il vantaggio dello sharding per range è il supporto efficiente per le scan query ordinate e le range query. Il limite intrinseco è la vulnerabilità agli access pattern non uniformi: se il carico si concentra su un intervallo ristretto dello spazio delle chiavi, come accade con chiavi sequenziali (timestamp, auto-increment), un singolo shard diventa un bottleneck. Spanner mitiga questo rischio con lo sharding automatico guidato dal carico; CockroachDB implementa il load-based splitting, che suddivide i range non solo in base alla dimensione ma anche in base alla frequenza di accesso [8].

Directory-based sharding

Lo sharding basato su directory utilizza un servizio di lookup centralizzato che mantiene la mappatura tra chiavi e shard. Questa strategia offre la massima flessibilità, qualsiasi politica di assegnazione può essere implementata, ma introduce un single point of failure e un potenziale bottleneck nel servizio di directory. I sistemi moderni che adottano questo approccio tipicamente replicano il servizio di directory e utilizzano caching aggressivo per mitigare il problema della latenza. Apache HBase, ad esempio, utilizza un servizio di metadati (hbase:meta) che mappa le chiavi di riga ai RegionServer responsabili, con la tabella di metadati a sua volta distribuita e cached dai client [3].


Consistent hashing e le sue evoluzioni

L'algoritmo originale

Il consistent hashing, introdotto da Karger et al. nel 1997 [9], risolve il problema del ribilanciamento massivo definendo una funzione di mapping che minimizza il numero di chiavi riassegnate quando i nodi cambiano. L'intuizione fondamentale è mappare sia le chiavi sia i nodi su un anello hash circolare di dimensione $2^m$. Data una funzione hash $h$, ogni nodo $n_i$ viene posizionato in $h(n_i)$ sull'anello; ogni chiave $k$ viene assegnata al primo nodo incontrato percorrendo l'anello in senso orario a partire da $h(k)$.

Formalmente, la chiave $k$ è assegnata al nodo $\text{successor}(h(k))$, dove $\text{successor}(p)$ è il nodo con la posizione più piccola $\geq p$ sull'anello (con wrap-around). Quando un nodo viene aggiunto o rimosso, solo le chiavi nel segmento dell'anello tra il nodo modificato e il suo predecessore devono essere riassegnate. In un sistema con $n$ nodi e $K$ chiavi, il numero atteso di chiavi da spostare è $K/n$, contro $K(n-1)/n$ nel caso del modulo semplice.

Il lavoro originale di Karger et al. si inseriva nel contesto del web caching distribuito, ma l'applicazione al data partitioning è stata consacrata dal sistema Dynamo di Amazon [10]. DeCandia et al. hanno descritto nel 2007 come Dynamo utilizzi il consistent hashing con nodi virtuali per distribuire i dati su un cluster di storage altamente disponibile. Il protocollo Chord [11], proposto da Stoica et al. nel 2001, ha formalizzato il lookup distribuito basato su consistent hashing, dimostrando che la risoluzione di una query richiede $O(\log n)$ messaggi in un sistema con $n$ nodi.

Nodi virtuali

Il consistent hashing base soffre di un problema di bilanciamento: con pochi nodi fisici, la distribuzione delle chiavi può essere significativamente non uniforme. La soluzione, adottata sia da Dynamo [10] sia da Apache Cassandra [12], consiste nell'assegnare a ogni nodo fisico molteplici posizioni sull'anello, dette nodi virtuali (vnodes). Con $v$ nodi virtuali per nodo fisico, ogni nodo è responsabile di $v$ segmenti non contigui dell'anello, e la deviazione standard della distribuzione delle chiavi decresce proporzionalmente a $1/\sqrt{v}$.

Apache Cassandra utilizza di default 256 vnodes per nodo, con il Murmur3Partitioner come funzione hash predefinita. Quando un nodo viene aggiunto al cluster, i vnodes vengono posizionati casualmente sull'anello e il nodo riceve approssimativamente una quota uguale di dati da ciascuno dei nodi esistenti [12]. Questa granularità fine del ribilanciamento rappresenta un vantaggio operativo significativo rispetto allo spostamento di partizioni intere, particolarmente in cluster di grandi dimensioni dove la convergenza verso una distribuzione uniforme è più rapida.

Jump consistent hash

Lamping e Veach hanno proposto nel 2014 un algoritmo alternativo, il jump consistent hash [13], che raggiunge una distribuzione più uniforme delle chiavi con complessità temporale $O(\ln n)$, zero overhead di memoria e un'implementazione di circa cinque righe di codice. A differenza del ring-based consistent hashing, il jump hash non utilizza un anello ma calcola direttamente il bucket di destinazione attraverso una sequenza di "salti" deterministici:

int32_t JumpConsistentHash(uint64_t key, int32_t num_buckets) {
    int64_t b = -1, j = 0;
    while (j < num_buckets) {
        b = j;
        key = key * 2862933555777941757ULL + 1;
        j = (b + 1) * (double(1LL << 31) / double((key >> 33) + 1));
    }
    return b;
}

Il jump hash garantisce che l'aggiunta del bucket $n+1$ riassegni esattamente la frazione $1/(n+1)$ delle chiavi, il minimo teorico. Il limite principale è che i bucket devono essere numerati sequenzialmente: solo l'ultimo bucket aggiunto può essere rimosso senza violare l'invariante di distribuzione. Questa restrizione rende il jump hash inadatto a scenari con fallimenti arbitrari di nodi, ma lo rende ideale per sistemi dove i nodi sono gestiti centralmente e rimossi in ordine inverso di aggiunta, come accade in molte architetture di storage distribuite gestite da un orchestratore.


Partition pruning e ottimizzazione delle query

Il partition pruning è una tecnica di ottimizzazione che consente al query planner di escludere dalla scansione le partizioni che non possono contenere righe rilevanti per la query. L'efficacia del pruning determina in larga misura il beneficio prestazionale del partizionamento: senza pruning, una tabella partizionata potrebbe richiedere la scansione di tutti i frammenti, annullando i vantaggi della suddivisione.

Pruning statico e dinamico

Il pruning statico opera durante la fase di pianificazione della query, analizzando i predicati nella clausola WHERE e confrontandoli con le definizioni delle partizioni. Se il planner può dimostrare che una partizione non contiene righe che soddisfano il predicato, la esclude dal piano di esecuzione. PostgreSQL implementa questa ottimizzazione nella partitioning dichiarativa: data una tabella partizionata per range sulla colonna created_at, una query con predicato WHERE created_at >= '2025-01-01' AND created_at < '2025-02-01' accede solo alla partizione di gennaio 2025, eliminando tutte le altre dal piano [5].

Il pruning dinamico estende questa ottimizzazione alla fase di esecuzione, gestendo predicati il cui valore è noto solo a runtime. Un caso tipico è il join tra una tabella partizionata e una tabella di dimensioni ridotte: il risultato del join sulla tabella piccola produce valori che vengono utilizzati come filtro per eliminare partizioni della tabella grande durante l'esecuzione. Oracle Database distingue esplicitamente tra static pruning (determinato dal predicato letterale) e dynamic pruning (determinato da subquery, bind variable o risultati intermedi) [14].

Predicate pushdown e interazione con il partizionamento

Nei sistemi di storage basati su file, come quelli dei data lake, il predicate pushdown rappresenta il meccanismo attraverso cui i filtri della query vengono propagati al livello di lettura dei file. L'interazione tra predicate pushdown e partizionamento opera su due livelli: a livello di partizione (eliminazione di intere directory o gruppi di file) e a livello di file (utilizzo delle statistiche nei metadati dei file, come min/max per colonna nei footer di Parquet, per saltare row group irrilevanti) [17].

La combinazione di partition pruning e predicate pushdown a livello di file può ridurre di ordini di grandezza la quantità di dati letti. In un data lake con partizionamento giornaliero su una tabella di eventi, una query che filtra per giorno e per tipo di evento beneficia del pruning a livello di partizione (elimina le directory dei giorni non rilevanti) e del predicate pushdown a livello di file (elimina i row group all'interno dei file del giorno rilevante che non contengono il tipo di evento richiesto).

Implicazioni della scelta della chiave di partizionamento

L'efficacia del partition pruning dipende criticamente dalla correlazione tra la chiave di partizionamento e i predicati più frequenti nelle query. Una scelta errata della chiave di partizionamento può rendere il pruning inefficace: se la tabella è partizionata per user_id ma le query filtrano prevalentemente per created_at, nessuna partizione può essere eliminata. L'analisi del workload è quindi un prerequisito per una strategia di partizionamento efficace. I sistemi più avanzati, come Apache Iceberg, mitigano questo problema attraverso il partizionamento nascosto e l'evoluzione delle partizioni, consentendo di modificare la strategia senza riscrivere i dati [4].


Partizionamento temporale nei sistemi analitici

Il partizionamento basato sul tempo rappresenta la strategia più diffusa nei sistemi analitici e nei data warehouse, dove la dimensione temporale è quasi sempre presente e costituisce il predicato di filtro più frequente. La ragione è duplice: i dati analitici sono tipicamente append-only e ordinati temporalmente, e le query analitiche operano quasi sempre su finestre temporali definite (ultimo mese, ultimo trimestre, anno corrente).

Granularità del partizionamento temporale

La scelta della granularità, annuale, mensile, giornaliera, oraria, influenza direttamente il bilanciamento tra efficacia del pruning e overhead di gestione. Una granularità troppo fine produce un numero elevato di partizioni (fenomeno noto come small files problem nei data lake), con conseguente overhead nei metadati, nella pianificazione delle query e nella gestione del file system. Una granularità troppo grossolana riduce l'efficacia del pruning e aumenta la quantità di dati letti per ogni query.

La regola empirica suggerisce di scegliere la granularità in base alla finestra temporale tipica delle query: se le query operano su intervalli giornalieri, il partizionamento giornaliero è appropriato; se operano su intervalli mensili, il partizionamento mensile è preferibile. Nei sistemi con volumi elevati, un partizionamento giornaliero può generare partizioni di dimensioni appropriate (nell'ordine delle centinaia di megabyte o dei pochi gigabyte), mentre un partizionamento orario potrebbe produrre file troppo piccoli. La documentazione di Databricks raccomanda esplicitamente di evitare il partizionamento quando le partizioni risultanti contengono meno di un gigabyte di dati ciascuna [15].

Gestione del ciclo di vita delle partizioni

Il partizionamento temporale abilita politiche efficienti di gestione del ciclo di vita dei dati. Le operazioni di archiviazione, compressione e cancellazione possono essere eseguite a livello di partizione intera, senza impatto sulle partizioni attive. In PostgreSQL, il detach di una partizione dalla tabella parent è un'operazione sui metadati che rende la partizione invisibile alle query sulla tabella principale, ma mantiene i dati accessibili come tabella indipendente per eventuale archiviazione [5]. Nei data lake, la rimozione di una partizione temporale corrisponde alla cancellazione di una directory e dei relativi file, un'operazione significativamente più efficiente della cancellazione selettiva di righe.

Questa proprietà rende il partizionamento temporale particolarmente adatto a scenari con requisiti di data retention regolamentati, dove i dati devono essere eliminati dopo un periodo definito. L'eliminazione di una partizione intera è un'operazione atomica, deterministica e verificabile, a differenza di un DELETE condizionale su una tabella non partizionata.


Partition evolution nei data lake

Il problema dell'immutabilità del partizionamento

Nei sistemi tradizionali, da Hive a molti data warehouse on-premise, lo schema di partizionamento è una proprietà strutturale definita alla creazione della tabella e sostanzialmente immutabile. Modificare la strategia di partizionamento (ad esempio, passare da un partizionamento mensile a uno giornaliero, o aggiungere una chiave di partizionamento) richiede la riscrittura completa della tabella, un'operazione costosa, rischiosa e che tipicamente impone un periodo di indisponibilità. Questa rigidità è problematica perché i requisiti di accesso ai dati evolvono nel tempo: una tabella creata con partizionamento annuale può richiedere, con la crescita dei volumi, un partizionamento più granulare.

Hidden partitioning in Apache Iceberg

Apache Iceberg introduce un approccio radicalmente diverso, denominato hidden partitioning [4]. A differenza di Hive, dove l'utente deve specificare esplicitamente i valori delle partizioni nelle query (ad esempio, aggiungendo WHERE year = 2025 AND month = 3), Iceberg separa lo schema logico dalla strategia di partizionamento fisico. Le partizioni vengono derivate automaticamente dalle colonne della tabella attraverso trasformazioni (partition transforms): year(ts), month(ts), day(ts), hour(ts), bucket(col, N), truncate(col, L).

Questa separazione elimina una classe intera di errori applicativi. In Hive, una query che omette il filtro sulla colonna di partizionamento scatena una scansione completa della tabella; in Iceberg, il motore di query applica automaticamente il pruning basato sulle trasformazioni di partizionamento definite nei metadati, indipendentemente da come l'utente formula la query. Se la tabella è partizionata con day(event_timestamp) e la query filtra per event_timestamp >= '2025-03-01', Iceberg deduce automaticamente quali partizioni giornaliere escludere senza che l'utente conosca la strategia di partizionamento [4].

Partition evolution come operazione sui metadati

La caratteristica più innovativa di Iceberg è la capacità di evolvere lo schema di partizionamento come operazione esclusivamente sui metadati, senza riscrittura dei dati esistenti [16]. Quando lo schema di partizionamento viene modificato, ad esempio, passando da month(ts) a day(ts), i nuovi dati vengono scritti con il nuovo schema, mentre i dati esistenti mantengono il layout originale. La tabella contiene quindi file con partition spec diverse, e il motore di query gestisce questa eterogeneità durante la pianificazione: per ogni file, consulta il partition spec con cui è stato scritto e applica il pruning appropriato.

Questa architettura è resa possibile dal modello di metadati di Iceberg, dove ogni snapshot della tabella fa riferimento a un elenco di manifest file, ciascuno dei quali traccia i data file con il relativo partition spec. L'aggiunta di un campo di partizionamento, la rimozione di un campo esistente o la modifica della granularità temporale sono tutte operazioni che producono un nuovo partition spec nei metadati senza alterare i file già scritti [16].

Confronto con Delta Lake e Hudi

Delta Lake utilizza un partizionamento basato su directory simile a Hive (con struttura /year=2025/month=03/), e il supporto alla partition evolution è limitato rispetto a Iceberg. La modifica della strategia di partizionamento in Delta Lake richiede tipicamente la riscrittura dei dati tramite operazioni di compaction o la specifica manuale della nuova strategia durante la scrittura di nuovi dati. Apache Hudi offre funzionalità intermedie, con supporto per diverse strategie di partizionamento ma senza la trasparenza delle partition transforms di Iceberg [15].

La differenza architetturale fondamentale risiede nel livello di astrazione: Iceberg tratta il partizionamento come un attributo dei metadati completamente separato dallo schema logico e dal layout fisico, mentre Delta Lake e Hudi mantengono un accoppiamento più stretto tra struttura delle directory e schema di partizionamento. Questa scelta progettuale ha implicazioni dirette sulla flessibilità operativa: in ambienti dove i pattern di accesso cambiano frequentemente, la partition evolution di Iceberg riduce significativamente il costo operativo dell'adattamento.


Limiti, trade-off e problemi aperti

Il compromesso tra distribuzione uniforme e località dei dati

Il trade-off fondamentale del partizionamento è tra distribuzione uniforme del carico e località dei dati. Lo sharding basato su hash garantisce distribuzione uniforme ma distrugge la località: le range query richiedono l'interrogazione di tutti gli shard. Lo sharding basato su range preserva la località per le scan ordinate ma è vulnerabile agli hotspot. Nessuna strategia singola risolve entrambi i problemi simultaneamente, e i sistemi reali adottano approcci ibridi. CockroachDB, ad esempio, utilizza range-based sharding di default ma supporta hash-sharded indexes per colonne con access pattern sequenziali che genererebbero hotspot [8].

Transazioni distribuite e partizionamento

Il partizionamento introduce complessità nelle transazioni che attraversano più partizioni. Una transazione che modifica dati in partizioni diverse richiede un protocollo di commit distribuito (tipicamente two-phase commit o una sua variante), con costi significativi in termini di latenza e disponibilità. Spanner affronta questo problema con il TrueTime API, che consente commit non bloccanti per le transazioni read-only e riduce la latenza delle transazioni read-write attraverso l'uso di clock sincronizzati a livello globale [7]. La scelta della chiave di partizionamento influenza direttamente la frequenza delle transazioni cross-partition: una chiave che co-localizza i dati acceduti insieme riduce la necessità di coordinamento distribuito.

Ribilanciamento e disponibilità

Il ribilanciamento dei dati tra nodi, necessario quando i nodi vengono aggiunti, rimossi o quando il carico diventa sbilanciato, rappresenta un'operazione critica che può impattare la disponibilità e le prestazioni del sistema durante la sua esecuzione. I sistemi più maturi implementano il ribilanciamento online, dove i dati vengono migrati gradualmente senza interrompere il servizio. Il ribilanciamento completamente automatico, sebbene desiderabile, comporta rischi: una cascata di ribilanciamenti innescata da un fallimento temporaneo può amplificare il problema anziché risolverlo. Per questa ragione, diversi sistemi (tra cui Couchbase e le prime versioni di Riak) adottano un approccio semi-automatico, dove il sistema propone un piano di ribilanciamento che richiede l'approvazione dell'operatore [3].

Problemi aperti

Diversi problemi rimangono aperti nella ricerca sul partizionamento dei dati. L'ottimizzazione automatica della strategia di partizionamento basata sul workload osservato è un'area attiva: studi recenti hanno esplorato approcci basati su machine learning per analizzare i pattern di carico e predire la strategia ottimale [18], ma questi sistemi non hanno ancora raggiunto la maturità necessaria per l'adozione in produzione su larga scala. Il partizionamento adattivo, che modifica dinamicamente la granularità e la strategia in risposta ai cambiamenti del workload senza intervento manuale, resta in gran parte un obiettivo di ricerca. Infine, la combinazione di partizionamento con altre tecniche di ottimizzazione, come la materializzazione incrementale, il caching distribuito e l'indicizzazione adattiva, apre spazi di progettazione ampi e ancora poco esplorati dalla letteratura.


Riferimenti

[1] S. Navathe e M. Ra, "Vertical Partitioning for Database Design: A Graphical Algorithm," in Proc. ACM SIGMOD, 1989. https://dl.acm.org/doi/10.1145/66926.66966

[2] S. Agrawal, V. Narasayya e B. Yang, "Integrating Vertical and Horizontal Partitioning into Automated Physical Database Design," in Proc. ACM SIGMOD, 2004. https://dl.acm.org/doi/10.1145/1007568.1007609

[3] M. Kleppmann, Designing Data-Intensive Applications, O'Reilly Media, 2017.

[4] Apache Software Foundation, "Apache Iceberg — Partitioning," 2025. https://iceberg.apache.org/docs/latest/partitioning/

[5] PostgreSQL Global Development Group, "PostgreSQL Documentation: Table Partitioning," 2025. https://www.postgresql.org/docs/current/ddl-partitioning.html

[6] Y. Chen et al., "TDSQL: Tencent Distributed Database System," in Proc. VLDB Endowment, vol. 17, 2024. https://www.vldb.org/pvldb/vol17/p3869-chen.pdf

[7] J. C. Corbett et al., "Spanner: Google's Globally-Distributed Database," in Proc. OSDI, 2012. https://www.usenix.org/system/files/conference/osdi12/osdi12-final-16.pdf

[8] Cockroach Labs, "CockroachDB Architecture: Distribution Layer," 2025. https://www.cockroachlabs.com/docs/stable/architecture/distribution-layer

[9] D. Karger et al., "Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web," in Proc. ACM STOC, 1997. https://dl.acm.org/doi/10.1145/258533.258660

[10] G. DeCandia et al., "Dynamo: Amazon's Highly Available Key-value Store," in Proc. ACM SOSP, 2007. https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf

[11] I. Stoica et al., "Chord: A Scalable Peer-to-peer Lookup Service for Internet Applications," in Proc. ACM SIGCOMM, 2001. https://pdos.csail.mit.edu/papers/chord:sigcomm01/chord_sigcomm.pdf

[12] Apache Software Foundation, "Apache Cassandra Documentation: Dynamo," 2025. https://cassandra.apache.org/doc/latest/cassandra/architecture/dynamo.html

[13] J. Lamping e E. Veach, "A Fast, Minimal Memory, Consistent Hash Algorithm," arXiv:1406.2294, 2014. https://arxiv.org/abs/1406.2294

[14] Oracle Corporation, "Oracle Database VLDB and Partitioning Guide: Partition Pruning," 2024. https://docs.oracle.com/en/database/oracle/oracle-database/21/vldbg/partition-pruning.html

[15] Databricks, "When to Partition Tables on Databricks," 2025. https://docs.databricks.com/aws/en/tables/partitions

[16] Apache Software Foundation, "Apache Iceberg — Evolution," 2025. https://iceberg.apache.org/docs/latest/evolution/

[17] Apache Software Foundation, "Apache Parquet Documentation," 2025. https://parquet.apache.org/docs/

[18] Z. Yang et al., "A Survey of Data Partitioning Techniques," Journal of Computer Science and Technology, vol. 39, no. 2, 2024. https://jcst.ict.ac.cn/fileup/1000-9000/PDF/JCST-2024-2-9-3538-346.pdf

Data Partitioning

Raccontaci la situazione. Rispondiamo entro 24 ore nei giorni lavorativi.

Tweaks

Light mode
Atmospheric (glass)
Client logos
Terminal hero