Oracle Partitioning: 5^ parte, operazioni sulle partizioni

Una volta che si ha una tabella partizionata vi sono diverse operazioni che si possono fare.

Partition exchange

la prima operazione di modifica su tabelle di cui parlo è secondo me molto interessante. E’ utile in ambienti di datawarehouse ma è anche un’operazione che può tornare utile nel caso si voglia passare da una tabella non partizionata a una partizionata. Di fatto questa operazione permette di scambiare il segmento tra una tabella e una partizione. Questa tecnica risulta utile nei database datawarehouse per caricare massivamente blocchi di dati. Io ora faccio un esempio di passaggio da una tabella non partizionata  a una partizionata:

SQL>CREATE TABLE ticket
2 ( atickid number(10,0),
3 atickdataora timestamp(6),
4 atickfk number(10,0),
5 atickdescrizione varchar2(100 char)
6 );
Table created.

SQL>create index idx_ticket_2 on ticket (atickdataora);
Index created.

SQL>create index idx_ticket_3 on ticket (atickfk);
Index created.

SQL>begin for i in 1..10000 loop insert into ticket (atickid,atickdataora,atickfk,atickdescrizione) values (i,sysdate-520+i/100,mod(i,100),i); end loop; end;
2 /
PL/SQL procedure successfully completed.

SQL>commit;
Commit complete.

SQL>CREATE TABLE p_ticket
2 ( atickid number(10,0),
3 atickdataora timestamp(6),
4 atickfk number(10,0),
5 atickdescrizione varchar2(100 char)
6 )
7 PARTITION BY RANGE (atickdataora) INTERVAL (NUMTOYMINTERVAL(1,'MONTH'))
8 SUBPARTITION BY LIST (atickfk)
9 SUBPARTITION TEMPLATE
10 ( SUBPARTITION P_1 VALUES (1)
11 , SUBPARTITION P_2 VALUES (2)
12 , SUBPARTITION P_3 VALUES (3)
13 , SUBPARTITION P_4 VALUES (4)
14 , SUBPARTITION P_5 VALUES (5)
15 , SUBPARTITION p_others VALUES (DEFAULT)
16 )
17 ( PARTITION before_2018 VALUES LESS THAN (TO_DATE('01-01-2018','dd-MM-yyyy'))
18 ( SUBPARTITION before_2018_s1 values (DEFAULT) )
19 );
Table created.

SQL>create index idx_p_ticket_2 on p_ticket (atickdataora) local;
Index created.

SQL>create index idx_p_ticket_3 on p_ticket (atickfk) local;
Index created.

SQL>ALTER TABLE p_ticket EXCHANGE SUBPARTITION BEFORE_2018_S1 WITH TABLE ticket including indexes WITH VALIDATION ;
Table altered.
SQL>@count p_ticket
COUNT(*)
----------
10000

SQL>@count ticket
COUNT(*)
----------
0

SQL> select subpartition_name,index_name,status from user_ind_SUBPARTITIONS where INDEX_name LIKE 'IDX_P_TICKET%';
SUBPARTITION_NAME INDEX_NAME STATUS
------------------------------ ------------------------------ --------
BEFORE_2018_S1 IDX_P_TICKET_2 USABLE
BEFORE_2018_S1 IDX_P_TICKET_3 USABLE

La gestione degli indici può essere un po’ complicata, ad esempio se ho un indici univoco sulla tabella di partenza non me lo posso portare sulla tabella partizionata con l’exchange.

 

Add, Drop, Merge e Split

Riunisco in un unico capitoletto queste quattro operazioni perché penso di avere poco da dire. Parlo di partizioni o sottopartizioni nel caso la modalità sia per lista (list) o per intervallo (range), per la modalità hash le cose sono leggermente diverse.

Se devo aggiungere nuove partizioni per liste di valori o intervalli che non erano gia previsti posso facilmente fare un “ADD”. Sembra che sulle tabelle con partizionamento INTERVAL non sia possibile farlo:

SQL>alter table p_ticket add PARTITION before_2019 VALUES LESS THAN (TO_DATE('01-01-2019','dd-MM-yyyy'));
alter table p_ticket add PARTITION before_2019 VALUES LESS THAN (TO_DATE('01-01-2019','dd-MM-yyyy'))
*
ERROR at line 1:
ORA-14760: ADD PARTITION is not permitted on Interval partitioned objects

La cosa ha un senso, le partizioni vengono aggiunte in automatico. Forse si può aggirare l’ostacolo ma non vedendone il motivo per ora non me ne occupo. Da notare che non si possono aggiungere partizioni “prima”, per questo tipo di operazioni si fa lo split.

SQL > alter table range_sales add PARTITION p_1 VALUES LESS THAN (TO_DATE('01-01-2007','dd-MM-yyyy'));
alter table range_sales add PARTITION p_1 VALUES LESS THAN (TO_DATE('01-01-2007','dd-MM-yyyy'))
*
ERROR at line 1:
ORA-14074: partition bound must collate higher than that of the last partition

 

In questo caso comunque:

