domenica 17 ottobre 2010

Il male non viene dai numeri, ovvero il tipo NUMERIC in PostgreSQL

Secondo Mr. Celko (e moltissimi altri) non si dovrebbe mai pensare allo spazio occupato da un campo ... ehm ... da una colonna, quanto invece al suo effettivo uso. Siamo nel XXI secolo da un po' e mi sembra che questa considerazione abbia molto senso.
Per questo motivo ho deciso di sostituire tutte le colonne REAL e DOUBLE PRECISION con colonne di tipo NUMERIC.
Il tipo NUMERIC non impone limitazioni sullo spazio necessario a rappresentare i numeri e può per questo motivo essere utilizzato (con una buona approssimazione) per l'aritmetica a precisione arbitraria.
Chiunque abbia (avuto) a che fare con quantità esatte di prodotti e con cambi di valuta sa quanto tutto ciò possa essere importante e pericoloso allo stesso tempo. Soprattutto nel caso di applicazioni monetarie.
Ovviamente il passaggio a NUMERIC non l'ho voluto fare in modo bovino (non me ne vogliano i mansueti bovidi), ma ragionato e, magari, provato e pesato.
Prima di tutto ho voluto provare a fare un qualche tipo di test per confrontare i soliti tipi numerici FLOAT (ovvero REAL) e FLOAT8 (cioè il DOUBLE PRECISION) con il NUMERIC.
Che il NUMERIC possa essere meno efficiente di rappresentazioni native per le CPU è un fatto assodato: occupa più spazio in memoria e non ha un'implementazione nativa nella CPU (in realtà non è proprio così).
Ma quanto pesa veramente questa perdita di prestazioni?
Per prima cosa ho creato tre tabelle per contenere una quantità significativa di numeri.



pg=# CREATE TABLE tab_numeric ( n NUMERIC );
CREATE TABLE
pg=# INSERT INTO tab_numeric SELECT random()*generate_series( 1,1000000 );
INSERT 0 1000000
pg=# SELECT * from tab_numeric limit 10;
      n
-------------------
0.752664533443749
1.9673418533057
2.00164743466303
1.29505263641477
1.14009823650122
2.51548232976347
2.09525664849207
3.09556493535638
4.67498113028705
5.38833205588162
(10 rows)

pg=# CREATE TABLE tab_float ( f FLOAT );
CREATE TABLE
pg=# INSERT INTO tab_float SELECT random()*generate_series( 1,1000000 );
INSERT 0 1000000
pg=# CREATE TABLE tab_float8 ( f8 FLOAT8 );
CREATE TABLE
pg=# INSERT INTO tab_float8 SELECT random()*generate_series( 1,1000000 );
INSERT 0 1000000

Un obbiettivo è quello di testare la funzione sum() su quei dati memorizzati, 1 milione di valori reali compresi fra 0 e 1 milione, appunto.
Per aggirare problemi di caching (buffer, file system e PostgreSQL stesso) ho fatto ripartire l'intero sistema operativo. Non si sa mai.
Ed ecco i risultati:



pg=# \timing
Timing is on.
pg=# SELECT sum( n ) FROM tab_numeric;
          sum
------------------------------
249966920426.730893782620486
(1 row)

Time: 388,452 ms
pg=# SELECT sum( f ) FROM tab_float;
    sum
------------------
249743083324.389
(1 row)

Time: 142,194 ms
pg=# SELECT sum( f8 ) FROM tab_float8;
    sum
------------------
250076635234.095
(1 row)

Time: 142,298 ms
pg=# SELECT relname, pg_size_pretty(relpages::BIGINT*8*1024) AS size, relkind, reltuples::BIGINT AS rows, relpages, relfilenode FROM pg_class WHERE relname like 'tab\\_%';
relname   | size  | relkind |  rows   | relpages | relfilenode 
-------------+-------+---------+---------+----------+-------------
 tab_float   | 35 MB | r       | 1000000 |     4425 |       34969
 tab_float8  | 35 MB | r       | 1000000 |     4425 |       34972
 tab_numeric | 42 MB | r       | 1000000 |     5406 |       34963
(3 rows)

Come direbbe Mr. Spok, "Interessante!".

  1. Il tipo FLOAT8 funziona tanto bene quanto il tipo FLOAT, probabilmente per via dell'implementazione hardware della sezione a virgola mobile della CPU a 64 bit che ho usato (vedi anche punto 3).
  2. Il tipo NUMERIC è banalmente più lento del FLOAT8, ma solo di un fattore circa 2,7.
  3. La tab_numeric occupa solo il 20% di spazio disco in più rispetto alla tab_float8 che a sua volta non occupa più spazio della tab_float.
  4. La precisione può essere importante. Molto importante. Basta confrontare i risultati delle 3 query.
Ovviamente non abbiamo realizzato un test reale e forse non è nemmeno il massimo della scientificità.
Penso inoltre che ci siano veramente poche applicazioni che si limitino a sommare fra di loro 1 milione di valori numerici contenuti in una tabella.
Ciò che questo test credo mostri è invece quale sia un limite superiore alle performance relative fra le varie rappresentazioni numeriche e relative operazioni di PostgreSQL.
Direi che nel mondo reale, dove il calcolo non è utilizzato in modo così massiccio, le performance relative (in termini di tempo macchina) dovrebbero risultare con valori molto simili fra di loro. La grande differenza la farebbe, quindi, la precisione ottenuta dai NUMERIC.


