57 votes

Utilisation de ScrollableResults d'Hibernate pour lire lentement 90 millions d'enregistrements

J'ai simplement besoin de lire chaque ligne d'une table de ma base de données MySQL à l'aide d'Hibernate et d'écrire un fichier sur cette base. Mais il y a 90 millions de lignes et elles sont plutôt grosses. Il m'a donc semblé que la méthode suivante serait appropriée :

ScrollableResults results = session.createQuery("SELECT person FROM Person person")
            .setReadOnly(true).setCacheable(false).scroll(ScrollMode.FORWARD_ONLY);
while (results.next())
    storeInFile(results.get()[0]);

Le problème est que le programme ci-dessus va essayer de charger les 90 millions de lignes dans la RAM avant de passer à la boucle while... et cela va tuer ma mémoire avec OutOfMemoryError : Java heap space exceptions :(.

Je suppose donc que ScrollableResults n'est pas ce que je recherchais ? Quelle est la bonne façon de gérer cela ? Cela ne me dérange pas si cette boucle while prend des jours (enfin, j'aimerais bien que ce ne soit pas le cas).

Je suppose que la seule autre façon de gérer cela est d'utiliser setFirstResult et setMaxResults pour itérer dans les résultats et utiliser les résultats Hibernate normaux au lieu des ScrollableResults. Mais cela semble inefficace et prendra un temps fou lorsque j'appellerai setFirstResult à la 89 millionième ligne...

MISE À JOUR : setFirstResult/setMaxResults ne fonctionne pas, il s'avère que cela prend un temps anormalement long pour arriver aux offsets comme je le craignais. Il doit y avoir une solution ! N'est-ce pas une procédure assez standard ? Je suis prêt à renoncer à Hibernate et à utiliser JDBC ou ce qu'il faut.

MISE À JOUR 2 : la solution que j'ai trouvée, qui fonctionne bien, mais pas super, est essentiellement de la forme suivante :

select * from person where id > <offset> and <other_conditions> limit 1

Comme j'ai d'autres conditions, même toutes dans un index, ce n'est toujours pas aussi rapide que je le voudrais... donc toujours ouvert à d'autres suggestions...

31voto

Michael Points 4166

L'utilisation de setFirstResult et setMaxResults est votre seule option à ma connaissance.

Traditionnellement, un jeu de résultats défilant ne transfère les lignes au client qu'en fonction des besoins. Malheureusement, le MySQL Connector/J fait semblant, il exécute l'ensemble de la requête et la transporte vers le client, de sorte que le pilote a en fait l'ensemble des résultats chargés en RAM et vous les transmet au compte-gouttes (comme en témoignent vos problèmes de mémoire insuffisante). Vous avez eu la bonne idée, c'est juste des lacunes dans le pilote java de MySQL.

Je n'ai trouvé aucun moyen de contourner ce problème, et j'ai donc choisi de charger de gros morceaux en utilisant les méthodes habituelles setFirst/max. Désolé d'être le porteur de mauvaises nouvelles.

Assurez-vous simplement d'utiliser une session sans état, afin qu'il n'y ait pas de cache au niveau de la session ou de suivi sale, etc.

EDIT :

Votre UPDATE 2 est le meilleur que vous obtiendrez à moins que vous ne sortiez du J/Connecteur MySQL. Mais il n'y a aucune raison pour que vous ne puissiez pas augmenter la limite de la requête. Si vous disposez de suffisamment de RAM pour contenir l'index, cette opération devrait être relativement bon marché. Je la modifierais légèrement, en saisissant un lot à la fois, et en utilisant l'identifiant le plus élevé de ce lot pour saisir le lot suivant.

Remarque : cela ne fonctionnera que si autres_conditions utiliser l'égalité (aucune condition d'intervalle n'est autorisée) et avoir la dernière colonne de l'index en tant que id .

select * 
from person 
where id > <max_id_of_last_batch> and <other_conditions> 
order by id asc  
limit <batch_size>

21voto

Sean S. Points 101

Vous devriez être en mesure d'utiliser un ScrollableResults mais il faut quelques incantations magiques pour le faire fonctionner avec MySQL. J'ai rédigé mes conclusions dans un article de blog ( http://www.numerati.com/2012/06/26/reading-large-result-sets-with-hibernate-and-mysql/ ) mais je vais résumer ici :

"La documentation [JDBC] dit :

To enable this functionality, create a Statement instance in the following manner:
stmt = conn.createStatement(java.sql.ResultSet.TYPE_FORWARD_ONLY,
                java.sql.ResultSet.CONCUR_READ_ONLY);
stmt.setFetchSize(Integer.MIN_VALUE);

Cela peut être fait en utilisant l'interface Query (cela devrait également fonctionner pour Criteria) dans la version 3.2+ de l'API Hibernate :

Query query = session.createQuery(query);
query.setReadOnly(true);
// MIN_VALUE gives hint to JDBC driver to stream results
query.setFetchSize(Integer.MIN_VALUE);
ScrollableResults results = query.scroll(ScrollMode.FORWARD_ONLY);
// iterate over results
while (results.next()) {
    Object row = results.get();
    // process row then release reference
    // you may need to evict() as well
}
results.close();

Cela vous permet de transmettre l'ensemble des résultats en continu, mais Hibernate continuera à mettre les résultats en cache dans le dossier de l'utilisateur. Session Vous devez donc appeler session.evict() ou session.clear() de temps en temps. Si vous ne lisez que des données, vous pouvez envisager d'utiliser un fichier de type StatelessSession mais vous devriez lire sa documentation au préalable."

19voto

Haris Points 273

Définissez la taille de recherche dans la requête à une valeur optimale comme indiqué ci-dessous.

De même, lorsque la mise en cache n'est pas nécessaire, il peut être préférable d'utiliser StatelessSession.

ScrollableResults results = session.createQuery("SELECT person FROM Personne person") .setReadOnly(true). setFetchSize( 1000 ) .setCacheable(false).scroll(ScrollMode.FORWARD_ONLY)

9voto

ChechuHa Points 41

FetchSize doit être Integer.MIN_VALUE sinon cela ne fonctionnera pas.

Elle doit être prise littéralement dans la référence officielle : http://dev.mysql.com/doc/refman/5.0/en/connector-j-reference-implementation-notes.html

3voto

einnocent Points 757

En fait, vous auriez pu obtenir ce que vous vouliez - des résultats défilants à faible mémoire avec MySQL - si vous aviez utilisé la réponse mentionnée ici :

Streaming de grands ensembles de résultats avec MySQL

Notez que vous aurez des problèmes avec le chargement paresseux d'Hibernate, car il lancera une exception sur toute requête effectuée avant la fin du défilement.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X