web-dev-qa-db-fra.com

Utilisation d'Hibernate ScrollableResults 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 avec Hibernate et d'écrire un fichier basé sur celle-ci. Mais il y a 90 millions de lignes et elles sont assez grandes. Donc, il semblait 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, nous allons essayer de charger les 90 millions de lignes dans RAM avant de passer à la boucle while ... et cela va tuer ma mémoire avec OutOfMemoryError: Java:.

Donc 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 que cette boucle prenne plusieurs jours.

Je suppose que la seule autre façon de gérer cela consiste à utiliser setFirstResult et setMaxResults pour parcourir les résultats et simplement utiliser les résultats Hibernate normaux au lieu de ScrollableResults. Cela semble être inefficace et va prendre un temps ridiculement long lorsque j'appelle setFirstResult sur la 89 millionième rangée ...

UPDATE: setFirstResult/setMaxResults ne fonctionne pas, cela prend un temps inhabituellement long pour arriver aux compensations comme je le craignais. Il doit y avoir une solution ici! N'est-ce pas une jolie procédure standard ?? Je suis prêt à renoncer à Hibernate et à utiliser JDBC ou peu importe.

UPDATE 2: la solution proposée qui fonctionne bien, mais pas géniale, 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 souhaiterais ... donc toujours ouvert pour d'autres suggestions ..

50
at.

Utiliser setFirstResult et setMaxResults est votre seule option à ma connaissance.

Traditionnellement, un jeu de résultats à défilement ne transférait que des lignes au client selon les besoins. Malheureusement, le connecteur MySQL Connector/J le simule, il exécute la requête entière et la transporte vers le client. Le pilote a donc l'ensemble du résultat chargé dans RAM et vous le transmettra au fil de votre des problèmes de mémoire). Vous avez eu la bonne idée, ce sont juste des lacunes dans le pilote MySQL Java.

Je n'ai trouvé aucun moyen de contourner cela, alors je suis allé avec le chargement de gros morceaux en utilisant les méthodes régulières setFirst/max. Désolé d'être le porteur de mauvaises nouvelles.

Assurez-vous simplement d'utiliser une session sans état afin d'éviter tout cache au niveau de la session, aucun suivi incorrect, etc.

MODIFIER:

Votre UPDATE 2 est ce que vous obtiendrez de mieux, sauf si vous sortez de MySQL J/Connector. Bien qu'il n'y ait aucune raison que vous ne puissiez pas dépasser la limite de la requête. Si vous avez assez de RAM pour tenir l’index, cela devrait être une opération un peu moins chère. Je le modifiais légèrement, saisissais un lot à la fois et utilisais l'identifiant le plus élevé de ce lot pour saisir le prochain lot.

Remarque: cela ne fonctionnera que si other_conditions utilise l'égalité (aucune condition de plage n'est autorisée) et que la dernière colonne de l'index est définie comme id

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

Vous devriez pouvoir utiliser ScrollableResults, bien que quelques incantations magiques soient nécessaires pour travailler avec MySQL. J'ai écrit mes résultats dans un article de blog ( http://www.numerati.com/2012/06/26/reading-large-result-sets-with-hibernate-and-mysql/ ) mais je ' ll 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 diffuser sur le jeu de résultats, mais Hibernate conservera toujours les résultats dans la variable Session, vous devrez donc appeler session.evict() ou session.clear() de temps à autre. Si vous ne lisez que des données, vous pouvez envisager d’utiliser une variable StatelessSession, mais vous devez lire sa documentation au préalable. "

19
Sean S.

Définissez la taille d'extraction dans la requête sur une valeur optimale, comme indiqué ci-dessous.

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

ScrollableResults results = session.createQuery("SELECT person FROM Person person")
        .setReadOnly(true)
        .setFetchSize( 1000 ) // <<--- !!!!
        .setCacheable(false).scroll(ScrollMode.FORWARD_ONLY)
16
Haris

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

Il doit être pris littéralement de la référence officielle: https://dev.mysql.com/doc/connector-j/5.1/fr/connector-j-reference-implementation-notes.html

7
ChechuHa

En fait, vous auriez pu obtenir ce que vous vouliez - des résultats défilables avec peu de 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 lève une exception pour toutes les requêtes effectuées avant la fin du défilement.

3
einnocent

