Statement.setFetchSize(nSize)方法在SQL Server JDBC驱动程序中到底是做什么的?

70

我有一张非常大的表,每天都会有几百万条记录,每天结束时我都会提取前一天的所有记录。我是这样做的:

String SQL =  "select col1, col2, coln from mytable where timecol = yesterday";
Statement.executeQuery(SQL);

问题在于这个程序需要使用2GB内存,因为它将所有结果都加载到内存中进行处理。

我尝试设置了Statement.setFetchSize(10),但它从操作系统中占用的内存量完全相同,没有任何区别。我正在使用Microsoft SQL Server 2005 JDBC Driver

是否有办法像Oracle数据库驱动程序一样,在执行查询时分批读取结果,仅显示少量行,并随着向下滚动显示更多结果?

8个回答

86
在JDBC中,setFetchSize(int)方法对于控制JVM从数据库获取数据的网络调用次数以及ResultSet处理所使用的RAM量非常重要。
如果正在调用setFetchSize(10)但驱动程序忽略它,则可能只有两个选择:
1.尝试使用将遵循fetch-size提示的不同JDBC驱动程序。 2.查看连接上的特定于驱动程序的属性(在创建Connection实例时的URL和/或属性映射)。
结果集是响应查询在DB中编组的行数。行集是每次从JVM到DB的调用中提取的行块。这些调用数量和处理所需的RAM取决于fetch-size设置。
因此,如果结果集具有100行且fetch-size为10,则将进行10个网络调用以检索所有数据,在任何给定时间使用大约10 * {row-content-size} RAM。
默认的fetch-size是10,这相当小。在发布的情况下,驱动程序似乎忽略了fetch-size设置,在一个调用中检索所有数据(RAM需求大,最少的最佳网络调用)。
ResultSet.next()下面发生的事情是,它实际上并不逐行从结果集中提取数据。它从(本地)行集中提取数据,并在本地客户端用完时从服务器中不可见地提取下一个行集。
所有这些都取决于驱动程序,因为设置只是一种“提示”,但实际上我发现这是许多驱动程序和数据库的工作方式(在许多版本的Oracle、DB2和MySQL中进行了验证)。

我知道这是一个旧答案。但是我有一个问题。fetchsize和scroll之间有什么区别?据我理解,滚动是用来使我们不一次性获取所有结果的。 - Jigar Shah
2
滚动是通过结果集向前/向后移动的过程。为了滚动,人们固有地获取或可能已经获取到JVM内存中。尽管JDBC提供了向后滚动获取的选项,但各种数据库中的游标可能仅支持向前。因此,fetch-size仍然是设置从数据库上跨网络获取多少数据的方式。滚动设置不会影响JDBC中的这一点。 - Darrell Teague
我认为OP并没有说setFetchSize被忽略了。你可以使用较小的fetch size,但驱动程序仍然会拉取整个结果集,这可能很大,并且会使用相当多的内存。 - jbu
该帖子中指出调用了fetchSize()方法,但返回的行集仍然会消耗2GB的内存(实际上忽略了fetch size)。大多数驱动程序将遵守fetchSize()调用,从而不会“拉取整个结果集”(进入JVM中的客户端/内存)。请参见有关结果集、行集以及如何设置JDBC驱动程序以使其有效的整个答案和讨论。可以说,不遵守该调用的驱动程序可能不符合规范,或者可能是在供应商专有方式中被覆盖。 - Darrell Teague
仅供参考,MSSQL文档本身引用了setFetchSize()的使用来“减少获取的行数”。https://learn.microsoft.com/en-us/sql/connect/jdbc/using-adaptive-buffering?view=sql-server-ver15 - Darrell Teague

33
fetchSize参数是对JDBC驱动程序的提示,表示从数据库中一次获取多少行数据。但驱动程序可以选择忽略该提示并执行其认为合适的操作。一些驱动程序(例如Oracle)会分批获取行,因此您可以读取非常大的结果集而不需要大量内存。其他驱动程序则只会一次性读取整个结果集,我猜想您的驱动程序就是这样做的。
您可以尝试升级驱动程序到SQL Server 2008版本(可能更好),或者使用开源jTDS驱动程序。

2
绝对正确。对于 MSSQL,jTDS 驱动程序是更好的选择。 - BalusC
如何在jTDS驱动程序上将responseBuffering设置为自适应模式?我指的是在查询级别上而不是在驱动程序级别上。 - eugenevd
MSSQL驱动程序非常独特,并且围绕BLOB数据类型有许多讨论,因为它具有自适应缓冲功能。但是其中隐藏着JDBC setFetchSize()方法,可以“减少检索的行数”。请参见我的答案。https://learn.microsoft.com/en-us/sql/connect/jdbc/using-adaptive-buffering?view=sql-server-ver15 - Darrell Teague

19

您需要确保连接上的自动提交已关闭,否则设置setFetchSize将没有效果。

dbConnection.setAutoCommit(false);

编辑:记得当我使用这个修复程序时,它是针对Postgres的,但希望它仍然适用于SQL Server。


