Oracle Data Redaction

Uno degli argomenti che ho studiato per conseguire la certificazione a Oracle 12 è stato questo modulo che va precisato che, a parte per le soluzioni cloud di Oracle, richiede la licenza per l’opzione “Oracle Advanced Security” che include un sacco di funzionalità legate alla gestione della “sicurezza”.

Al solito, quanto scrivo è tratto dalla documentazione ufficiale di Oracle e da miei test.

Oracle Data Redaction è un modulo che permette di mascherare (redact) dati restituiti dalle query lanciate dalle applicazioni. Il mascheramente avviene al momento dell’esecuzione delle query (runtime) senza influire sui vincoli di integrità o sulla modalità di memorizzazione dei dati, quindi è un sistema che può essere applicato a sistemi già in produzione. Per le sue caratteristiche è una componente complementare ad altri soluzioni facendi parte del pacchetto “Oracle Database Solutions”.

Oracle Data Redaction non fa cose poi tanto complesse, maschera i dati al volo sulla base di “policy” che stabiliscono cosa mascherare, come e quando. Il cosa tipicamente è la colonna di una tabella o vista. Il come ricade nelle sei seguenti possibilità:

  1. Full redaction (default, DBMS_REDACT.FULL)
  2. Partial redaction (DBMS_REDACT.PARTIAL)
  3. Regular Expressions (DBMS_REDACT.REGEXP)
  4. Random (DBMS_REDACT.RANDOM)
  5. No redaction (DBMS_REDACT.NONE)
  6. Nullify (DBMS_REDACT.NULLIFY) (da 12.2)

La creazione, la configurazione e la gestione delle policy avviene tramite il pacchetto PL/SQL DBMS_REDACT. Il pacchetto contiene varie costanti e le procedure per creare, modificare, abilitare, disabilitare o rimuore le policy.

Le policy mascherano i dati quanto una espressione fornita risulta vera. L’espressione può essere un banale “1=1” che indica sempre vero oppure qualcosa che si basa su varibili di contesto (context).

Non entro nel dettaglio mi limito a riportare un esempio di utilizzo con espressioni basate su “application context”).

L’esempio si basa su questo schema: c’è un utente “amministratore” che gestisce gli “application context” e le policy di data redaction, lo chiamo sysadmin_ctx come sul manuale; c’è un utente/schema che possiede una tabella con dati “sensibili”, lo chiamo “cristian” e infine c’è un utente che deve poter accedere ai dati della tabella con l’esclusione della colonna con la parte di dato “sensibile”, questo utente si chiama “lettore”. L’utente cristian ha una tabella che ho chiamato test1 con questo tracciato:

Name Type
----------- -------------------
ID NUMBER(10)
NOME VARCHAR2(40 CHAR)
COGNOME VARCHAR2(40 CHAR)
USERNAME VARCHAR2(40 CHAR)
PASSWORD VARCHAR2(171 CHAR)
INIZIO DATE
FINE DATE

 

Un primo esempio banale di policy è basato sul “ROLE” assegnato all’utente:

EXEC DBMS_REDACT.DROP_POLICY(OBJECT_SCHEMA=>'CRISTIAN',OBJECT_NAME=>'TEST1', policy_name => 'cristian_pol_3');
BEGIN
DBMS_REDACT.ADD_POLICY(
object_schema => 'CRISTIAN',
object_name => 'TEST1',
column_name => 'PASSWORD',
policy_name => 'cristian_pol_3',
function_type => DBMS_REDACT.FULL,
function_parameters => NULL,
policy_description => 'nasconde password',
column_description => 'users passwords',
expression => 'sys_context(''SYS_SESSION_ROLES'',''LETTORE_ROLE'')=''TRUE''');
END;
/

Il parametro “espression” specifica l’espressione che stabilisce quando la policy deve essere applicata e i dati mascherati. Il parametro function_type=> DBMS_REDACT.FULL specifica che il mascheramento deve essere completo, in questo caso per default le stringhe vengono sostituite da uno spazio vuoto.

LETTORE@svil183p1 > select sys_context('SYS_SESSION_ROLES','LETTORE_ROLE') FROM DUAL;

SYS_CONTEXT('SYS_SESSION_ROLES','LETTORE_ROLE')
------------------------------------------------------------------------------------------------------------------------
TRUE

LETTORE@svil183p1 > select id,password from cristian.test1;

