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

Correct use of cursors for very large result sets in Postgres

我的问题的简短版本:

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

更多详情

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

Note: The current implementation of RETURN NEXT and RETURN QUERY stores the entire result set before returning from the function, as discussed above. That means that if a PL/pgSQL function produces a very large result set, performance might be poor: data will be written to disk to avoid memory exhaustion, but the function itself will not return until the entire result set has been generated.

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

但是(如果我理解正确的话)当你编写一个 returns 游标的函数时,在函数的用户可以开始使用任何东西之前,整个查询不会缓冲到内存(和磁盘)中,而是可以一点一点地消耗结果。 (设置和使用游标的开销更多,但避免为非常大的结果集分配大量缓冲区是值得的。)

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

我想了解这与通过线路连接到 Postgres 服务器的 SELECTS 和 FETCHES 有何关系。

在所有情况下,我都在谈论使用客户端代码的结果,客户端代码在幕后的套接字上与 Postgres 通信(实际上在我的例子中使用 Npgsql 库)。

问题 1:如果我尝试执行 "SELECT * FROM AstronomicallyLargeTable" 作为我通过网络连接到 Postgres 的唯一命令会怎样?那会为整个 select 分配所有内存,然后开始将数据发回给我吗?或者它会(有效地)生成自己的游标并一次将数据流回一点(服务器上没有大量额外的缓冲区分配)?

问题 2:如果我已经有了一个对天文数字大的结果集的游标引用(比如因为我已经完成了一次往返,并从某个函数取回了游标引用),然后我执行 "FETCH ALL FROM cursorname" 通过电线连接到 Postgres?这是愚蠢的,因为它会在将任何内容发回给我之前为 Postgres 服务器 上的所有结果 分配所有内存?还是 "FETCH ALL FROM cursorname" 真的会像我希望的那样工作,在我使用数据时缓慢地流回数据,而不会在 Postgres 服务器上发生任何大量缓冲区分配?

编辑:进一步说明

我问的是这样一种情况,我知道我的数据访问层一次一行地将数据从服务器流式传输给我(所以 没有 大型客户端那里涉及缓冲区,无论数据流有多长)并且我也知道我自己的应用程序一次消耗一行数据然后丢弃它(所以 no 客户端缓冲区在那里, 任何一个)。我绝对不想将所有这些行提取到客户端内存中,然后对它们进行处理。我知道那将是完全愚蠢的!

所以我认为所有的问题(对于刚才描述的用例)都是关于 PostgreSQL 需要多长时间才能开始流式传输以及它将分配多少内存缓冲区,对于 FETCH ALL。如果(这是一个很大的 'IF'...)PostgreSQL 不会 在开始之前为所有行分配一个巨大的缓冲区,并且如果它将行流回 Npgsql 一个在有一段时间,快速开始,然后我相信(但请告诉我 why/if 我错了)FETCH ALL FROM cursorname!

仍然有一个明确的用例

当您需要处理天文数字的大数据集并使用 SELECT * FROMRETURN QUERY 时,您不仅在服务器上而且在客户端上都需要一个天文数字的大缓冲区。然后您需要等待天文数字般长的时间才能让它通过您的网络到达。内部没有使用游标。

当使用 CURSOR 时,您可以克服缓冲,但是 FETCH ALL 只会很愚蠢,因为您迫使游标放弃它的设计目的:零碎地呈现数据库中的数据.在服务器端,您可以避免缓冲,因为数据在生成时通过网络发送,但客户端仍需要缓冲所有数据。

一些框架(如 Hibernate)在幕后进行缓冲,但我不知道 lower-level 库中的类似功能,如 Npgsql 或 JDBC 驱动程序。但是这种缓冲也是有代价的,特别是天文数字上的大量 SELECT * FROM table LIMIT 1000 OFFSET 23950378000 或类似的东西。

在任何一种情况下,如果您确实有如此大量的数据要处理,那么您更好地进行处理server-side,例如在 PL/pgSQL 函数中,然后将结果发送给客户端。服务器计算机不仅通常比客户端更强大,而且您还可以避免大部分网络开销。

经过一些试验,PostgreSQL 的行为似乎是这样的:

  • 使用 SELECT * FROM large 获取许多行不会在服务器端创建临时文件,数据在扫描时流式传输。

  • 如果您使用 returns refcursor 函数创建服务器端游标并从游标中获取行,则首先在服务器上收集所有返回的行。如果您 运行 FETCH ALL.

  • 这会导致创建一个临时文件

这是我对包含 1000000 行的 table 进行的实验。 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;
    

    结果:创建了一个大约 140MB 的临时文件。

我真的不知道为什么 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 来做一些 non-sql foo,我会推迟到这里的其他答案,但这确实需要在这里说。