Postgresql 中文操作指南

34.5. Pipeline Mode #

libpq 管道模式允许应用程序发送查询,而无需读取之前发送查询的结果。利用管道模式,客户端等待服务器的时间更少,因为可以在单个网络事务中发送/接收多个查询/结果。

虽然管道模式提供了显着的性能提升,但使用管道模式编写客户端更为复杂,因为它涉及管理待处理查询队列并查找队列中哪个结果对应哪个查询。

管道模式通常也在客户端和服务器上消耗更多内存,尽管仔细积极地管理发送/接收队列可以缓解这种情况。这适用于连接是否处于阻塞或非阻塞模式。

虽然 libpq 的管道 API 是在 PostgreSQL 14 中引入的,但它是一个客户端特性,不需要特殊服务器支持,并且可以在支持 v3 扩展查询协议的任何服务器上工作。有关更多信息,请参阅 Section 55.2.4

34.5.1. Using Pipeline Mode #

若要发出管道,应用程序必须将连接切换到管道模式,此操作可通过 PQenterPipelineMode 完成。 PQpipelineStatus 可用于测试管道模式是否处于活动状态。在管道模式下,仅允许利用扩展查询协议的 asynchronous operations ,不允许包含多个 SQL 命令的命令字符串,也不允许 COPY 。使用同步命令执行函数(如 PQfnPQexecPQexecParamsPQpreparePQexecPreparedPQdescribePreparedPQdescribePortal )是一种错误状态。 PQsendQuery 也不允许,因为它使用简单查询协议。在所有已派发的命令的处理其结果后,且最终管道结果已使用,应用程序可以使用 PQexitPipelineMode 返回到非管道模式。

Note

最好在 non-blocking mode 中与 libpq 结合使用管道模式。如果在阻塞模式中使用,则会出现客户端/服务器死锁。 [15 ]

34.5.1.1. Issuing Queries #

在进入管道模式后,应用程序使用 PQsendQueryParams 或其已准备查询兄弟 PQsendQueryPrepared 派发请求。这些请求在客户端排队,直至刷新到服务器; PQpipelineSync 用于在管道中建立同步点或调用 PQflush 时,就会发生这种情况。函数 PQsendPreparePQsendDescribePreparedPQsendDescribePortal 也可以在管道模式中使用。结果处理在下文中描述。

服务器按客户端发送的顺序执行语句,并返回结果。服务器将立即开始执行管道中的命令,而不等待管道结束。请注意,结果缓存在服务器端;当使用 PQpipelineSync 建立同步点,或者调用 PQsendFlushRequest 时,服务器将刷新该缓冲区。如果任何语句遇到错误,服务器将中止当前事务,并在下一个同步点之前不会执行队列中的任何后续命令;针对每个此类命令生成 PGRES_PIPELINE_ABORTED 结果。(即使管道中的命令回滚事务,这种情况仍然成立。)查询处理在同步点之后恢复。

对于一个操作依赖于前一个操作的结果,这是正常的;例如,一个查询可能定义一个表,管道中下一个查询使用该表。类似地,应用程序可以在管道中使用后面的语句创建命名的已准备语句并执行它。

34.5.1.2. Processing Results #

要处理管道中一个查询的结果,应用程序重复调用 PQgetResult 并处理每个结果,直到 PQgetResult 返回 null。然后可以使用 PQgetResult 再次检索管道中下一个查询的结果,并重复该循环。应用程序按正常方式处理各个语句结果。当管道中所有查询的结果都已返回时,PQgetResult 将返回包含状态值 PGRES_PIPELINE_SYNC 的结果

客户端可以选择延迟结果处理,直到完整管道已发送,或者用发送 pipeline 中的进一步查询与之交叉;请参见 Section 34.5.1.4

要进入单行模式,请在使用 PQgetResult_检索结果之前调用 _PQsetSingleRowMode。此模式选择仅对当前正在处理的查询有效。要获取有关 _PQsetSingleRowMode_用法的更多信息,请参阅 Section 34.6