ID PASSWORD
---------- -------------------------
44
1
102
21
61
81
82
101
103
100
141
142
143
320
361
242
380
480
500
520
540
640
641
120
163
451
200
561
580
600
140
280
220
319
340
400
560
620

38 rows selected.

Se si vuole applicare modalità più sofisticate si possono definire appositi “context”:

CREATE OR REPLACE PACKAGE set_lettore_ctx_pkg IS 
PROCEDURE set_lettore_id; 
END; 
/
CREATE OR REPLACE PACKAGE BODY set_lettore_ctx_pkg IS
PROCEDURE set_lettore_id 
IS 
BEGIN 
DBMS_SESSION.SET_CONTEXT('cri_ctx_test2', 'lettore_id', 1); 
END;
END;
/
CREATE CONTEXT cri_ctx_test2 USING set_lettore_ctx_pkg;

CREATE TRIGGER set_lettore_ctx_trig AFTER LOGON ON DATABASE
BEGIN
if USER='LETTORE' THEN 
sysadmin_ctx.set_lettore_ctx_pkg.set_lettore_id;
end if;
END;
/

E quindi ecco un altro esempio di policy:

EXEC DBMS_REDACT.DROP_POLICY(OBJECT_SCHEMA=>'CRISTIAN',OBJECT_NAME=>'TEST1', policy_name => 'cristian_pol_3');
BEGIN
DBMS_REDACT.ADD_POLICY(
object_schema => 'CRISTIAN',
object_name => 'TEST1',
column_name => 'PASSWORD',
policy_name => 'cristian_pol_3',
function_type => DBMS_REDACT.REGEXP,
regexp_pattern => '.',
regexp_replace_string =>DBMS_REDACT.RE_REDACT_WITH_SINGLE_X,
regexp_position => DBMS_REDACT.RE_BEGINNING,
regexp_occurrence => DBMS_REDACT.RE_ALL,
regexp_match_parameter => DBMS_REDACT.RE_CASE_INSENSITIVE,
policy_description => 'nasconde password',
column_description => 'users passwords',
expression => 'SYS_CONTEXT(''cri_ctx_test2'',''lettore_id'') = 1');
END;
/

 

In questo caso oltre all’espressione ho modificato la modalità di mascheramento usando un caso banale di espressione regolare, qui chi ha dimestichezza con le espressioni regolari si può sbizzarrire, questo per mascherare pezzi di numeri di carte di credito, indirizzi email ecc.

LETTORE@svil183p1 > select id,password from cristian.test1;

ID PASSWORD
---------- -------------------------
44 XXXXXXX
1 XXXXXXX
102 XXXXXXX
21
61 XXXXXXX
81 XXXXXXX
82 XXXXXXX
101 XXXXXXX
103
100 XXXXXXX
141 XXXXXXX
142 XXXXXXX
143 XXXXXXX
320 XXXXXXX
361 XXXXXXX
242
380
480 XXXXXXX
500 XXXXXXX
520 XXXXXX
540 X
640 XXXXXXX
641 XXXXXXX
120
163 XXXX
451 XXXXXXX
200 XXXXXXX
561 XXXXXXX
580 XXXXXXX
600
140 XXXXXXX
280 XXXXXXX
220 XXXXXXX
319 XXXXXXX
340
400
560 XXXXXXX
620 XXX

 

 

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.

 

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.

Ricerche case sensitive in Oracle 11gR2 e 12cR1

Questo post è un aggiornamento del post “Database case sensitive su 10.2.0.3“,  aggiornamento necessario, essendo il post risalente ad oltre sette anni fa. In realtà ho fatto qualche test in questi sette anni ma non ben formalizzato, quando poi mi è stato chiesto per l’ennesima volta se e come era possibile fare ricerche case insensitive su database Oracle (versione 11.2) ho prima dato la solita risposta, poi però mi è venuto lo scrupolo di ricontrollare se fosse cambiato qualcosa sulla versione che attualmente usiamo maggiormente (11.2.0.3 o 11.2.0.4) e sulla nuova 12.1 che installai qualche mese fa per i primi test sulle novità della versione. Riporto sotto il test completo che ho eseguito rispettivamente su un Oracle 10.2.0.5, un 11.2.0.3 e un 12.1.0.1, prima però anticipo alcune conclusioni: con la 11.2.0.3 le cose sono migliorate rispetto alla 10.2, mentre fra la 11.2 e la 12.1 non noto differenze rilevanti, neanche confrontanto velocemente lo stesso capitolo della documentazione nelle tre versioni:

