Introduzione
Lo sviluppo di applicazioni JDBC mirate all'ottimizzazione
delle prestazioni non è semplice. I driver JDBC
non lanciano alcuna eccezione per segnalare quando il
codice in esecuzione è tropo lento.
La
seguente serie di indicazioni relative alle prestazioni
presenta alcune linee guida di valenza generale per
il miglioramento dell'efficienza di un'applicazioni
JDBC, la cui stesura è stata realizzata a seguito
dell'esame di numerose implementazioni di applicazioni
JDBC correntemente rilasciate. Queste linee guida includono:
Utilizzo
appropriato dei metodi per database che sfruttano i
metadata
- Recupero
dei soli dati richiesti
- Selezione
di funzioni che ottimizzano le prestazioni
- Gestione
delle connessioni e degli aggiornamenti
Negli
ultimi suggerimenti relativi alle prestazioni sono state
presentate alcune indicazioni su come ottenere il recupero
dei soli dati che effettivamente si sono richiesti.
Le seguenti regole generali sulla selezione di funzioni
che ottimizzino le prestazioni dovrebbero aiutare a
risolvere alcuni problemi comuni di prestazione dei
sistemi JDBC.
Le linee guida presentate in questo articolo aiuteranno
a selezionare i metodi e gli oggetti JDBC che forniscono
le prestazioni migliori.
Utilizzo
di parametri posizionali come argomenti delle stored
procedure
Nelle chiamate a stored procedure, vanno sempre utilizzati
parametri posizionali per gli argomenti posizionali
invece che argomenti letterali. I driver JDBC possono
chiamare stored procedure sul server del database sia
eseguendo tali procedure come una qualsiasi altra query
SQL, che ottimizzando l'esecuzione mediante chiamata
ad una Remote Procedure Call (RCP) direttamente dentro
il server del database. Quando la stored procedure viene
eseguita come una query SQL, il server del database
analizza il comando, abilita i tipi di argomenti e converte
questi ultimi nel tipo di dato corretto.
Si
ricordi che l'SQL è sempre inviato al server
come una stringa di caratteri, per esempio "{call
getCustName (12345)}". In questo caso, anche se
il programmatore dell'applicazione può assumere
che l'unico argomento di getCustName sia un intero,
in realtà l'argomento viene passato al server
all'interno di una stringa di caratteri. Il server del
database analizzerebbe la query SQL, consulterebbe i
metadata del database per determinare l'insieme dei
tipi dei parametri della procedura, isolerebbe il valore
del singolo argomento 1234, poi convertirebbe la stringa
'1234' in un valore intero prima di eseguire finalmente
la procedura come un evento del linguaggio SQL.
Invocando
una RCPC entro il server del database, si evita l'appesantimento
dovuto all'utilizzo di una stringa di caratteri SQL.
Infatti, diversamente da quanto visto nel caso precedente,
un driver JDBC costruirà un pacchetto per la
rete contenente i parametri nel formato originari del
loro tipi di dato ed eseguirà la procedura in
remoto.
Caso
1
In
questo esempio la stored procedure non può essere
ottimizzata mediante l'utilizzo di una RPC lato server.
Il server del database deve trattare la richiesta SQL
come l'evento di un linguaggio normale che include l'analisi
del comando, l'abilitazione dei tipi d'argomento e la
conversione degli argomenti nel corretto tipo di dato,
prima di eseguire la procedura.
CallableStatement
cstmt = conn.prepareCall ("{call getCustName (12345)}");
ResultSet rs = cstmt.executeQuery ();
Caso
2
In
questo esempio, la stored procedure può essere
ottimizzata mediante l'utilizzo di una RPC lato server.
Poiché l'applicazione evita argomenti letterali
e chiama la procedura specificando tutti gli argomenti
come parametri, il driver JDBC può ottimizzare
l'esecuzione chiamando la stored procedure direttamente
dentro il database come una RCP. Viene evitata l'elaborazione
di linguaggio SQL sul server del database e il tempo
di esecuzione risulta grandemente migliorato.
CallableStatement
cstmt = conn.prepareCall("{call getCustName (?)}");
cstmt.setLong (1,12345);
ResultSet rs = cstmt.executeQuery();
Uso
dell'oggetto Statement anziché dell'oggetto PreparedStatement
I driver JDBC vengono ottimizzati sulla base della percezione
di utilizzo delle funzioni che sono in esecuzione. La
scelta fra l'oggetto PreparedStatement e l'oggetto Statement
dipende dall'uso pianificato. L'oggetto Statement è
ottimizzato per un'esecuzione singola di un comando
SQL. Al contrario, l'oggetto PreparedStatement è
ottimizzato per comandi SQL da eseguire due o più
volte.
Il
sovraccarico per l'esecuzione iniziale di un oggetto
PreparedStatement è notevole. Il vantaggio viene
nelle seguenti riesecuzioni del comando SQL. Per esempio,
si supponga di preparare e poi eseguire una query che
restituisca informazioni su un dipendente basandosi
su un ID. La via più probabile in cui il driver
JDBC elaborerà la richiesta preparata sarà
l'inoltro, attraverso la rete, di una domanda al server
del database per analizzare ed ottimizzare la query.
L'esecuzione, poi, si traduce in un'altra richiesta
di rete. All'interno dei driver JDBC la riduzione del
traffico di rete è l'ultima delle priorità.
Se l'applicazione sottopone una tale richiesta una sola
volta durante la sua vita, allora va usato l'oggetto
Statement. Con un oggetto Statement, l'esecuzione della
medesima query si traduce semplicemente in un singolo
viaggio di andata e ritorno verso e dal il server del
database attraverso la rete.
Queste
linee guida vengono complicate dall'uso di raggruppamenti
(pool) di prepared statement poiché lo scope
dell'esecuzione risulta più lungo. Quando si
utilizzano pool di prepared statement, se la query è
ad-hoc e probabilmente non sarà più usata,
allora va adottato l'oggetto Statement. Se la query
non verrà eseguita frequentemente, ma comunque
potrà esserlo più volte durante la vita
del pool di statement, all'interno di una connection
pool, allora va usato un PreparedStatement. Nelle medesime
circostanze, qualora non sia presente un pool di statement,
va utilizzato un oggetto Statement.
Uso
di batch anziché prepared statement
La preparazione di un comando di INSERT e la sua esecuzione
ripetuta richiede tipicamente l'aggiornamento di una
grande quantità di dati, il che si traduce in
numerosi viaggi di andata e ritorno attraverso la rete.
Per ridurre il numero di chiamate JDBC e migliorare
le prestazioni, si possono inviare in una sola volta
molteplici query al database facendo uso del metodo
addBatch() dell'oggetto PreparedStatement. Per esempio,
confrontiamo le seguenti implementazioni, Caso 1 e Caso
2.
Caso
1: Esecuzione ripetuta di prepared statement
PreparedStatement
ps = conn.prepareStatement("INSERT into employees
values
(?, ?, ?)");
for
(n = 0; n < 100; n++) {
ps.setString(name[n]);
ps.setLong(id[n]);
ps.setInt(salary[n]);
ps.executeUpdate();
}
Caso
2: Utilizzo di un batch
PreparedStatement
ps = conn.prepareStatement("INSERT into employees
values
(?, ?, ?)");
for
(n = 0; n < 100; n++) {
ps.setString(name[n]);
ps.setLong(id[n]);
ps.setInt(salary[n]);
ps.addBatch();
}
ps.executeBatch();
Nel
Caso 1, viene utilizzato un prepared statement per eseguire
molteplici volte un comando INSERT. In questo caso sono
necessari 101 viaggi di andata e ritorno sulla rete
per eseguire 100 operazioni d'nserimento: 1 andata e
ritorno per preparare il comando e 100 andate e ritorno
per eseguire le iterazioni (una per ogni iterazione).
Se invece si utilizza il metodo addBatch() per compattare
100 operazioni di INSERT, come dimostrato nel Caso 2,
sono necessari solo due andate e ritorno attraverso
la rete: una per preparare il comando ed un'altra per
eseguire il batch. Anche se con l'utilizzo di batch
sono richiesti più cicli di CPU sul database,
vi è comunque un guadagno in prestazioni grazie
alla riduzione di viaggi di andata e ritorno sulla rete.
Si ricordi che con i driver JDBC il maggior guadagno
in prestazioni si trova mediante la riduzione dello
scambio di comunicazioni sulla rete fra il driver JDBC
e il server del database.
Scelta
del giusto cursore
Un'appropriata scelta del tipo di cursore permette di
massimizzare la flessibilità dell'applicazione.
Questa sessione riassume le caratteristiche relative
alle prestazioni di tre tipi di cursore.
Un cursore di tipo forward-only fornisce ottime prestazioni
nel caso di letture sequenziali di tutte le righe di
una tabella. Per il recupero dei dati da tabelle non
vi è metodo più veloce, per ottenere le
righe dei risultati, che quello di far uso di un cursore
di tipo forward-only. Comunque, tale cursore non può
essere adottato quando l'applicazione deve elaborare
le righe in modo non sequenziale.
L'utilizzo
dei cursori di tipo insensitive da parte dei driver
JDBC è ideale per applicazioni che richiedono
un alto livello di concorrenza sul server del database
e l'abilità di scorrere sia avanti che in dietro
sui set dei risultati. La prima richiesta rivolta a
un cursore di tipo insensitive fa il fetch di tutte
le righe (o di molte, ma non tutte, quando il driver
JDBC fa uso di un fetch "pigro") e le memorizza
sul client. Perciò la prima richiesta è
molto lenta, soprattutto quando si ricercano dati lunghi.
Le richieste seguenti non comportano alcun traffico
di rete (o un traffico limitato quando un driver utilizza
un fetch "pigro") e sono elaborate velocemente.
Vista la lentezza della prima elaborazione, l'uso di
un cursore di tipo insensitive non è adeguato
nel caso di richiesta singola di un'unica riga. Gli
sviluppatori dovrebbero evitare l'utilizzo di cursori
di tale tipo quando i dati che vengono restituiti sono
lunghi, perché si potrebbe esaurire lo spazio
di memoria disponibile. Alcune implementazioni di cursori
di tipo insensitive immagazzinano i dati in tabelle
temporanee sul server del database, evitando così
tale problema, ma la maggior parte di questo tipo di
cursori memorizza le informazioni localmente all'applicazione.
Alcuni
cursori di tipo sensitive, talvolta indicati come "keyset-driven",
utilizzano identificatori, quali un ROWID, che esistono
già nel database. Quando si scorre il set dei
risultati vengono reperiti i dati inerenti tali identificatori.
Poiché ogni richiesta genera traffico di rete,
l'esecuzione può essere molto lenta. Comunque,
la restituzione di righe non consecutive non influenza
ulteriormente le prestazioni.
Per
illustrare ciò più estesamente, si consideri
un'applicazione che ritornerebbe normalmente 1000 righe.
Durante l'esecuzione o alla prima richiesta di una riga,
un driver JDBC non eseguirebbe il comando SELECT fornito
dall'applicazione. Bensì il driver JDBC sostituirebbe
la lista SELECT della query con l'dentificatore di chiave,
per esempio, ROWID. La query modificata sarebbe poi
eseguita dal driver e tutti i valori delle 1000 chiavi
verrebbero recuperati dal server del database e tenuti
per essere utilizzate dal driver. Ogni richiesta, da
parte dell'applicazione, di una riga del risultato,
conduce il driver JDBC a cercare il valore della chiave
relativo alla riga appropriata nella propria memoria
locale, a costruire una query ottimizzata che contiene
un'istruzione condizionale WHERE simile a 'WHERE ROWID
= ?', a eseguire la query modificata e quindi a recuperare
dal server la singola riga del risultato.
I
cursori di tipo sensitive sono il modello preferito
di cursore dinamico quando l'applicazione non può
sostenere la memorizzazione localmente dei dati forniti
da un cursore insensitive.
Utilizzo
efficiente dei metodi 'get'
JDBC fornisce una varietà di metodi per il recupero
di dati dal set dei risultati, quali getInt(), getString()
e getObject(). Il metodo getObject() è il più
generico e fornisce le prestazioni peggiori qualora
vengano specificate mappature non di default. Ciò
perché il driver JDBC deve fare elaborazioni
extra per determinare il tipo di valore ricercato e
generare la mappatura appropriata. Va utilizzato sempre
il metodo specifico per il tipo di dati.
Per
migliorare ulteriormente le prestazioni, si fornisce
il numero della colonna ricercata, per esempio, getString(1),
getLong(2) e getInt(3), invece del nome della colonna.
Se il numero di colonna non è specificato, il
traffico di rete non ne risente, ma aumentano le conversioni
e le ricerche costose. Per esempio, si supponga di usare
getString("foo") ... Un driver si trova a
dover convertire il delimitatore foo in lettere maiuscole,
se necessario, e poi a dover comparare "foo"
con i nomi delle colonne nella loro lista. Se invece
il driver andasse direttamente alla colonna 23 deli
risultati, verrebbe risparmiato un quantitativo significativo
di elaborazioni.
Per
esempio, si supponga di avere un set di risultati con
15 colonne e 100 righe, e che i nomi delle colonne non
sia incluso in tale set. Si sia interessati a tre colonne:
EMPLOYEENAME (di tipo stringa), EMPLOYEENUMBER (di tipo
intero lungo) e SALARY (di tipo intero). Se si specifica
getString("EmployeeName"), getLong("EmployeeNunmber")
e getInt("Salary"), ogni nome di colonna deve
essere convertito nell'appropriato formato della colonna,
nei metadata del database, e le ricerche crescono considerevolmente.
Le prestazioni migliorerebbero significativamente se
si specificasse getString(1), getLong(2) e getInt(15).
Recupero
di chiavi auto-generate
Molti database hanno colonne nascoste (chiamate pseudo-colonne)
che rappresentano chiavi uniche relativamente ad ogni
riga in una tabella. Tipicamente l'utilizzo di questi
tipi di colonne in una query costituisce la via più
veloce di accesso a una riga poiché le pseudo-colonne
usualmente rappresentano l'indirizzo fisico del dato
sul disco. Prima della versione 3.0 di JDBC, un'applicazione
poteva recuperare i valori delle pseudo-colonne solo
mediante l'esecuzione di un comando SELECT immediatamente
dopo l'inserimento dei dati.
//insert
row
int rowcount = stmt.executeUpdate ("insert into
LocalGeniusList (name)
values
('Karen')");
//
now get the disk address - rowid - for the newly inserted
row
ResultSet rs = stmt.executeQuery ("select rowid
from LocalGeniusList where
name
= 'Karen'");
Questo
metodo di recupero delle pseudo-colonne ha due pecche
principalmente. La prima è che il reperimento
di pseudo-colonne richiede l'invio attraverso la rete
e l'esecuzione sul server di una query separata. La
seconda è che, poiché può non esservi
una chiave primaria sulla tabella, la condizione di
ricerca della query può non essere in grado di
identificare in modo univoco la riga. In quest'ultimo
caso possono essere restituiti molteplici valori di
pseudo-colonna e l'applicazione può non essere
in grado di determinare quale sia effettivamente quello
relativo alla riga di più recente inserzione.
Una
prerogativa opzionale delle specifiche di JDBC 3.0 è
l'abilità di recuperare informazioni su chiavi
auto-generate relative ad una riga, quando essa viene
inserita in una tabella.
int
rowcount = stmt.executeUpdate ("insert into LocalGeniusList
(name)
values
('Karen')",
// insert row AND return key
Statement.RETURN_GENERATED_KEYS);
ResultSet rs = stmt.getGeneratedKeys ();
// key is automatically available
Ora
l'applicazione contiene un valore che può essere
utilizzato in una condizione di ricerca per permettere
l'accesso più rapido possibile alla riga e un
valore che identifica in modo univoco la riga, anche
quando sulla tabella non esiste una chiave primaria.
L'abilità
di recuperare chiavi auto-generate fornisce flessibilità
per lo sviluppatore JDBC e determina aumenti di prestazione
quando si accede ai dati.
Scelta
del giusto tipo di dati
Il recupero e l'invio di certi tipi di dati può
essere costoso. Quando si disegna uno schema, va selezionato
il tipo di dati che può essere elaborato più
efficientemente. Per esempio i dati di tipo intero sono
di più facile elaborazione rispetto a quelli
in virgola mobile e ai decimali. I dati in virgola mobile
sono definiti secondo formati interni specifici del
database, solitamente in forma compressa. I dati devono
essere decompressi e convertiti in un diverso formato
così da potere essere elaborati dal protocollo
di basso livello del database.
Recupero
dei set di risultati
La maggior parte dei driver JDBC non possono implementare
cursori dinamici, a causa del supporto limitato fornito
per tale tipo di cursori dal database. A meno che non
si sia certi che il database supporti set di risultati
scorribili (per esempio, rs), per determinare quante
siano le righe presenti nel set dei risultati non si
devono chiamare i metodi rs.last() e rs.getRow(). Per
i driver JDBC che emulano i cursori dinamici, una chiamata
ad rs.last() si traduce nel recupero, da parte loro,
di tutti i risultati attraverso la rete per raggiungere
l'ultima riga. Al posto di ciò si può
sia contare le righe iterando sul il set dei risultati
che ottenere il numero delle righe mediante l'inoltro
di una query con un colonna COUNT nell'istruzione SELECT.
In
generale è buona norma non scrivere codice che
si appoggi sul numero di righe del risultato di una
query, poiché in tal caso i driver si trovano
costretti a fare il fetch di tutte le righe in esso
presenti per sapere quante ne restituirà la query.
L'articolo
è stato originariamente pubblicato su theserverside.com
|