PQgetResult_的行为与正常的异步处理相同,只是它可能包含新 _PGresult_类型 _PGRES_PIPELINE_SYNCPGRES_PIPELINE_ABORTEDPGRES_PIPELINE_SYNC 对于 pipeline 中相应位置处的每个 PQpipelineSync_仅报告一次。_PGRES_PIPELINE_ABORTED 在第一次错误及其所有后续结果中代替普通的查询结果发出,直到下一个 PGRES_PIPELINE_SYNC;请参见 Section 34.5.1.3

PQisBusyPQconsumeInput 等在处理管道结果时按正常方式运行。特别是,如果到目前为止已提交所有查询的结果,则在管道中间调用 PQisBusy 时返回 0。

libpq 不向应用程序提供有关当前正在处理的查询的任何信息(除了 PQgetResult 返回 null 来指示我们开始返回下一个查询的结果)。应用程序必须跟踪它发送查询的顺序,以便将其与对应结果关联起来。应用程序通常将使用状态机或 FIFO 队列来实现此目的。

34.5.1.3. Error Handling #

从客户端视角,在 PQresultStatus 返回 PGRES_FATAL_ERROR 之后,管道就会标记为已中止。对于中止管道中剩余的每个排队操作,PQresultStatus 将报告 PGRES_PIPELINE_ABORTED 结果。PQpipelineSync 的结果以 PGRES_PIPELINE_SYNC 的形式报告,表示中止管道的结束和正常结果处理的恢复。

在错误恢复期间,客户端 must 使用 PQgetResult 处理结果。

如果管道使用隐式事务,那么已经执行的操作将被回滚,而紧跟在失败操作之后排队等待的操作将被完全跳过。如果管道启动并提交单个显式事务(即第一个语句是 BEGIN,最后一个是 COMMIT),则会出现同样的行为,只不过会话在管道结束时仍处于中止的事务状态。如果管道包含 multiple explicit transactions,则在错误发生前提交的所有事务仍然保持提交,当前正在进行的事务被中止,所有后续操作(包括后续事务)都将被完全跳过。如果同步点在一个中止状态的显式事务块中发生,那么除非后续命令使用 ROLLBACK 将事务置于正常模式,否则下一个管道会立即中止。

Note

当该客户端 sends 一个 COMMIT 时,客户端不得假设工作已提交,只有在收到相应结果以确认已完成提交后才有此假设。由于错误会异步到达,因此应用程序需要能够从最近一次 received 提交的更改重新启动,并在出现问题时重新发送此点之后完成的工作。

34.5.1.4. Interleaving Result Processing and Query Dispatch #

为了避免大型管道中的死锁,客户端应围绕非阻塞事件循环进行构建,使用操作系统设施,如 selectpollWaitForMultipleObjectEx 等。

通常而言,客户端应用程序应维护一个待调度任务队列和一个已经被调度但尚未处理其结果的任务队列。当套接字可写时,它应调度更多任务。当套接字可读时,它应读取结果并处理结果,将其与相应结果队列中的下一个条目进行匹配。根据可用内存,应频繁读取套接字中的结果:无需等到管道结束才读取结果。管道应限定在逻辑工作单元中,通常(但不一定)是每个管道一个事务。无需退出管道模式并在管道之间重新进入该模式,或等待一个管道完成再发送下一个管道。

关于使用 select() 和一个简单状态机来跟踪已发送和已接收任务的示例,见 PostgreSQL 源代码分发中的 src/test/modules/libpq_pipeline/libpq_pipeline.c

34.5.2. Functions Associated with Pipeline Mode #

  • PQpipelineStatus #

    • 返回 libpq 连接的当前管道模式状态。

PGpipelineStatus PQpipelineStatus(const PGconn *conn);
  • PQpipelineStatus 可以返回以下值之一:

    • PQenterPipelineMode #

  • 如果连接当前处于空闲状态或已处于管道模式,则使其进入管道模式。

