Utiliser les résultats de Scrollableresultsd'Hibernate pour lire lentement 90 millions de disques
j'ai simplement besoin de lire chaque ligne dans une table dans ma base de données MySQL en utilisant Hibernate et écrire un fichier basé sur elle. Mais il y a 90 millions de rangs et ils sont assez grands. Il semble donc que ce qui suit serait approprié:
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 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 que ScrollableResults n'est pas ce que je cherchais? Quelle est la bonne façon de gérer cela? Cela ne me dérange pas si cette boucle de temps prend des jours (Eh bien, je ne l'aimerais pas).
je suppose que la seule autre façon de gérer cela est d'utiliser setFirstResult et setMaxResults pour itérer à travers les résultats et juste utiliser des résultats hibernants réguliers au lieu de ScrollableResults. C'est comme si ça allait être inefficace et prendre un temps ridiculement long quand j'appelle premier résultat sur la 89 millionième rangée...
mise à jour: setFirstResult/setMaxResults ne fonctionne pas, il s'avère prendre un temps inutile pour obtenir les offsets comme je le craignais. Il doit y avoir une solution ici! N'est-ce pas une procédure assez standard?? Je suis prêt à renoncer à hiberner et à utiliser JDBC ou ce qu'il faut.
mise à jour 2: la solution que j'ai trouvé qui fonctionne bien, pas grand, est essentiellement de la forme:
select * from person where id > <offset> and <other_conditions> limit 1
comme j'ai d'autres conditions, même dans un index, ce n'est pas aussi rapide que je le voudrais... donc encore ouvert pour d'autres suggestions..
12 réponses
utiliser setFirstResult et setMaxResults est la seule option que je connaisse.
traditionnellement, un jeu de résultats déroulable ne transférerait des lignes au client que selon les besoins. Malheureusement, le connecteur MySQL / J le simule, exécute la requête entière et le transporte vers le client, de sorte que le pilote a en fait l'ensemble du résultat chargé en mémoire vive et vous l'alimentera en goutte à goutte (mis en évidence par vos problèmes de mémoire). Vous avez eu la bonne idée, ce ne sont que des défauts dans le pilote Java MySQL.
Je n'ai trouvé aucun moyen de contourner cela, donc est allé avec le chargement de gros morceaux en utilisant les méthodes setFirst/max régulière. Désolé d'être le porteur de mauvaises nouvelles.
assurez-vous juste d'utiliser une session apatride pour qu'il n'y ait pas de cache de niveau de session ou de suivi sale etc.
EDIT:
votre mise à jour 2 est la meilleure que vous allez obtenir à moins que vous ne sortiez de la MySQL J / Connecteur. Bien qu'il n'y ait aucune raison que vous ne puissiez pas augmenter la limite de la requête. Si vous avez assez de mémoire vive pour tenir l'index, cela devrait être une opération assez bon marché. Je le modifierais légèrement, et je prendrais un lot à la fois, et j'utiliserais le plus grand id de ce lot pour prendre le prochain lot.
Note: cela ne fonctionnera que si autres_conditions utiliser égalité (aucune condition de portée permise) et avoir la dernière colonne de l'index comme id .
select *
from person
where id > <max_id_of_last_batch> and <other_conditions>
order by id asc
limit <batch_size>
vous devriez être en mesure d'utiliser un ScrollableResults
, bien qu'il nécessite quelques incantations magiques pour obtenir de travailler avec MySQL. J'ai écrit mes conclusions dans un billet de blog ( http://www.numerati.com/2012/06/26/reading-large-result-sets-with-hibernate-and-mysql / ) mais je résumerai ici:
"[JDBC] documentation 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 de requête (cela devrait fonctionner pour les critères aussi bien) dans 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 streamer au-dessus du jeu de résultats, cependant Hibernate gardera les résultats de cache dans le Session
, de sorte que vous aurez besoin d'appeler session.evict()
ou session.clear()
de temps en temps. Si vous ne lisez que des données , vous pourriez utiliser un StatelessSession
, mais vous devriez lire sa documentation à l'avance."
définit la taille du fetch dans la requête à une valeur optimale comme indiqué ci-dessous.
aussi, lorsque la mise en cache n'est pas nécessaire, il peut être préférable d'utiliser Statelessession.
ScrollableResults results = session.createQuery("SELECT person FROM Person person")
.setReadOnly(true)
.setFetchSize( 1000 ) // <<--- !!!!
.setCacheable(false).scroll(ScrollMode.FORWARD_ONLY)
FetchSize doit être Integer.MIN_VALUE
, sinon ça ne marchera pas.
il doit être littéralement tiré de la référence officielle: https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-implementation-notes.html
en fait, vous auriez pu obtenir ce que vous vouliez -- résultats de scrollable à faible mémoire avec MySQL -- si vous aviez utilisé la réponse mentionnée ici:
la diffusion de gros ensembles de résultats avec MySQL
notez que vous aurez des problèmes avec le chargement paresseux Hibernate car il lancera une exception sur toutes les requêtes effectuées avant que le scroll ne soit terminé.
avec 90 millions de disques, on dirait que vous devriez faire vos sélections. J'en ai fini avec Oracle quand je fais le chargement initial dans une cache détournée. En regardant la documentation MySQL, l'équivalent semble utiliser la clause limite: http://dev.mysql.com/doc/refman/5.0/en/select.html
voici un exemple:
SELECT * from Person
LIMIT 200, 100
cela renverrait les lignes 201 à 300 du tableau Person
.
vous devez obtenir le nombre d'enregistrements de votre table d'abord et ensuite le diviser par la taille de votre lot et de travailler sur votre boucle et LIMIT
paramètres à partir de là.
L'autre avantage de ce serait parallélisme - vous pouvez exécuter plusieurs threads en parallèle ce pour un traitement plus rapide.
traiter 90 millions de disques ne semble pas être le bon endroit pour utiliser Hibernate.
le problème pourrait être, que Hibernate garde des références à tous les objets de la session jusqu'à ce que vous fermiez la session. Cela n'a rien à voir avec la mise en cache des requêtes. Peut-être que cela aiderait à expulser() les objets de la session, une fois que vous aurez terminé d'écrire l'objet dans le fichier. Si elles ne sont plus des références par la session, le collecteur d'ordures peut libérer la mémoire et vous ne manquerez plus de mémoire.
je propose plus qu'un exemple de code , mais un modèle de requête basé sur Hibernate
pour faire ce contournement pour vous ( pagination
, scrolling
et clearing
session D'hibernation).
il peut aussi être facilement adapté pour utiliser un EntityManager
.
j'ai utilisé la fonctionnalité de défilement Hibernate avec succès avant sans qu'il ne lit le résultat entier mis en. Quelqu'un a dit que MySQL ne fait pas de vrais curseurs de défilement, mais il prétend à basé sur le dmd JDBC.supportsResultSetType (ResultSet.TYPE_SCROLL_INSENSITIVE) et la recherche autour de lui semble comme d'autres personnes l'ont utilisé. Assurez - vous qu'il ne cache pas les objets de la personne dans la session-je l'ai utilisé sur les requêtes SQL où il n'y avait pas d'entité à mettre en cache. Vous pouvez appeler expulser à la fin de la boucle pour être sûr ou tester avec une requête sql. Aussi jouer avec setFetchSize pour optimiser le nombre de déplacements vers le serveur.
récemment, j'ai travaillé sur un problème comme celui-ci, et j'ai écrit un blog sur comment faire face à ce problème. c'est comme, j'espère être utile pour n'importe qui. j'utilise une approche de liste paresseuse avec une acquisition partielle. j'ai remplacé la limite et l'offset ou la pagination de la requête par une pagination manuelle. Dans mon exemple, le select renvoie 10 millions d'enregistrements, je les obtiens et les insère dans une "table temporelle":
create or replace function load_records ()
returns VOID as $$
BEGIN
drop sequence if exists temp_seq;
create temp sequence temp_seq;
insert into tmp_table
SELECT linea.*
FROM
(
select nextval('temp_seq') as ROWNUM,* from table1 t1
join table2 t2 on (t2.fieldpk = t1.fieldpk)
join table3 t3 on (t3.fieldpk = t2.fieldpk)
) linea;
END;
$$ language plpgsql;
après cela, je peux paginer sans compter chaque ligne mais en utilisant la séquence attribuée:
select * from tmp_table where counterrow >= 9000000 and counterrow <= 9025000
du point de vue de java, j'ai implémenté cette pagination par le biais d'une acquisition partielle avec une liste paresseuse. c'est une liste qui s'étend de l'Abstrait liste et implémente la méthode get (). La méthode get peut utiliser une interface d'accès aux données pour continuer à obtenir l'ensemble suivant de données et libérer le tas de mémoire:
@Override
public E get(int index) {
if (bufferParcial.size() <= (index - lastIndexRoulette))
{
lastIndexRoulette = index;
bufferParcial.removeAll(bufferParcial);
bufferParcial = new ArrayList<E>();
bufferParcial.addAll(daoInterface.getBufferParcial());
if (bufferParcial.isEmpty())
{
return null;
}
}
return bufferParcial.get(index - lastIndexRoulette);<br>
}
par ailleurs, l'interface d'accès aux données utilise query pour paginer et implémente une méthode pour itérer progressivement, chaque 25000 dossiers à remplir.
les résultats de cette approche sont disponibles ici http://www.arquitecturaysoftware.co/2013/10/laboratorio-1-iterar-millones-de.html
une autre option si vous êtes "à court de mémoire vive" est de simplement demander say, une colonne au lieu de l'objet entier comment utiliser les critères d'hibernation pour retourner seulement un élément d'un objet au lieu de l'objet entier? (économise beaucoup de temps de processus CPU au démarrage).
pour moi il a fonctionné correctement en mettant useCursors=true, sinon le jeu de résultat Scrollable ignore toutes les implémentations de taille fetch, dans mon cas il était de 5000 mais le jeu de résultat Scrollable a récupéré des millions d'enregistrements à la fois causant une utilisation excessive de mémoire. la DB sous-jacente est MSSQLServer.
jdbc: jtds: sqlserver: / / localhost:1433 / ACS;TDS=8.0; useCursors =true