↑↓ 选择↵ 打开⌫ 切换范围完整搜索

PG.CENTER 连接 PostgreSQL 文档、百科与生态知识。由 Pigsty 维护。

支持中的版本: 当前版本 (18) / 17 / 16 / 15 / 14
开发中的版本: 19 / 20devel
预发布版本文档。 PostgreSQL 19beta4 为测试版本,最终发布内容可能有所不同。

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 也可用于管道模式。结果处理方式见下文。

服务器按客户端发送的顺序执行语句并返回结果。服务器会立即开始执行管道中的命令,无需等待管道结束。结果会缓存在服务器端;使用 PQpipelineSync 或 PQsendPipelineSync 建立同步点,或调用 PQsendFlushRequest 时,服务器会发送该缓冲区中的数据。如果任何语句发生错误,服务器会中止当前事务,并停止执行队列中的后续命令,直到下一个同步点;每条被跳过的命令都会产生一个 PGRES_PIPELINE_ABORTED 结果。(即使管道中的命令会回滚事务,也仍然如此。)到达同步点后,查询处理恢复。

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

32.5.1.2. 处理结果 #

要处理管道中一个查询的结果,应用程序应重复调用 PQgetResult 并处理每个结果,直到 PQgetResult 返回空指针。然后再次调用 PQgetResult 获取管道中下一个查询的结果,重复这一过程。应用程序按通常方式处理各条语句的结果。当管道中所有查询的结果都已返回时,PQgetResult 会返回一个包含状态值 PGRES_PIPELINE_SYNC 的结果。

客户端可以等到整条管道发送完毕后再处理结果,也可以在继续发送管道中的查询时交错处理结果;参见第 32.5.1.4 节。

PQgetResult 的行为与普通异步处理相同,只是返回结果可能具有新的 PGresult 类型 PGRES_PIPELINE_SYNC 和 PGRES_PIPELINE_ABORTED。对于每次 PQpipelineSync 或 PQsendPipelineSync 调用,都会在管道中的对应位置恰好报告一次 PGRES_PIPELINE_SYNC。对于第一个错误及其后的所有结果,会用 PGRES_PIPELINE_ABORTED 代替正常查询结果,直到下一个 PGRES_PIPELINE_SYNC;参见第 32.5.1.3 节。

PQisBusy、PQconsumeInput 等函数在处理管道结果时照常工作。特别地,在管道处理过程中调用 PQisBusy 时,如果到目前为止已发出查询的所有结果均已被读取,则返回 0。

libpq 不向应用程序提供当前正在处理哪个查询的信息(除了 PQgetResult 返回空指针,表示开始返回下一个查询的结果)。应用程序必须跟踪查询的发送顺序,以便将查询与对应结果关联起来。应用程序通常会使用状态机或 FIFO 队列实现这一点。

32.5.1.3. 错误处理 #

从客户端的角度看,PQresultStatus 返回 PGRES_FATAL_ERROR 后,管道就会被标记为中止。对于已中止管道中剩余的每个排队操作,PQresultStatus 都会报告一个 PGRES_PIPELINE_ABORTED 结果。PQpipelineSync 或 PQsendPipelineSync 的结果报告为 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 #

通过发送同步消息并将发送缓冲区中的数据发往服务器,在管道中标记同步点。同步点作为隐式事务的分界符和错误恢复点;见第 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 秒。

如果应用程序需要执行大量小型 INSERT、UPDATE 和 DELETE 操作,而这些操作又难以转换为集合操作或 COPY 操作,就可以使用管道命令。

如果客户端必须获得前一个操作的信息,才能生成下一个操作,管道模式就没有帮助。在这种情况下,客户端必须引入同步点,并等待一次完整的客户端与服务器往返,才能获得所需结果。不过,通常可以调整客户端设计,让所需信息在服务器端交换。读取、修改、写入的循环尤其适合这样改进。例如:

BEGIN;
SELECT x FROM mytable WHERE id = 42 FOR UPDATE;
-- 结果:x=2
-- 客户端将 x 加 1:
UPDATE mytable SET x = 3 WHERE id = 42;
COMMIT;

可以改写为以下效率更高的操作:

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

当单个管道包含多个事务时,使用管道的收益较小,复杂度也更高(见第 32.5.1.3 节)。



[15] 客户端在尝试向服务器发送查询时阻塞,而服务器在尝试将已处理查询的结果发送给客户端时阻塞。只有当客户端在转而处理服务器输入之前,发送了足够多的查询,填满自身的输出缓冲区和服务器的接收缓冲区,才会发生这种情况;但很难准确预测何时会发生。

报告文档问题

阅读 上游文档. 反馈更正前请先核对 当前版本手册.