Как прочитать все строки из огромной таблицы? - PullRequest
48 голосов
/ 10 сентября 2010

У меня проблема с обработкой всех строк из базы данных (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 ]

64 голосов
/ 12 сентября 2010

Короткая версия - звоните stmt.setFetchSize(50); и conn.setAutoCommit(false);, чтобы избежать чтения всего ResultSet в память.

Вот что говорят документы :

Получение результатов на основе курсора

По умолчанию драйвер собирает все результаты запроса сразу. Это может быть неудобно для больших наборов данных, поэтому драйвер JDBC предоставляет средство для базирования ResultSet на курсоре базы данных и только извлечение небольшого количества строк.

Небольшое количество строк кэшируется на стороне клиента соединения и когда исчерпан, следующий блок строк извлекается изменить положение курсора.

Примечание:

  • ResultSets на основе курсора не может использоваться во всех ситуациях. Есть ряд ограничений, которые заставят водителя молча вернуться к получению всего ResultSet сразу.

  • Соединение с сервером должно осуществляться по протоколу V3. Это значение по умолчанию (и поддерживается только) версиями сервера 7,4 и позже .-

  • Соединение не должно находиться в режиме автоматической фиксации. Бэкэнд закрывает курсоры в конце транзакции, поэтому в режиме автоматической фиксации бэкэнд закроет курсор, прежде чем что-либо может быть взято из него .-

  • Оператор должен быть создан с типом ResultSet ResultSet.TYPE_FORWARD_ONLY. Это значение по умолчанию, поэтому код не будет нужно переписать, чтобы воспользоваться этим, но это также означает, что вы не можете прокрутить назад или прыгать в ResultSet .-

  • Данный запрос должен представлять собой один оператор, а не несколько операторов, соединенных точкой с запятой.

Пример 5.2. Установка размера выборки для включения и выключения курсоров.

Переключение кода в режим курсора так же просто, как установка размера выборки в операторе соответствующего размера. Установка размера выборки обратно в 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();

38 голосов
/ 10 сентября 2010

Используйте CURSOR в PostgreSQL или , чтобы драйвер JDBC справился с этим для вас .

LIMIT и OFFSET будут работать медленно при обработке больших наборов данных.

6 голосов
/ 27 ноября 2017

Таким образом, выясняется, что суть проблемы заключается в том, что по умолчанию Postgres запускается в режиме «autoCommit», а также ему нужны / используются курсоры, чтобы иметь возможность «пролистывать» данные (например: читать первые 10K результатов). , затем следующий, затем следующий), однако курсоры могут существовать только внутри транзакции. Таким образом, по умолчанию всегда считываются все строки в ОЗУ, а затем разрешается вашей программе начать обработку «первой строки результата, затем второй» после того, как все это поступило, по двум причинам, это не в транзакции (поэтому курсоры не работает), а также размер выборки не был установлен.

Итак, как инструмент командной строки 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;
  }

Это дает преимущество в том, что требуется меньше оперативной памяти, и, по моим результатам, в целом она работает быстрее, даже если вам не нужно экономить оперативную память. Weird. Это также дает преимущество в том, что ваша обработка первой строки «начинается быстрее» (поскольку она обрабатывает страницу за раз).

А вот как это сделать «курсором необработанного postgres» вместе с полным демо кодом , хотя в моих экспериментах казалось, что способ JDBC, приведенный выше, был несколько быстрее по любой причине.

Другим вариантом будет отключение режима autoCommit везде, хотя вам все равно придется всегда вручную указывать fetchSize для каждого нового оператора (или вы можете установить размер выборки по умолчанию в строке URL).

2 голосов
/ 10 сентября 2010

Я думаю, что ваш вопрос похож на эту тему: 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

0 голосов
/ 14 февраля 2013

Как минимум в моем случае проблема была на клиенте, который пытается получить результаты.

Хотел получить .csv со ВСЕМИ результатами.

Я нашел решение, используя

psql -U postgres -d dbname  -c "COPY (SELECT * FROM T) TO STDOUT WITH DELIMITER ','"

(где dbname имя базы данных ...) и перенаправление в файл.

0 голосов
/ 12 сентября 2010

Я сделал это, как показано ниже. Не самый лучший способ, я думаю, но он работает :)

    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;
        }
    }
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...