将大型 ResultSet 写入文件

Posted

技术标签:

【中文标题】将大型 ResultSet 写入文件【英文标题】:Writing a large ResultSet to a File 【发布时间】:2011-08-25 18:01:26 【问题描述】:

我正在尝试将一个大的 ResulSet(~1mm 行)写入单个文件。在 Java 1.6 中是否有首选/有效的方法来执行此操作?

【问题讨论】:

~1mm 有多大?只是好奇 @Bozho:该副本的答案没有考虑任何 JDBC 细节。 【参考方案1】:

这取决于使用的 JDBC 驱动程序。您需要指示 JDBC 驱动程序预先将整个 ResultSet 加载到 Java 的内存中,而是在每次 next() 调用时按行加载它。然后,在ResultSet#next() 循环中,您需要立即将数据写入文件,而不是将其保存在List 或其他东西中。

不清楚您使用的是什么 JDBC 驱动程序,但例如,可以指示 mysql JDBC 驱动程序按照MySQL JDBC driver documentation 的以下方式按每行提供结果集:

结果集

默认情况下,ResultSet 被完全检索并存储在内存中。在大多数情况下这是最有效的操作方式,并且由于 MySQL 网络协议的设计更容易实现。如果您正在使用具有大量行或大值的 ResultSet,并且无法在 JVM 中为所需的内存分配堆空间,您可以告诉驱动程序一次将结果流回一行。

要启用此功能,您需要通过以下方式创建一个 Statement 实例:

 stmt = connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
 stmt.setFetchSize(Integer.MIN_VALUE);

这是一个具体的启动示例:

try (
    PrintWriter writer = new PrintWriter(new OutputStreamWriter(new BufferedOutputStream(new FileOutputStream("/records.txt")), "UTF-8"));
    Connection connection = dataSource.getConnection();
    Statement statement = connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
) 
    statement.setFetchSize(Integer.MIN_VALUE);

    try (ResultSet resultSet = statement.executeQuery("SELECT col1, col2, col3 FROM sometable")) 
        while (resultSet.next()) 
            writer.append(resultSet.getString("col1")).append(",")
                  .append(resultSet.getString("col2")).append(",")
                  .append(resultSet.getString("col3")).println();
        
    

顺便说一句,我会先检查数据库是否没有内置的 SQL 支持,这样可以更有效地完成此操作。例如,MySQL 对此有一个SELECT INTO OUTFILE construct。

SELECTSELECT ... INTO OUTFILE 'file_name' 形式将选定的行写入文件。该文件是在服务器主机上创建的,因此您必须具有FILE 权限才能使用此语法。 file_name 不能是现有文件,这样可以防止 /etc/passwd 和数据库表等文件被破坏。从 MySQL 5.1.6 开始,character_set_filesystem 系统变量控制文件名的解释。

【讨论】:

@Nathan:这取决于 JDBC 驱动程序。 API 文档字面意思是 “给 JDBC 驱动程序一个提示...” 注意,一个提示。我在回答中引用的 MySQL 文档中提到了正确的值。 是的,您的答案肯定就在这里。我只是想知道为什么 setFetchSize(1) 不是一个足够好的提示。也许我可以按照这些思路提出另一个问题。 @Nathan:同样,这取决于 JDBC 驱动程序。 JDBC API 中没有指定有效/允许值的内容。您需要查阅 JDBC 驱动程序实现特定文档。 @balusc 感谢您的意见。我正在使用 Oracle 11g 驱动程序。我已经尝试过性能改进有限的 setFetchSize()。我认为这个问题是两方面的——一个是加载大的 ResultSet,另一个是写入文件。在我的桌面(3GHz Core 2 Duo、4 GB RAM@800MHz 总线和 7200RPM HDD)上,使用不带缓冲的 FileOutputStream(w/或 w/o setFetchSize()),这需要大约 40 分钟来写入。我想知道您是否有任何使用 NIO 包的示例,由于通过 JVM 进行 DMA 访问,这可能有助于更快地写入磁盘。 相关:webmoli.com/2009/02/01/… 您可能希望将 Oracle 默认提取大小增加到例如100(如果内存允许的话)。你真的应该用BufferedOutputStream缓冲FileOutputStream,如我的代码示例所示。这会有很大的不同。如果 Oracle 不支持 SELECT INTO OUTFILE 之类的东西,我已经环顾四周,但似乎你必须为此编写一个 proc。我不是Oracle DB,所以我不能详细说明,对不起。【参考方案2】:

来自 GitHub:https://github.com/OhadR/ohadr.common/blob/master/src/main/java/com/ohadr/common/utils/resultset/ResultSetConverters.java

public static void writeResultSetToWriter(ResultSet resultSet, PrintWriter writer) throws SQLException

ResultSetMetaData metadata = resultSet.getMetaData();
int numColumns = metadata.getColumnCount();
int numRows = 0;

while(resultSet.next())             //iterate rows

    ++numRows;
    JSONObject obj = new JSONObject();      //extends HashMap
    for (int i = 1; i <= numColumns; ++i)           //iterate columns
    
        String column_name = metadata.getColumnName(i);
        obj.put(column_name, resultSet.getObject(column_name));
    
    writer.println(obj.toJSONString());

    if(numRows % 1000 == 0)
        writer.flush();

【讨论】:

以上是关于将大型 ResultSet 写入文件的主要内容,如果未能解决你的问题,请参考以下文章

如何在大型 php 应用程序中写入文件(多问)

在执行期间将大型多维数组读取和写入二进制文件并返回数组?

什么java类型可以写入BLOB类型中

输入sql语句,将结果写入到xml文件

将整个列表写入java中的文件

在 PHP 中不使用太多内存的情况下读取/写入大型 XML