Правильное использование курсоров для очень больших результирующих наборов в Postgres

краткая версия моего вопроса:

Если я держу ссылку курсора на астрономически огромный результирующий набор в моем клиентском коде, было бы смешно (т. е. полностью побеждает точку курсоров) выдавать "FETCH ALL FROM cursorname" в качестве моей следующей команды? Или это будет медленно передавать данные обратно мне, когда я их потребляю (по крайней мере, в принципе, предполагая, что у меня есть хорошо написанный драйвер, сидящий между мной и Postgres)?

больше деталь

Если я все понимаю правильно, то курсоры Postgres действительно предназначены для решения следующей проблемы [даже если они могут использоваться (злоупотреблять?) для других вещей, таких как возврат нескольких разных наборов результатов из одной функции]:

Примечание: текущая реализация запроса RETURN NEXT и RETURN сохраняет весь результирующий набор перед возвращением из функции, как рассмотренный выше. Это означает, что если функция PL/pgSQL производит очень большой результирующий набор, производительность может быть плохой: данные будут записаны на диск, чтобы избежать нехватки памяти, но сама функция не будет возврат до тех пор, пока не будет создан весь результирующий набор.

(ref: https://www.postgresql.org/docs/9.6/static/plpgsql-control-structures.html)

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

(ref:https://www.postgresql.org/docs/9.6/static/plpgsql-cursors.html#AEN66551)

Я хотел бы понять, как это относится к выбору и извлечению по проводу в Postgres сервер.

во всех случаях я говорю о потреблении результатов из клиентского кода, который общается с Postgres на сокете за кулисами (используя библиотеку Npgsql в моем случае, на самом деле).

Q1: что делать, если я попытаюсь выполнить "SELECT * FROM AstronomicallyLargeTable" как мою единственную команду по проводу к Postgres? Будет ли это выделить всю память для всего выбора, а затем начать отправлять данные обратно мне? Или он (эффективно) создаст свой собственный курсор и поток данные возвращаются немного за раз (без огромного дополнительного выделения буфера на сервере)?

Q2: что делать, если у меня уже есть ссылка курсора на астрономически большой результирующий набор (скажем, потому что я уже сделал один раунд и получил ссылку курсора из некоторой функции), а затем я выполняю "FETCH ALL FROM cursorname" по проводу в Postgres? Это глупо, потому что он выделит всю память для всех результатов на сервере Postgres перед посылаешь что-нибудь мне? Или "FETCH ALL FROM cursorname" действительно будет работать так, как я хотел бы, потоковая передача данных назад медленно, когда я ее потребляю, без какого-либо массивного выделения буфера на сервере Postgres?

EDIT: дальнейшее уточнение

Я спрашиваю о случае, когда я знаю, что мой уровень доступа к данным передает данные с сервера мне по одной строке за раз (so нет большие клиентские буферы нет, однако долго потоки данных для) и где я также знаю, что мое собственное приложение потребляет данные по одной строке за раз, а затем отбрасывает его (so нет буферы на стороне клиента там, либо). Я определенно не хочу извлекать все эти строки в память на стороне клиента, а затем что-то с ними делать. Я вижу, что это было бы полным безумием!

поэтому я думаю, что все проблемы (для только что описанного варианта использования) касаются того, сколько времени PostgreSQL займет для запуска потоковой передачи и сколько памяти буфер, который он выделит, для FETCH ALL. Если (и это большое "если"...) В PostgreSQL не выделите огромный буфер всех строк перед запуском, и если он будет передавать строки обратно в Npgsql по одному, начиная быстро, то я считаю (но, пожалуйста, скажите мне, почему/если я ошибаюсь), что все еще есть четкий вариант использования для FETCH ALL FROM cursorname!

3 ответов


после некоторых экспериментов кажется, что PostgreSQL ведет себя так:

  • получение многих строк с SELECT * FROM large не будет создавать временный файл на стороне сервера, данные передаются, как они проверяются.

  • если вы создаете курсор на стороне сервера с помощью функции, которая возвращает refcursor и извлекать строки из курсора, все возвращенные строки сначала собираются на сервере. Это приводит к созданию временного файла при запуске FETCH ALL.

вот мои эксперименты с таблицей, которая содержит 1000000 строк. work_mem установлено в 64kb (минимум). log_temp_files имеет значение 0, так что временные файлы сообщаются в журнале сервера.

  • первая попытка:

    SELECT id FROM large;
    

    результат: временный файл не создается.

  • вторая попытка:

    CREATE OR REPLACE FUNCTION lump() RETURNS refcursor
       LANGUAGE plpgsql AS
    $$DECLARE
       c CURSOR FOR SELECT id FROM large;
    BEGIN
       c := 'c';
       OPEN c;
       RETURN c;
    END;$$;
    
    BEGIN;
    SELECT lump();
     lump
    ------
     c
    (1 row)
    
    FETCH NEXT FROM c;
     id
    ----
      1
    (1 row)
    
    FETCH NEXT FROM c;
     id
    ----
      2
    (1 row)
    
    COMMIT;
    

    результат: нет временного файла создан.

  • третья попытка:

    BEGIN;
    SELECT lump();
     lump
    ------
     c
    (1 row)
    
    FETCH all FROM c;
       id
    ---------
           1
           2
           3
    ...
      999999
     1000000
    (1000000 rows)
    
    COMMIT;
    

    результат: создается временный файл размером около 140 МБ.

Я действительно не знаю, почему PostgreSQL ведет себя таким образом.


одна вещь, которая отсутствует в вашем вопросе, если вам действительно нужна функция plpgsql, а не встроенная функция sql. Я поднимаю его только потому, что ваше описание-простой сценарий -select * from hugetable. Поэтому я собираюсь ответить на вопрос, основываясь на этой информации.

в таком случае, ваша проблема на самом деле не проблема, потому что вызов функции может быть невидимым. Я имею в виду, что если вы можете написать функцию как встроенную функцию SQL, которую вы не указываете одним способом или другое, вам не нужно беспокоиться об этом конкретном ограничении plpgsql RETURN QUERY.

CREATE OR REPLACE FUNCTION foo()
RETURNS TABLE (id INT)
AS
$BODY$
SELECT * FROM bar;
$BODY$
LANGUAGE SQL STABLE;

посмотрите на план:

EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM foo() LIMIT 1;

QUERY PLAN
-------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.00..0.01 rows=1 width=4) (actual time=0.017..0.017 rows=1 loops=1)
   Buffers: shared hit=1
   ->  Seq Scan on bar  (cost=0.00..14425.00 rows=1000000 width=4) (actual time=0.014..0.014 rows=1 loops=1)
         Buffers: shared hit=1
 Planning time: 0.082 ms
 Execution time: 0.031 ms
(6 rows)

нет весь результирующий набор заполнен возвратилась.

https://wiki.postgresql.org/wiki/Inlining_of_SQL_functions

Я буду полагаться на другие ответы здесь, Если вам действительно нужен plpgsql, чтобы сделать некоторые не-sql foo, но это действительно нужно было сказать здесь.


когда вам нужно обработать астрономически большой набор данных и вы используете SELECT * FROM или RETURN QUERY вам нужен астрономически большой буфер не только на сервере, но и на клиенте. И тогда вам нужно астрономически долго ждать, пока он прибудет по вашей сети. Курсоры не используются внутри.

при использовании CURSOR вы можете преодолеть буферизацию, но FETCH ALL было бы просто глупо, потому что вы заставляете курсор отказаться от того, что он был разработан: представить данные из базы данных по частям. На стороне сервера вы избегаете буферизации, потому что данные отправляются по сети по мере их создания, но клиентской стороне все равно нужно будет буферизировать все данные.

некоторые фреймворки (например, Hibernate) выполняют буферизацию за кулисами, но я не знаю подобной функциональности в библиотеках нижнего уровня, таких как Npgsql или драйвер JDBC. Но эта буферизация тоже имеет свою цену, в частности астрономически большое количество SELECT * FROM table LIMIT 1000 OFFSET 23950378000 или что-то вроде что.

в любом случае, если у вас действительно есть такие большие объемы данных для обработки, вы много лучше делать обработку на стороне сервера, например, в функции PL/pgSQL, а затем отправлять результаты клиенту. Мало того, что серверные компьютеры обычно более способны, чем клиенты, вы также избегаете большинства сетевых накладных расходов.