2010-05-13 72 views
45

我只需要使用Hibernate讀取MySQL數據庫中的表中的每一行並根據它編寫一個文件。但是有9000萬行,它們非常大。因此,它似乎像以下是合適的:使用Hibernate的ScrollableResults緩慢讀取9000萬條記錄

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

的問題是上面會嘗試在移動到while循環之前,所有90個百萬行加載到RAM中......這會殺了我的記憶的OutOfMemoryError :Java堆空間異常:(。

所以我想ScrollableResults不是我在找什麼?處理這個問題的正確方法是什麼?我不介意如果這個while循環需要幾天(好吧我會愛它不)

我想唯一的其他方式來處理這個是使用setFirstResult和setMaxResults遍歷結果,只是我們定期的Hibernate結果而不是ScrollableResults。這感覺就像是效率低下,而且我會在第89萬行調用setFirstResult時開始花費很長時間...

更新:setFirstResult/setMaxResults不起作用,事實證明需要如我擔心的那樣長時間無法抵達偏移。這裏必須有一個解決方案!這不是一個非常標準的程序嗎?我願意放棄Hibernate並使用JDBC或其他方法。

更新2:該解決方案,我想出來的,其工作正常,不是很大,是基本形式:

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

因爲我有其他的條件下,即使所有的索引,它仍然不是儘快我希望它是...所以仍然打開其他建議..

+0

你也許能夠分割你的數據,所以你不必一次讀取多少,參考:http://stackoverflow.com/questions/8325745/how-to-implement-several-threads-in -java-for-downloading-a-single-table-data/29502316#29502316 – rogerdpack 2015-04-16 21:13:07

回答

28

使用setFirstResult和setMaxResults是我知道的唯一選項。

傳統上,可滾動結果集只會根據需要將行傳輸到客戶端。不幸的是,MySQL Connector/J實際上僞裝它,它執行整個查詢並將其傳輸到客戶端,所以驅動程序實際上已將整個結果集加載到RAM中,並將滴灌給您(由您的內存不足問題證明) 。你有正確的想法,這只是MySQL java驅動程序中的缺點。

我發現沒有辦法解決這個問題,所以使用常規setFirst/max方法加載大塊。對不起,成爲壞消息的使者。

只要確保,所以沒有會話級高速緩存或髒跟蹤等使用無狀態會話

編輯:

您的更新2就是你要得到的,除非你打出來的最好的MySQL J/Connector。儘管沒有理由無法提高查詢的限制。假如你有足夠的內存來保存索引,這應該是一個稍微便宜的操作。我會稍微修改它,並且一次抓取一個批次,並使用該批次的最高ID來抓取下一批。

注:這一點,如果other_conditions用平等(不允許範圍的條件),並有索引的最後一列ID纔會工作。

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

使用StatelessSession是特別好的提示! – javashlook 2010-05-13 14:15:49

+0

setFirstResult和setMaxResults不是一個可行的選項。我猜測它的速度會慢得不可思議。也許這適用於小桌子,但很快它就會花費太長時間。你可以通過簡單地運行「select * from anything limit 1 offset 3000000」來在MySQL控制檯中測試它。這可能需要30分鐘... – 2010-05-13 16:59:26

+0

運行「select * from geoplanet_locations limit 1 offset 1900000;」針對YAHOO Geoplanet數據集(5 mil行),返回1.34秒。如果你有足夠的內存來保存內存中的索引,那麼我認爲你的30分鐘數字一直沒有變化。有趣的是「select * from geoplanet_locations,其中id> 56047142限制爲10;」幾乎沒有時間返回(普通客戶只返回0.00)。 – Michael 2010-05-14 00:01:24

1

有9000萬條記錄,這聽起來像你應該批量你的選擇。在初始加載到分佈式緩存中時,我已經完成了Oracle的工作。縱觀MySQL文檔,相當於好像是用LIMIT子句:http://dev.mysql.com/doc/refman/5.0/en/select.html

下面是一個例子:

SELECT * from Person 
LIMIT 200, 100 

這將返回行201通過Person表300。

您需要首先從您的表中獲取記錄計數,然後將其除以批量大小,然後從中找出循環和LIMIT參數。

這樣做的另一個好處是並行性 - 您可以在此平行執行多個線程,以加快處理速度。

處理9000萬條記錄聽起來並不像使用Hibernate的甜蜜點。

+0

這也行不通...嘗試做一個選擇(批量或其他)的偏移量在數百萬,它會需要一個非常長時間。我願意繞過Hibernate,沒有Hibernate的任何建議嗎? – 2010-05-13 17:01:50

+0

請試試這篇文章以獲得LIMIT性能的解決方案:http://www.facebook.com/note.php?note_id=206034210932 – SteveD 2010-05-13 21:02:17

+0

偉大的文章stevendick!絕對有幫助,謝謝。 – 2010-05-15 10:24:43

0

我已經成功地使用過Hibernate的滾動功能,而其閱讀設置。有人說,MySQL不會做真正的滾動遊標的整個結果,但它聲稱基於JDBC的dmd.supportsResultSetType(的ResultSet .TYPE_SCROLL_INSENSITIVE)並在其周圍搜索似乎其他人已經使用它。確保它沒有在會話中緩存Person對象 - 我在沒有實體緩存的SQL查詢中使用它。您可以在循環結束時調用evict來確保或使用sql查詢進行測試。還可以使用setFetchSize來優化到服務器的訪問次數。

1

問題可能在於,Hibernate會在會話中保持對會話中所有對象的引用,直到您關閉會話。這與查詢緩存無關。在完成將對象寫入文件之後,可能會有助於將會話中的對象逐出()。如果它們不再被會話引用,則垃圾回收器可以釋放內存,並且不會再耗盡內存。

+0

問題是,hibernate甚至沒有從查詢中返回,直到檢索到所有行,所以我甚至無法驅逐()任何東西,直到它全部加載。 – 2010-07-16 05:39:51

+0

對不起,我錯過了這個問題。如果它真的是MySQL驅動程序的問題,那麼可能沒有其他選項,然後將查詢分解爲多個查詢,因爲它已經發布。我使用ScrollableResults和MSSQL的jTDS驅動程序,這有助於防止在處理來自數據庫的大數據集時出現OutOfMemoryErrors,所以這個想法本身可能沒有錯。 – Reboot 2010-07-19 09:35:17

3

其實你可能已經得到了你想要的東西 - 低內存滾動結果與MySQL的 - 如果你曾使用這裏提到的答案:

Streaming large result sets with MySQL

請注意,您有問題與Hibernate懶因爲它會在滾動完成之前執行的任何查詢中拋出異常。

15

將查詢中的提取大小設置爲如下所示的最佳值。

另外,當不需要緩存時,最好使用StatelessSession。

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

這是要走的路。請參閱http://javaquirks.blogspot.dk/2007/12/mysql-streaming-result-set.html以獲取更多參考。 – sbrattla 2013-05-01 06:02:21

+0

那麼你們是否說MYSql使用Integer.MIN_VALUE,但是對於Oracle或其他人,您應該將獲取大小設置爲合理數量? – markthegrea 2013-08-28 14:18:29

+0

該解決方案不依賴於數據庫。對於任何數據庫同樣適用。 – Haris 2013-09-09 10:14:18

19

您應該能夠使用,儘管它需要一些魔法咒語讓使用MySQL。我寫了我的發現在一篇博客文章(http://www.numerati.com/2012/06/26/reading-large-result-sets-with-hibernate-and-mysql/),但我會在這裏總結:

「的[JDBC]文件說:

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

這可以使用查詢界面來完成(這應該工作對於在Hibernate API的版本3.2+標準以及):

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

這可以讓你流過的結果集,但是Hibernate會仍然緩存導致Session,所以你需要調用session.evict()或每隔一段時間。如果你只是讀取數據時,你可能會考慮使用StatelessSession,但應事先閱讀它的文檔。」

+3

爲什麼會議#flush()與只讀會話?你確定你不是指會話#evict(行)或會話#clear(),這將有助於保持1級緩存大小在控制之下。 – 2013-08-20 01:03:43

+0

(對於追隨者,代碼示例曾用於提及刷新,但現在提到驅逐或清除) – rogerdpack 2015-04-16 17:06:07

1

我建議超過sample code多,但基於Hibernate查詢模板來做到這一點的解決辦法爲你(paginationscrollingclearing Hibernate會話)。

它也很容易地適應使用EntityManager

0

最近我工作過的一個問題是這樣的,我寫了一篇關於如何面對這個問題博客,很喜歡,我希望對任何人都有幫助。 我使用懶列表方法進行部分搜索。我將查詢的限制和偏移或分頁替換爲手動分頁。 在我的例子中,選擇收益1000萬的紀錄,我讓他們在一個「時間表」將它們插入:

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; 

在那之後,我可以分頁不指望每一行,但使用分配的順序:

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

從java的角度來看,我通過部分採用惰性列表實現了這種分頁。這是一個從Abstract列表擴展並實現get()方法的列表。 get方法可以使用數據訪問接口以繼續獲得下一組數據並釋放內存堆:由另一方面

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

,數據訪問接口使用查詢分頁並實現了一個方法,逐步迭代,每個25000條記錄完成全部。

結果這種方法可以在這裏 http://www.arquitecturaysoftware.co/2013/10/laboratorio-1-iterar-millones-de.html

+2

請注意,[僅鏈接答案](http://meta.stackoverflow.com/tags/link-only-answers/info)是不鼓勵,所以SO答案應該是尋求解決方案的終點(而另一個參考的中途停留時間往往會隨着時間的推移而變得陳舊)。請考慮在此添加獨立的摘要,並將鏈接保留爲參考。 – kleopatra 2013-10-28 16:23:44