58

我只需要使用 Hibernate 读取 MySQL 数据库中表中的每一行并基于它编写一个文件。但是有 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]);

问题是上面将尝试将所有 9000 万行加载到 RAM 中,然后再进入 while 循环......这会用 OutOfMemoryError: Java heap space exceptions 杀死我的内存:(。

所以我猜 ScrollableResults 不是我想要的?处理这个问题的正确方法是什么?我不介意这个 while 循环是否需要几天时间(我不希望这样)。

我想处理这个问题的唯一其他方法是使用 setFirstResult 和 setMaxResults 来遍历结果,并且只使用常规的 Hibernate 结果而不是 ScrollableResults。感觉好像它效率低下,并且当我在第 89 百万行调用 setFirstResult 时将开始花费非常长的时间......

更新: setFirstResult/setMaxResults 不起作用,结果需要很长时间才能达到我担心的偏移量。这里一定有解决办法!这不是一个非常标准的程序吗?我愿意放弃 Hibernate 并使用 JDBC 或任何它需要的东西。

更新2:我想出的解决方案可以正常工作,但不是很好,基本上是以下形式:

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

由于我还有其他条件,即使全部在索引中,它仍然没有我希望的那么快......所以仍然开放其他建议..

4

12 回答 12

32

使用 setFirstResult 和 setMaxResults 是我知道的唯一选择。

传统上,可滚动结果集只会根据需要将行传输到客户端。不幸的是,MySQL Connector/J 实际上是伪造的,它执行整个查询并将其传输到客户端,因此驱动程序实际上将整个结果集加载到 RAM 中,并将滴灌给您(您的内存不足问题证明了这一点) . 您的想法是正确的,这只是 MySQL java 驱动程序中的缺点。

我发现没有办法解决这个问题,所以使用常规的 setFirst/max 方法加载大块。很抱歉成为坏消息的带来者。

只要确保使用无状态会话,就没有会话级缓存或脏跟踪等。

编辑:

除非您突破 MySQL J/Connector,否则您的 UPDATE 2 是您将获得的最好的。虽然没有理由不能提高查询的限制。如果您有足够的 RAM 来保存索引,这应该是一个便宜的操作。我会稍微修改一下,一次抓取一批,然后使用该批次的最高 id 来抓取下一批。

注意:这仅在other_conditions使用相等(不允许范围条件)并且索引的最后一列为id时才有效。

select * 
from person 
where id > <max_id_of_last_batch> and <other_conditions> 
order by id asc  
limit <batch_size>
于 2010-05-13T11:56:15.407 回答
21

您应该能够使用 a ScrollableResults,尽管它需要一些魔法咒语才能使用 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 接口(这也适用于 Criteria)来完成:

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()or session.clear()。如果您只是读取数据,您可能会考虑使用StatelessSession,但您应该事先阅读它的文档。”

于 2012-07-31T18:08:31.260 回答
19

将查询中的提取大小设置为最佳值,如下所示。

此外,当不需要缓存时,使用 StatelessSession 可能会更好。

ScrollableResults results = session.createQuery("SELECT person FROM Person person")
        .setReadOnly(true)
        .setFetchSize( 1000 ) // <<--- !!!!
        .setCacheable(false).scroll(ScrollMode.FORWARD_ONLY)
于 2011-01-25T11:44:29.403 回答
9

FetchSize 必须是Integer.MIN_VALUE,否则将不起作用。

它必须从官方参考中直接获取:https ://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-implementation-notes.html

于 2012-01-18T19:39:28.123 回答
3

实际上,如果您使用了此处提到的答案,您本可以得到您想要的——使用 MySQL 的低内存可滚动结果:

使用 MySQL 流式传输大型结果集

请注意,您将遇到 Hibernate 延迟加载的问题,因为它会在滚动完成之前执行的任何查询上引发异常。

于 2010-12-10T18:47:13.787 回答
1

拥有 9000 万条记录,听起来您应该对 SELECT 进行批处理。在将初始加载到分布式缓存中时,我已经使用 Oracle 完成了。查看 MySQL 文档,等效的似乎是使用 LIMIT 子句:http ://dev.mysql.com/doc/refman/5.0/en/select.html

这是一个例子:

SELECT * from Person
LIMIT 200, 100

这将返回Person表的第 201 到 300 行。

您需要先从表中获取记录数,然后将其除以批量大小,然后LIMIT从那里计算出循环和参数。

这样做的另一个好处是并行性 - 您可以在此并行执行多个线程以加快处理速度。

处理 9000 万条记录听起来也不是使用 Hibernate 的最佳选择。

于 2010-05-13T11:45:34.643 回答
1

问题可能是,Hibernate 会保留对会话中所有对象的引用,直到您关闭会话。这与查询缓存无关。在将对象写入文件之后,从会话中 evict() 对象可能会有所帮助。如果它们不再被会话引用,垃圾收集器可以释放内存,你就不会再用完内存了。

于 2010-07-15T13:19:11.737 回答
1

我建议的不仅仅是一个示例代码,而是一个基于查询模板Hibernate来为你做这个解决方法(paginationscrollingHibernateclearing会话)。

它也可以很容易地适应使用EntityManager.

于 2013-04-07T07:57:58.517 回答
0

在没有读取整个结果集的情况下,我之前成功使用了 Hibernate 滚动功能。有人说 MySQL 不做真正的滚动游标,但它声称基于 JDBC dmd.supportsResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE) 并在它周围搜索好像其他人用过。确保它没有缓存会话中的 Person 对象——我已经在没有实体缓存的 SQL 查询中使用了它。您可以在循环结束时调用 evict 来确定或使用 sql 查询进行测试。还可以使用 setFetchSize 来优化访问服务器的次数。

于 2010-05-13T18:25:53.173 回答
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 的角度来看,我通过使用惰性列表的部分获取来实现这种分页。这是一个从抽象列表扩展并实现 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

于 2013-10-28T16:18:27.287 回答
0

如果您“内存不足”,另一种选择是仅请求说一列而不是整个对象如何使用休眠条件仅返回对象的一个​​元素而不是整个对象?(节省了大量的 CPU 进程启动时间)。

于 2015-04-16T21:15:00.140 回答
0

对我来说,在设置 useCursors=true 时它可以正常工作,否则 Scrollable Resultset 会忽略所有获取大小的实现,在我的情况下它是 5000,但 Scrollable Resultset 一次获取了数百万条记录,导致内存使用过多。底层数据库是 MSSQLServer。

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

于 2018-04-27T04:56:01.263 回答