2024年9月26日: PostgreSQL 17 发布!
支持的版本:当前 (17) / 16 / 15 / 14
开发版本:devel

32.5. 管道模式 #

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

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

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

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

32.5.1. 使用管道模式 #

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

注意

最好在 非阻塞模式 下使用 libpq 中的管道模式。如果在阻塞模式下使用,则可能会发生客户端/服务器死锁。[15]

32.5.1.1. 发出查询 #

进入管道模式后,应用程序使用 PQsendQueryParams 或其预备查询同级 PQsendQueryPrepared 分派请求。这些请求在客户端排队,直到刷新到服务器;当使用 PQpipelineSync 在管道中建立同步点,或调用 PQflush 时,就会发生这种情况。函数 PQsendPreparePQsendDescribePreparedPQsendDescribePortalPQsendClosePreparedPQsendClosePortal 也在管道模式下工作。结果处理将在下面描述。

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

一个操作依赖于先前操作的结果是可以的;例如,一个查询可以定义一个表,该表由同一管道中的下一个查询使用。类似地,应用程序可以创建命名预备语句并在同一管道中的后续语句中执行它。

32.5.1.2. 处理结果 #

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

客户端可以选择延迟结果处理,直到整个管道发送完毕,或者将其与在管道中发送更多查询交织在一起;请参阅第 32.5.1.4 节

PQgetResult 的行为与普通异步处理相同,只是它可能包含新的 PGresult 类型 PGRES_PIPELINE_SYNCPGRES_PIPELINE_ABORTEDPGRES_PIPELINE_SYNC 在管道中对应的位置为每个 PQpipelineSyncPQsendPipelineSync 精确报告一次。 PGRES_PIPELINE_ABORTED 在第一个错误和所有后续结果(直到下一个 PGRES_PIPELINE_SYNC)中代替正常的查询结果发出;请参阅第 32.5.1.3 节

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

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

32.5.1.3. 错误处理 #

从客户端的角度来看,在 PQresultStatus 返回 PGRES_FATAL_ERROR 后,管道被标记为中止。 PQresultStatus 将为中止管道中每个剩余的排队操作报告 PGRES_PIPELINE_ABORTED 结果。 PQpipelineSyncPQsendPipelineSync 的结果报告为 PGRES_PIPELINE_SYNC,以表示中止管道的结束和正常结果处理的恢复。

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

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

注意

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

32.5.1.4. 交织结果处理和查询分派 #

为了避免大型管道上的死锁,客户端的结构应围绕使用操作系统工具(如 selectpollWaitForMultipleObjectEx 等)的非阻塞事件循环。

客户端应用程序通常应维护一个待分派的作业队列和一个已分派但尚未处理其结果的作业队列。当套接字可写时,它应该分派更多作业。当套接字可读时,它应该读取结果并处理它们,并将它们与对应结果队列中的下一个条目匹配。根据可用内存,应经常读取套接字的结果:无需等到管道结束才能读取结果。管道应限定在逻辑工作单元的范围内,通常(但并非总是)每个管道一个事务。无需在管道之间退出管道模式并重新进入,也无需等待一个管道完成才能发送下一个管道。

PostgreSQL 源代码分发版中的 src/test/modules/libpq_pipeline/libpq_pipeline.c 中有一个使用 select() 和简单的状态机来跟踪已发送和已接收作业的示例。

32.5.2. 与管道模式相关的函数 #

PQpipelineStatus #

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

PGpipelineStatus PQpipelineStatus(const PGconn *conn);

PQpipelineStatus可以返回以下值之一:

PQ_PIPELINE_ON

libpq连接处于管道模式。

PQ_PIPELINE_OFF

libpq连接处于管道模式。

PQ_PIPELINE_ABORTED

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

PQenterPipelineMode #

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

int PQenterPipelineMode(PGconn *conn);

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

PQexitPipelineMode #

如果连接当前处于管道模式且队列为空且没有挂起的结果,则使其退出管道模式。

int PQexitPipelineMode(PGconn *conn);

成功返回1。如果未处于管道模式,则返回1且不执行任何操作。如果当前语句尚未完成处理,或者PQgetResult尚未被调用以收集所有先前发送查询的结果,则返回0(在这种情况下,使用PQerrorMessage获取有关失败的更多信息)。

PQpipelineSync #

通过发送同步消息并刷新发送缓冲区,在管道中标记一个同步点。这充当隐式事务和错误恢复点的分隔符;参见第 32.5.1.3 节

int PQpipelineSync(PGconn *conn);

成功返回1。如果连接未处于管道模式或发送同步消息失败,则返回0。

PQsendPipelineSync #

通过发送同步消息而不刷新发送缓冲区,在管道中标记一个同步点。这充当隐式事务和错误恢复点的分隔符;参见第 32.5.1.3 节

int PQsendPipelineSync(PGconn *conn);

成功返回1。如果连接未处于管道模式或发送同步消息失败,则返回0。请注意,消息本身不会自动刷新到服务器;如有必要,请使用PQflush

PQsendFlushRequest #

发送请求,要求服务器刷新其输出缓冲区。

int PQsendFlushRequest(PGconn *conn);

成功返回1。任何失败都返回0。

服务器会自动刷新其输出缓冲区,作为调用PQpipelineSync的结果,或者在非管道模式下进行任何请求时;此函数用于在管道模式下导致服务器刷新其输出缓冲区,而无需建立同步点。请注意,请求本身不会自动刷新到服务器;如有必要,请使用PQflush

32.5.3. 何时使用管道模式 #

与异步查询模式类似,使用管道模式时没有明显的性能开销。它增加了客户端应用程序的复杂性,并且需要格外小心以防止客户端/服务器死锁,但管道模式可以提供相当大的性能改进,以换取由于保留状态时间更长而导致的内存使用量增加。

当服务器距离较远(即网络延迟(ping时间)较高)且在快速连续执行许多小型操作时,管道模式最有用。当每个查询执行时间是客户端/服务器往返时间的许多倍数时,使用管道命令通常好处不大。在距离 300 毫秒往返时间的服务器上运行 100 条语句的操作,仅网络延迟就需要 30 秒;使用管道,它可能只需要 0.3 秒等待服务器的结果。

当您的应用程序执行大量难以转换为集合操作或COPY操作的小型INSERTUPDATEDELETE操作时,请使用管道命令。

当客户端需要来自一个操作的信息来生成下一个操作时,管道模式就没有用处。在这种情况下,客户端必须引入一个同步点并等待完整的客户端/服务器往返才能获取所需的结果。但是,通常可以调整客户端设计以在服务器端交换所需的信息。读-修改-写循环尤其适合这种情况;例如

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;

当单个管道包含多个事务时,管道效率较低且更复杂(参见第 32.5.1.3 节)。



[15] 客户端将阻塞以尝试向服务器发送查询,但服务器将阻塞以尝试从其已处理的查询向客户端发送结果。只有当客户端发送足够的查询来填充其输出缓冲区和服务器的接收缓冲区,然后才切换到处理来自服务器的输入时,才会发生这种情况,但这很难准确预测何时会发生。

提交更正

如果您在文档中看到任何不正确的内容、与您对特定功能的体验不符的内容或需要进一步澄清的内容,请使用此表单报告文档问题。