In conclusione, se le prestazioni sono più importanti della precisione in modo assoluto, allora il FLOAT8 è sicuramente la scelta giusta. Per tutto il resto c'è ... NUMERIC.

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.

venerdì 8 ottobre 2010

GMail ha l'aiuto in linea per i comandi da tastiera! Lo sapevate?

Basta premere il tasto '?' ed ecco comparire in sovraimpressione una finestra d'aiuto analoga a questa:
Snapshot

Cliccando su "Apri in una nuova finestra" si aprirà la corrispondente pagina della guida di GMail in italiano.
Cliccando sull'altro link, invece ...

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.

giovedì 23 settembre 2010

Un router di alto profilo a "quattro soldi"

Sono ormai anni che provo a cercare una soluzione decente per i miei clienti, qualsiasi sia la connettività.
A parte un paio di "sfortunati" che sono obbligati ad usare una connessione GSM/UMTS, la maggior parte usa una "semplice" ADSL, una manciata ha una connessione in fibra ottica e qualcuno la connettività HDSL/sHDSL.
I problemi principali da risolvere sono:

  1. il costo deve essere congruo con tutto il resto
  2. deve supportare i diversi tipi di connettività
  3. deve supportare: NAT, VPN e DNS dinamico
  4. deve essere gestibile da remoto in sicurezza
  5. deve essere supportato con continuità al livello di firmware
Per il punto n.2 sono pervenuto alla decisione di separare la porzione di routing da quella di connessione. Il router lavora solo su ethernet, niente modem ADSL o UMTS integrato. I motivi sono sostanzialmente tre.
Il primo è che i router con modem integrato sono di solito adatti ad applicazioni "small office". Basta una dozzina di client che facciano un po' di traffico e il router-modem diventa un collo di bottiglia.
Il secondo è che questi funzionano solo per ADSL e UMTS. Per HDSL e fibra serve comunque un router classico.
Il terzo è che diversità di firmware (e di marca) spesso sfociano nell'impossibilità di far dialogare le VPN, a meno di non riallineare firmware (e marcha e modelli).
Per i punti 3, 4 e 5,  ho dovuto sudare molto non solo con produttori (almeno in teoria) di alto livello, ma anche con i loro sviluppatori di firmware (quasi tutti indiani). Senza cavare alla fine un ragno da buco.
Almeno finché non sono incappato in un progetto opensource, OpenWRT e nel suo "fratello" a vocazione più commerciale DD-WRT.
Sapevo, già dai tempi del mio buon NetGear 384 della possibilità di alcune soluzioni alternative, ma di progetti così articolati e completi ero completamente all'oscuro, almeno fino ad un annetto fa.
I due progetti "WRT" prendono il nome dalla famosa quanto fortunata serie di router-access point della Linksys-CISCO, di cui il WRT54G è stato capostipite. Il fatto che il produttore, in ottemperanza ai dettami della licenza GPLv2, rilasci tutto il codice sorgente della distribuzione Linux usata, è il fattore che ha determinato la nascita dei due progetti.
Questi offrono, risorse a bordo del router permettendo, un ventaglio di funzionalità senza precedenti, soprattutto se è presente anche hardware per il WiFi.
OpenWRT abbarraccia la filosofia delle distribuzioni Linux. Il tutto è a riga di comando, tassativamente via SSHv2, con un'interfaccia web minimale e le funzioni possono essere estese con l'istallazione di pacchetti aggiuntivi, anche scaricati direttamente da internet. DD-WRT prevede una manciata di versioni, a seconda della quantità di memoria (flash e RAM) a bordo dei router e delle funzionalità che si ritengono necessarie.
I due progetti restano comunque compatibili, per cui è possibile installare i pacchetti di OpenWRT su un router "powered by" DD-WRT.
Un giro sui relativi siti web non può che illuminare.

Il mio primo esperimento s'è svolto, per conto di un cliente, con una coppia Linksys WRT320N. Con 200 EURO e un po' di lavoro, le due sedi sono in VPN su ADSL ad una frazione del costo preventivato dai fornitori "ufficiali" e "blasonati" (si cita il peccato, non il peccatore). Solo che, invece dell'OpenWRT ho dovuto utilizzare il DD-WRT, fortunatamente gratuito su questo modello.
Inutile dire che in mezza giornata tutto era funzionante alla perfezione.
Ma il vero colpo l'ho fatto con due "tranquilli " TP-Link WR1043ND, 60 EURO l'uno con caratteristiche hardware uguali se non superiori al precedente. Porta USB compresa. Anche qui ho installato il firmware di DD-WRT, ma il supporto di OpenWRT è totale e penso che in futuro passerò a questa. Quella porta USB può essere usata per realizzare un print server, un file server e, udite udite, per collegare un modem UMTS USB (le cosiddette "chiavette internet"). Le antenne staccabili consentiranno l'aggancio di un'antenna direttiva ad alto guadagno per realizzare un ponte radio con una terza sede a 2 chilometri di distanza.

La lista di hardware supportato da entrambi i progetti è molto vasta e, normalmente, aggiornata. per cui la scelta di un router di partenza è determinata dalle sue caratteristiche hardware e, ovviamente, dal costo.
Ma alla fine ciò che si acquista non sarà altro che "ferro" su cui far girare un firmware di alto profilo.

Concludo dicendo che, dal momento che il firmware usato è sostanzialmente lo stesso, non è più importante che i router acquistati siano della stessa marca o lo stesso modello. Importa solo che possano ospitare le stesse funzioni.
Non male, direi. E una donazione in denaro ai progetti è un obbligo morale.

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.