我在处理数据库中的所有行时遇到问题(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;
}
}
答案 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 PostgreSQL或let 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的名称...)并重定向到文件。