SQL > alter table range_sales add PARTITION p_1 VALUES LESS THAN (TO_DATE('01-01-2011','dd-MM-yyyy'));
alter table range_sales add PARTITION p_1 VALUES LESS THAN (TO_DATE('01-01-2011','dd-MM-yyyy'))
*
ERROR at line 1:
ORA-14074: partition bound must collate higher than that of the last partition
GEOCALL@scmspagna_svil > select PARTITION_NAME,HIGH_VALUE from user_tab_Partitions where table_name='RANGE_SALES';
PARTITION_NAME HIGH_VALUE
------------------------------ --------------------------------------------------------------------------------
P0 TO_DATE(' 2008-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
P1 TO_DATE(' 2009-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
P2 TO_DATE(' 2009-07-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
P3 TO_DATE(' 2010-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
POTHER MAXVALUE

 

Avendo definito la partizione POTHER con limite “MAXVALUE” non posso aggiungere partizioni, qui occorre usare la funzione di “SPLIT”. Lo split è l’operazione per cui si può suddividere una partizione in due partizioni, ecco un esempio:

SQL > select PARTITION_NAME,HIGH_VALUE from user_tab_Partitions where table_name='RANGE_SALES';
PARTITION_NAME HIGH_VALUE
------------------------------ --------------------------------------------------------------------------------
P0 TO_DATE(' 2008-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
P1 TO_DATE(' 2009-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
P2 TO_DATE(' 2009-07-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
P3 TO_DATE(' 2010-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
POTHER MAXVALUE

SQL > alter table RANGE_SALES split partition pOTHER at (TO_dATE('01-01-2011','DD-MM-YYYY')) into (Partition P4, Partition POTHER);
Table altered.

SQL > select PARTITION_NAME,HIGH_VALUE from user_tab_Partitions where table_name='RANGE_SALES';
PARTITION_NAME HIGH_VALUE
------------------------------ --------------------------------------------------------------------------------
P0 TO_DATE(' 2008-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
P1 TO_DATE(' 2009-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
P2 TO_DATE(' 2009-07-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
P3 TO_DATE(' 2010-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
P4 TO_DATE(' 2011-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
POTHER MAXVALUE

La prossima possibile operazione è quella di “drop”, di eliminazione di una partizione:

SQL > alter table range_sales drop partition p0;
Table altered.

SQL > select PARTITION_NAME,HIGH_VALUE from user_tab_Partitions where table_name='RANGE_SALES';
PARTITION_NAME HIGH_VALUE
------------------------------ --------------------------------------------------------------------------------
P1 TO_DATE(' 2009-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
P2 TO_DATE(' 2009-07-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
P3 TO_DATE(' 2010-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
P4 TO_DATE(' 2011-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
POTHER MAXVALUE

Infine, è possibile riunire in unica partizione due partizioni:

SQL > ALTER TABLE range_sales MERGE PARTITIONS P3, P4 INTO PARTITION P3;
ALTER TABLE range_sales MERGE PARTITIONS P3, P4 INTO PARTITION P3
*
ERROR at line 1:
ORA-14275: cannot reuse lower-bound partition as resulting partition

SQL > ALTER TABLE range_sales MERGE PARTITIONS P3, P4 INTO PARTITION P4;
Table altered.

 

La documentazione afferma che le due partizioni originali vengono eliminate e ne viene creata una nuova.

Per tutte le operazioni non ho parlato di indici, cerco di riassumere in breve come avviene la loro gestione anche se confesso di non aver testato nel dettaglio tutti i casi. Per tutte le operazioni è prevista una clausola “UPDATE INDEXES” che si occupa di gestire aggiornando gli indici. Se la clausola non è specificata in generale quello che può accadere è che gli indici globali diventano invalidi (“UNUSABLE”) mentre per gli indici “locali” sono inusabili le singole partizioni coinvolte. Nel caso di ADD ho notato che ciò non accade, probabilmente perché è una situazione più facile da gestire. Nel caso di DROP non avviene se la partizione è vuota (almeno così è risultato da un piccolo test su una tabella partizionata per intervallo.

Row Movement

Tutte le operazioni di manipolazione delle partizioni non ne sono intaccate, però per default sulle tabelle non è abilitato il “row movement”, questo significa che operazioni DML che cambiano il rowid di un record non sono possibili. Un operazione di update che modifica la chiave di partizionamento di un record in modo da farlo ricadere su una partizione diversa da quella in cui risiede è uno di quei casi (per la verità non so se ne esistono altri). Riporto un esempio completo:

SQL > select PARTITION_NAME,HIGH_VALUE from user_tab_Partitions where table_name='RANGE_SALES';
PARTITION_NAME HIGH_VALUE
------------------------------ --------------------------------------------------------------------------------
P1 TO_DATE(' 2009-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
P3 TO_DATE(' 2011-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
POTHER MAXVALUE

SQL > select * from range_sales;
PROD_ID CUST_ID TIME_ID C PROMO_ID QUANTITY_SOLD AMOUNT_SOLD
---------- ---------- ------------------- - ---------- ------------- -----------
2 2 08-03-2009 00:00:00 B 2 2 2
1 1 18-04-2010 00:00:00 A 1 1 1

SQL > update range_sales set time_id=to_date('12-01-2011','dd-mm-yyyy') where prod_id=1;
update range_sales set time_id=to_date('12-01-2011','dd-mm-yyyy') where prod_id=1
*
ERROR at line 1:
ORA-14402: updating partition key column would cause a partition change

SQL > ALTER TABLE RANGE_SALES ENABLE ROW MOVEMENT;
Table altered.

SQL > update range_sales set time_id=to_date('12-01-2011','dd-mm-yyyy') where prod_id=1;
1 row updated.
 

Vi sono poi altre operazioni di modifica delle partizioni, ad esempio TRUNC o MOVE, per i dettagli però rimando al manuale.

 

Annunci

Oracle Partitioning: 4^ parte, creazione tabelle partizionate

Cominciamo a vedere un po’ i dettagli su come si definiscono e si gestiscono tabelle e indici partizionati. Una tabella nasce come partizionata alla sua creazione. In oracle 12.1 sembra sia possibile convertire una tabella non partizionata in partizionata, si tratta però di una cosa che devo ancora studiare quindi per ora non ne scrivo. Riporto solo una piccola annotazione:  Christian Antognini, nel suo libro dopo aver descritto nel dettaglio i vari “percorsi” di accesso che può utilizzare l’ottimizzatore Oracle con tabelle partizionate, spiega come nella sua esperienza l’uso del partitioning sia una cosa che deve rientrare nel progetto iniziale di una applicazione e non una cosa che si può aggiungere a posteriori sperando di risolvere dei problemi.

Rimanendo quindi sulla versione 11.2 se abbiamo una tabella esistente, popolata non partizionata e a un certo punto decidiamo di partizionarla, non lo possiamo fare in maniera diretta con un semplice “ALTER”. Che la cosa non possa essere così diretta è anche intuitivo: comunque dovremo spostare fisicamente dei dati per distribuirli su partizioni diverse e “indipendenti”. Cominciamo quindi con un esempio di creazione di una nuova tabella partizionata per “range” su un campo data:

CREATE TABLE range_sales
( prod_id NUMBER(6)
, cust_id NUMBER
, time_id DATE
, channel_id CHAR(1)
, promo_id NUMBER(6)
, quantity_sold NUMBER(3)
, amount_sold NUMBER(10,2)
) 
PARTITION BY RANGE (time_id) 
( PARTITION p0 VALUES LESS THAN (TO_DATE('1-1-2008', 'DD-MM-YYYY')),
PARTITION p1 VALUES LESS THAN (TO_DATE('1-1-2009', 'DD-MM-YYYY')),
PARTITION p2 VALUES LESS THAN (TO_DATE('1-7-2009', 'DD-MM-YYYY')),
PARTITION p3 VALUES LESS THAN (TO_DATE('1-1-2010', 'DD-MM-YYYY')),
PARTITION p_other VALUES LESS THAN (MAXVALUE) );

 Con questo comando si crea una tabella che ha cinque partizioni, p0 conterrà i record con valori nel campo time_id inferiori (antecedenti) al 1-1-2008, p1 quelli superiori a quella data e fino al 1-1-2009. p_other conterrà tutti i record con valori superiori a 1-1-2010 o null. Anche se poi ci sono piccoli dettagli che rendono alcune operazioni più “complicate” (vedi ad esempio qui: https://connor-mcdonald.com/2017/08/01/interval-partitioning-just-got-better/). Faccio fatica a trovare un motivo per cui l’esempio precedente non debba utilizzare l’estensione “interval partitioning” che diventerebbe ad esempio:

CREATE TABLE interval_sales
( prod_id NUMBER(6)
, cust_id NUMBER
, time_id DATE
, channel_id CHAR(1)
, promo_id NUMBER(6)
, quantity_sold NUMBER(3)
, amount_sold NUMBER(10,2)
) 
PARTITION BY RANGE (time_id) 
INTERVAL(NUMTOYMINTERVAL(1, 'MONTH'))
( PARTITION p0 VALUES LESS THAN (TO_DATE('1-1-2008', 'DD-MM-YYYY')),
PARTITION p1 VALUES LESS THAN (TO_DATE('1-1-2009', 'DD-MM-YYYY')),
PARTITION p2 VALUES LESS THAN (TO_DATE('1-7-2009', 'DD-MM-YYYY')),
PARTITION p3 VALUES LESS THAN (TO_DATE('1-1-2010', 'DD-MM-YYYY')) );

 In questo caso ho omesso l’ultima partizione con il limite “MAXVALUE”. Con l'”interval partitioning” Oracle prende a riferimento l’ultima partizione e al bisogno creerà nuove partizioni che contengono intervalli di date di un mese. Per questo motivo Oracle non mi lascia eliminare l’ultima partizione:

SQL> ALTER TABLE INTERVAL_SALES DROP PARTITION P0;
Table altered.
SQL > ALTER TABLE INTERVAL_SALES DROP PARTITION P3;
ALTER TABLE INTERVAL_SALES DROP PARTITION P3
*
ERROR at line 1:
ORA-14758: Last partition in the range section cannot be dropped

 (ed è il tema trattato nel post di Connor McDonald che ho citato sopra)

 Inseriamo un po’ di dati a caso e vediamo cosa succede:

create sequence sinterval_sales;
insert into interval_sales (prod_id,cust_id,time_id) values (sinterval_sales.nextval,1, to_date('10-11-2004','dd-mm-yyyy'));
insert into interval_sales (prod_id,cust_id,time_id) values (sinterval_sales.nextval,1, to_date('10-11-2008','dd-mm-yyyy'));
insert into interval_sales (prod_id,cust_id,time_id) values (sinterval_sales.nextval,1, to_date('10-02-2009','dd-mm-yyyy'));
insert into interval_sales (prod_id,cust_id,time_id) values (sinterval_sales.nextval,1, to_date('10-09-2009','dd-mm-yyyy'));
insert into interval_sales (prod_id,cust_id,time_id) values (sinterval_sales.nextval,1, to_date('10-09-2010','dd-mm-yyyy'));
insert into interval_sales (prod_id,cust_id,time_id) values (sinterval_sales.nextval,1, to_date('10-05-2010','dd-mm-yyyy'));
 
SQL > select partition_name, high_value,interval from user_tab_partitions where table_name='INTERVAL_SALES';
PARTITION_NAME HIGH_VALUE INT
------------------------------ -------------------------------------------------------------------------------- ---
P1 TO_DATE(' 2009-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA NO
P2 TO_DATE(' 2009-07-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA NO
P3 TO_DATE(' 2010-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA NO
SYS_P145 TO_DATE(' 2010-10-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA YES
SYS_P146 TO_DATE(' 2010-06-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA YES

 

 Ora vediamo un esempio di partizionamento per “lista”:

 CREATE TABLE list_sales
( prod_id NUMBER(6)
, cust_id NUMBER
, time_id DATE
, channel_id CHAR(1)
, promo_id NUMBER(6)
, quantity_sold NUMBER(3)
, amount_sold NUMBER(10,2)
) 
PARTITION BY list (channel_id) 
( PARTITION p0 VALUES ('A'),
PARTITION p1 VALUES ('B'),
PARTITION p3 VALUES ('C','D'),
PARTITION p_other VALUES (DEFAULT) );

 E senza esitazione passiamo a un esempio di hash partitioning:

CREATE TABLE hash_sales
( prod_id NUMBER(6)
, cust_id NUMBER
, time_id DATE
, channel_id CHAR(1)
, promo_id NUMBER(6)
, quantity_sold NUMBER(3)
, amount_sold NUMBER(10,2)
) 
PARTITION BY hash (prod_id) 
( PARTITION p0,
PARTITION p1,
PARTITION p3,
PARTITION p4);

 Oppure:

CREATE TABLE hash2_sales
( prod_id NUMBER(6)
, cust_id NUMBER
, time_id DATE
, channel_id CHAR(1)
, promo_id NUMBER(6)
, quantity_sold NUMBER(3)
, amount_sold NUMBER(10,2)
) 
PARTITION BY hash (prod_id) partitions 8;

 L’hash partitioning prevede due sintassi leggermente diverse. Nel primo caso ho la possibilità si specificare in creazione proprietà diverse per ciascuna partizione, ad esempio tablespace diverse, nel secondo gli dico solo di fare 8 partizioni.

Farei un ultimo esempio di creazione introducendo il “subpartitioning”:

CREATE TABLE p_ticket
( atickid number(10,0),
atickdataora timestamp(6),
atickfk number(10,0),
atickdescrizione varchar2(100 char)
)
PARTITION BY RANGE (atickdataora) INTERVAL (NUMTOYMINTERVAL(1,'MONTH'))
SUBPARTITION BY LIST (atickfk)
SUBPARTITION TEMPLATE
( SUBPARTITION P_1 VALUES (1)
, SUBPARTITION P_2 VALUES (2)
, SUBPARTITION P_3 VALUES (3)
, SUBPARTITION P_4 VALUES (4)
, SUBPARTITION P_5 VALUES (5)
, SUBPARTITION p_others VALUES (DEFAULT)
)
( PARTITION before_2018 VALUES LESS THAN (TO_DATE('01-01-2018','dd-MM-yyyy'))
( SUBPARTITION before_2018_s1 values (DEFAULT) )
);
INSERT INTO P_TICKET (ATICKID,ATICKDATAORA,ATICKFK,ATICKDESCRIZIONE) VALUES (1,to_date('18-07-1973','dd-mm-yyyy'),1,'a'); 
INSERT INTO P_TICKET (ATICKID,ATICKDATAORA,ATICKFK,ATICKDESCRIZIONE) VALUES (1,to_date('21-01-2019','dd-mm-yyyy'),2,'b');

 

L’esempio sopra è un po’ particolare perché ho voluto metterci dentro un po’ di tutto. Ho utilizzato un “SUBPARTITION TEMPLATE”, è una tecnica alternativa allo specificare le sottopartizioni per ogni partizione esplicitamente. Nel caso di sottopartizionamenti di tipo INTERVAL-* l’uso del template è obbligatorio in quanto indica ad Oracle come partizionare le nuove partizioni create per gli intervalli. Il template è modificabile e incide sullo schema di sottopartizionamento delle nuove partizioni create. Poi ho anche definito per la partizione di partenza uno schema di sottopartizionamento diverso. Se avessi definito più partizioni avrei potuto definire per ciascuna partizione uno schema di sottopartizionamento diverso. Per non farla troppo lunga non riporto qui i test che ho fatto ma posso dire che se non si specifica il template le nuove partizioni avranno una unica sottopartizione che prendono tutti i valori.

Oracle Partitioning: 3^ parte, modalità di partizionamento

Siccome mi trovo a lavorare su un database con versione 11gR2 parto con le caratteristiche e funzionalità disponibili in questa versione. Cercherò poi di parlare delle estensioni introdotte con le versioni successive.

Le modalità di partionamento disponibili sono tre:

  • range

  • list

  • hash

Vi è poi la possibilità di estendere il partizionamento a un secondo livello, cioè si partizionano a loro volta le partizioni; in questo caso si parla di “composite partitioning” o “subpartitioning”. Sono possibili tutte le combinazioni; tratto in inganno da questa parte del manuale pensavo facessero eccezione le combinazioni con primo livello basato su hash ma il manuale “SQL Language Reference” non prevede questa esclusione; nel dubbio ho provato con successo a creare una tabella partizionata per hash e sottopartizionata ancora per hash (che poi sia utile è un altro discorso).

Interessante il fatto, scoperto con dei test, che posso avere “sotto-partizionamenti” diversi per partizioni diverse, farò vedere in seguito un esempio di cosa intendo. Questo per ribadire che c’è grande flessibilità. Solo per range e hash è possibile specificare una chiave di partizionamento basata su più colonne, il numero massimo di colonne su cui è possibile basare il partizionamento è 16. L’hash partitioning su più colonne è possibile dalla versione 11.2. E’ possibile anche partizionare su colonne virtuali.

Il numero massimo di partizioni e sottopartizioni possibili è 1024K-1 (1024*1024-1), come specificato nel manuale “SQL Language Reference“, assieme ad altri limiti e dettagli.

Range partitioning

Con questo schema di partizionamento i dati vengono suddivisi sulla base di intervalli prefissati sui valori dei campi scelti come chiave. Ho già accennato all’esempio classico del campo data ma può essere anche un campo numerico. Le partizioni si definiscono impostando sempre l’estremo superiore, vi è poi la possibilità di definire una partizione che contiene tutti gli altri valori usando come limite la parola chiave “MAXVALUE”. I record per cui i campi della chiave di partizionamento sono NULL vengono messi nella partizione con limite superiore MAXVALUE. Una estensione di questa cosa, che però non permette valori null è l'”INTERVAL Partitioning” di cui spiegherò meglio i dettagli più avanti.

List partitioning

Questa schema di partizionamento si basa sulla specifica esplicita di valori per i campi chiave del partizionamento per cui i record devono cadere nella specifica partizione. L’esempio riportato dal manuale parla di singoli stati per elementi da elencare. Per i valori non specificati esplicitamente si può definire una partizione per cui anziché la lista di valori si specifica la parola chiave “DEFAULT” (nella versione 12.2 è stato introdotto l'”Automatic List Partitioning”).

Hash partitioning

Questo schema di partizionamento è particolare e come tale risulta utile solo in casi specifici. In questo caso non si predetermina la partizione di destinazione di un record, la partizione viene selezionata da Oracle sulla base di una funzione di hash che viene applicata alla chiave di partizionamento. In fase di creazione della tabella partizionata quindi si specificano i campi chiave e il numero di partizioni, per una distribuzione equa dei dati tra le partizioni Kyte spiega e dimostra nel suo libro come sia necessario che il numero di partizioni sia sempre una potenza di due.

Mi sembra di poter affermare che la maggior utilità data da questo schema di partizionamento sia quella di ridurre la concorrenza nell’accesso alle strutture di gestione del segmento in fase di inserimento e aggiornamento dati. Certo abbiamo ancora partizioni più piccole su cui lavorare e “indipendenti”, che possono essere messe offline separatamente dalle altre, però i dati sono distribuiti in modo imprevedibile fra le partizioni, quindi qualunque sia la partizione offline è possibile, se non probabile, che contenga dei dati che in quel momento servono. Se si fanno interrogazioni per intervalli questo schema di partizionamento non può che avere un peggioramento in termini di prestazioni.

Estensioni

Agli schemi base di partizionamento e sottopartizionamento descritti in precedenza si sono aggiunte nel tempo delle estensioni che aumentano la flessibilità e il campo di utilizzo del partitioning riducendo le operazioni di manutenzione. Queste estensioni sono:

  • Interval partitioning

  • Reference partitioning

Interval partitioning

Secondo me una delle estensioni più utili introdotte nel tempo, si tratta di una estensione del “range partitioning” che permette la creazione automatica di partizioni per intervalli determinati. Facciamo l’esempio più facile e classico, un partizionamento per intervalli di data. Supponiamo di partizionare la tabella degli ordini per anno, possiamo partire con una singola partizione per l’anno corrente, quanto si inseriranno valori con date successive Oracle crea in automatico la partizione relative a quell’anno. Prima che fosse disponibile questa estensione l’unica possibilità era pre-creare in anticipo le partizioni necessarie.

Reference partitioning

Mentre l’estensione “interval partioning” semplifica molto la gestione senza aggiungere molto alle prestazioni, il reference partitioning interviene sul lato delle prestazioni, permettendo di partizionare una tabella legata tramite una chiave esterna ad un’altra già partizionata. Lo scenario è quello classico delle tabelle in relazione, dette anche master-detail o padre-figlio. Facciamo l’esempio delle tabelle ordini, testate e righe. Abbiamo partizionato la tabella delle testate in base alla data (range partition), possiamo partizionare la tabella delle righe in modalità reference partitioning in modo che i record sulla tabella delle righe degli ordini sono partizionanati in modo che i dati di una partizione della tabella padre abbiano i record figli tutti nella stessa partizione nella tabella figlia. Questo si riflette in un miglioramento nelle prestazioni nei casi in cui i dati vengano interrogati con operazioni di join.

Partizionamento degli indici

Come è possibile partizionare una tabella è possibile partizionare gli indici, seppur con qualche differenza. Non se ne parla molto, forse perché ha poco senso, però è possibile anche partizionare un indice su una tabella non partizionata.

Una prima suddivisione sulla modalità di partizionamento degli indici è quella fra indici LOCAL e indici GLOBAL. Nel primo caso gli indici sono partizionati allo stesso modo della tabella a cui si riferiscono, questo fa si che si estenda il livello di migliore gestione conseguente al partitioning e si aumenti la “disponibilità”; se spostiamo o mettiamo offline una singola partizione ne subirà le conseguenze solo la relativa partizione sugli indici. Nel caso indici partizionati GLOBAL l’indice viene partizionato con un criterio diverso dalla sua tabella, le possibilità sono limitate a range e hash, quindi non è prevista la possibilità di partizionare globalmente un indice in modalità “LIST”. Vi è poi la possibilità di definire alla creazione un indice come “GLOBAL” ma senza nessun criterio di partizionamento, in questo caso semplicemente l’indice non è partizionato. La documentazione (qui quella relativa alla versione 18c) afferma che un indice globale è composto da un unico B-tree. Nel caso di indici partizionati globalmente o non partizionati se una partizione viene eliminata o messa offline l’indice diventa inusabile. Per poter definire un indice univoco esso deve essere globale o contenere la chiave di partizionamento. Gli indici possono essere prefissati o no, se hanno come prima parte la chiave di partizionamento sono prefissati altrimenti no, gli indici globali possono essere solo prefissati, cioè le prime colonne su cui devono essere indicizzati devono essere la chiave di partizionamento. Riassumendo quindi, gli indici partizionati vengono classificati in tre tipologie:

  • “Local prefixed”

  • “Local nonprefixed”

  • “Global prefixed”

Oracle Partitioning: 2^ parte, i vantaggi

Come ho accennato nella prima parte i vantaggi attribuiti a Oracle partitioning sono comunemente questi tre:

  • Migliori prestazioni, sia delle query che delle operazioni “DML”

  • Facilitazione della gestione

  • Migliore “disponibilità”

Vediamoli meglio nel dettaglio.

Migliori prestazioni

si tratta senza dubbio di quello viene maggiormente considerato quando si parla di partitioning. Il guaio è che spesso lo si fa senza una analisi un minimo approfondita di come funziona il partitioning e di come può contribuire a migliorare le condizioni.

Per molti sembra che partitioning equivalga in automatico a migliori prestazioni, soprattutto nelle interrogazioni SQL, indipendentemente dal volume dei dati e dal tipo di interrogazioni fatte su questi dati. Nel suo libro, kyte dedica molto spazio a spiegare come spesso Partitioning venga percepita come una soluzione per migliorare in automatico le prestazioni e spiega come invece il miglioramento delle prestazioni sia solo una parte dei vantaggi che può dare e che li da solo in alcuni specifici ambiti. Va ricordato che Partitioning è una “opzione” del database Oracle. Una “opzione” del database Oracle significa gia parlare di Enterprise Edition, non Standard Edition. Solo  questo significa un aumento del costo delle licenze non trascurabile. Poi la seconda regola delle “opzioni” Oracle (dopo il fatto che sono disponibili solo con la Enterprise Edition) è che sono a pagamento. Da quel che ho capito da una rapida occhiata al listino Oracle passare da licenza Standard Edition a Enterprise Edition con Partitioning significa, al netto di sconti, un aumento da 3 a 4 volte del costo delle licenze. Presumo poi che ci siano svariate tipologie di accordi commerciali che possono ridurre il rapporto di costo ma su questo ne so ben poco e se ne sapessi non ne potrei certo parlare qui; già il listino viene marcato come “confidenziale”. Il sospetto che ho, alimentato dal fatto che più volte mi è stato chiesto: “perché non usiamo partitioning?…” è che qualcuno spinga per giustificare il costo delle licenze, senza avere una visione tecnica di dettaglio.

La verità è che il partitioning da molti vantaggi, molti dei quali li ho realmente capiti solo facendo diversi test dopo aver letto la documentazione approfonditamente. In molti casi le prestazioni non cambiano o possono avere anche un peggioramento. Il tutto in cambio di una gestione leggermente diversa. Devo ammettere che prima di lavorarci e fare un po’ di prove ero prevenuto e attribuivo alla gestione del partitioning un sovraccosto superiore a quello reale. Va anche detto che versione dopo versione sono state introdotte nuove funzionalità e migliorie che hanno reso questa opzione veramente potente e flessibile.

Come accennato prima il partitioning permette di suddividere una grossa tabella in tante piccole tabelline (segmenti), la tabella viene comunque vista come una unica tabella. Il criterio con qui suddividere la tabella viene fissato all’inizio e si basa sui valori che le righe della tabella hanno per una o più colonne prefissate. Il criterio può essere per intervallo (range, ad esempio intervalli di date) o per una lista di valori (se il campo prescelto ha un numero di valori distinti “limitato”). Oppure può essere “casuale” sulla base di una funzione di hashing sui valori di un campo.

Ad esempio, supponiamo di avere la classica tabella degli ordini possiamo pensare di partizionarla per intervalli di date, ad esempio con una partizione per ogni anno. In alternativa possiamo pensare di suddividere gli ordini per filiale dalla quale sono partiti, in questo caso probabilmente può andare bene il partizionamento per lista di valori (list partitioning), la lista può comprendere uno o più valori, quindi ogni partizione può contenere gli ordini di una o più filiali. Infine, possiamo suddividere la tabella in più partizioni sulla base dell’hash dell’id univoco usato come chiave primaria. La tabella degli ordini sarà vista dalle query SQL sempre come non fosse cambiato nulla, nel senso che le query sono scritte allo stesso modo; l’ottimizzatore di Oracle però è in grado di utilizzare il cosiddetto “partition pruning”, cioè al momento di prelevare i dati può escludere le partizioni non rilevanti se nei filtri delle query sono inclusi i campi sui cui valori si è partizionata la tabella. Prendendo la nostra tabella degli ordini partizionata per anno, se andiamo a cercare gli ordini relativi solo a un singolo anno l’ottimizzatore sarà in grado di andare direttamente e solo sulla partizione su cui stanno i record per cui i valori del campo data ricadono in quell’anno. Ecco, prima di andare avanti, se interrogo la tabella senza dare un filtro sulla data ma ad esempio uno solo sull’area geografica di origine dell’ordine allora il fatto di aver partizionato la tabella non migliorerà le prestazioni, anzi potrebbe peggiorarle, avendo da esaminare più segmenti. Il concetto di base che per quella che è la mia esperienza è che se la tabella che si vuole partizionare poi non viene interrogata prevalentemente con filtri sul o sui campi per cui è partizionata allora il partitioning non da nessun vantaggio in termini di prestazioni, anzi potrebbe peggiorare la situazione. Questo considerazione sembra quasi banale ma non lo è, in base alla mia esperienza in pochi ne tengono conto quanto sentono parlare per la priva volta del partitioning.

In realtà poi vi sono meccanismi complessi come le elaborazioni parallele che possono comunque beneficiare del partitioning anche in casi avversi, non mi addentro in questi casi perché è un argomento su cui sono ancora impreparato. Mi limito a ricordare che a monte di tutto occorre anche tenere in considerazione il concetto di database utilizzato per OLTP o Datawarehouse. Si tratta di due utilizzi che si pongono a due estremi contrapposti di tipologia di utilizzo di un database. Da un lato abbiamo molti utenti che fanno piccole transazioni e tante interrogazioni “puntuali”, dall’altro abbiamo pochi utenti che fanno interrogazioni massive per avere dati aggregati su cui fare analisi globali. In un OLTP non va molto bene che un singolo utente sfrutti elaborazioni parallele per prendersi tutte le risorse del sistema per soddisfare la propria richiesta, lasciando gli altri utenti ad aspettare.

Se l’applicazione è molto vicina a un OLTP e quindi le interrogazioni sono puntali è ben difficile che il partitioning possa dare grandi vantaggi in termini di prestazioni e quindi tempi di risposta dell’applicazione: le ricerche sono per lo più indicizzate quindi se le cose vengono fatte come si deve al più non abbiamo peggioramenti. Il partizionamento può essere applicato anche agli indici, essi possono essere partizionati con lo stesso criterio della tabella (LOCAL) o con un criterio diverso (GLOBAL). Oppure possono non essere partizionati per nulla.

Secondo me Thomas Kyte nel suo libro spiega e riassume molto bene le principali caratteristiche del partitioning e i casi in cui queste caratteristiche possono essere sfruttate positivamente. Non ha senso che mi metta a copiare qui quello che ha gia scritto lui. Faccio un ultimo accenno al partizionamento basato sull’hash, un caso particolare che può migliorare le prestazioni in un caso particolare, quello di numerosi inserimenti concorrenti. Facendo l’hash sulla chiave primaria e avendo partizionato la tabella in “n” partizioni, ogni inserimento consecutivo potrebbe andare su partizioni diverse, quindi segmenti diversi, questo ridurrebbe problemi di concorrenza nell’accesso alle strutture che gestisticono lo spazio del segmento stesso.

Facilitazione nella gestione

Gestire tabelle tabelle molto grandi può essere in certe condizioni piuttosto complicato. Va comunque stabilito anche cosa si intende per tabelle molto grandi. Non è un caso che il manuale Oracle dedicato al partitioning sia “Oracle database VLDB and partitioning guide“. Nell’introduzione di questo manuale si parla di database da diverse centinaia di GB o anche TB. Nella stessa introduzione poi si accenna a come in database OLTP il partitioning possa dare vantaggi in termini di gestione e “disponibilità” mentre in ambienti Datawarehouse possa dare miglioramenti in termini di prestazioni e gestione. Se abbiamo una tabella che pesa svariati GB e dobbiamo spostarla, abbiamo poco tempo a disposizione per lavorare “offline” e limitate risorse la cosa può essere problematica. Se la stessa tabella è divisa in parti più piccole possiamo lavorare sulle singole parti separatamente mantenendo le altre disponibili. Questo significa che ad esempio possiamo spostare di tablespace una singola partizione senza inibire l’accesso alla singola tabella. Se poi si usano indici partizionati localmente anche la gestione di questi sarà altrettanto più facile e comporta minor disservizio.

Migliore disponibilità

Come già scritto al capitolo “Facilitazione della Gestione” se ho necessità di spostare una tabella da una tablespace all’altra con una tabella partizionata posso spostare una partizione alla volta in modo più agevole mantenendo le altre disponibili e interrogabili senza succhiare tutte le risorse della macchina. si parla di indipendenza fra le partizioni. Per citare poi un esempio di Kyte anche il rebuild di una tabella per sistemare un po’ di “migrated rows” può essere più agevole e richiede meno spazio libero disponibile.

In questo ambito, sconfinando in quello che per me è il fantamondo dei database si può anche pensare al “restore & recovery” di singole partizioni da un backup. Operazione senz’altro relativamente facile e fattibile se le partizioni stanno su tablespace separate.

Vale la pena di ricordare che partizioni diverse di una stessa tabella possono avere strategie di compressione diverse. Anche qui aggiungo una piccola nota legata al mio piccolo mondo e quindi alla mia personale e ridotta esperienza. Nei libri e nella documentazione si spiega come si può risparmiare sullo storage dedicando disco più lenti e meno costosi a partizioni con dati più vecchi e meno usati. Si può poi usare strategie di compressione migliori riducendo ancora l’uso di risorsa spazio disco. Ora, io lavoro in un ambito relativamente piccolo ma abbiamo anche un cliente molto grande. La parte su cui lavoriamo noi è molto piccola ma già li vedo che nonostante non sembrano esserci problemi di licenze (cosa non secondaria) non ci pone neppure il problema sull’uso di storage il che mi fa da pensare abbia un costo che non preoccupa… o forse banalmente non è stata fatta una analisi dedicata.

Oracle Partitioning – Introduzione

La funzionalità Oracle Partitioning esiste da molto tempo ma fino ad oggi non ho mai avuto occasione di usarla e lavorarci e quindi di studiarne in modo approfondito i dettagli. Avendo avuto modo recentemente di fare dei test per valutarne l’utilizzo ho deciso di raccogliere un po’ di appunti e organizzarli pubblicandoli qui. L’argomento è molto più complesso e ampio di quanto si possa immaginare ad una prima occhiata alla documentazione introduttiva per cui spero e confido di riuscire a riassumere ed organizzare l’argomento in una breve serie di post di qui questo rappresenta l’introduzione.

Le fonti di studio che ho utilizzato sono primariamente i manuali Oracle (docs.oracle.com) poi vi sono libri interessanti, primo fra tutti “Expert Oracle Database Architecture” di Thomas Kyte.

La funzionalità “Partitioning” è stata introdotta da Oracle per la prima volta con la versione 8.0 del suo RDBMS. Il principio alla base di questa funzionalità è molto semplice: dividere oggetti (tabelle e indici) molto grossi in oggetti più piccoli e facilmente gestibili. L’obiettivo è da un lato semplificare la gestione dall’altro migliorare le prestazioni delle operazioni su tabelle e/o indici che possono essere molto grandi. Tutto questo viene fatto in modo trasparente per l’utente e/o l’applicazione, nel senso che ci accede ai dati non deve fare assolutamente nulla in conseguenza dell’uso di questa caratteristica perché la tabella e gli indici interessati vengono interrogati nello stesso identico modo nel caso siano partizionati o no.

Il “partitioning” (che possiamo benissimo tradurre con partizionamento) consiste nella suddivisione di una tabella ed eventualmente dei suoi indici in “pezzi” (partizioni) più piccole che possono essere gestite in modo separate perché corrispondo a oggetti fisici diversi.

In un database Oracle a una tabella di tipo heap standard corrisponde un oggetto “fisico” chiamato segmento che rappresenta la struttura di memoria in cui vengono salvati i dati della tabella. Quando una tabella viene “partizionata” essa viene logicamente suddivisa in più parti chiamate partizioni, a questo punto non c’è più un segmento associato alla tabella ma un segmento per ogni partizione.

I vantaggi comunemente attribuiti all’uso del partitioning sono solitamente:

  • Migliori prestazioni, sia delle query che delle operazioni “DML”

  • Facilitazione della gestione

  • Migliore “disponibilità”

Cercherò di analizzare nel dettaglio ciascuno di questi vantaggi spiegandone l’origine, i limiti e il valore. Siccome però ciascuno di questi punti richiede una discussione piuttosto lunga vi dedicherò ciascuno un post separato.

Istogrammi su tipo dato *CHAR

Premessa importante: il caso a cui mi riferisco è su Oracle 11.2.0.4, pur essendo una versione ormai piuttosto obsoleta è una versione su cui abbiamo diverse installazioni.

Seconda premessa meno importante: da un po’ di tempo non riesco a riportare nei post il codice formattato in maniera decente, oggi ho provato a cambiare tema, visto che il precedente non è più supportato da questa piattaforma. Il codice continua a essere formattato in modo mediocre, credo che sia un limite del piano gratuito.

Finite le premesse andiamo al dunque. Per la prima volta mi è capitato un caso in cui un istogramma, nelle statistiche oracle dei dati di una tabella, faceva compiere all’ottimizzatore Oracle una scelta non buona. In prima istanza ho attribuito la causa del non utilizzo di un indice su un campo i cui valori sono praticamente univoci al fatto che il tipo dato del campo fosse NVARCHAR2. In realtà quello è una concausa ma non è il motivo principale. Di fatto, osservando l’esito dell’interrogazione della vista USER_TAB_HISTOGRAMS, ho notato una cosa particolare grazie al campo ENDPOINT_ACTUAL_VALUE. Cioè mi sono reso conto, facendo poi una ulteriore verifica sui dati della tabella, che c’erano una serie di valori per il campo di tipo NVARCHAR2 uguali per la prima parte della stringa che poi compariva come valore del campo ENDPOIN_ACTUAL_VALUE di un singolo bucket dell’istogramma.  In realtà a quel bucket corrispondeva un terzo dei record della tabella. Questo in pratica implicava che per le interrogazioni per quell’intervallo di valori, anche se su un valore specifico, che filtrava un unico record, l’ottimizzatore comunque sceglieva di non utilizzare l’indice sul campo. Prima di riportare i riferimenti ad articoli che spiegano bene e nel dettaglio cosa succede voglio riportare un caso di test che sono riuscito a preparare:

CRIS@svil112 > create table test_nvarchar2(id number, bnv nvarchar2(150));

Table created.

CRIS@svil112 > begin
2 for i in 10001..20000 loop
3 insert into test_nvarchar2 (id,bnv) values (i,'TXTOOOOOOOOOOOO'||to_char(i));
4 insert into test_nvarchar2 (id,bnv) values (i,to_char(i));
5 end loop;
6 end;
7 /

PL/SQL procedure successfully completed.

CRIS@svil112 > @GATHER_TABLE_STATS TEST_NVARCHAR2

PL/SQL procedure successfully completed.

Avendo ora un tabella di prova facciamo un paio di interrogazioni:

CRIS@svil112 > select * from test_nvarchar2 where bnv='TXTOOOOOOOOOOOO11234';

ID BNV
---------- ----------------------------------------
11234 TXTOOOOOOOOOOOO11234

CRIS@svil112 > @PLAN

PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------------------------------------------
SQL_ID 7j6w8gg693f0k, child number 0
-------------------------------------
select * from test_nvarchar2 where bnv='TXTOOOOOOOOOOOO11234'

Plan hash value: 1043612967

------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 170 (100)| |
|* 1 | TABLE ACCESS FULL| TEST_NVARCHAR2 | 9867 | 298K| 170 (0)| 00:00:03 |
------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

1 - filter("BNV"=U'TXTOOOOOOOOOOOO11234')


18 rows selected.

CRIS@svil112 > select * from test_nvarchar2 where bnv='11234';

ID BNV
---------- ----------------------------------------
11234 11234

SCRIS@svil112 > @PLAN

PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------------------------------------------
SQL_ID 38faau52d83fb, child number 0
-------------------------------------
select * from test_nvarchar2 where bnv='11234'

Plan hash value: 1818196464

----------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 4 (100)| |
| 1 | TABLE ACCESS BY INDEX ROWID| TEST_NVARCHAR2 | 1 | 31 | 4 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | TEST_NVARCHAR2_IDX_1 | 1 | | 3 (0)| 00:00:01 |
----------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

2 - access("BNV"=U'11234')

Come si può vedere per due interrogazioni “puntuali” usa due piani di esecuzione diversi, uno con l’utilizzo dell’indice e l’altro no, anche se entrambe le query estraggono un record solo, perchè di fatto i valori del campo BNV sono univoci, anche se non certificati da un indice univoco. L’indizio sulla causa dell’anomalo comportamento lo abbiamo gia sui dati forniti con il piano e sta nelle stime: nel primo caso l’ottimizzatore si aspetta di trovare 9867 record, nel secondo uno solo. Andiamo quindi a vedere nel dettaglio le statistiche su cui si basa l’ottimizzatore, in particolare l’istogramma sui dati della colonna BNV:

CRIS@svil112  > select histogram,num_distinct,num_buckets from user_Tab_col_statistics where column_name='BNV';

HISTOGRAM       NUM_DISTINCT NUM_BUCKETS
--------------- ------------ -----------
HEIGHT BALANCED        19954          75

CRIS@svil112  > sELECT ENDPOINT_NUMBER,ENDPOINT_VALUE,ENDPOINT_ACTUAL_VALUE FROM USER_TAB_HISTOGRAMS WHERE COLUMN_NAME='BNV';

ENDPOINT_NUMBER ENDPOINT_VALUE ENDPOINT_ACTUAL_VALUE
--------------- --------------------------------------------- --------------------------------
0 993852926086080000000000000000000 1 0 0 0 1
1 993852926095525000000000000000000 1 0 2 6 7
2 993852926109692000000000000000000 1 0 5 3 4
3 993852926123859000000000000000000 1 0 8 0 1
4 993853235571091000000000000000000 1 1 0 6 8
5 993853235585258000000000000000000 1 1 3 3 5
6 993853235599424000000000000000000 1 1 6 0 2
7 993853235608869000000000000000000 1 1 8 6 9
8 993853545060823000000000000000000 1 2 1 3 6
9 993853545074989000000000000000000 1 2 4 0 3
10 993853545084435000000000000000000 1 2 6 7 0
11 993853545098602000000000000000000 1 2 9 3 7
12 993853854550555000000000000000000 1 3 2 0 4
13 993853854560000000000000000000000 1 3 4 7 1
14 993853854574167000000000000000000 1 3 7 3 8
15 993854164026120000000000000000000 1 4 0 0 5
16 993854164035565000000000000000000 1 4 2 7 2
17 993854164049732000000000000000000 1 4 5 3 9
18 993854164063899000000000000000000 1 4 8 0 6
19 993854473511130000000000000000000 1 5 0 7 3
20 993854473525297000000000000000000 1 5 3 4 0
21 993854473539464000000000000000000 1 5 6 0 7
22 993854473548909000000000000000000 1 5 8 7 4
23 993854783000862000000000000000000 1 6 1 4 1
24 993854783015029000000000000000000 1 6 4 0 8
25 993854783024474000000000000000000 1 6 6 7 5
26 993854783038641000000000000000000 1 6 9 4 2
27 993855092490594000000000000000000 1 7 2 0 9
28 993855092500039000000000000000000 1 7 4 7 6
29 993855092514206000000000000000000 1 7 7 4 3
30 993855401966159000000000000000000 1 8 0 1 0
31 993855401975604000000000000000000 1 8 2 7 7
32 993855401989771000000000000000000 1 8 5 4 4
33 993855402003938000000000000000000 1 8 8 1 1
34 993855711451169000000000000000000 1 9 0 7 8
35 993855711465336000000000000000000 1 9 3 4 5
36 993855711479503000000000000000000 1 9 6 1 2
37 993855711488948000000000000000000 1 9 8 7 9
74 1703749641784290000000000000000000 T X T O O O O O O O O O O O O 1
75 1703749641784290000000000000000000 T X T O O O O O O O O O O O O 2

40 rows selected.

Per una spiegazione dettagliata sugli istogrammi posso rimandare ai libri e al blog di Jonathan Lewis, mi limito a dire che di fatto quello che succede, come ben spiegato da Thomas Kyte in questo post, è che per costruire l’istogramma vengono presi solo i primi 32 byte della stringa  per cui vengono presi i valori ‘TXTOOOOOOOOOOOO1’ e ‘TXTOOOOOOOOOOOO2’. Al valore vengono assegnati 37 bucket, che sono quasi metà dei totali, infatti i record per cui la prima parte della stringa contenuta nel campo BNV è ‘TXTOOOOOOOOOOOO1’  sono effettivamente 9999, quasi metà. Il problema è che per lui così quei valori sono tutti uguali. Da notare come 20000/75*37=9866.667 che arrotondato fa proprio 9867, il numero di righe stimato nel primo piano di esecuzione.

In un caso come questo, dove i valori sono distribuiti in modo uniforme, estremamente uniforme, essendo di fatto univoci, l’istogramma in realtà è di scarsa utililità. A questo punto ho trovato comodo questo post del blog del gruppo dell’ottimizzatore del database di Oracle in quanto spiega come usare la procedura DBMS_STAT.GATHER_TABLE_STATS e in particolar modo il parametro METHOD_OPT che regola la generazione degli istogrammi.

CRIS@svil112 > select * from test_nvarchar2 where bnv='TXTOOOOOOOOOOOO11234';

ID BNV
---------- ----------------------------------------
11234 TXTOOOOOOOOOOOO11234

CRIS@svil112 > @plan

PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID 7j6w8gg693f0k, child number 0
-------------------------------------
select * from test_nvarchar2 where bnv='TXTOOOOOOOOOOOO11234'

Plan hash value: 1818196464

----------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 4 (100)| |
| 1 | TABLE ACCESS BY INDEX ROWID| TEST_NVARCHAR2 | 1 | 31 | 4 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | TEST_NVARCHAR2_IDX_1 | 1 | | 3 (0)| 00:00:01 |
----------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

2 - access("BNV"=U'TXTOOOOOOOOOOOO11234')

CRIS@svil112> select histogram,num_distinct,num_buckets from user_Tab_col_statistics where column_name='BNV';

HISTOGRAM       NUM_DISTINCT NUM_BUCKETS
--------------- ------------ -----------
NONE                   19954           1

Come si vede, eliminando l’istogramma l’ottimizzatore si baserà solo sul numero di valori distinti e si comporta in modo corretto. In questo post viene poi spiegato come eventualmente salvare questa impostazione di non ricalcolo dell’istogramma.

Va notato come a peggiorare la situazione sia stato in effetti il fatto che il tipo campo è NVARCHAR2, il che significa che nel mio caso usa il charset AL16UTF16 che codifica con 2 byte ogni carattere:

 

CRIS@svil112 > select length(bnv),lengthb(bnv) from test_nvarchar2 where bnv='TXTOOOOOOOOOOOO11234';

LENGTH(BNV) LENGTHB(BNV)
----------- ------------
20           40

e come ha spiegato bene Kyte nel calcolo dell’istogramma vengono presi i primi 32 byte. Jonathan Lewis da più dettagli in una serie di post, anche se si riferisce ai “Frequency Histograms”, per cui dovrò indagare ulteriormente. Pare che con la versione 12c i 32 byte siano diventati 64 e poi ci sono nuove tipologie di istogrammi volte forse a risolvere anche questo problemi. Anche su questo devo approfondire.

Nel caso che riporto la soluzione è relativamente facile in quanto i dati sono distribuiti uniformemente, non fosse così la soluzione adottata non andrebbe bene. Fra le cose da approfondire e capire mi rimarrebbe anche il criterio con cui viene valorizzato il campo ENDPOINT_ACTUAL_VALUE della USER_TAB_HISTOGRAMS, Lewis ne da una spiegazione ed io ho fatto qualche test ma comunque non sono riuscito a capirlo bene.

LOB e temporary tablespace in Oracle

I LOB (Large OBject) sono un tipo dato introdotto nei database relazionali per estenderne l’utilizzo per gestire tipi dato non strutturato quali grandi file binari e grandi file di testo. La caratteristica principale del tipo dato LOB, come indica il nome, è quella di poter immagazzinare oggetti molto grossi, quindi si può trattare di file pdf, fotografie digitali, file eseguibili o qualunque altro tipo di file binario di piccole o grosse dimensioni; possono essere anche file di testo come file html e xml. I file binari e i file di testo vengono gestiti in modo leggermente differente, per cui ci sono due tipi di LOB: BLOB (Binary LOB) per i dati binari e CLOB (Character LOB) per i dati formato testo. Non posso e non voglio fare qui una descrizione dettagliata dei LOB nei database relazionali, dico solo che sono una estensione che permette di consolidare la gestione anche di file all’interno di un database relazionale.

In Oracle il tipo dato LOB ha un limite sulla dimensione massima piuttosto alto, come spiegato qui il limite è i (2^32-1)*<CHUNK SIZE> byte e in condizioni di default la chunk size è pari alla dimensione del blocco. Con la classica dimensione del blocco da 8 KB fa un limite di quasi 32 TB.

I LOB, vista la possibilità che siano di grosse dimensioni, devono essere gestiti attraverso delle funzioni specifiche. Esiste una interfaccia PL/SQL ed essendo un tipo dato “standard” esiste una implementazione delle funzioni di gestione anche nei driver per Java JDBC. Siccome dove lavoro si usa quest’ultima interfaccia su questa mi sono dedicato un po’ e la uso per descrivere l’argomento oggetto di questo post.

Tutto è nato dal fatto che su una installazione del cliente abbiamo verificato che le sessioni aperte sul database dall’applicazione mantengono una quota di spazio nella temporary tablespace dell’utente Oracle allocata. Dopo aver fatto un classica ricerca, prima genericamente su internet e poi sul supporto Oracle ho trovato qualche indizio ed ho cominciato a fare qualche test con lo scopo di circoscrivere le possibili cause e individuare delle soluzioni.

l riferimento per utilizzare LOB su un database Oracle in programmi java è il manuale “Oracle JDBC Developer’s Guide. Ho cercato di leggerlo ma piuttosto frettolosamente quindi non ho ancora ben chiari tutti i meccanismi. La parte che mi interessa sembra essere quella del paragrafo 14.5 dove si parla di “Temporary LOB” i quali sono fatti per dati di passaggio (transient data). Quando in java si utilizza un metodo che implementa l’interfaccia java.sql.Connection.createBlob viene creato un temporary lob. Il temporary blob è di fatto un variabile di tipo LOB che utilizza spazio allocato sulla temporary tablespace configurata per l’utente. Ho verificato che se da un programma java “prelevo” un campo blob con questo tipo di codice:

Blob blob_ris;
rs.next();
blob_ris = rs.getBlob(“contenuto”);

non viene creato un temporary lob, in questo caso si ottiene un puntatore direttamente al LOB interessato. Non viene allocato spazio sulla TEMP (interrogando la v$tempseg_usage non esce nulla). Utilizzando la chiamata ((BLOB)blob_aris).isTemporary() se ne ha la conferma. Viceversa, se si deve creare un dato di tipo LOB da zero da un programma Java è possibile utilizzare la chiamata del codice simile a questo:

Blob myblob =conn.createBlob();

Come scritto sopra si tratta di una chiamata di un metodo che implementa l’interfaccia java.sql.Connection.createBlob, che crea un temporary lob che utilizza spazio allocato sulla temporary tablespace.

Uno dei problemi che può capitare con i temporary lob è che lo spazio allocato nella temp non venga rilasciato. La nota del supporto Oracle 802897.1 spiega come in effetti se all’interno della sessione si effettua la chiamata al metodo “free” (o freeTemporary) lo spazio viene rilasciato all’interno della sessione e può essere riutilizzato all’interno della stessa sessione ma non è disponibile per le altre sessioni. Per liberarlo definitivamente occorre chiudere la sessione. La stessa nota dice che dalla versione 10.2.0.4 è stato introdotto l’evento 60025 che permette di liberare completamente lo spazio senza dover chiudere la sessione. Si deve quindi eseguire il comando:

alter session set events ‘60025 trace name context forever’;

L’evento si dovrebbe poter attivare a livello di sistema, però questo non l’ho provato.

La nota 1384829.1 riporta un programmino java che dimostra come funziona l’evento 60025. La premessa nella nota è che il test è stato fatto con driver jdbc 11.2.0.3 su datatabase 11.2.0.3. Io ho cominciato a giocare con il programma e fare delle prove con il driver che utilizziamo al momento maggiormente, versione 12.1 con una patch. Con questo driver il programma non funziona come descritto, cioè lo spazio nella temporary tablespace non viene liberato se non con la chiusura della connessione. Ho provato con varie versioni di driver 12.1, con java 6 e java 7 ma il risultato è sempre lo stesso, lo spazio non viene rilasciato. Poi mi sono deciso a provare con il driver 18.3; dalla 12.2 in poi il driver jdbc viene fornito solo per jdk 8, se si vuole usare versioni jdk precedenti Oracle suggerisce di usare driver precedenti. Con la versione 18.3 l’evento 60025 torna a funzionare, ovvero la chiamata lob.free() libera lo spazio definitivamente sulla temporary tablespace. Presumo quindi che ci sia qualche problema con i driver 12.1 e l’evento 60025; anche con un driver 11.2.0.3 il programma di test funziona. Ho provato a fare una ricerca più mirata sul supporto Oracle e la cosa più pertinente che ho trovato è stata la nota 2297060.1 che fa riferimento a questo problema e indica fra le versioni interessate dall 9.2 alla 12.2. Quello che viene scritto nelle conclusioni e nella sezione “solution” non mi è affatto chiara. Ho provato allora a fare delle varianti del programmino java per capirne di più. Il risultato è che a un certo punto non capivo proprio nulla perché sembrava che anche con i driver 12.1 funzionasse la liberazione dello spazio TEMP.  La verità è che effettivamente con l’evento 60025 attivo la chiamata del metodo lob.free() libera definitivamente lo spazio occupato dal temporary LOB. Il motivo per cui sembra che l’esempio non funzioni con i driver 12.1 è la riga di codice:

System.out.println(rs.getString(1));

che si trova prima dell’instanziazione della variabile/oggetto lob. Commentando via quella riga di codice l’esempio funziona anche con i driver 12.1.

Riassumento quello che ho capito. Se eseguo una query come questa:

select to_clob(‘bla’) from dual; 

con il codice:

ResultSet rs = stmt.executeQuery(SQL1_syntax);

viene gia creato un temporary lob, viene allocato dello spazio nella temporary tablespace. Anche chiudendo il ResultSet lo spazio non viene liberato; l’unico modo per liberarlo è instanziare l’oggetto, ad esempio con una semplice chiamata come questa:

my_clob = rs.getClob(1);

e poi chiamare il metodo free dell’oggetto:

my_clob.free();

Se l’evento 60025 è stato attivato lo spazio nella temporary tablespace viene liberato.

La chiamata rs.getString(1) probabilmente crea un riferimento che impedisce per motivi che ignoro che lo spazio nella temp venga liberato.

Riferimenti:

  1. How to Release the Temp LOB Space and Avoid Hitting ORA-1652 (Doc ID 802897.1)
  2. How to Release Temporary LOB Segments without Closing the JDBC Connection (Doc ID 1384829.1)