Bind Variables e V$SQL_BIND_CAPTURE

Sono rientrato lunedì da due settimane di “vacanze” ma ancora non posso dire di essere in forma, chissà perché ma 7/8 ore di sonno al giorno per me non sono sufficenti :(

Ultimamente forse anche a causa della stanchezza, non ho molti spunti per scrivere nuovi post, ieri però ne ho trovato uno grazie ad un thread di discussione su oracleportal. Siccome avevo analizzato la vista V$SQL_BIND_CAPTURE tempo fa (ne avevo anche parlato in un mio post di un anno e mezzo fa) e ieri non mi ricordavo bene come funzionava e che informazioni mostra,  approfitto per un post riassuntivo.

La vista V$SQL_BIND_CAPTURE è stata introdotta con la versione 10g di Oracle e serve a visualizzare i valori delle bind variables utilizzati nell’esecuzione delle query sul database, ma con certe limitazioni, spiegate sulla documentazione. La prima è che vengono “catturare” solo le bind variables presenti nelle clausole WHERE o HAVING degli statement SQL, quindi quelle negli INSERT e negli UPDATE non sono mai catturate e visualizzate da nessuna parte. La seconda limitazione, sempre documentata, ma in modo più nascosto, è che le bind variables per uno stesso cursore vengono catturate al massimo ogni 15 minuti, per non sovraccaricare il sistema; questo è spiegato nella descrizione del campo LAST_CAPTURED.  Più precisamente il popolamento di questa vista è controllato da due parametri “nascosti” e non documentati:

  • _CURSOR_BIND_CAPTURE_INTERVAL che definisce in secondi l’intervallo minimo fra due “campionamenti” delle bind variables per due cursori; il suo valore di default è 900, pari proprio a 15 minuti
  • _CURSOR_BIND_CAPTURE_AREA_SIZE che definisce la dimensione massima dell’area di memoria occupata dalla vista, e quindi il numero massimo di record.

Una discussione su V$SQL_BIND_CAPTURE e la “cugina” V$SQL_BIND_METADATA si può trovare in un post del blog di Miladin Modrakovic che approfitto per segnalare.

Al riguardo scrisse un post anche Jonathan Lewis il quale rivelava di aver confuso il contenuto di quella vista con i valori utilizzati dall’ottimizzatore per la generazione dei piani (bind peeking). Risulta invece che il contenuto di V$SQL_BIND_CAPTURE è la versione formatatta ed estesa del contenuto del campo BIND_DATA della vista V$SQL, il quale ho verificato con due prove cambia insieme al contenuto di V$SQL_BIND_CAPTURE (non ho fatto un’analisi precisa ma vado in fiducia).

Per completezza dell’informazione, come precisato benissimo da Dion Cho in un commento dello stesso post di Lewis, le peeked binds vengono salvate da oracle nel campo v$sql_plan.other_xml, solitamente nella prima riga del piano di esecuzione.

La vista V$SQL_BIND_CAPTURE è una vista “istantanea”, nel senso che ad ogni suo aggiornamento il contenuto precedente va perso, però AWR ,per default ogni ora, salva una foto della vista nella tabella DBA_HIST_SQLBIND e quindi chi ha acquistato il Diagnostic Pack può interrogare questa tabella per fare qualche analisi, non indagini precise a causa appunto delle limitazioni del campionamento ogni 15 minuti effettuato per la V$SQL_BIND_CAPTURE E e di ogni ora per la DBA_HIST_SQLBIND.

Concorrenza e consistenza in Oracle

Siccome ormai si avvicinano le mie vacanze e per due settimane starò lontano dai computer, scrivo un post riportando degli appunti che scrissi ormai circa 3 anni e mezzo fa, l’argomento è in qualche modo collegato al post precedente, in cui ho parlato della struttura interna dei blocchi dati oracle, di ITL e di lock byte.

La gestione della concorrenza ha un ruolo fondamentale nel determinare la scalabilità delle applicazioni che si appoggiano su un sistema di gestione di  database. La gestione della concorrenza influisce anche sulla gestione della consistenza che in oracle è basata quella che viene chiamato  multi-versionamento dei dati (multi versioning).

Concorrenza

Molte logiche di elaborazione dei dati richiedono che determinate operazioni sugli stessi dati siano serializzate, questo al fine di garantire la correttezza logica delle stesse operazioni. D’altro canto però la serializzazione inibisce la scalabilità, ovvero il numero di operazioni che possono essere fatte per unità di tempo.

Oracle ha un sistema molto sofisticato di gestione della concorrenza che riduce al minimo la serializzazione al fine di massimizzare la scalabilità. Questo è stato possibile introducendo anche un meccanismo per la gestione della consitenza basato sulla gestione multi-versione dei dati.

Infatti, detto in breve, se una sessione modifica dati su una tabella Oracle blocca solo i record interessati, cosìcchè finche la sessione non committa non è possibile che un’altra sessione modifichi contemporaneamente gli stessi record.  In questo caso una sessione che volesse modificare gli stessi record rimarrebbe in attesa. Questo però non impedisce che altre sessioni interroghino le stesse tabelle che in quel momento la sessione sta aggiornando. Questo nonostante l’isolation level fornito da Oracle sia read committed, cioè le altre sessioni vedono solo i dati risultanti  da transazioni commitatte. Quindi se una sessione a fatto un update su un record e non ha committato, le altre sessioni vedono il record com’era prima dell’update.  Questo perchè Oracle quando viene fatto l’update modifica effettivamente i dati, però salva la versione precedente in quella struttura dati che fino alle versioni pre-9i era chiamata Rollback Segment e dalla versione 9i viene chiamata Undo. Quindi per fare vedere i dati in modo consistente Oracle li ricostruisce al volo con le informazioni salvate nell’undo.

Come funziona una transazione

Quando una transazione inizia acquisisce uno slot nella tabella delle transazioni di un segmento di rollback (undo), tale tabella si trova nell’header del segmento ed ogni slot contiene un puntatore all’ultimo extent in cui si trovano i dati di UNDO. Ogni transazione ha un identificatore univoco che è la combinazione del numero del segmento di rollback  e dello slot della tabella delle transazioni (ricordo che una transazione non può stare su più segmenti di rollback).

Un DML Lock acquisisce almeno due lock: uno “shared TM” sulla tabella (serve a far si che non venga modificata la struttura della tabella o non venga rimossa) un “exclusive TX” che è usato per implementare i lock per riga (ovvero, se un’altra sessione ha bisogno di modificare la riga si mette in coda su questo lock e enqueue) su tutte le righe che la transazione sta modificando. Il lock sulla riga viene registrato settando  un “lock byte” che si trova nell’intestazione delle riga stessa.

Nota:

Il lock byte punta ad uno slot nella Interested Transaction List (ITL) che si trova sempre nello header del data block. L’identificativo univoco della transazione (XID) è memorizzato nello slot dell’ITL (insieme all’SCN) per ogni riga lockata dalla transazione. Il lock al livello di riga  può essere solo in modalità esclusiva e una transazione può avere un numero illimitato di righe bloccate.

Riporto ora un pezzo un di dump di un blocco come esempio:


*** SESSION ID:(38.2363) 2005-11-15 09:18:07.000
Start dump data blocks tsn: 4 file#: 10 minblk 16597 maxblk 16597
buffer tsn: 4 rdba: 0x028040d5 (10/16597)
scn: 0x0000.2912e6c2 seq: 0x01 flg: 0x00 tail: 0xe6c20601
frmt: 0x02 chkval: 0x0000 type: 0x06=trans data
Block header dump:  0x028040d5
 Object id on Block? Y
 seg/obj: 0x2761  csc: 0x00.2912e6c0  itc: 2  flg: E  typ: 1 - DATA
 brn: 0  bdba: 0x28040d1 ver: 0x01
 inc: 0  exflg: 0

 Itl           Xid                  Uba         Flag  Lck        Scn/Fsc
0x01   0x0002.014.0001b8c4  0x008003ba.2f14.46  C---    0  scn 0x0000.264a3344
0x02   0x0005.01d.0001703e  0x0080004c.1cce.31  C---    0  scn 0x0000.1e5a5b1b

data_block_dump,data header at 0x8a41064
===============
tsiz: 0x1f98
hsiz: 0x54
pbl: 0x08a41064
bdba: 0x028040d5
 76543210
flag=--------
ntab=1
nrow=33
frre=-1
fsbo=0x54
fseo=0x182e
avsp=0x1a3b
tosp=0x1a3b
0xe:pti[0]    nrow=33    offs=0

tab 0, row 12, @0x19f8
tl: 39 fb: --H-FL-- lb: 0x0  cc: 7
col  0: [ 2]  c1 0b
col  1: [ 2]  c1 25
col  2: [12]  4f 50 45 52 41 54 4f 52 45 20 43 43
col  3: [ 2]  c1 02
col  4: [ 1]  31
col  5: [ 7]  78 66 09 14 0b 30 02
col  6: [ 3]  c2 04 45

*** 2005-11-15 09:18:18.000
Start dump data blocks tsn: 4 file#: 10 minblk 16597 maxblk 16597
buffer tsn: 4 rdba: 0x028040d5 (10/16597)
scn: 0x0000.2912e7cc seq: 0x01 flg: 0x00 tail: 0xe7cc0601
frmt: 0x02 chkval: 0x0000 type: 0x06=trans data
Block header dump:  0x028040d5
 Object id on Block? Y
 seg/obj: 0x2761  csc: 0x00.2912e6c0  itc: 2  flg: E  typ: 1 - DATA
 brn: 0  bdba: 0x28040d1 ver: 0x01
 inc: 0  exflg: 0

 Itl           Xid                  Uba         Flag  Lck        Scn/Fsc
0x01   0x0002.014.0001b8c4  0x008003ba.2f14.46  C---    0  scn 0x0000.264a3344
0x02   0x0006.025.0001e42d  0x00800139.281c.17  ----    1  fsc 0x0000.00000000

data_block_dump,data header at 0x8a41064
===============
tsiz: 0x1f98
hsiz: 0x54
pbl: 0x08a41064
bdba: 0x028040d5
 76543210
flag=--------
ntab=1
nrow=33
frre=-1
fsbo=0x54
fseo=0x182e
avsp=0x1a3b
tosp=0x1a3b
0xe:pti[0]    nrow=33    offs=0

tab 0, row 12, @0x19f8
tl: 39 fb: --H-FL-- lb: 0x2  cc: 7
col  0: [ 2]  c1 0b
col  1: [ 2]  c1 25
col  2: [12]  4f 50 45 52 41 54 4f 52 45 20 43 43
col  3: [ 2]  c1 02
col  4: [ 1]  31
col  5: [ 7]  78 69 0b 0f 0a 13 10
col  6: [ 3]  c2 04 45

Quando  la transazione committa l’informazione sui lock non viene spianata nei datablock, cio’ avviene quando un query successiva ripassa sul blocco. Questo meccanismo viene chiamato delayed block cleanout. In realtà questo accade se i blocchi vengo scritti sui datafile prima che la transazione venga committata. In particolare per ogni transazione oracle non mantiene nella cache tutti i blocchi se questi occupano più del 10% circa della dimensione della cache stessa, quindi se una transazione modifica un numero di blocchi che occupa oltre il 10% della cache prima che la transazione committi senz’altro alcuni di questi blocchi verranno scritti su disco e saranno soggetti a delayed block cleanout.  I blocchi rimasti in cache invece vengono “puliti” nel momento in cui viene fatto il commit. La query che fa il cleanout controlla lo stato della transazione e l’SCN nella tabella delle transazioni dell’header del segmento di rollback. Ecco una descrizione (ho annotato che è di Mark Bobak, credo fosse su un forum del metalink, ma ora non ho l’URL esatto) di come avviene il locking a livello di riga:

  1. la transazione inizia: sia esplicitamente via “set transaction …” o implicitamente, a causa di una qualunque istruzione DML.
  2. Viene allocata uno slot nell’header un segmento di rollback:  il segmento di rollback viene selezionato o con meccanismo round-robin o esplicitamente (gestione manuale dei rollback segments) via “set transaction use rollback segment …”
  3. Lo statement viene eseguito, vengono identificati i blocchi da cambiare: questo avviene tramite il piano di esecuzione, utilizzando gli access path li specificati. Per ogni  blocco identificato le righe vengono prese come segue:
  4. Ora la transazione cerca uno slot libero nell’ITL e lo  alloca, mettendoci il puntatore all’header del segemento di
    rollback precedentemente allocato.
  5. Una volta allocato uno slot nell’ITL le righe interessate devono essere marcate come lockate. Questo avviene nella sezione dell’header del datablock “row directory” (nota, questo non è vero, il lock byte fa parte dell’instestazioen della singola riga e non si trova nella row direcot0ry, vedere commenti sotto). Il lock byte viene settato per puntare allo slot nell’ITL di questa transazione.
  6. Alla fine un commit o un rollback rilascerà il lock. In caso di commit, l’unica cosa che accadrà sarà che nella
    tabella delle transazioni nell’header del segmento di rollback verrà registrato che la transazione è committata.

E’ interessante notare che nelle  informazioni di rollback viene salvato oltre allo slot dell’ITL anche lo slot dell’header del rollback segment (docid 94589.999  metalink, da verificare) che poi viene soprascritto, questo puo’ permettere di recuperare le informazioni di tale slot, perché potrebbe essere stato soprascritto lo slot ma non l’extent usato.

Inoltre, come sottolineato anche in un’interessante documento di Connor McDonald (anche di questo ora non il riferimento),  i blocchi che compongono Header dei segment di UNDO vengono trattati come ogni altro blocco nel database, quindi Oracle è in grado di fare l”undo” di tali blocchi. Immagino che non sia fattibile fare altrettanto per i blocchi di UNDO veri e propri perché la dimensione aumenterebbe esponenzialmente, cioè se quando creo un segmento di UNDO devo salvare prima il contenuto dell’undo precendete creo segmenti di undo che via via crescono sempre.

Consistenza

Quando una query inizia l’esecuzione, l’SCN corrente viene registrato per stabilire l’istante temporale di inizio della query. Questo SCN viene poi confrontato con l’SCN del blocco dati estratto per recuperare i dati richiesti. Questo confronto serve a stabilire se i dati contenuti nel blocco sono consistenti ovvero che non siano stati modificati dopo l’inizio della query. Se l’SCN del blocco è più piccolo, allora i dati nel blocco non sono stati toccati dopo l’inizio della query  e quindi sono consistenti. Se l’SCN del blocco è più grande dell’SCN salvato viene creata in cache una versione del blocco (chiamata CR constant read o consistent read) applicando elementi di Undo al blocco fino a riportarlo ad una versione con SCN più piccolo di quello salvato. Una volta applicati gli elementi di Undo Oracle ricontrolla l’SCN, se è ancora più grande di quello salvato Oracle riesamina l’ITL (ricostruita anch’essa tramite l’applicazione di undo) e reitera il processo di applicazione dell’undo.  Come già detto la stessa cosa Oracle può farla con i blocchi che compongono l’header del segmento di undo interessato.

Quando scrissi questi appunti utilizzavo solo Oracle 9, si può notare dal dump, infatti nella versione 10g il dump contiene anche il dump esadecimale oltre a quello codificato. Ho scoperto solo poco tempo, sul sito di Julian Dyke, che è possibile fare il dump in esadecimale anche sulla 9i.  Sul dump in esadecimale mi sono cimentato in questi giorni comprendendo meglio il discorso “little endian”, che rende la vita alquanto difficile , ma questa è un’altra storia e forse l’argomento del prossimo post.

Struttura interna di un blocco dati Oracle

Un po’ di tempo fa  (esattamente era il 20 maggio) Jonathan Lewis ha lanciato sul suo blog un quiz che chiedeva per una tabella (di cui riportava lo script di creazione) con una colonna e con il parametro pctfree=0 quante “row entries” si potevano creare in un blocco da 8k.

Il quiz non attirò molto la mia attenzione, ma la soluzione, forse per il fascino dell’occulto che si cela dietro l’esplorazione dei meccanismi interni di Oracle, mi ha incuriosito. Ho quindi deciso di fare alcune prove per capire meglio la problematica e l’argomento proposto da Jonathan Lewis.  Molti aspetti di ciò che dice Lewis in quei post (e nei commenti) mi sono diventati più chiari solo dopo un po’ di prove e di studi, anche perché l’argomento non è banale e il post di spiegazione è piuttosto sintetico.

Nella formulazione della domanda Lewis metteva in guardia sul fatto che la domanda era “insidiosa” con il P.S. “it’s a bit of a trick question.

Infatti il punto non era quante righe Oracle mette al massimo in un blocco (che vedremo essere 733 in una tablespace LMT) ma quante “row entries” possono essere create. Le row entries sono le componenti della row directory ovvero l’indice delle righe nella parte di “intestazione” del blocco.

Oracle Data Block Format (10g Concepts Manual)

Oracle Data Block Format (10g Concepts Manual)

La figura sopra riportata, presa direttamente dal manuale “Concepts” della versione 10gR2 è semplificata rispetto alla realtà in quanto è sufficente per descrivere i concetti spiegati nel manuale e per il normale utilizzo. In realtà subito dopo la figura si dice che i primi tre elementi (data block header, table directory, and row directory) chiamati “overhead” sommano tra gli 84 e i 107 byte cosa inesatta, in quanto solo la row directory può arrivare a 2*2015 byte (sempre per un blocco da 8k in una tablespace LMT).

Ho fatto varie ricerche sull’esatta struttura dell’header di un blocco dati Oracle ma tutte le ricerche mi portavano alle informazioni presenti ad esempio in questo sito. Informazioni presenti anche sul metalink ma che suppongo riferite a tablespace Dictionary Managed (DMT). Ora,io utilizzo oracle dalla 9iR2 e tutti i miei database hanno la tablespace SYSTEM Locally Managed (possibilità introdotta proprio con 9iR2, con la 9i, versione con cui è stata introdotta questa caratteristica, la system non poteva essere creata così), come da scritp generati da DBCA. Ancora nella 10gR2 il default per la system sarebbe Dictionary Managed. Come indicato sul manuale se la tablespace SYSTEM è LMT allora non si possono creare tablespace Dictionary Managed e quindi non avendo voglia di creare un nuovo database di test non ho fatto test su questo tipo di tablespace (fra l’altro deprecate).

Una descrizione precisa e dettagliata l’ho trovata in questo documento, dal quale si ricava che l’intestazione di un blocco dati oracle è così:

  1. 20 byte di “block header structure“, intestazione fissa uguale per tutti i blocchi
  2. 72 byte di “Transaction Fixed Header Structure” che si divide a sua volta in:
    1. 24 byte di sub-instestazione fissi
    2. 48 byte per due slot ITL (interested transaction list)
  3. 14 byte per la “Data Header Structure
  4. 4 byte per la “Table Directory Entry Structure

Vi è poi la Row Directory, lo spazio libero, i dati (il blocco viene riempito partendo dal basso) e gli ultimi 4 byte sono riservati alla “Tail check“, una specie di checksum utilizzato per controllare l’integrità del blocco.

Risulterebbero 110 byte sull’header e 4 byte di tailcheck.  Un commentatore però riporta pero’ sempre sul blog di Lewis la formula

BLOCK_SIZE = 122 + (2 + 9) * NUM_ROWS

Che risulta essere corretta, ma della quale non riuscivo a comprendere il termine “122″, a me mancavano 8 byte all’appello. Guardando il documento di Graham Thornton a pagina 14 e i dump dei blocchi (che dalla versione 10g contiene anche il dump in esadecimale) risulta che la parte “Data Header Structure” comincia al centounesimo byte (indirizzo 100) e che i byte fra il novantreesimo e il il centesimo (92-99) sono a zero, quindi o inutilizzati o riservati per usi particolari. Non ho trovato una spiegazione logica per questo buco, fatto sta che contando questi 8 byte si arriva a un minimo di 122 byte utilizzati in un blocco dati per informazioni di struttura fissi.

Dunque, il numero massimo di record che Oracle mette in un blocco da 8k (in una LMT) è 733 come ho detto sopra, secondo la formula sopra scritta però:

MAX_ROWS= (BLOCK_SIZE – 122) /(2+1+1+1+6)

dove in (2+1+1+1+6 sono rispettivamente l’occupazione della row entry, il flag byte, il lock byte, il count column e il restricted rowid (occupazione minima in caso di una migrated row come spiegato da Lewis qui).

Interessanti informazioni sul contenuto e il formato di un blocco dati Oracle possono essere ricavate dalla presentazione di Rich Niemec intitolata “Tuning Oracle at the Block Level; Beginners, Go Away!” scaricabile dal sito di TUSC.

In particolare, tornando alla spiegazione di Lewis sulle Row entries il punto che mi ha colpito è stato il fatto che c’è un limite invalicabile di 4096 row entries che in oracle 10gR2 con il testcase di Lewis provoca il seguente errore:

ORA-08007: Further changes to this block by this transaction not allowed

Il punto è che la struttura della ITL è così fatta:

  1. 8 bytes di XID
  2. 8 bytes di UBA
  3. 2 byte di Flag+Lock
  4. 6 byte di  scn/fsc (la cui struttura non mi è completamente chiara)

Una descrizione di questi elementi si trova nel documento di Niemec nelle pagine 23 e 24.


Itl           Xid                  Uba         Flag  Lck        Scn/Fsc
0x01   0x0006.015.000021ed  0x00803290.0f44.5b  ----  4092  fsc 0x0007.00000000
0x02   0x0000.000.00000000  0x00000000.0000.00  ----    0  fsc 0x0000.00000000

La parte flag occupa 4 bit e alla parte lck, che indica il numero di righe “loccate” nel blocco rimangono 12 bit, 2^12=4096 da qui il limite imposto da Oracle (fra l’altro aggiunto dopo la segnalazione di corruzioni che probabilmente sconfinavano nella parte flag).

Esplorare e comprendere la struttura interna di un blocco dati oracle non so quanto sia utile, sicuramente richiede tempo ed energie. Aiuta a capire casi strani come quello riportato da Jonathan  Lewis e alcuni limiti. E’ una cosa che inspiegabilmente mi attira molto e che mi ha portata a investire un po’ di tempo. Buona parte dei risultati delle mie investigazioni sono state riassunte in questo post. Non ho potuto chiaramente descrivere ogni dettaglio, forse lo farò in post successivi.

Moltiplicazione dei cursori con JDBC

Oggi ho fatto un piccolo test con un programma java che inserisce dati in una tabella un po’ di record. Il test nasce da una segnalazione che ho avuto molto tempo fa. Il test mi ha dato occasione di approfondire un po’ l’argomento JDBC e secondo me entra nell’argomento “applicazione indipendenti dal database”. Infatti ho verificato come in certe condizioni, l’utilizzo di JDBC standard, senza le estensioni oracle,  si può avere una proliferazione di cursori nella Shared Pool di Oracle con inevitabili conseguenze sulle prestazioni.

Cominciamo dall’inizio: tempo fa mi giunse la segnalazione che in corrispondenza dell’esecuzione di una particolare procedura (java) si aveva un picco di consumo della CPU da parte di Oracle, al punto da saturare questa risorsa sulla macchina e rallentare tutte le altre attività. Mi venne subito segnalato che vi era una proliferazione di cursori per uno statement. La cosa pareva strana, in quanto l’applicazione fa un uso abbastanza rigororoso di “bind variables”, ma un esame della vista V$SQL_SHARED_CURSOR evidenziava diversi “BIND MISMATCH”. Effettivamente, nonostante l’uso delle bind variables, vi erano delle situzioni che oggi ho finalmente approfondito, la frequente assenza di valori per alcune colonne, mai le stesse per i diversi insert. La tabella in oggetto avveva un numero elevato di campi ed ogni insert aveva dei null fra i valuri da inserire, non sempre nelle stesse colonne.

Illustro meglio cosa intendo dire con un esempio, ho una tabella T così fatta:


SVILUPPO40@perseo10 > desc t
Nome   Nullo?   Tipo
------ -------- ------------
A               NUMBER
B               NUMBER
C               TIMESTAMP(6)

e un programmino java di cui riporto un pezzo:


PreparedStatement stmt = _connection1.prepareStatement("INSERT /* - 1 */ INTO T (A, B, C) VALUES (?,?,?)");
stmt.setLong(1,1);
stmt.setLong(2,11);
stmt.setObject(3,ts);
int retval = stmt.executeUpdate();
stmt.setLong(1,1);
stmt.setLong(2,11);
stmt.setObject(3,null);
retval = stmt.executeUpdate();
stmt.close();

Inserisco due record, nel primo nella terza colonna passo un oggetto ts (è un timestamp java), nel secondo passo null. Ebbene, interrrogando la V$SQL il risultato è :


SQL_ID        CHILD_NUMBER EXECUTIONS SQLTXT
------------- ------------ ---------- ------------------------------
3823umzxhg563            0          1 INSERT /* - 1 */ INTO T (A, B,
3823umzxhg563            1          1 INSERT /* - 1 */ INTO T (A, B,

Utilizzando uno dei comodissimi script messi a disposizione da Dion Cho:


SYSTEM@perseo10 > @shared_cursor
SYSTEM@perseo10 > set echo off
Immettere un valore per 1: INSERT /* - 1 */ INTO T%
vecchio  14:           and q.sql_text like ''&1''',
nuovo  14:           and q.sql_text like ''INSERT /* - 1 */ INTO T%''',
SQL_TEXT                       = INSERT /* - 1 */ INTO T (A, B, C) VALUES (:1,:2,:3)
SQL_ID                         = 3823umzxhg563
ADDRESS                        = 2A54B848
CHILD_ADDRESS                  = 24957B88
CHILD_NUMBER                   = 0
--------------------------------------------------
SQL_TEXT                       = INSERT /* - 1 */ INTO T (A, B, C) VALUES (:1,:2,:3)
SQL_ID                         = 3823umzxhg563
ADDRESS                        = 2A54B848
CHILD_ADDRESS                  = 24977EB8
CHILD_NUMBER                   = 1
BIND_MISMATCH                  = Y
--------------------------------------------------

E infine approfondendo con la vista V$SQ_BIND_METADATA:


SYSTEM@perseo10 > select * from v$sql_bind_metadata where  address='24957B88';

ADDRESS    POSITION   DATATYPE MAX_LENGTH  ARRAY_LEN BIND_NAME
-------- ---------- ---------- ---------- ---------- -------------------------
24957B88          3        180         11          0 3
24957B88          2          2         22          0 2
24957B88          1          2         22          0 1

SYSTEM@perseo10 > select * from v$sql_bind_metadata where  address='24977EB8';

ADDRESS    POSITION   DATATYPE MAX_LENGTH  ARRAY_LEN BIND_NAME
-------- ---------- ---------- ---------- ---------- -------------------------
24977EB8          3          1         32          0 3
24977EB8          2          2         22          0 2
24977EB8          1          2         22          0 1

Mi pare interessante notare che quando da java viene fatto setObject(3,null), Oracle prende il tipo della bind variable 1 che da documentazione corrisponde a varchar2.

Dando un’occhiata alla documentazione Java ho notato l’esistenza di un metodo setNull dell’interfaccia PreparedStatement e l’ho provato:


/* caso tre */
 stmt = _connection1.prepareStatement("INSERT /* - 3 */ INTO T (A, B, C) VALUES (?,?,?)");
 stmt.setLong(1,1);
 stmt.setLong(2,11);
 stmt.setObject(3,ts);
 retval = stmt.executeUpdate();
 stmt.setLong(1,1);
 stmt.setNull(2,java.sql.Types.DECIMAL);
 stmt.setObject(3,ts);
 retval = stmt.executeUpdate();
 stmt.close();

in questo caso non c’è bind mismatch, infatti:


SQL_ID        CHILD_NUMBER EXECUTIONS SQLTXT
------------- ------------ ---------- ------------------------------
0zsbqbdnk1ssx            0          2 INSERT /* - 3 */ INTO T (A, B,

il cursore viene riutilizzato (le esecuzioni sono due e non ci sono altri child).

Per gestire tutti i propri tipi dato Oracle ha introdotto delle estensioni nei driver JDBC, ad esempio ha esteso l’interfaccia PreparedStatement con l’interfaccia OraclePreparedStatement, nella documentazione viene proprio spiegato questo e come utilizzarlo con un esempio.

Si vede quindi che anche la programmazione con JDBC richiede particolare attenzione per evitare situazioni di “incompatibilità di tipo dato” (datatype mismatch) che possono in ultimo incidere negativamente sulle prestazioni del database server Oracle

Stessa query con risultati diversi

Alcuni giorni fa mi è stato sottoposto una query molto particolare che si comportava in modo anomalo: facendo SELECT count(*) …. dava un numero, facendo SELECT * …. dava un numero diverso di record.  Analizzando un po’ la query ho subito notato che la differenza nei due casi era il piano di esecuzione (francamente spero non vi siano altre possibilità). Ho provato un pochino ad analizzare i piani di esecuzione ed ha trasformare la query ma senza capire molto. Oggi però sono riuscito, senza capire dove sta esattamente l’inghippo, a riprodurre il problema su uno degli schemi di esempio di Oracle.

Breve parentesi, con Oracle 10g gli schemi di esempio (HR,SH, OE, ecc.)  sono inclusi nel “companion CD”, oggi l’ho scaricato ed ho provato a lanciarne l’installazione, ma pare che per installare qualche KByte di script sia necessario installare oltre 700 MB di roba sulla macchina, spazio che sulla macchina di sviluppo dove volevo installare tali schemi non ho, allora mi sono estratto manualmente dal pacchettone di installazione solo gli script che mi servivano.

Fine della parentesi.

Come dicevo sono riuscito in qualche modo a riprodurre un caso analogo a quello segnalatomi dal nostro laboratorio di sviluppo, in cui in sostanza la stessa query da risultati diversi. Ho cercato fra gli schemi di esempio uno che avesse una struttura adatta a riprodurre il mio caso, lo schema che ho individuato è OE,  la query che ho scritto, inspirandomi non ad una logica sensata ma all’obbiettivo di riprodurre con il minimo sforzo il caso è questa:


select
 count(*)
 from
 products pi , inventories inv
 where
 pi.product_id=inv.product_id(+)
 and inv.warehouse_id(+) =
 case when
 exists
 (select ct.category_id
 from  categories_tab ct,
 product_prices pp
 where ct.category_id=pp.category_id
 and ct.category_id=pi.category_id)
 then to_number(11)
 else to_number(1)
 end

Sono stato molto fortunato, perché non avevo la forza di pensare molto e nonostante ciò in poco tempo sono riuscito a riprodurre il caso che volevo. Fra l’altro la logica originale non era poi così distante dal caso che ho inventato io perché si trattava proprio di una query su articoli, listini, magazzini ecc.

Questa è la versione che credo corretta:


OE@perseo10 > select
 2     count(*)
 3    from
 4    products pi , inventories inv
 5   where
 6   pi.product_id=inv.product_id(+)
 7   and inv.warehouse_id(+) =
 8    case when
 9     exists
 10      (select ct.category_id
 11      from  categories_tab ct,
 12             product_prices pp
 13      where ct.category_id=pp.category_id
 14             and ct.category_id=pi.category_id)
 15      then to_number(11)
 16      else to_number(1)
 17    end
 18  /

 COUNT(*)
----------
 288

Piano di esecuzione
----------------------------------------------------------
Plan hash value: 2199577315

-----------------------------------------------------------------------------------------------
| Id  | Operation               | Name                | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT        |                     |     1 |    21 |    10   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE         |                     |     1 |    21 |            |          |
|   2 |   NESTED LOOPS OUTER    |                     |   288 |  6048 |     5   (0)| 00:00:01 |
|   3 |    NESTED LOOPS OUTER   |                     |   288 |  4032 |     5   (0)| 00:00:01 |
|   4 |     TABLE ACCESS FULL   | PRODUCT_INFORMATION |   288 |  2016 |     5   (0)| 00:00:01 |
|*  5 |     INDEX UNIQUE SCAN   | PRD_DESC_PK         |     1 |     7 |     0   (0)| 00:00:01 |
|*  6 |    INDEX RANGE SCAN     | INVENTORY_IX        |     1 |     7 |     0   (0)| 00:00:01 |
|   7 |     NESTED LOOPS        |                     |    11 |   165 |     5   (0)| 00:00:01 |
|*  8 |      INDEX UNIQUE SCAN  | SYS_C0056077        |     1 |     2 |     0   (0)| 00:00:01 |
|   9 |      VIEW               | PRODUCT_PRICES      |    11 |   143 |     5   (0)| 00:00:01 |
|  10 |       SORT GROUP BY     |                     |    11 |    33 |     5   (0)| 00:00:01 |
|* 11 |        TABLE ACCESS FULL| PRODUCT_INFORMATION |    17 |    51 |     5   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------

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

 5 - access("D"."PRODUCT_ID"(+)="I"."PRODUCT_ID" AND
 "D"."LANGUAGE_ID"(+)=SYS_CONTEXT('USERENV','LANG'))
 6 - access("INV"."WAREHOUSE_ID"(+)=CASE  WHEN  EXISTS (SELECT 0 FROM  (SELECT
 "CATEGORY_ID" "CATEGORY_ID" FROM OE."PRODUCT_INFORMATION" "PRODUCT_INFORMATION" WHERE
 "CATEGORY_ID"=:B1 GROUP BY "CATEGORY_ID") "PP","CATEGORIES_TAB" "CT" WHERE
 "CT"."CATEGORY_ID"=:B2) THEN 11 ELSE 1 END  AND "I"."PRODUCT_ID"="INV"."PRODUCT_ID"(+))
 8 - access("CT"."CATEGORY_ID"=:B1)
 11 - filter("CATEGORY_ID"=:B1)

A questo punto, per forzare il cambiamento di piano di esecuzione (e di risultato) utilizzo un hint:


OE@perseo10 > select
 2    /*+ RULE */ count(*)
 3    from
 4    products pi , inventories inv
 5   where
 6   pi.product_id=inv.product_id(+)
 7   and inv.warehouse_id(+) =
 8    case when
 9     exists
 10      (select ct.category_id
 11      from  categories_tab ct,
 12             product_prices pp
 13      where ct.category_id=pp.category_id
 14             and ct.category_id=pi.category_id)
 15      then to_number(11)
 16      else to_number(1)
 17    end
 18  /

 COUNT(*)
----------
 36

Piano di esecuzione
----------------------------------------------------------
Plan hash value: 2133092042

---------------------------------------------------------
| Id  | Operation                 | Name                |
---------------------------------------------------------
|   0 | SELECT STATEMENT          |                     |
|   1 |  SORT AGGREGATE           |                     |
|*  2 |   FILTER                  |                     |
|   3 |    NESTED LOOPS OUTER     |                     |
|   4 |     NESTED LOOPS OUTER    |                     |
|   5 |      TABLE ACCESS FULL    | PRODUCT_INFORMATION |
|*  6 |      INDEX UNIQUE SCAN    | PRD_DESC_PK         |
|*  7 |     INDEX RANGE SCAN      | INVENTORY_IX        |
|   8 |      MERGE JOIN           |                     |
|*  9 |       INDEX UNIQUE SCAN   | SYS_C0056077        |
|* 10 |       FILTER              |                     |
|  11 |        VIEW               | PRODUCT_PRICES      |
|  12 |         SORT GROUP BY     |                     |
|  13 |          TABLE ACCESS FULL| PRODUCT_INFORMATION |
|  14 |    MERGE JOIN             |                     |
|* 15 |     INDEX UNIQUE SCAN     | SYS_C0056077        |
|* 16 |     FILTER                |                     |
|  17 |      VIEW                 | PRODUCT_PRICES      |
|  18 |       SORT GROUP BY       |                     |
|  19 |        TABLE ACCESS FULL  | PRODUCT_INFORMATION |
---------------------------------------------------------

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

 2 - filter("INV"."WAREHOUSE_ID"(+)=CASE  WHEN  EXISTS (SELECT 0 FROM
 (SELECT "CATEGORY_ID" "CATEGORY_ID" FROM OE."PRODUCT_INFORMATION"
 "PRODUCT_INFORMATION" GROUP BY "CATEGORY_ID") "PP","CATEGORIES_TAB"
 "CT" WHERE "CT"."CATEGORY_ID"=:B1 AND
 "CT"."CATEGORY_ID"="PP"."CATEGORY_ID") THEN 11 ELSE 1 END )
 6 - access("D"."PRODUCT_ID"(+)="I"."PRODUCT_ID" AND
 "D"."LANGUAGE_ID"(+)=SYS_CONTEXT('USERENV','LANG'))
 7 - access("INV"."WAREHOUSE_ID"(+)=CASE  WHEN  EXISTS (SELECT 0 FROM
 (SELECT "CATEGORY_ID" "CATEGORY_ID" FROM OE."PRODUCT_INFORMATION"
 "PRODUCT_INFORMATION" GROUP BY "CATEGORY_ID") "PP","CATEGORIES_TAB"
 "CT" WHERE "CT"."CATEGORY_ID"=:B1 AND
 "CT"."CATEGORY_ID"="PP"."CATEGORY_ID") THEN 11 ELSE 1 END  AND
 "I"."PRODUCT_ID"="INV"."PRODUCT_ID"(+))
 9 - access("CT"."CATEGORY_ID"=:B1)
 10 - filter("CT"."CATEGORY_ID"="PP"."CATEGORY_ID")
 15 - access("CT"."CATEGORY_ID"=:B1)
 16 - filter("CT"."CATEGORY_ID"="PP"."CATEGORY_ID")

Note
-----
 - rule based optimizer used (consider using cbo)

Nel caso originale in realtà non era necessario alcun hint, ma era sufficente passare da count(*) a * nella select list per forzare l’ottimizzatore ad utilizzare un piano di esecuzione diverso ed a produrre un “result set” diverso.

Il punto sta nella parte:


and inv.warehouse_id(+) =
 case

Il piano di esecuzione corretto prevede solo “NESTED LOOPS OUTER” quello che produce un risultato “non corretto” (in pratica non applica l’operatore (+) ) usa in questo caso un MERGE JOIN. Nella query originale, molto più complessa per la verità, non vi sono MERGE JOIN c’è solo un HASH OUTER JOIN, ne riporto un tratto:

—————————————-
| Id  | Operation                      |
—————————————-
|   0 | SELECT STATEMENT               |
|   1 |  SORT ORDER BY                 |
|*  2 |   FILTER                       |
|*  3 |    HASH JOIN OUTER             |
|*  4 |     HASH JOIN                  |
|*  5 |      TABLE ACCESS FULL         |
|*  6 |      TABLE ACCESS FULL         |
|   7 |     TABLE ACCESS FULL          |
|   8 |    NESTED LOOPS                |
|*  9 |     TABLE ACCESS BY INDEX ROWID|
|* 10 |      INDEX RANGE SCAN          |
|* 11 |     INDEX UNIQUE SCAN          |
—————————————-

Ora, purtroppo non ho ancora studiato bene i piani di esecuzione, ma sotto compare:

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

2 – filter(”campo_fk”(+)=CASE  WHEN  EXISTS

e non mi è chiaro se questo filtro viene applicato dopo la HASH JOIN OUTER della riga 3.

Il database originale è 11g, quello che ho usato io per il test è 10gR2, il caso originale l’ho riprodotto anche su 9iR2. La query fa’ un utilizzo  un po’ ardito dell’operatore (+) ma è sintatticamente corretta e tradurla in sintassi ANSI JOIN non mi pare cosa fattibile (perché in effetti una join non è… o si?) soprattutto non mi sento in grado di farlo con l’assoluta certezza dell’equivalenza.

La documentazione Oracle sull’operatore (+) non dice molto, qualcosa in più lo dicono qui, e la mia impressione in effetti è che il costrutto è molto simile a una “lateral view” un concetto che da un po’ sto cercando di digerire ma che sta ancora li, non ben definito nella mia testa.

Altro Blog interessante

Oggi sono capitato casualmente su un nuovo blog su Oracle, quello tenuto da Dion Cho in inglese chiamato “Dion Cho – Oracle Performance Storyteller” . Ha iniziato a scrivere a gennaio di quest’anno e sto leggendo gli arretrati e ho trovato diversi post interessanti. Ho gia aggiunto anche questo blog al mio feed reader.

Ripristino di una macchina virtuale Linux

L’altro giorno avevo bisogno di testare una funzionalità su Oracle 11g, per questo motivo ho pensato di accendere la macchina virtuale che ho gia utilizzato tempo fa per fare un po’ di test su questa release di Oracle che in azienda ancora non utiliziamo. Dopo aver lanciato il comando “xm create test11g” ho atteso un po’ ed ho provato a collegarmi via SSH con putty, senza successo, infatti la macchina non rispondeva neppure ai ping. Allora mi sono collegato via VNC alla macchina “madre” (quella che ospita le macchine virtuali che ho creato con XEN, una Oracle Enterprise Linux Server 5) e da li ho aperto il “Virtual Machine Manager” che permette fra l’altro di accedere alla console delle macchine virtuali. Ho così scoperto con mio disappunto che la fase di boot della mia macchina virtuale test11g (quella con installato Oracle 11g) era bloccata perché i processo non trovata il file /etc/fstab. Il file sembrava essere li, però al posto dei suoi attributi comparivano dei punti di domanda e cercando di visualizzarlo ad esempio con “cat” si ottenere un input/output error.

Io non sono un sistemista esperto, e situazioni del genere non mi erano mai capitate. Per prima cosa ho seguito il consiglio che compariva in console di lanciare fsck, procedura che però falliva  perché si lamentava proprio del fatto di non riuscire ad accedere al file /etc/fstab; solo oggi ho visto che il comando fsck vuole come parametro di input un “mount point” o un device su cui c’è il filesystem da controllare e che altrimenti usa /dev/fstab. In ogni caso facendo una ricerca veloce su Google sono arrivato al comando e2fsck che a differenza di fsck accetta solo un device come input, quindi sono riuscito a lanciarlo e a sistemare il filesystem. A quel punto ho provato a fare un reboot, operazione però fallita di nuovo in quanto e2fsck aveva eliminato il cadavere di /etc/fstab che era rimasto, quindi a quel punto dovevo ricreare il file /dev/fstab, operazione facilitata per un verso dal fatto di avere ancora una copia di mtab creata all’ultimo avvio sano, ma resa difficoltosa dal fatto che nella shell di emergenza aperta al boot il filesystem di root era montato in modalità read-only. Ho così dovuto scoprire ed utilizzare l’opzione “-o remount” del comando mount per rimontare il filesystem di root in modalità read-write (essendo filesystem di root non me lo lasciava smontare e rimontare).

In realtà nella fretta in fstab ho messo solo il filesystem di root e il filesystem virtuale /proc ma oggi riaccendendo la macchina virtuale e cercando di collegarmi via ssh con putty ho trovato l’errore: “Server refused to allocate pty“.

Ho così scoperto che in /dev/fstab è bene ci sia anche la riga:

none /dev/pts devpts rw,gid=5,mode=620 0 0

Questo grazie a questo link.

Non mi ero mai posto il problema di cosa fosse /dev/pts, ma oggi mi ci sono scontrato, ho trovato un paio di spiegazioni, qui e qui scoprendo che per risparmiare qualche file sotto /dev è stato introdotto questo meccanismo basato su un’altro filesystem virtuale.

Non essendo un sistemista professionista sono comunque riuscito a cavarmela, ho cercato di riassumere qui velocemente il procedimento che ho seguito per arrangiarmi più velocemente la prossima volta che mi capiterà.

SQL JOIN – aggiornamento

Quasi due anni fa ho scritto un post sui Join in Oracle. Tre giorni fa Jonathan Lewis ha scritto sul suo blog un post intitolato “Dependent Plans” in cui descrive una query per ottenere tutti i piani di esecuzione in cache di query che riferiscono oggetti dipendenti da un particolare oggetto specificato; nel descrivere la query Lewis cita il meccanismo del “lateral join” e mette un link a un suo vecchio post intitolato “Lateral LOBs“, che parla in realtà di lateral views . In questo post viene descritta un’interessante funzione per estrarre un lob a pezzi di varchar2. Jonathan rimanda per la definizione di lateral views a un post del blog “Inside the Oracle Optimizer” intitolato “OuterJoins in Oracle“.

Sono due giorni che studio tutti questi post e i link che ho trovato nei commenti per capire bene il concetto di lateral view. Uno dei link citati rimanda a un post di Jonathan Gennick del 2002 intitolato “What’s in a Condition?” che da un indicazione su come intendere il concetto di join per comprendere meglio i diversi risultati che si possono avere sulle outer join spostando delle condizioni dalla sezione “ON” della join alla sezione “WHERE”. Ebbene, il punto è  che come spiegano quel gruppo di sviluppo dell’ottimizzatore Oracle, almeno in Oracle, le clausole inserite nella parte ON sono valutate prima della JOIN, quelle nella WHERE dopo.

Confrontando però gli esempi che portai quasi due anni fa nel mio post sulle join con la spiegazione di JOIN non riuscivo a capire bene:

SVILUPPO40@perseo10 > create table a (num number);

Tabella creata.

SVILUPPO40@perseo10 > create table b (num number);

Tabella creata.

SVILUPPO40@perseo10 > insert into a values (1);

Creata 1 riga.

SVILUPPO40@perseo10 > insert into b values (2);

Creata 1 riga.

SVILUPPO40@perseo10 > select * from a full outer join b on (1=1);

NUM        NUM
———- ———-
1          2

SVILUPPO40@perseo10 > select * from a full outer join b on (1=0);

NUM        NUM
———- ———-
1
2

SVILUPPO40@perseo10 > select * from a,b;

NUM        NUM
———- ———-
1          2

Dalla spiegazione di Gennick non riuscivo a spiegarmi il caso di select * from a full outer join b on (1=0); in cui ho due record. Ebbene, nella sua spiegazione manca un pezzo, perché dal prodotto cartesiano si in questo caso si ottiene una sola riga. Il suo modello funziona bene per left o right outer join, per la full, probabilmente occorre fare una estensione. Un full outer join in effetti può essere vista come l’unione di una left outer join e una right outer join,  e questa forse è sufficente e corretta come precisazione del modello mentale. Nel mio caso la condizione (1=0) sempre falsa fa eliminare l’unico record risultante dal prodotto cartesiano, quindi subentra il punto 3. dell’articolo di Gennick, che va applicato prima per una tabella e poi per l’altra.

L’articolo di “Inside the Oracle Optimizer” è molto interessante ed istruttivo, l’unico punto un  po’ vago è quello proprio sulle “Lateral Views”, che sono un concetto che fa parte di qualche standard SQL (secondo il manuale SQL Foundation 2003) e in Oracle è implementato tramite la funzione “TABLE” che in sostanza serve a tirare fuori gli elementi da una collection direttamente via SQL, oppure, come mostra Jonathan Lewis nei suoi interessanti esempi, si utilizza con le funzioni “PIPELINED” (ne ho parlato qui).

Effetti di SGA piccola in 10g

Stamattina, leggendo l’ultimo post di Kerry Osborne intitolato “Extremes“, mi è venuto in mente un bizzarro comportamento segnalatomi da un programmatore su un nostro database interno di test qualche tempo fa.

Il database è un 10.2.0.4 su win32, con SGA_MAX_SIZE=SGA_TARGET=432M. La macchina dispone di un GigaByte di RAM che però deve essere condivisa oltre che dal SO e da Oracle, anche a dall’application server, da ciò deriva l’esiguità della SGA. La gestione della SGA è automatica e il risultato di ciò era che la SHARED_POOL aveva all’incirca 60 Mbyte di spazio disponibile.

Il sintomo segnalato dal programmatore era il fatto che una sequence saltava frequentemente blocchi di venti numeri, al punto tale che due chiamate  consecutive della query

SELECT SMIASEQUENCE.nextval FROM DUAL;

davano due numeri con un buco in mezzo di venti, ad esempio la prima chiamata dava 1121 , la seconda 1141. La cosa era un po’ esagerata.  Dopo un po’ di verifiche ho notato il ridotto dimensionamento del database (di cui a dire il vero neanche ricordavo l’esistenza :) ) ed ho provato a fare un controllo tramite la vista V$ROWCACHE, la quale ha un riga dedicata proprio alle sequence e che in effetti mostrava un numero crescente di “FLUSH”, ho provato ad aumentare la shared pool, portandola fino un valore minimo di 240 M e le cose sono decisamente migliorate.

Informazioni sul Cloud Computing

Per uno  strano giro oggi sono arrivato ad un sito (blog) che parla di Cloud Computing, il sito è CloudDB.info ed è gestito da Lewis Cunningham, un “Oracle ACE director” (non ho ancora capito se ACE è una sigla e cosa significa) che manutiene anche altri due siti/blog, database-geek.comdatabasewisdom.com. Il sito CloudDB.info mi è sembrato molto interessante, non ho avuto tempo di leggere molto, ma ho capito Lewis è un appassionato di Cloud Computing, ha scritto un bel po’ di post sull’argomento e sul sito ha messo un po’ di link interessanti, creando quindi un buon punto di partenza per informarsi sull’argomento.

Dicevo che vi sono arrivato attraverso uno strano giro che voglio descrivere; oggi Skyone ha scritto un post intitolato “Ma Twitter è utile? Is Twitter useful?” , leggento il post mi è subito venuto in mente un post di qualche tempo fa sul blog nicopi.wordpress.com, in cui c’era un filmatino che spiegava bene il senso di Twitter.  Nonostante la spiegazione e il mio tentativo di usare Twitter non sono riuscito a trovarlo utile. In ogni caso mi sono chiesto se mi sfuggisse qualcosa, ed allora sono andato a guardare meglio un articolo comparso su otn fra gli articoli recenti, intitolato “Twitter meets Oracle: ORA_Tweet” in cui l’autore spiega come con un packetto PL/SQL si possono utilizzare delle API di Twitter per pubblicare su Twitter. L’autore è proprio Lewis Cunningham e in coda all’articolo sono riportate alcune informazioni su di lui fra cui appunto il link al sito CloudDB.info che ha attirato la mia attenzione.