Le problème pourrait être qu'Hibernate conserve les références à tous les objests de la session jusqu'à sa fermeture. 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 avez fini d'écrire l'objet dans le fichier. S'ils ne sont plus des références par la session, le ramasse-miettes peut libérer de la mémoire et vous ne serez plus à court de mémoire.

1
Reboot

Je propose plus qu'un exemple de code , mais un modèle de requête basé sur Hibernate pour effectuer cette solution de contournement pour vous (pagination, scrolling et clearing session Hibernate).

Il peut également être facilement adapté pour utiliser un EntityManager.

1
smalbequi

Avec 90 millions d’enregistrements, il semble que vous devriez regrouper vos SELECT. Je l'ai fait avec Oracle lors du chargement initial dans un cache distribué. Dans la documentation MySQL, l’équivalent semble utiliser la clause LIMIT: http://dev.mysql.com/doc/refman/5.0/fr/select.html

Voici un exemple:

SELECT * from Person
LIMIT 200, 100

Cela renverrait les lignes 201 à 300 de la table Person.

Vous devez d'abord obtenir le nombre d'enregistrements de votre table, puis le diviser par la taille de votre lot et définir vos paramètres de boucle et LIMIT à partir de là.

L’autre avantage est le parallélisme: vous pouvez exécuter plusieurs threads en parallèle pour accélérer le traitement.

Traiter 90 millions d’enregistrements ne semble pas non plus être l’endroit idéal pour utiliser Hibernate.

1
SteveD

Pour moi, cela fonctionnait correctement en définissant useCursors = true; sinon, le jeu de résultats défilable ignore toutes les implémentations de la taille d'extraction; dans mon cas, il s'agissait de 5 000, mais Scrollable Resultset a extrait des millions d'enregistrements à la fois, entraînant une utilisation excessive de la mémoire. La base de données sous-jacente est MSSQLServer.

jdbc: jtds: sqlserver: // localhost: 1433/ACS; TDS = 8.0; useCursors = true

0
manu

Une autre option si vous "manquez de RAM" est de demander par exemple une colonne au lieu de l'objet entier Comment utiliser les critères de veille prolongée pour renvoyer un seul élément d'un objet à la place de l'objet entier? (enregistre beaucoup de temps de processus CPU pour démarrer).

0
rogerdpack

récemment, j'ai travaillé sur un problème comme celui-ci, et j'ai écrit un blog sur la façon dont ce problème est résolu. est très semblable, j'espère être utile pour n'importe qui. J'utilise l'approche par liste paresseuse avec acquisition partielle. i Remplacé la limite et l'offset ou la pagination de la requête à une pagination manuelle. Dans mon exemple, la sélection retourne 10 millions d’enregistrements, je les récupère et les insère dans un "tableau temporel":

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 assignée:

select * from tmp_table where counterrow >= 9000000 and counterrow <= 9025000

Du point de vue de Java, j’ai implémenté cette pagination par acquisition partielle avec une liste paresseuse. c'est-à-dire une liste qui s'étend de la liste abstraite et implémente la méthode get (). La méthode get peut utiliser une interface d'accès aux données pour continuer à obtenir le prochain jeu de données et libérer le segment 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>
}

d'autre part, l'interface d'accès aux données utilise une requête pour paginer et implémente une méthode pour itérer progressivement, chacun des 25 000 enregistrements pour compléter le tout.

les résultats de cette approche peuvent être consultés ici http://www.arquitecturaysoftware.co/2013/10/laboratorio-1-iterar-millones-de.html

0
user2928872

J'ai déjà utilisé la fonctionnalité de défilement Hibernate avec succès avant de lire l'ensemble des résultats. Quelqu'un a dit que MySQL ne vérifie pas les curseurs de défilement mais se base sur le dmd.supportsResultSetType de JDBC et effectue une recherche à ce sujet semble que d'autres personnes l'ont utilisé. Assurez-vous que les objets Personne ne sont pas mis en cache dans la session. Je l'ai utilisé pour des requêtes SQL sur lesquelles il n'y avait aucune entité à mettre en cache. Vous pouvez appeler evict à la fin de la boucle pour en être sûr ou tester avec une requête SQL. Jouez également avec setFetchSize pour optimiser le nombre de voyages sur le serveur.

0
Brian Deterling