在Postgres中正确使用游标来获得非常大的结果集

Mik*_*ton 9 postgresql cursor

我的问题的简短版本:

如果我在我的客户端代码中持有一个游标引用天文数字巨大的结果集,那么发出"FETCH ALL FROM cursorname"作为我的下一个命令会是荒谬的(即完全失去游标的点)吗?或者当我消耗它时,这会慢慢地将数据流回给我(至少在原则上,假设我有一个写得很好的驱动程序坐在我和Postgres之间)?

更多详情

如果我完全理解了事情,那么Postgres游标真的可以处理以下问题[即使它们可以被用来(滥用?)用于其他事情,例如从一个函数返回多个不同的结果集]:

注意:RETURN NEXT和RETURN QUERY的当前实现在从函数返回之前存储整个结果集,如上所述.这意味着如果PL/pgSQL函数产生非常大的结果集,性能可能会很差:数据将写入磁盘以避免内存耗尽,但在生成整个结果集之前,函数本身不会返回.

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

但是(再次,如果我理解正确)当你编写一个返回游标的函数时,整个查询不会被缓冲到内存(和磁盘)之前,函数的用户可以开始消耗任何东西,但是结果可以被消耗掉一点一滴.(设置和使用游标的开销更多,但是为了避免对非常大的结果集进行大量缓冲区分配,这是值得的.)

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

我想了解这是如何通过线路连接到Postgres服务器上的SELECTS和FETCHES.

在所有情况下,我都在讨论从客户端代码中获取结果,这些客户端代码在幕后的套接字上与Postgres进行通信(实际上在我的情况下使用Npgsql库).

Q1:如果我尝试执行"SELECT*FROM AstronomicallyLargeTable"作为我对Postgres的唯一命令,该怎么办?是否会为整个选择分配所有内存,然后开始向我发送数据?或者它(有效地)生成自己的游标并一次一点地回流数据(在服务器上没有大量额外的缓冲区分配)?

Q2:如果我已经有一个天文大的结果集的游标引用(比如因为我已经完成了一次往返,从一些函数中取回了游标引用),然后我执行"FETCH ALL FROM cursorname",该怎么办? Postgres的电线?这是愚蠢的,因为它会在向Postgres服务器发送任何内容之前为Postgres服务器上的所有结果分配所有内存吗?或者"FETCH ALL FROM cursorname"实际上是按照我的意愿工作,在我消耗数据的同时慢慢地将数据流回来,而Postgres服务器上没有发生任何大量的缓冲区分配?

编辑:进一步澄清

我问的是一个案例,我知道我的数据访问层将数据从服务器一次一行地传输到我一行(所以没有大的客户端缓冲区涉及到那里,无论数据流是多长时间)我也在哪里知道我自己的应用程序一次消耗一行数据然后丢弃它(因此也没有客户端缓冲区).我绝对不想将所有这些行提取到客户端内存中,然后用它们做一些事情.我看到那将是完全愚蠢的!

所以我认为所有的问题(对于刚才描述的用例)都是关于PostgreSQL开始流的时间和它将分配多少内存缓冲区的问题FETCH ALL.IF(并且它是一个很大的'IF'...)PostgreSQL 在开始之前不会为所有行分配一个巨大的缓冲区,如果它一次一行地将行流回Npgsql,快速启动,那么我相信(但请告诉我为什么/如果我错了)还有一个明确的用例FETCH ALL FROM cursorname!

Lau*_*lbe 5

经过一些实验后,似乎PostgreSQL的行为如下:

  • 获取许多行SELECT * FROM large不会在服务器端创建临时文件,数据在扫描时进行流式处理.

  • 如果使用返回refcursor并从游标中获取行的函数创建服务器端游标,则首先在服务器上收集所有返回的行.如果运行,这会导致创建临时文件FETCH ALL.

以下是我使用包含1000000行的表的实验.work_mem设置为64kb(最小值).log_temp_files设置为0,以便在服务器日志中报告临时文件.

  • 第一次尝试:

    SELECT id FROM large;
    
    Run Code Online (Sandbox Code Playgroud)

    结果:未创建临时文件.

  • 第二次尝试:

    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;
    
    Run Code Online (Sandbox Code Playgroud)

    结果:未创建临时文件.

  • 第三次尝试:

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

    结果:创建了大约140MB的临时文件.

我真的不知道PostgreSQL为什么会那样.

  • @ laurenz-albe,服务器端缓冲似乎是`generate_series()`的结果,而不是使用游标.如果您创建一个巨大的表(例如,通过执行`SELECT*INTO data FROM generate_series(1,10000000);`)然后更改您的函数以从中进行选择,则不会生成临时文件.这表明游标确实*不*总是导致服务器端缓冲. (4认同)