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

32.5. 管道模式 #

libpq 管道模式允许应用程序在无需读取先前发送查询结果的情况下发送查询。利用管道模式,客户端与服务器之间的等待时间会缩短,因为多个查询/结果可以在一次网络事务中发送/接收。

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

管道模式通常会增加客户端和服务器的内存消耗,尽管通过仔细且积极地管理发送/接收队列可以缓解这种情况。无论连接是阻塞模式还是非阻塞模式,这都适用。

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

32.5.1. 使用管道模式 #

要执行管道操作,应用程序必须将连接切换到管道模式,这通过 PQenterPipelineMode 完成。PQpipelineStatus 可用于测试管道模式是否处于活动状态。在管道模式下,只允许使用扩展查询协议的异步操作,不允许使用包含多个 SQL 命令的命令字符串,也不允许使用 COPY。使用同步命令执行函数,例如 PQfn, PQexec, PQexecParams, PQprepare, PQexecPrepared, PQdescribePrepared, PQdescribePortal, PQclosePrepared, PQclosePortal,是一种错误情况。由于 PQsendQuery 使用简单查询协议,因此也不允许使用。PQexitPipelineMode 可让应用程序在所有已分派命令的结果都已处理并且管道结束结果已被消耗后,返回到非管道模式。

注意

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

32.5.1.1. 执行查询 #

进入管道模式后,应用程序使用 PQsendQueryParams 或其准备语句同胞 PQsendQueryPrepared 来分派请求。这些请求在客户端排队,直到使用 PQpipelineSync 建立管道同步点,或调用 PQflush 时才会刷新到服务器。PQsendPrepare, PQsendDescribePrepared, PQsendDescribePortal, PQsendClosePrepared, 和 PQsendClosePortal 函数在管道模式下也可用。结果处理如下所述。

服务器按客户端发送的顺序执行语句并返回结果。服务器将立即开始执行管道中的命令,而不是等待管道结束。请注意,结果在服务器端被缓冲;当使用 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 节

在处理管道结果时,PQisBusy, PQconsumeInput 等函数按正常方式运行。特别地,在管道中间调用 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. 交错结果处理和查询分派 #

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

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

在 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 #

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

int PQpipelineSync(PGconn *conn);

如果连接不处于管道模式或发送 sync 消息失败,则返回 0。否则返回 1。

PQsendPipelineSync #

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

int PQsendPipelineSync(PGconn *conn);

如果连接不处于管道模式或发送 sync 消息失败,则返回 0。否则返回 1。请注意,消息本身不会自动刷新到服务器;如果需要,请使用 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] 客户端将阻塞尝试向服务器发送查询,但服务器将阻塞尝试从已处理的查询向客户端发送结果。这仅发生在客户端发送了足够的查询来填满其输出缓冲区和服务器的接收缓冲区,然后才开始处理来自服务器的输入时,但很难精确预测何时会发生这种情况。

提交更正

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