J'ai un problème avec le traitement de toutes les lignes de la base de données (PostgreSQL). Je reçois une erreur: org.postgresql.util.PSQLException: Ran out of memory retrieving query results.
Je pense que je dois lire toutes les lignes en petits morceaux, mais cela ne fonctionne pas - il ne lit que 100 lignes (code ci-dessous). Comment faire ça?
int i = 0;
Statement s = connection.createStatement();
s.setMaxRows(100); // bacause of: org.postgresql.util.PSQLException: Ran out of memory retrieving query results.
ResultSet rs = s.executeQuery("select * from " + tabName);
for (;;) {
while (rs.next()) {
i++;
// do something...
}
if ((s.getMoreResults() == false) && (s.getUpdateCount() == -1)) {
break;
}
}
Utilisez un CURSEUR dans PostgreSQL ou laissez le pilote JDBC gérer cela pour vous .
LIMIT et OFFSET seront lents lors du traitement de grands ensembles de données.
La version courte est, appelez stmt.setFetchSize(50);
et conn.setAutoCommitMode(false);
pour éviter de lire la totalité de ResultSet
en mémoire.
Voici ce que disent les docs :
Obtenir des résultats basés sur un curseur
Par défaut, le pilote collecte tous les résultats de la requête en même temps . Cela peut être gênant pour les grands ensembles de données, explique le pilote JDBC fournit un moyen de baser un ResultSet sur un curseur de base de données et uniquement chercher un petit nombre de lignes.
Un petit nombre de lignes sont mises en cache du côté client de la connexion et quand épuisé le prochain bloc de lignes est récupéré par repositionner le curseur.
Remarque:
Les ResultSets basés sur le curseur ne peuvent pas être utilisés dans toutes les situations. Il existe un certain nombre de restrictions qui rendent le conducteur silencieux retomber à chercher tout le ResultSet à la fois.
La connexion au serveur doit utiliser le protocole V3. Ceci est la valeur par défaut pour (et est uniquement prise en charge par) les versions de serveur 7.4 et plus tard.-
La connexion ne doit pas être en mode autocommit. Le système ferme les curseurs à la fin des transactions, donc en mode de validation automatique le backend aura fermé le curseur avant que rien ne puisse être récupéré de lui.-
L'instruction doit être créée avec un type ResultSet de ResultSet.TYPE_FORWARD_ONLY. C'est la valeur par défaut, donc aucun code ne sera besoin d’être réécrit pour en tirer parti, mais c’est aussi signifie que vous ne pouvez pas faire défiler vers l'arrière ou sauter d'une autre manière dans le ResultSet.-
La requête donnée doit être une instruction unique, et non plusieurs instructions attachées avec des points-virgules.
Exemple 5.2. Définition de la taille d'extraction pour activer et désactiver les curseurs.
Changer le code en mode curseur est aussi simple que de définir la taille d'extraction de l'instruction à la taille appropriée. Si vous redéfinissez la taille d'extraction sur 0, toutes les lignes seront mises en cache (comportement par défaut).
// make sure autocommit is off
conn.setAutoCommit(false);
Statement st = conn.createStatement();
// Turn use of the cursor on.
st.setFetchSize(50);
ResultSet rs = st.executeQuery("SELECT * FROM mytable");
while (rs.next()) {
System.out.print("a row was returned.");
}
rs.close();
// Turn the cursor off.
st.setFetchSize(0);
rs = st.executeQuery("SELECT * FROM mytable");
while (rs.next()) {
System.out.print("many rows were returned.");
}
rs.close();
// Close the statement.
st.close();
Le problème est donc que, par défaut, Postgres démarre en mode "autoCommit" et nécessite/utilise des curseurs pour pouvoir "passer en revue" les données (par exemple, lire les 10 premiers résultats, puis le next, ensuite next), cependant les curseurs ne peuvent exister que dans une transaction. La valeur par défaut est donc de lire toutes les lignes, toujours, dans la RAM, puis de permettre à votre programme de commencer à traiter "la première ligne de résultat, puis la seconde" après son arrivée, pour deux raisons, ce n'est pas dans une transaction (donc ne fonctionne pas), et aussi une taille d'extraction n'a pas été définie.
Ainsi, la manière dont l'outil de ligne de commande psql
réalise une réponse par lots (son paramètre FETCH_COUNT
) pour les requêtes consiste à "envelopper" ses requêtes sélectionnées dans une transaction à court terme (si une transaction n'est pas encore ouverte), afin que les curseurs puissent fonctionner. Vous pouvez faire quelque chose comme ça aussi avec JDBC:
static void readLargeQueryInChunksJdbcWay(Connection conn, String originalQuery, int fetchCount, ConsumerWithException<ResultSet, SQLException> consumer) throws SQLException {
boolean originalAutoCommit = conn.getAutoCommit();
if (originalAutoCommit) {
conn.setAutoCommit(false); // start temp transaction
}
try (Statement statement = conn.createStatement()) {
statement.setFetchSize(fetchCount);
ResultSet rs = statement.executeQuery(originalQuery);
while (rs.next()) {
consumer.accept(rs); // or just do you work here
}
} finally {
if (originalAutoCommit) {
conn.setAutoCommit(true); // reset it, also ends (commits) temp transaction
}
}
}
@FunctionalInterface
public interface ConsumerWithException<T, E extends Exception> {
void accept(T t) throws E;
}
Cela donne l'avantage de nécessiter moins de RAM et, dans mes résultats, semblait globalement fonctionner plus rapidement, même si vous n'avez pas besoin de sauvegarder la RAM. Bizarre. Cela donne également l’avantage que votre traitement de la première ligne "démarre plus rapidement" (puisqu’il traite une page à la fois).
Et voici comment procéder de la manière suivante: "curseur postgres brut", avec démo code complets, même si, dans mes expériences, il semblait que la méthode JDBC, ci-dessus, était légèrement plus rapide pour une raison quelconque.
Une autre option serait de désactiver le mode autoCommit
partout, même si vous devez toujours spécifier manuellement fetchSize pour chaque nouvelle instruction (ou vous pouvez définir une taille d'extraction par défaut dans la chaîne d'URL).
Je pense que votre question est semblable à celle-ci: JDBC Pagination qui contient des solutions à vos besoins.
En particulier, pour PostgreSQL, vous pouvez utiliser les mots clés LIMIT et OFFSET dans votre requête: http://www.petefreitag.com/item/451.cfm
PS: En code Java, je vous suggère d’utiliser PreparedStatement au lieu de simples Statements: http://download.Oracle.com/javase/tutorial/jdbc/basics/prepared.html
Je l'ai fait comme ci-dessous. Pas le meilleur moyen que je pense, mais ça marche :)
Connection c = DriverManager.getConnection("jdbc:postgresql://....");
PreparedStatement s = c.prepareStatement("select * from " + tabName + " where id > ? order by id");
s.setMaxRows(100);
int lastId = 0;
for (;;) {
s.setInt(1, lastId);
ResultSet rs = s.executeQuery();
int lastIdBefore = lastId;
while (rs.next()) {
lastId = Integer.parseInt(rs.getObject(1).toString());
// ...
}
if (lastIdBefore == lastId) {
break;
}
}
Au moins dans mon cas, le problème concerne le client qui tente d'extraire les résultats.
Voulait obtenir un .csv avec TOUS les résultats.
J'ai trouvé la solution en utilisant
psql -U postgres -d dbname -c "COPY (SELECT * FROM T) TO STDOUT WITH DELIMITER ','"
(où nombase le nom de la base de données ...) et redirigeant vers un fichier.