SQL*Plus: Release 12.1.0.1.0 Production on Fri Aug 22 15:57:31 2014

Copyright (c) 1982, 2013, Oracle.  All rights reserved.

Connected to:
Oracle Database 10g Enterprise Edition Release 10.2.0.5.0 - 64bit Production
With the Partitioning, OLAP, Data Mining and Real Application Testing options

PL/SQL procedure successfully completed.

Session altered.

CRISTIAN@geotutf8 > create table test_ci_search (id number,string1 varchar2(30),string2 varchar2(30) ,numero number,data date, datastamp timestamp);

Table created.

CRISTIAN@geotutf8 > create sequence stest_ci_search;

Sequence created.

CRISTIAN@geotutf8 > insert into test_ci_search
2  select stest_ci_search.nextval, dbms_random.string('A',30), dbms_random.string('P',30), dbms_random.random,sysdate, systimestamp
3  from dual connect by level <= 100000;

100000 rows created.

CRISTIAN@geotutf8 > insert into test_ci_search select stest_ci_search.nextval, upper(string1), upper(String2), numero, data,datastamp from test_ci_search;

100000 rows created.

CRISTIAN@geotutf8 > insert into test_ci_search select stest_ci_search.nextval, lower(string1), lower(String2), numero, data,datastamp from test_ci_search where id<=100000
;

100000 rows created.

CRISTIAN@geotutf8 > insert into test_ci_search values (stest_ci_search.nextval,'Cristian','Cudizio',1,sysdate,systimestamp);

1 row created.

CRISTIAN@geotutf8 > insert into test_ci_search values (stest_ci_search.nextval,'CriStian','Cudizio',1,sysdate,systimestamp);

1 row created.

CRISTIAN@geotutf8 > insert into test_ci_search values (stest_ci_search.nextval,'CRISTIAN','Cudizio',1,sysdate,systimestamp);

1 row created.

CRISTIAN@geotutf8 > insert into test_ci_search values (stest_ci_search.nextval,'cristian','Cudizio',1,sysdate,systimestamp);

1 row created.

CRISTIAN@geotutf8 > commit;

Commit complete.

CRISTIAN@geotutf8 > select * from test_ci_search where string1 ='cristian';

ID STRING1                        STRING2                            NUMERO DATA
---------- ------------------------------ ------------------------------ ---------- -------------------
DATASTAMP
---------------------------------------------------------------------------
300004 cristian                       Cudizio                                 1 22-08-2014 15:57:54
22-AUG-14 03.57.54.832366 PM

CRISTIAN@geotutf8 > ALTER SESSION SET NLS_COMP=LINGUISTIC;

Session altered.

CRISTIAN@geotutf8 > ALTER SESSION SET NLS_SORT=BINARY_CI;

Session altered.

CRISTIAN@geotutf8 > select * from test_ci_search where string1 ='cristian';

ID STRING1                        STRING2                            NUMERO DATA
---------- ------------------------------ ------------------------------ ---------- -------------------
DATASTAMP
---------------------------------------------------------------------------
300001 Cristian                       Cudizio                                 1 22-08-2014 15:57:54
22-AUG-14 03.57.54.012672 PM

300002 CriStian                       Cudizio                                 1 22-08-2014 15:57:54
22-AUG-14 03.57.54.031840 PM

300003 CRISTIAN                       Cudizio                                 1 22-08-2014 15:57:54
22-AUG-14 03.57.54.051397 PM

300004 cristian                       Cudizio                                 1 22-08-2014 15:57:54
22-AUG-14 03.57.54.832366 PM

CRISTIAN@geotutf8 > select * from test_ci_search where string1 like 'cris%';

ID STRING1                        STRING2                            NUMERO DATA
---------- ------------------------------ ------------------------------ ---------- -------------------
DATASTAMP
---------------------------------------------------------------------------
300001 Cristian                       Cudizio                                 1 22-08-2014 15:57:54
22-AUG-14 03.57.54.012672 PM

300002 CriStian                       Cudizio                                 1 22-08-2014 15:57:54
22-AUG-14 03.57.54.031840 PM

300003 CRISTIAN                       Cudizio                                 1 22-08-2014 15:57:54
22-AUG-14 03.57.54.051397 PM

300004 cristian                       Cudizio                                 1 22-08-2014 15:57:54
22-AUG-14 03.57.54.832366 PM

