如何从巨大的表中读取所有行?

时间:2010-09-10 06:23:15

标签: java postgresql jdbc

我在处理数据库中的所有行时遇到问题(PostgreSQL)。我收到一个错误:org.postgresql.util.PSQLException: Ran out of memory retrieving query results.我认为我需要以小块读取所有行,但它不起作用 - 它只读取100行(下面的代码)。怎么做?

    int i = 0;      
    Statement s = connection.createStatement();
    s.setMaxRows(100); // bacause of: org.postgresql.util.PSQLException: Ran out of memory retrieving query results.
    ResultSet rs = s.executeQuery("select * from " + tabName);      
    for (;;) {
        while (rs.next()) {
            i++;
            // do something...
        }
        if ((s.getMoreResults() == false) && (s.getUpdateCount() == -1)) {
            break;
        }           
    }

6 个答案:

答案 0 :(得分:64)

简短版本是,致电stmt.setFetchSize(50);conn.setAutoCommitMode(false);,以避免将整个ResultSet读入内存。

以下是docs所说的内容:

  

根据光标获取结果

     

默认情况下,驱动程序会立即收集查询的所有结果。   这对于大型数据集而言是不方便的,因此JDBC驱动程序也是如此   提供了一种仅在数据库游标上建立ResultSet的方法   获取少量行。

     

在连接的客户端缓存少量行   当用尽时,检索下一行行   重新定位光标。

     

注意:

     
      
  • 不能在所有情况下使用基于游标的ResultSet。有许多限制会使司机默默无闻   回过头来立即获取整个ResultSet。

  •   
  • 与服务器的连接必须使用V3协议。这是服务器版本的默认设置(仅受支持)   7.4及以后.-

  •   
  • 连接不得处于自动提交模式。后端在事务结束时关闭游标,因此在自动提交模式下   在任何事情发生之前,后端都会关闭光标   取自它.-

  •   
  • 必须使用ResultSet类型ResultSet.TYPE_FORWARD_ONLY创建Statement。这是默认值,因此没有代码   需要重写以利用这一点,但它也   意味着你不能向后滚动或以其他方式跳转   在ResultSet中.-

  •   
  • 给出的查询必须是单个语句,而不是与分号串在一起的多个语句。

  •   

示例5.2。设置提取大小以打开和关闭光标。

将代码更改为游标模式就像将Statement的获取大小设置为适当的大小一样简单。将提取大小设置为0将导致缓存所有行(默认行为)。

// make sure autocommit is off
conn.setAutoCommit(false);
Statement st = conn.createStatement();

// Turn use of the cursor on.
st.setFetchSize(50);
ResultSet rs = st.executeQuery("SELECT * FROM mytable");
while (rs.next()) {
   System.out.print("a row was returned.");
}
rs.close();

// Turn the cursor off.
st.setFetchSize(0);
rs = st.executeQuery("SELECT * FROM mytable");
while (rs.next()) {
   System.out.print("many rows were returned.");
}
rs.close();

// Close the statement.
st.close();

答案 1 :(得分:37)

使用a CURSOR in PostgreSQLlet the JDBC-driver handle this for you

处理大型数据集时,LIMIT和OFFSET会变慢。

答案 2 :(得分:5)

事实证明问题的症结在于,默认情况下,Postgres以“autoCommit”模式启动,并且它还需要/使用游标来“分页”通过数据(例如:读取前10K结果)然后是下一个,然后是下一个),但游标只能存在于事务中。所以默认是将所有行总是读入RAM,然后允许程序在它全部到达之后开始处理“第一个结果行,然后是第二个”,原因有两个,它不在事务中(所以游标)不起作用),并且还没有设置提取大小。

那么psql命令行工具如何实现查询的批量响应(FETCH_COUNT设置),就是在短期事务中“包装”其选择查询(如果事务不是但是打开),游标可以工作。您也可以使用JDBC执行类似的操作:

  static void readLargeQueryInChunksJdbcWay(Connection conn, String originalQuery, int fetchCount, ConsumerWithException<ResultSet, SQLException> consumer) throws SQLException {
    boolean originalAutoCommit = conn.getAutoCommit();
    if (originalAutoCommit) {
      conn.setAutoCommit(false); // start temp transaction
    }
    try (Statement statement = conn.createStatement()) {
      statement.setFetchSize(fetchCount);
      ResultSet rs = statement.executeQuery(originalQuery);
      while (rs.next()) {
        consumer.accept(rs); // or just do you work here
      }
    } finally {
      if (originalAutoCommit) {
        conn.setAutoCommit(true); // reset it, also ends (commits) temp transaction
      }
    }
  }
  @FunctionalInterface
  public interface ConsumerWithException<T, E extends Exception> {
    void accept(T t) throws E;
  }

这提供了需要更少RAM的好处,并且在我的结果中,即使您不需要保存RAM,它似乎总体上运行得更快。奇怪的。它还带来了第一行处理“更快启动”的好处(因为它一次处理一页)。

以下是如何使用“原始postgres游标”的方式,以及完整的演示版code,尽管在我的实验中,上面的JDBC方式似乎稍微快一点。

另一种选择是在任何地方都关闭autoCommit模式,但您仍然必须始终为每个新语句手动指定fetchSize(或者您可以在URL字符串中设置默认提取大小)。

答案 3 :(得分:2)

我认为您的问题类似于此主题:JDBC Pagination,其中包含您需要的解决方案。

特别是对于PostgreSQL,您可以在请求中使用LIMIT和OFFSET关键字:http://www.petefreitag.com/item/451.cfm

PS:在Java代码中,我建议你使用PreparedStatement而不是简单的语句:http://download.oracle.com/javase/tutorial/jdbc/basics/prepared.html

答案 4 :(得分:0)

我是这样做的。不是我想的最好的方式,但它有效:)

    Connection c = DriverManager.getConnection("jdbc:postgresql://....");
    PreparedStatement s = c.prepareStatement("select * from " + tabName + " where id > ? order by id");
    s.setMaxRows(100);
    int lastId = 0;
    for (;;) {
        s.setInt(1, lastId);
        ResultSet rs = s.executeQuery();

        int lastIdBefore = lastId;
        while (rs.next()) {
            lastId = Integer.parseInt(rs.getObject(1).toString());
            // ...
        }

        if (lastIdBefore == lastId) {
            break;
        }
    }

答案 5 :(得分:0)

至少在我的情况下,问题出在客户端上,试图获取结果。

想要获得所有结果的.csv。

我使用

找到了解决方案
psql -U postgres -d dbname  -c "COPY (SELECT * FROM T) TO STDOUT WITH DELIMITER ','"

(其中dbname是db的名称...)并重定向到文件。