Visualizzazione post con etichetta partizionamento. Mostra tutti i post
Visualizzazione post con etichetta partizionamento. Mostra tutti i post

lunedì 11 ottobre 2010

Divagazioni sul table partitioning in PostgreSQL - parte 4

Affinché tutto quanto abbiamo detto finora abbia un senso nel mondo reale c'è bisogno che PostgreSQL sia in grado di gestire i vari meccanismi sottostanti in modo efficiente. Vediamo il perché.
Nel caso si utilizzi l'ereditarietà è importante che il planner sappia scegliere in modo efficiente quali tabelle figlie effettivamente interrogare. In questo caso definirei "efficiente" un algoritmo a complessità sub-lineare, cioè il cui tempo di esecuzione cresce meno che una funzione lineare. E' il caso tipico della ricerca dicotomica che impiega tempi logaritmici rispetto al numero di elementi dell'insieme.
Lo stesso ragionamento vale per la tecnica che sfrutta il partizionamento degli indici, dove la ricerca si sposta dalle tabelle della gerarchia alla lista degli indici della tabella unica.
Questo perché se il numero di partizioni aumenta (e di norma lo fa col tempo), non vorremmo che aumentasse (troppo) il tempo che il planner impiega a capire quali tabelle figlie (o indici parziali) scartare e quali no.
Con un insieme 100 volte più grande quella ricerca dovrebbe richiede al massimo poche iterazioni in più per essere completata, come nella ricerca dicotomica.
E PostgreSQL?
Stando a quanto riportato nelle mailing list ufficiali da sviluppatori di prima linea del progetto, una gerarchia con più di una dozzina di tabelle figlie è considerata non praticabile. E, per quanto se ne sa, nessuno ha mai preso in considerazione gerarchie con più di un livello!
Le stesse considerazioni portano alle stesse conclusioni in merito al numero di indici parziali.
Questo perché, non solo a mio sentire, nei meandri più nascosti del planner si annidano algoritmi (o loro implementazioni) che non solo non sono sub-lineari ma, questo è il pericolo, sono addirittura super-lineari (che crescono cioè come i polinomi e oltre).
Cosa fare dunque? Le possiblità non sono molte.

  1. Rinunciare al partizionamento "automatico" e fare in modo che l'applicazione sappia scegliere da sé quali tabelle effettivamente interrogare.
  2. Rinunciare al partizionamento tout-court e affidarsi a grandi quantità di memoria per tenere in cache quanta più parte è possibile delle tabelle in questione e relativi indici;
  3. Rimboccarsi le maniche e mettere mano al codice del planner;
  4. Provare a rientrare nei limiti di una dozzina o poco più di tabelle figlie (o indici parziali).
Mi sembra che al momento (sfidanti, fatevi avanti!) sia le funzioni di ereditarietà che quelle di indicizzazione parziale siano più dei "segnaposto" accademici (per dire "ce l'ho") che delle caratteristiche utili nel mondo reale. Limitazioni che stanno ben dentro al primo ordine di grandezza tengono certamente lontano un progetto come PostgreSQL dal mondo enterprise, dove i numeri girano per miliardi come unità di misura.
La mia non vuole essere una feroce critica a quello che ritengo veramente essere il più avanzato RDBMS opensource esistente. Vorrebbe essere piuttosto uno sprone alla comunità per una revisione di quelle parti che, per dirla in termini tecnici, "non scalano bene" parallelamente a tutte le altre interessanti attività di cui si sta già occupando per la prossima versione 9.1.

Concludo con un parallelo. Il kernel di Linux ha cominciato a prendere piede nei territori dell' "enterprise grade" (a misura di impresa industriale) anche grazie alla riprogettazione dello scheduler che altrimenti aveva complessità lineare: più processi da gestire = più tempo necessario per gestirli. Ora quel costo è praticamente indipendente  non solo dal numero di processi, ma anche di processori. Aggiungere feature chiaramente non basta.

mercoledì 6 ottobre 2010

Divagazioni sul table partitioning in PostgreSQL - parte 3