int PQenterPipelineMode(PGconn *conn);
  • 返回 1 表示成功。如果连接当前不处于空闲状态,即它已有准备就绪的结果,或正在等待服务器的更多输入,等等,则返回 0 且不产生任何效果。此函数实际上不会向服务器发送任何内容,它只更改 libpq 连接状态。

    • PQexitPipelineMode #

  • 如果当前在管道模式下,且队列为空,没有待处理的结果,则使连接退出管道模式。

int PQexitPipelineMode(PGconn *conn);
  • 成功则返回 1。不在管道模式下返回 1 且不执行任何操作。如果当前语句未完成处理,或者尚未调用 PQgetResult 以收集从所有先前发送的查询的结果,则返回 0(在这种情况下,使用 PQerrorMessage 了解有关故障的更多信息)。

    • PQpipelineSync #

  • 通过发送 sync message并刷新发送缓冲区,在 pipeline 中标记一个同步点。这用作隐式事务的分隔符和错误恢复点;请参见 Section 34.5.1.3

int PQpipelineSync(PGconn *conn);
  • 成功时返回 1。如果连接不在 pipeline 模式中或发送 sync message失败,则返回 0。

    • PQsendFlushRequest #

  • 发送一个请求给服务器,使其刷新输出缓冲区。

int PQsendFlushRequest(PGconn *conn);
  • 返回 1 表示成功。在任何失败时返回 0。

  • 服务器会自动刷新其输出缓冲区,因为已调用 PQpipelineSync,或在 pipeline 模式之外时的任何请求上;此函数用于导致服务器在 pipeline 模式下刷新其输出缓冲区,而不会建立同步点。请注意,请求本身不会自动刷新到服务器;如有必要,请使用 PQflush

    • PQ_PIPELINE_ON

  • libpq 连接处于管道模式。

    • PQ_PIPELINE_OFF

  • libpq 连接处于 not 管道模式。

    • PQ_PIPELINE_ABORTED

  • libpq 连接处于管道模式,在处理当前管道时发生错误。当 PQgetResult 返回类型为 PGRES_PIPELINE_SYNC 的结果时,中止标志将被清除。

34.5.3. When to Use Pipeline Mode #

与异步查询模式非常类似,使用管道模式不会产生有意义的性能开销。它会增加客户端应用程序的复杂性,而且需要格外小心才能防止客户端/服务器死锁,但是管道模式可以大幅提升性能,但会增加内存使用量,因为状态停留的时间更长。

当服务器距离较远(例如网络延迟(“ping时间”)较高),并且当快速连续执行很多小型操作时,管道模式最有用。在每次查询执行很多倍客户端/服务器往返时间时,使用管道化的命令通常收益较小。300毫秒往返时间距离的服务器上,执行一个100语句操作,那么仅在网络延迟上,该服务器将花费30秒而无需进行管道;通过管道化,它可能只需要花费0.3秒来等待服务器发送结果。

当应用程序执行大量小的 INSERT、_UPDATE_和 _DELETE_操作(这些操作无法轻松地转换成针对设置的操作或 _COPY_操作)时,请使用管道命令。

当客户端需要来自一个操作的信息来生成下一个操作时,管道模式没有意义。在这种情况下,客户端必须引入一个同步点,并等待完成一个完整的客户端/服务器往返才能获取它需要的结果。但是,通常可以通过调整客户端设计来交换所需的服务器端信息。读写改循环是特别好的候选者;例如:

BEGIN;
SELECT x FROM mytable WHERE id = 42 FOR UPDATE;
-- result: x=2
-- client adds 1 to x:
UPDATE mytable SET x = 3 WHERE id = 42;
COMMIT;

可以通过以下方式更高效地完成:

UPDATE mytable SET x = x + 1 WHERE id = 42;

当一个管道包含多个事务(请参见 Section 34.5.1.3)时,管道不太有用,并且更复杂。

[15 ] 客户端将阻止尝试向服务器发送查询,但服务器会阻止尝试向客户端发送已处理的查询结果。仅在客户端发送足够的查询以在其输出缓冲区和服务器的接收缓冲区中填充数据,但它不会首先处理来自服务器的输入时,才会发生这种情况,但是很难准确预测将会何时发生这种情况。