CRISTIAN@geotutf8 > create index test_ci_search_idx on test_ci_search (nlssort(string1,'nls_sort=binary_ci'));

Index created.

CRISTIAN@geotutf8 >  set autotrace on
CRISTIAN@geotutf8 > select * from test_ci_search where string1 like 'cris%';

ID STRING1                        STRING2                            NUMERO DATA
---------- ------------------------------ ------------------------------ ---------- -------------------
DATASTAMP
---------------------------------------------------------------------------
300001 Cristian                       Cudizio                                 1 22-08-2014 15:57:54
22-AUG-14 03.57.54.012672 PM

300002 CriStian                       Cudizio                                 1 22-08-2014 15:57:54
22-AUG-14 03.57.54.031840 PM

300003 CRISTIAN                       Cudizio                                 1 22-08-2014 15:57:54
22-AUG-14 03.57.54.051397 PM

300004 cristian                       Cudizio                                 1 22-08-2014 15:57:54
22-AUG-14 03.57.54.832366 PM

Execution Plan
----------------------------------------------------------
Plan hash value: 18365057

------------------------------------------------------------------------------------
| Id  | Operation         | Name           | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |                |    46 |  3772 |   913   (1)| 00:00:11 |
|*  1 |  TABLE ACCESS FULL| TEST_CI_SEARCH |    46 |  3772 |   913   (1)| 00:00:11 |
------------------------------------------------------------------------------------

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

1 - filter("STRING1" LIKE 'cris%')

Note
-----
- dynamic sampling used for this statement

&nbsp;


C:\tmp>sqlplus cristian/cristian@//10.110.3.52/salespdb

SQL*Plus: Release 12.1.0.1.0 Production on Fri Aug 22 13:08:07 2014

Copyright (c) 1982, 2013, Oracle.  All rights reserved.

ERROR:
ORA-28002: the password will expire within 7 days

Last Successful login time: Wed Apr 09 2014 15:21:53 +02:00

Connected to:
Oracle Database 12c Release 12.1.0.1.0 - 64bit Production

PL/SQL procedure successfully completed.

Session altered.

CRISTIAN@//10.110.3.52/salespdb > create table test_ci_search (id number,string1 varchar2(30),string2 varchar2(30) ,numero number,data date, datastamp timestamp);

Table created.

CRISTIAN@//10.110.3.52/salespdb > create sequence stest_ci_search;

Sequence created.

CRISTIAN@//10.110.3.52/salespdb > insert into test_ci_search
2  select stest_ci_search.nextval, dbms_random.string('A',30), dbms_random.string('P',30), dbms_random.random,sysdate, systimestamp
3  from dual connect by level <= 100000;

100000 rows created.

CRISTIAN@//10.110.3.52/salespdb > insert into test_ci_search select stest_ci_search.nextval, upper(string1), upper(String2), numero, data,datastamp from test_ci_search;

100000 rows created.

CRISTIAN@//10.110.3.52/salespdb > insert into test_ci_search select stest_ci_search.nextval, lower(string1), lower(String2), numero, data,datastamp from test_ci_search wh
ere id<=100000;

100000 rows created.

CRISTIAN@//10.110.3.52/salespdb >
CRISTIAN@//10.110.3.52/salespdb > insert into test_ci_search values (stest_ci_search.nextval,'Cristian','Cudizio',1,sysdate,systimestamp);

1 row created.

CRISTIAN@//10.110.3.52/salespdb > insert into test_ci_search values (stest_ci_search.nextval,'CriStian','Cudizio',1,sysdate,systimestamp);

1 row created.

CRISTIAN@//10.110.3.52/salespdb > insert into test_ci_search values (stest_ci_search.nextval,'CRISTIAN','Cudizio',1,sysdate,systimestamp);

1 row created.

CRISTIAN@//10.110.3.52/salespdb > insert into test_ci_search values (stest_ci_search.nextval,'cristian','Cudizio',1,sysdate,systimestamp);

1 row created.

CRISTIAN@//10.110.3.52/salespdb >
CRISTIAN@//10.110.3.52/salespdb > commit;

Commit complete.

CRISTIAN@//10.110.3.52/salespdb > select * from test_ci_search where string1 ='cristian';

ID STRING1                        STRING2                            NUMERO DATA
---------- ------------------------------ ------------------------------ ---------- -------------------
DATASTAMP
---------------------------------------------------------------------------
300004 cristian                       Cudizio                                 1 22-08-2014 13:11:06
22-AUG-14 01.11.06.484698 PM