Torniamo per un momento al concetto originario di partizionamento di una tabella. E in particolare alla domanda ultima (non quella sulla vita, l'universo e tutto quanto): perché mai partizionare una tabella?
La risposta non è unica, come sempre. Personalmente ne vedo almeno 3:

  1. accelerare l'accesso ai dati quando questo non è completamente causale ma limitato ad alcuni sottoinsiemi della tabella originaria;
  2. semplificare lo "smaltimento" di gruppi di righe non più necessari alle operazioni correnti;
  3. allocare le singole partizioni su tablespace distinti (per accelerare l'accesso in modo più sottile).
Del primo caso abbiamo già detto. Eseguire una DROP TABLE su una tabella figlia, dopo averla esclusa dall'albero genealogico, è certamente più efficiente che una classica DELETE FROM ... WHERE, oltre al fatto che già la semplice esclusione dall'albero genealogico potrebbe essere sufficiente (ALTER TABLE ... NO INHERIT ...) ad un costo a dir poco ridicolo.
Quella dell'allocazione delle tabelle figlie su tablespace separati è una tecnica raffinata che richiede però uno studio comparativo attento fra i benefici derivanti dall'ottimizzazione sull' I/O rispetto al costo intrinseco derivante dalla gestione dell'albero genealogico. Su database molto "trafficati" in genere le ottimizzazioni sull' I/O dei dischi portano sempre a risultati significativi in positivo.
Tornando alla prima considerazione, va notato come in realtà la velocità dell'accesso dipende solo minimamente (ammesso che possa veramente dipendere) dalle dimensioni della tabella in sé. Una definizione oculata degli indici e delle query realmente in uso è la vera ricetta del successo. Del resto con dischi che stanno "sforando" la barriera del terabyte e con la gestione dei volumi logici, il problema di alloggiare tabelle enormi è diventato sempre meno sentito.
In sostanza, durante le query il planner (ovvero l'ottimizzatore) verifica se esistono uno o più indici, che di norma risiedono sui dischi, che possano tornare utili a recuperare le righe interessanti. In caso positivo cerca di caricare in memoria quelle porzioni di indici che possono servire per poi procedere al recupero effettivo delle righe.
Ciò porta quindi a pensare di partizionare gli indici piuttosto che le tabelle.
In questo modo rendo gli indici più piccoli e, se le query non "spazzolano" a caso sull'intera tabella, è probabile che alcuni di quei piccoli indici più utilizzati restino in cache, a tutto vantaggio della velocità.
Ovviamente PostgreSQL supporta nativamente questa tecnica tramite i cosiddetti "indici parziali". A questi è in sostanza associata un'espressione booleana che determina quali righe della tabella siano indicizzate. Si tratta di un'espressione che, ai nostri fini, ha le stesse finalità di quella della CHECK constraint usata nelle tecniche "ortodosse" illustrate in precedenza.
Chiaramente gli indici parziali, come anche le CHECK constraint, devono essere definite in modo che ogni riga della tabella possa "finire" in almeno uno degli indici parziali. Se è uno solo, tanto meglio.
Questa tecnica ha anche degli effetti collaterali benefici. Vediamone alcuni.

  1. Non serve un supporto in tempo reale (leggi "TRIGGER");
  2. Non serve un supporto "batch" per lo smistamento delle righe nelle tabelle figlie;
  3. Restando unica la tabella si può salvaguardare l'integrità referenziale.
Chiaramente c'è anche da pagare un qualche costo che è rappresentato dalla valutazione delle espressioni booleane associate agli indici ogni volta che il contenuto della tabella cambia (INSERT, UPDATE ecc.). Quindi, l'aggiornamento degli indici sarà intrinsecamente sempre più costoso.

E' chiaro, concludendo (o quasi) come non esista una ricetta unica e buona per tutti i palati. Del resto le condizioni a contorno della specifica applicazione (per non parlare degli specifici data set) cambiano da caso a caso. Lo scopo di queste divagazioni è quello di provare a guardare oltre lo status quo, sempre considerando i limiti, intrinseci e non, non solo delle tecniche ma anche di PostgreSQL.
Per i quali abbiamo dedicato le prossime divagazioni finali.

mercoledì 22 settembre 2010

Divagazioni sul table partitioning in PostgreSQL - parte 2

Una prima soluzione per nulla insoddisfacente l'ho trovata definendo i nomi delle tabelle in modo opportuno. "Codificando" cioè nei loro nomi la condizione che fa sì che una riga vi appartenga. Prima che iniziate a storcere il naso tengo a precisare che neanche a me piace molto questa cosa, ma bisogna ammettere che è funzionale e ragionevolmente efficiente.
In questo modo il trigger che intrappola, ad esempio, le INSERT può:
  1. "calcolare" il nome della tabella figlia candidata all'operazione
  2. verificare se questa esiste
  3. crearla in caso negativo
  4. realizzare l'operazione di INSERT vera e propria
In questo modo la "tabella di supporto" di cui ho parlato non sarebbe altro che il catalogo di sistema. Non serve alcuna tabella extra e relative operazioni di DML. Le CREATE TABLE fanno già tutto.
Quanto poi ad inserire informazioni nel nome di una tabella, a guardare bene, corrisponde ad inserire informazioni in una colonna di una tabella del catalogo. Il che non è poi così inelegante: La condizione da testare corrisponde ad una parte del testo (il nome della tabella) da ricercare nel catalogo.
Semplice come bere un bicchiere d'acqua ... stando a testa ingiù.
Ad esempio, le partizioni della tabella

CREATE TABLE movimenti_magazzino (
  maga int8 not null,
  prod int8 not null,
  qnta numeric not null,
  data timestamp not null
);

suddivisa in base al magazzino e alle settimane di movimentazione potrebbero chiamarsi:
"moma maga=42,week=19,year=2010","moma maga=42,week=20,year=2010","moma maga=42,week=21,year=2010" ...

usando il quoting dei nomi come da manuale.
Questa soluzione, inoltre, è chiaramente migliore di quest'altra:
  1. "calcolare" il nome della tabella figlia candidata all'operazione
  2. provare a realizzare la INSERT
  3. intrappolare l'eventuale errore con BEGIN...EXCEPTION...WHEN per creare la tabella figlia e rieseguire la INSERT
I  questo caso, come noto, il costrutto di intrappolamento degli errori nasconde una penalità di performance che impatterebbe su ogni singola INSERT. Questa penalità è certamente superiore a quella di un "IF NOT FOUND THEN". Si tratta più o meno, dello stesso problema che si trova, ad esempio, in C++ con le eccezioni e i blocchi "try { ... }".

Abbiamo poi trovato un'altra possibilità ancora, che si discosta un po' dallo schema ufficiale e che fa completamente a meno dei TRIGGER. Ecco la sostanza della tecnica.
Tutte le operazioni avvengono direttamente (e solamente) sulla tabella madre, definita con tutti gli indici e le constraint del caso e senza alcun TRIGGER.
Quando, e se possibile, questa viene scorsa da una procedura che realizza lo smistamento delle righe nelle relative tabelle figlie, creandole "on demand". Questa procedura può essere scritta in modo da limitare il numero di linee da smistare per volta. In questo modo la si può eseguire in modo "schedulato" e incrementale, al limite anche durante le normale operazioni sulla tabella madre senza creare LOCK troppo estesi.
In questo modo le penalizzazioni delle normali operazioni sono nulle ed il costo di "smistamento" può essere frazionato e diluito nel tempo o, se l'applicazione lo consente, effettuato tutto in una volta nella fase di manutenzione del DB, poco prima cioè di eseguire la VACUUM [ANALYZE].

L'ultima, almeno per il momento, soluzione "alternativa" diverge in modo totale da tutte quelle vista in precedenza. Nessun trigger, come per la soluzione di prima, e nemmeno ereditarietà.
Alla prossima.

giovedì 16 settembre 2010

Divagazioni sul table partitioning in PostgreSQL - parte 1

Il table partitioning, italianamente reso come partizionamento delle tabelle, è una tecnica/tecnologia degli RDBMS per snellire le operazioni su tabelle molto grandi.
In sostanza una tabella madre viene suddivisa in un numero di tabelle figlie secondo dei criteri per i quali ogni riga non può che appartenere ad una ed una sola tabella figlia.
Tabelle più piccole e maneggevoli consentono operazioni di manutenzione (leggi VACUUM)  più brevi, comportano indici più piccoli che stiano più facilmente in memoria.
Ad esempio un caso tipico utilizza una colonna di tipo DATE o TIMESTAMP per suddividere la tabella madre in "sezioni temporali".
PostgreSQL di per sé non supporta il partizionamento ma supporta delle funzionalità di base con le quali farne un implementazione. La documentazione ufficiale dedica un intero capitolo, il 5.9, all'argomento con anche una proposta abbastanza dettagliata per l'implementazione.
Alla base vi sono alcuni meccanismi:

  1. l'ereditarietà gerarchica per definire una relazione di "figliolanza" fra la tabella madre e quelle figlie;
  2. le table constraint per garantire (e documentare al query planner) il criterio di partizionamento;
  3. la constraint exclusion per ottimizzare le query (SELECT) limitandole alle sole tabelle figlie "interessanti".

In pratica le SELECT sulla tabella madre (che di fatto resta vuota) vengono dirottate su una o più tabelle figlie in base al contenuto della condizione WHERE.
Le altre operazioni (INSERT, UPDATE, DELETE) vengono "catturate" al volo tramite delle funzioni TRIGGER e dirottate sulla giusta tabella figlia.
In questo modo, peraltro, la tabella madre resta sempre vuota, come solo riferimento (o "avatar") di tutta la gerarchia.
La tecnica ufficiale impone due importanti vincoli:

  1. la struttura gerarchica deve essere definita prima di ogni operazione;
  2. la struttura gerarchica deve poter ospitare tutte le possibili righe di ogni operazione
In generale il primo vincolo non è un grosso problema se non lo è anche il secondo. Ad esempio: si gestiscono movimentazioni di magazzino (in senso generico) appartenenti al solo anno solare in corso e si sono create partizioni (tabelle figlie) per ogni mese dell'anno.
Se ad esempio esistono magazzini "virtuali" per gli ordini schedulati nel futuro o se si effettuano movimenti correttivi "retrodatati", allora il vincolo n.2 potrebbe non essere soddisfatto a meno di poter estendere la struttura gerarchica nel passato e/o nel futuro.
E' facile pensare ad una prima variante. Invece di creare la gerarchia in modo preventivo, la si può creare "on demand", man mano che le tabelle figlie diventano necessarie.
Chiaramente se pensiamo al partizionamento per mesi, la cosa ha poco senso. Se pensiamo ad un partizionamento per settimane, magazzini e, magari, categorie di prodotti, la considerazione assume un'altro valore e tutt'altro peso.
Risparmiandoci la creazione statica e preventiva della gerarchia, risolviamo in un colpo solo tutti e due i vincoli imposti dalla soluzione ufficiale. Ogni movimento, sia esso passato o futuro, troverà certamente posto in una tabella figlia.
Questa soluzione impone però un radicale cambiamento nell'implementazione.
Innanzitutto si deve automatizzare tutta la parte DML che serve per creare dinamicamente le tabelle figlie. Compito non facile, ma certamente un buon esercizio, soprattutto in PL/PgSQL.
In seconda battuta, va notato come l'implementazione ufficiale si basa su TRIGGER in cui una cascata di IF viene usata per selezionare la tabella figlia giusta. Non discuto sull'efficienza della cosa, anche perché non so se l'interprete PL/PgSQL operi qualche ottimizzazione (non credo, però). Mi preoccupo del fatto che quella cascata di IF va ricreata ogni volta che si aggiungono (o tolgono) tabelle figlie. Per poterlo fare serve una tabella di supporto in cui riportare da un lato il criterio di selezione e dall'altro il nome della corrispondente tabella. E possibilmente una funzione per automatizzare la cosa: devono essere anche ridefinite le funzioni TRIGGER.
E questo ci porta al prossimo secondo passo.