8
我不了解Postgres,但是设置自动提交与SELECT语句和/或查询获取大小没有任何关系(它们实际上是无关的)。 - Darrell Teague
@jwaddell 如果连接的自动提交已打开,则 setFetchSize 不起作用,这句话是否也适用于 Oracle 11g? - Kuldeep Jain
1
提交(Commit)是数据操作语言(DML)语句的一个函数,例如在两阶段提交(2PC)数据库中的INSERT、UPDATE、DELETE。这与查询无关,而“获取”(fetch)则与“SELECT”相关。因此,在Oracle、MySQL或SQLServer等任何其他关系型数据库管理系统中,自动提交(DML语句在执行时自动提交,而不必事后单独执行提交)与提取无关。 - Darrell Teague
这是多年前的事情,但我不能就此放过——你真的声称提交仅与2PC相关吗?而提交与查询没有任何关联?你需要重新审视ACID原则。提交在任何事务中都会提交DML。对于查询:在支持它的任何RDBMS中设置隔离级别为“可串行化”。进行查询,观察结果,但不要提交。在另一个事务中执行类似的查询。第二个事务将被阻塞,直到第一个事务提交(如果第一个查询进行了更新)。 - Dick Chesterwood
1
Postgres文档确认了这一点,这里给出的答案是正确的:“连接不能处于自动提交模式。后端在事务结束时关闭游标,因此在自动提交模式下,后端将在可以从中获取任何内容之前关闭游标。*语句必须使用ResultSet.TYPE_FORWARD_ONLY类型的ResultSet创建。这是默认设置,因此不需要重写任何代码来利用它,但这也意味着您无法向后滚动或以其他方式在ResultSet中跳转。” - Dick Chesterwood
1
ACID保证与查询结果相关,但除非修改标准数据库配置,否则SELECT语句(而不是SELECT-FOR-UPDATE)没有阻塞行为。否则,全球的数据库都会在每个DML操作上暂停查询。 - Darrell Teague

6

3
听起来像是mssql jdbc正在为您缓冲整个结果集。您可以添加一个连接字符串参数,说selectMode=cursor或responseBuffering=adaptive。如果您在2005 mssql jdbc驱动程序的2.0+版本上,则响应缓冲应默认为自适应。

http://msdn.microsoft.com/en-us/library/bb879937.aspx


当启用时,了解“自适应”属性实际上是做什么会很有用。在某个(套接字/网络)级别上,客户端从服务器获取一些结果集的块(如前面讨论的影响性能/JVM内存要求)。 - Darrell Teague
MSSQL文档本身引用了“setFetchSize()”方法。https://learn.microsoft.com/en-us/sql/connect/jdbc/using-adaptive-buffering?view=sql-server-ver15 - Darrell Teague

2

听起来您想在查询中限制返回的行数并分页显示结果。如果是这样,您可以像下面这样做:

select * from (select rownum myrow, a.* from TEST1 a )
where myrow between 5 and 10 ;

您只需要确定您的边界。

相关 - 一些数据库支持查询术语,如“LIMIT=x”,以限制结果集中的行数到一个固定值。然而,根据行排序等因素,这有点像是一个(任意的)钉锤。 - Darrell Teague
1
阅读MsSQL JDBC驱动程序的setFetchSize(size)SqlServer - srk

1

我在一个项目中遇到了完全相同的问题。问题在于,即使获取大小足够小,JDBCTemplate仍然会读取查询结果的所有内容,并将其映射到一个巨大的列表中,可能会耗尽内存。最终,我扩展了NamedParameterJdbcTemplate以创建一个函数,该函数返回一个对象流。该流基于JDBC通常返回的ResultSet,但仅在流需要时从ResultSet中提取数据。如果您不保留此流产生的所有对象的引用,则此方法可行。我在实现上受到了org.springframework.jdbc.core.JdbcTemplate#execute(org.springframework.jdbc.core.ConnectionCallback)的启发。唯一的真正区别在于如何处理ResultSet。最终,我编写了这个函数来包装ResultSet:

private <T> Stream<T> wrapIntoStream(ResultSet rs, RowMapper<T> mapper) {
    CustomSpliterator<T> spliterator = new CustomSpliterator<T>(rs, mapper, Long.MAX_VALUE, NON-NULL | IMMUTABLE | ORDERED);
    Stream<T> stream = StreamSupport.stream(spliterator, false);
    return stream;
}
private static class CustomSpliterator<T> extends Spliterators.AbstractSpliterator<T> {
    // won't put code for constructor or properties here
    // the idea is to pull for the ResultSet and set into the Stream
    @Override
    public boolean tryAdvance(Consumer<? super T> action) {
        try {
            // you can add some logic to close the stream/Resultset automatically
            if(rs.next()) {
                T mapped = mapper.mapRow(rs, rowNumber++);
                action.accept(mapped);
                return true;
            } else {
                return false;
            }
        } catch (SQLException) {
            // do something with this Exception
        }
    }
}

你可以添加一些逻辑使得该流变为“可自动关闭”,否则在完成后不要忘记关闭它。


这是一个典型的Spring模板示例,展示了什么是不应该做的(通过使其更加复杂、效率更低并完全错过重点)。抱歉...没有个人恩怨 - 显然有一些Spring教条主义过程,其中Java被扭曲成一个模板。简而言之,JDBC模板(像所有模板一样)只适用于Hello World 101类型的调用,以获取一行或其他内容。它不能直接允许设置fetchSize() - Hibernate也有同样的问题。这里的Streams和Splitters无法解决管理结果集大小的问题。 - Darrell Teague

1

试试这个:

String SQL = "select col1, col2, coln from mytable where timecol = yesterday";

connection.setAutoCommit(false);
PreparedStatement stmt = connection.prepareStatement(SQL, SQLServerResultSet.TYPE_SS_SERVER_CURSOR_FORWARD_ONLY, SQLServerResultSet.CONCUR_READ_ONLY);
stmt.setFetchSize(2000);

stmt.set....

stmt.execute();
ResultSet rset = stmt.getResultSet();

while (rset.next()) {
    // ......

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接