L'accesso ai dati mediante un driver JDBC e' uno dei metodi piu' diffusi per raccogliere e trattare dati con un database relazionale.
Anche con il database Open Source
PostgreSQL il driver JDBC e' uno dei metodi di accesso piu' utilizzati.
In questa paginetta cercheremo di vedere un esempio di corretto utilizzo del driver JDBC di PostgreSQL
per raccogliere dati da tabelle di grandi dimensioni.
Il driver JDBC di Postgres e' molto robusto ed affidabile,
pero' spesso vengono lamentati problemi di utilizzo eccessivo di memoria
accedendo a tabelle di grandi dimensioni...
In realta' il driver funziona perfettamente ed il suo comportamento e' esattamente
quello documentato ma c'e' un'impostazione che se non viene effettuata correttamente
puo' causare problemi di uso eccessivo di memoria come l'OOM (Out Of Memory).
Vediamo un semplice esempio di utilizzo del driver JDBC di Postgres. Vogliamo leggere i dati da una tabella di grosse dimensioni:
L'esempio presentato sopra funziona correttamente ma...
ma... e' sbagliato!
Il problema e' che l'intero contenuto della tabella viene caricato in memoria
quando viene effettata l'executeQuery. Anche se con 10M di record riporta i dati corretti
la sua esecuzione non e' efficiente.
La memoria utilizzata viene visualizza con il println ed e' oggettivamente troppo elevata.
Aumentando il numero di record della tabella [NdA ad esempio con 1G come valore per la generate_series]
si otterra' un errore come:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2390) at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:372) at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:517) at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:434) at org.postgresql.jdbc.PgStatement.executeWithFlags(PgStatement.java:356) at org.postgresql.jdbc.PgStatement.executeCachedSql(PgStatement.java:341) at org.postgresql.jdbc.PgStatement.executeWithFlags(PgStatement.java:317) at org.postgresql.jdbc.PgStatement.executeQuery(PgStatement.java:266) at Dimmelo3.main(Dimmelo3.java:40)
Il comportamento del driver e' esattamente quello riportato nella documentazione ufficiale [NdE in evidenza la parte piu' significativa]:
By default the driver collects all the results for the query at once. This can be inconvenient for large data sets so the JDBC driver provides a means of basing a ResultSet on a database cursor and only fetching a small number of rows.
Il modo corretto con i driver JDBC di PostgreSQL per raccogliere i dati con un cursore
da una tabella di grandi dimensioni e' quello di definire una fetchSize opportuna
(eg. setFetchSize(1000))
e disattivare l'autoCommit (setAutoCommit(false)).
Servono entrambe le impostazioni perche' per default
il fetchSize e' a 0, ovvero nessun limite;
mentre per default l'autoCommit e' abilitato, ovvero vengono chiuse le transazioni ed cursori ad ogni statement,
invalidando l'eventuale impostazione del fetchSize.
Le impostazioni possono essere effettuate in modi diversi, sia nel codice Java
che nelle proprieta' della connessione.
Con queste impostazioni l'esempio funziona correttamente e, soprattuto,
non alloca l'intero risultato della query in memoria ma solo quanto definito nella fetchSize.
Entrambe le impostazioni sono necessarie perche' se l'autoCommit e' a true (che e' il default)
il fetchSize viene reimpostato a 0.
Naturalmente e' possibile modificare l'esempio
commentando l'impostazione del setAutoCommit(false) o del setFetchSize(),
utilizzando una query fittizia con piu' colonne o una query reale,
... e verificando ogni volta l'allocazione effettiva di memoria.
Per effettuare caricamenti massivi tipicamente si imposta un fetchSize di grandi dimensioni; naturalmente questo richiede piu' memoria ed e' opportuno effettuare qualche prova per valutare l'effettivo vantaggio.
E' possibile impostare il fetchSize con la proprieta' JDBC defaultRowFetchSize ma non e' disponibile in pgJDBC una proprieta' per impostare ad off l'autoCommit nell'URI JDBC. Per impostare il fetchsize e' anche possibile utilizzare il piu' recente e flessibile adaptiveFetch che tuttavia per default e' disabilitato.
Per tabelle di piccole dimensioni le impostazioni di default sono quelle che danno le migliori prestazioni.
Per tabelle di grandi dimensioni l'allocazione della memoria nella JVM puo' diventare molto pesante
portando a rallentamenti, ad errori, all'attivazione dell'OOM Killer, ...
Se il richiamo avviene in un framework puo' essere difficile individuare la causa del problema
e comprendere dove effettuare le impostazioni corrette.
Ovviamente il problema dell'occupazione della memoria puo' essere aggirato utilizzando le clausole SQL standard LIMIT ed OFFSET. Ma si tratta di un workaround: in Postgres l'utilizzo della clausola OFFSET generalmente non e' efficiente dal punto di vista prestazionale. La soluzione corretta e' quella descritta che prevede l'impostazione di entrambe autoCommit e fetchSize. Il fetchSize puo' essere dimensionato anche elevato per esecuzioni batch: si ha maggior consumo di memoria ma un numero inferiore di fetch eseguite.
Tecnicamente si tratta di componenti differenti dal JDBC ma anche il client psql raccoglie tutti i record risultanti da una query, per default. Anche in questo caso l'impostazione per evitarlo e' semplice: impostare il parametro psql FETCH_COUNT. Non ci credete? Provate!
\timing select * from generate_series(1,10000000); \set FETCH_COUNT 100 select * from generate_series(1,10000000);
Altre paginette sull'utilizzo del JDBC con PostreSQL sono: Utilizzare PostgreSQL JDBC - Prepared Statements, Utilizzare PostgreSQL JDBC - Slow ?!
Le fonti definitive di tutte le informazioni sono il sito ufficiale JDBC PostgreSQL e GitHub.
Titolo: Utilizzare PostgreSQL JDBC - evitando OOM
Livello: Avanzato
Data:
14 Febbraio 2024 ❤️
Versione: 1.0.1 - 31 Ottobre 2024 🎃
Autore: mail [AT] meo.bogliolo.name