2010-05-13 22 views
45

Ho semplicemente bisogno di leggere ogni riga di una tabella nel mio database MySQL usando Hibernate e scrivere un file basato su di esso. Ma ci sono 90 milioni di file e sono piuttosto grandi. Così sembrava che quanto segue sarebbe opportuno:Utilizzo di ScrollableResults di Hibernate per leggere lentamente 90 milioni di record

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

Il problema è quanto sopra sarà cercare di caricare tutti i 90 milioni di righe nella RAM prima di passare al ciclo while ... e che ucciderà la mia memoria con OutOfMemoryError : Java heap space exceptions :(.

Quindi penso che ScrollableResults non è quello che stavo cercando? Qual è il modo corretto per gestire questo? Non mi importa se questo ciclo richiede giorni (beh, io lo adoro)

Immagino che l'unico altro modo per gestirlo sia usare setFirstResult e setMaxResults per iterare attraverso i risultati e solo noi e risultati di Hibernate regolari invece di ScrollableResults. Sembra che sarà inefficiente e inizierà a prendere un tempo ridicolmente lungo quando chiamerò setFirstResult sull'89 millesimo di fila ...

AGGIORNAMENTO: setFirstResult/setMaxResults non funziona, si scopre di prendere un tempo insolitamente lungo per arrivare agli offset come temevo. Ci deve essere una soluzione qui! Non è una procedura abbastanza standard ?? Sono disposto a rinunciare a Hibernate e utilizzare JDBC o qualsiasi altra cosa.

UPDATE 2: la soluzione che è venuta in mente che funziona ok, non eccezionale, è sostanzialmente di forma:

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

Dal momento che ho altre condizioni, anche il tutto in un indice, non è ancora Veloce come vorrei essere ... quindi ancora aperto per altri suggerimenti ..

+0

Potrebbe essere possibile suddividere i dati in modo che non sia necessario leggerli contemporaneamente, ad esempio: http://stackoverflow.com/questions/8325745/how-to-implement-several-threads-in -java-per-download-a-single-table-data/29502316 # 29502316 – rogerdpack

risposta

28

L'utilizzo di setFirstResult e setMaxResults è l'unica opzione di cui sono a conoscenza.

Tradizionalmente un set di risultati scorrevole può solo trasferire righe al client in base alle esigenze. Sfortunatamente il connettore MySQL/J in realtà si finge, esegue l'intera query e la trasporta nel client, quindi il driver ha effettivamente l'intero set di risultati caricato nella RAM e lo gocciolerà a voi (evidenziato dai problemi di memoria insufficiente) . Hai avuto l'idea giusta, sono solo carenze nel driver java MySQL.

Non ho trovato alcun modo per aggirare questo problema, così sono andato con il caricamento di blocchi di grandi dimensioni usando i normali metodi setFirst/max. Mi spiace essere il portatore di cattive notizie.

Basta fare in modo di utilizzare una sessione senza quindi non c'è alcun cache di livello di sessione o di monitoraggio sporco ecc

EDIT:

l'aggiornamento 2 è il meglio che si vuole ottenere a meno che non si interrompe fuori il J/Connettore MySQL. Anche se non c'è motivo per cui non si riesca a superare il limite della query. A condizione che tu abbia abbastanza RAM per contenere l'indice, questa dovrebbe essere un'operazione alquanto economica. Modificherei leggermente e afferro un batch alla volta e uso l'id più alto di quel batch per afferrare il batch successivo.

Nota: questo funziona solo se other_conditions uguaglianza uso (senza condizioni intervallo consentito) e ha l'ultima colonna dell'indice come id.

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

L'uso di StatelessSession è un consiglio particolarmente carino! – javashlook

+0

setFirstResult e setMaxResults non è un'opzione valida.Avevo ragione nella mia ipotesi che sarebbe stato insolitamente lento. Forse funziona per piccoli tavoli, ma molto velocemente ci vuole troppo tempo. Puoi testarlo nella console MySQL semplicemente eseguendo "seleziona * da qualsiasi limite 1 offset 3000000". Questo potrebbe richiedere 30 minuti ... –

+0

In esecuzione "select * from geoplanet_locations limit 1 offset 1900000;" contro il set di dati Geoplanet YAHOO (5 mil), ritorna in 1.34 secondi. Se hai abbastanza RAM per mantenere l'indice nella RAM, penso che i tuoi 30 minuti siano lontani. Stranamente "seleziona * da geoplanet_locations dove id> 56047142 limite 10;" ritorna in pochissimo tempo (il cliente regolare restituisce solo 0.00). – Michael

1

Con 90 milioni di record, sembra che si dovrebbe essere in batch i SELECT. Ho terminato con Oracle quando eseguivo il caricamento iniziale in una cache distribuita. Guardando la documentazione di MySQL, l'equivalente sembra essere utilizzando la clausola LIMIT: http://dev.mysql.com/doc/refman/5.0/en/select.html

Ecco un esempio:

SELECT * from Person 
LIMIT 200, 100 

Ciò restituire le righe da 201 a 300 della tabella Person.

Dovresti prima ottenere il numero di record dal tuo tavolo e poi dividerlo in base alle dimensioni del batch e calcolare i parametri di looping e LIMIT da lì.

L'altro vantaggio di questo sarebbe il parallelismo: è possibile eseguire più thread in parallelo su questo per un'elaborazione più rapida.

L'elaborazione di 90 milioni di record non sembra il punto ideale per l'utilizzo di Hibernate.

+0

Che non funziona neanche ... Prova a fare una selezione (batch o altro) dove l'offset è in milioni, ci vorrà molto a lungo. Sono disposto a bypassare Hibernate, qualche suggerimento per farlo senza Hibernate? –

+0

Prova questo articolo per una soluzione alla performance LIMIT: http://www.facebook.com/note.php?note_id=206034210932 – SteveD

+0

Ottimo articolo stevendick! Sicuramente utile, grazie. –

0

Ho usato la funzionalità di scorrimento Hibernate con successo prima senza di esso leggere l'intero set di risultati in. Qualcuno ha detto che MySQL non fa i cursori di scorrimento vero, ma sostiene di base JDBC dmd.supportsResultSetType (ResultSet .TYPE_SCROLL_INSENSITIVE) e la ricerca in giro sembra come se fossero state utilizzate da altre persone. Assicurati che non stia memorizzando nella cache gli oggetti Person nella sessione: l'ho usato su query SQL in cui non esisteva alcuna entità da memorizzare nella cache. Puoi chiamare sfratto alla fine del ciclo per essere sicuro o testare con una query sql. Gioca anche con setFetchSize per ottimizzare il numero di viaggi sul server.

1

Il problema potrebbe essere che Hibernate mantiene i riferimenti a tutti gli oggetti nella sessione finché non si chiude la sessione. Questo non ha nulla a che fare con il caching delle query. Forse sarebbe utile rimuovere() gli oggetti dalla sessione, dopo aver finito di scrivere l'oggetto sul file. Se non sono più riferimenti dalla sessione, il garbage collector può liberare la memoria e non si esaurirà più memoria.

+0

il problema è che l'ibernazione non ritorna nemmeno dalla query fino a quando non vengono recuperate tutte le righe, quindi non posso nemmeno rimuovere nulla() finché non viene caricato tutto. –

+0

Mi dispiace, l'ho perso nella domanda. Se è davvero un problema con il driver MySQL, probabilmente non ci sono altre opzioni quindi suddividere la query da solo in più query, poiché era già stata pubblicata. Sto utilizzando ScrollableResults con il driver jTDS per MSSQL e questo ha aiutato a prevenire OutOfMemoryErrors durante l'elaborazione di set di dati di grandi dimensioni da un database, quindi l'idea stessa non è probabilmente sbagliata. – Reboot

3

In realtà si potrebbe avere ottenuto quello che voleva - low-memory risultati scorrevoli con MySQL - se si fosse usato la risposta menzionati qui:

Streaming large result sets with MySQL

noti che si avranno problemi con Hibernate pigro -loading perché genererà un'eccezione su tutte le query eseguite prima dello scorrimento.

15

Imposta la dimensione del recupero in query su un valore ottimale come indicato di seguito.

Inoltre, quando la memorizzazione nella cache non è richiesta, potrebbe essere preferibile utilizzare StatelessSession.

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

Questa è la strada da percorrere. Vedi http://javaquirks.blogspot.dk/2007/12/mysql-streaming-result-set.html per ulteriore riferimento. – sbrattla

+0

Quindi voi ragazzi state dicendo che per MYSql usate Integer.MIN_VALUE ma per Oracle o altri dovreste impostare la dimensione del recupero su un numero ragionevole? – markthegrea

+0

Questa soluzione non dipende dal database. Stessi lavori per qualsiasi database. – Haris

19

si dovrebbe essere in grado di utilizzare un , anche se richiede un paio di incantesimi magici per ottenere lavorare con MySQL.Ho scritto i miei risultati in un post sul blog (http://www.numerati.com/2012/06/26/reading-large-result-sets-with-hibernate-and-mysql/), ma cercherò di riassumere qui:

"Il [JDBC] documentazione dice:

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); 

Questo può essere fatto utilizzando l'interfaccia di query (questo dovrebbe funzionare per Criteri pure) a partire dalla versione 3.2 delle API di 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(); 

Ciò consente di generare flussi sopra il set di risultati, tuttavia Hibernate risultati ancora di cache nella Session, quindi avrai bisogno di chiamare session.evict() o session.clear() ogni tanto. Se sei solo leggendo i dati, si potrebbe considerare l'utilizzo di un StatelessSession, anche se si dovrebbe leggere la documentazione in anticipo."

+3

Perché scegliere Session # flush() con una sessione di sola lettura? Sei sicuro di non aver voluto dire Session # evict (row) o Session # clear() che avrebbe aiutato a mantenere sotto controllo la dimensione della cache di livello 1. –

+0

(per i follower, l'esempio di codice utilizzato per indicare flush ma che ora indica sfratto o chiaro) – rogerdpack

1

propongo più di un sample code, ma un modello di query basata su Hibernate di fare questa soluzione per voi (pagination, scrolling e clearing sessione di Hibernate).

può anche essere facilmente adattato per usare un EntityManager.

0

recentemente ho lavorato su un problema come questo, e ho scritto un blog su come affrontare questo problema. è molto simile, spero di essere utile per chiunque. uso l'approccio lista pigro con adquisizione parziale. i Sostituito il limite e l'offset o l'impaginazione della query su una impaginazione manuale. Nel mio esempio, i selezionare restituisce 10 milioni di dischi, li faccio e li inseriscono in una "tabella temporale":

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; 

dopo che, posso impaginare senza contare ogni riga, ma con il progressivo assegnato:

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

Dalla prospettiva java, ho implementato questa impaginazione attraverso l'adquisizione parziale con una lista pigra. questo è, un elenco che si estende dalla lista astratta e implementa il metodo get(). Il metodo get può utilizzare un'interfaccia di accesso ai dati per continuare ottenere serie successiva di dati e rilasciare il mucchio di memoria:

@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> 
} 

da altro canto, la domanda di accesso ai dati uso interfaccia compagine e realizza un metodo per scorrere progressivamente, ciascuna 25000 record per completare tutto.

risultati di questo approccio può essere visto qui http://www.arquitecturaysoftware.co/2013/10/laboratorio-1-iterar-millones-de.html

+2

Nota che [risposte solo per collegamento] (http://meta.stackoverflow.com/tags/link-only-answers/info) sono scoraggiato, le risposte di SO dovrebbero essere il punto di arrivo di una ricerca di una soluzione (rispetto a un'altra fermata di riferimenti, che tendono a diventare obsoleti nel tempo). Si prega di considerare l'aggiunta di una sinossi autonoma qui, mantenendo il collegamento come riferimento. – kleopatra