CRISTIAN@//10.110.3.52/salespdb > ALTER SESSION SET NLS_COMP=LINGUISTIC;

Session altered.

CRISTIAN@//10.110.3.52/salespdb > ALTER SESSION SET NLS_SORT=BINARY_CI;

Session altered.

CRISTIAN@//10.110.3.52/salespdb > select * from test_ci_search where string1 ='cristian';

ID STRING1                        STRING2                            NUMERO DATA
---------- ------------------------------ ------------------------------ ---------- -------------------
DATASTAMP
---------------------------------------------------------------------------
300001 Cristian                       Cudizio                                 1 22-08-2014 13:11:06
22-AUG-14 01.11.06.457087 PM

300002 CriStian                       Cudizio                                 1 22-08-2014 13:11:06
22-AUG-14 01.11.06.466520 PM

300003 CRISTIAN                       Cudizio                                 1 22-08-2014 13:11:06
22-AUG-14 01.11.06.475596 PM

300004 cristian                       Cudizio                                 1 22-08-2014 13:11:06
22-AUG-14 01.11.06.484698 PM

CRISTIAN@//10.110.3.52/salespdb > select * from test_ci_search where string1 like 'cris%';

ID STRING1                        STRING2                            NUMERO DATA
---------- ------------------------------ ------------------------------ ---------- -------------------
DATASTAMP
---------------------------------------------------------------------------
300001 Cristian                       Cudizio                                 1 22-08-2014 13:11:06
22-AUG-14 01.11.06.457087 PM

300002 CriStian                       Cudizio                                 1 22-08-2014 13:11:06
22-AUG-14 01.11.06.466520 PM

300003 CRISTIAN                       Cudizio                                 1 22-08-2014 13:11:06
22-AUG-14 01.11.06.475596 PM

300004 cristian                       Cudizio                                 1 22-08-2014 13:11:06
22-AUG-14 01.11.06.484698 PM

CRISTIAN@//10.110.3.52/salespdb > create index test_ci_search_idx on test_ci_search (nlssort(string1,'nls_sort=binary_ci'));

Index created.

CRISTIAN@//10.110.3.52/salespdb > set autotrace on
ERROR:
ORA-28002: the password will expire within 7 days

SP2-0619: Error while connecting
SP2-0611: Error enabling STATISTICS report
CRISTIAN@//10.110.3.52/salespdb > select * from test_ci_search where string1 like 'cris%';

ID STRING1                        STRING2                            NUMERO DATA
---------- ------------------------------ ------------------------------ ---------- -------------------
DATASTAMP
---------------------------------------------------------------------------
300001 Cristian                       Cudizio                                 1 22-08-2014 13:11:06
22-AUG-14 01.11.06.457087 PM

300002 CriStian                       Cudizio                                 1 22-08-2014 13:11:06
22-AUG-14 01.11.06.466520 PM

300003 CRISTIAN                       Cudizio                                 1 22-08-2014 13:11:06
22-AUG-14 01.11.06.475596 PM

300004 cristian                       Cudizio                                 1 22-08-2014 13:11:06
22-AUG-14 01.11.06.484698 PM

Execution Plan
----------------------------------------------------------
Plan hash value: 1977262958

----------------------------------------------------------------------------------------------------------
| Id  | Operation                           | Name               | Rows  | Bytes | Cost (%CPU)| Time  |
----------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                    |                    |   780 |   159K|  1125   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| TEST_CI_SEARCH     |   780 |   159K|  1125   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN                  | TEST_CI_SEARCH_IDX |  1404 |       |    11   (0)| 00:00:01 |
----------------------------------------------------------------------------------------------------------

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

2 - access(NLSSORT("STRING1",'nls_sort=''BINARY_CI''')>=HEXTORAW('6372697300') AND
NLSSORT("STRING1",'nls_sort=''BINARY_CI''')<HEXTORAW('6372697400'))

Note
-----
- dynamic statistics used: dynamic sampling (level=2)

In sintesi, fino a Oracle 10.2.0.5 era possibile abilitare ricerche su stringhe in modalità case insensitive, il problema era però che dall’uso dell’indice linguistico erano escluso l’operatore LIKE, dalla versione 11.2 in poi invece anche con l’operatore LIKE viene usato l’indice. Ho riportato solo i test per 10.2.0.5 e 12.1.0.1, per 11.2.0.3 il risultato è uguale a quello di 12.1.0.1