Se hai mai ereditato una tabella dei fatti con due miliardi di righe e ti hanno detto “basta che aggiungi una partizione”, sai che la documentazione lo fa sembrare più semplice di quanto sia. Questa è la versione che avrei voluto qualcuno mi mettesse in mano il primo giorno.
Cos’è il partizionamento — e cosa non è
Il partizionamento divide una tabella logica in tante porzioni fisiche in base al valore di una colonna. Per chiunque faccia una SELECT, la tabella sembra ancora una sola tabella. Per lo storage engine è una fila di secchielli, e le query che filtrano sulla colonna di partizionamento devono aprire solo i secchielli che servono.
Due cose che il partizionamento non è:
- Una vittoria gratuita di performance. Uno schema di partizionamento sbagliato è più lento di nessun partizionamento, perché ora l’optimizer deve gestire la partition elimination in cima a tutto il resto.
- Un sostituto degli indici. Il partizionamento ti aiuta a escludere interi blocchi della tabella. Gli indici ti aiutano a trovare le righe. Quasi sempre vuoi entrambi.
La grande vittoria è operativa: puoi spostare un miliardo di righe dentro o fuori da una tabella partizionata in millisecondi con ALTER TABLE ... SWITCH, perché tutto quello che SQL Server deve fare è ri-puntare un puntatore di metadati. Nessun movimento di dati.
I tre pezzi
Ogni tabella partizionata in SQL Server è costruita da tre cose:
- Una partition function — definisce i confini tra i secchielli.
- Un partition scheme — mappa quei secchielli sui filegroup.
- Una tabella o un indice costruiti sullo scheme.
Ecco l’esempio end-to-end più piccolo possibile. Stiamo partizionando una tabella Orders per mese su OrderDate.
-- 1. Partition function: un confine al mese
CREATE PARTITION FUNCTION pf_OrdersByMonth (DATETIME2(0))
AS RANGE RIGHT FOR VALUES (
'2025-01-01', '2025-02-01', '2025-03-01',
'2025-04-01', '2025-05-01', '2025-06-01'
);
-- 2. Partition scheme: manda ogni partizione su PRIMARY
-- (in produzione di solito le distribuiresti su filegroup diversi)
CREATE PARTITION SCHEME ps_OrdersByMonth
AS PARTITION pf_OrdersByMonth
ALL TO ([PRIMARY]);
-- 3. La tabella vera e propria, costruita sullo scheme
CREATE TABLE dbo.Orders (
OrderId BIGINT NOT NULL,
CustomerId INT NOT NULL,
OrderDate DATETIME2(0) NOT NULL,
Amount DECIMAL(18,2) NOT NULL,
CONSTRAINT pk_Orders PRIMARY KEY CLUSTERED (OrderDate, OrderId)
ON ps_OrdersByMonth (OrderDate)
);
Due cose da notare:
RANGE RIGHTvsRANGE LEFT.RANGE RIGHTmette il valore di confine nella partizione superiore.'2025-02-01'vive nel secchiello di febbraio, non in quello di gennaio. Quasi sempre è quello che vuoi per il partizionamento per data, perché puoi descrivere un mese come un intervallo semiaperto pulito[start, next_start). UsareRANGE LEFTcon le date crea bug off-by-one che ti rovinano un pomeriggio intero.- La colonna di partizionamento deve stare nella chiave clustered. Per questo la primary key qui sopra è
(OrderDate, OrderId)invece che soloOrderId. Lo vedrai in ogni tutorial sul partizionamento di SQL Server e la prima volta sembra sempre sbagliato. Fattene una ragione.
Lo sliding window
Il pattern che fa sì che il partizionamento si guadagni il pane è lo sliding window: ogni mese (o giorno, o ora), aggiungi una nuova partizione vuota sul fronte avanzato e fai uscire la più vecchia.
-- Aggiungi una nuova partizione per il mese prossimo.
-- Prima dici allo scheme su quale filegroup vivrà la nuova partizione.
ALTER PARTITION SCHEME ps_OrdersByMonth
NEXT USED [PRIMARY];
ALTER PARTITION FUNCTION pf_OrdersByMonth ()
SPLIT RANGE ('2025-07-01');
-- Sposta il mese più vecchio in una tabella di staging per archivio/cancellazione.
-- La tabella di staging deve avere lo stesso schema, gli stessi indici e gli
-- stessi vincoli, e vivere sullo stesso filegroup della partizione che stai
-- spostando.
CREATE TABLE dbo.Orders_Archive_2024_12 (
OrderId BIGINT NOT NULL,
CustomerId INT NOT NULL,
OrderDate DATETIME2(0) NOT NULL,
Amount DECIMAL(18,2) NOT NULL,
CONSTRAINT pk_Orders_Archive_2024_12 PRIMARY KEY CLUSTERED (OrderDate, OrderId),
CONSTRAINT ck_Orders_Archive_2024_12_Range
CHECK (OrderDate >= '2024-12-01' AND OrderDate < '2025-01-01')
);
ALTER TABLE dbo.Orders SWITCH PARTITION 1 TO dbo.Orders_Archive_2024_12;
-- Ora collassa la partizione vuota che è rimasta indietro.
ALTER PARTITION FUNCTION pf_OrdersByMonth ()
MERGE RANGE ('2025-01-01');
Quel SWITCH è il momento magico. È un’operazione di metadati — millisecondi — indipendentemente dal fatto che la partizione contenga 100 righe o 100 milioni. Poi puoi droppare, archiviare o fare backup di Orders_Archive_2024_12 con calma, senza tenere alcun lock sulla tabella in produzione.
Consigli che ti salvano davvero la vita
- Tieni sempre una partizione vuota a ogni estremo. Splittare una partizione che contiene già righe logga ogni movimento di riga. Splittare una partizione vuota è istantaneo. Fai in modo che “la partizione vuota del mese prossimo” sia qualcosa che un job schedulato crea con largo anticipo.
- Usa
$PARTITIONper verificare prima di agire.SELECT $PARTITION.pf_OrdersByMonth(OrderDate), COUNT(*) FROM dbo.Orders GROUP BY $PARTITION.pf_OrdersByMonth(OrderDate);ti dice esattamente quante righe vivono in ogni partizione. Lancialo prima di ogniSWITCHcosì non potrai mai archiviare il mese sbagliato per sbaglio. - Solo indici allineati. Ogni indice non clustered su una tabella partizionata dovrebbe essere partizionato nello stesso modo (
ON ps_OrdersByMonth (OrderDate)). Indici non allineati bloccano completamente il partition switching — SQL Server rifiuterà loSWITCHcon un errore confuso e perderai 20 minuti per scoprire perché. - Tieni d’occhio le statistiche. Le tabelle partizionate possono sviluppare statistiche brutte sulla partizione “corrente” perché tutte le scritture vanno lì. Aggiorna le statistiche sulla partizione live spesso, non su tutta la tabella.
- Testa il vincolo. Il
CHECKconstraint sulla tabella di staging è ciò che dice a SQL Server che lo switch è sicuro. Sbaglia la disuguaglianza (<=invece di<per un confineRANGE RIGHT) e lo switch fallisce. Incolla sempre la data di confine in entrambi i lati della tua function e del tuo check constraint — copia-incolla, non riscrivere a mano.
Quando non partizionare
Il partizionamento non è gratis. Ti costa complessità nei piani di esecuzione, più parti in movimento al deploy, e tutta una nuova classe di bug (indici non allineati, statistiche sbilanciate, check constraint sbagliati). Si ripaga quando:
- Hai un pattern di accesso chiaramente basato sul tempo (l’ultimo mese è caldo, i mesi vecchi sono freddi).
- Ti servono bulk load veloci o bulk delete veloci che non lockino la tabella.
- La tabella è abbastanza grande che fare la scansione completa per archiviare fa male — di solito significa centinaia di milioni di righe o più.
Se la tua tabella ha 5 milioni di righe e la stai partizionando perché qualcuno ha letto un blog post, per favore non farlo. Un buon indice clustered e una query riscritta ti porteranno quasi tutto il risultato, senza nessuna delle complicazioni operative.