预编译语句为何在第六次执行后突然变慢

2026-08-25 39 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:6 分钟

在 PostgreSQL 中,预编译语句并不一定永远使用针对当前参数值优化的执行计划。前几次执行通常会生成 custom plan;当执行次数达到第六次时,系统可能改用 generic plan。这个变化能减少反复规划的成本,却也可能让原本很快的查询突然变慢。

第六次执行发生了什么

所谓 custom plan,是根据本次绑定参数生成的计划。它能利用参数分布,例如某个租户只有少量数据、另一个租户却占据了大多数表行。对于数据分布不均匀的列,custom plan 可能在索引扫描和顺序扫描之间做出完全不同的选择。

generic plan 则不依赖具体参数,可以在多次执行之间复用。规划成本较低,但它只能使用参数无关的估算。如果不同参数对应的最佳计划差异很大,generic plan 就可能成为性能瓶颈。

关键点是:慢并不一定从 SQL 文本变化开始。应用仍然发送同一条 prepared statement,数据库却可能在执行次数跨过这个阈值后选择另一种计划。因此,应用日志里常见的表现是“同一请求偶尔变慢”,而不是明显的语法或代码错误。

一个容易触发问题的查询

假设订单表按 tenant_id 查询,但租户之间的数据量差异很大:

CREATE TABLE orders (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    tenant_id integer NOT NULL,
    status text NOT NULL,
    created_at timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX orders_tenant_status_idx
    ON orders (tenant_id, status);

PREPARE tenant_orders(integer, text) AS
    SELECT id, created_at
    FROM orders
    WHERE tenant_id = $1
      AND status = $2
    ORDER BY created_at DESC
    LIMIT 50;

EXECUTE tenant_orders(7, 'open');
EXECUTE tenant_orders(7, 'open');
EXECUTE tenant_orders(7, 'open');
EXECUTE tenant_orders(7, 'open');
EXECUTE tenant_orders(7, 'open');
EXECUTE tenant_orders(7, 'open');

SELECT name, generic_plans, custom_plans
FROM pg_prepared_statements
WHERE name = 'tenant_orders';

这段 SQL 用于观察 prepared statement 的执行计数。真实诊断时,还应为代表性参数分别运行 EXPLAIN (ANALYZE, BUFFERS),比较计划形状、扫描行数和实际耗时。不要只看平均延迟,因为某一类参数可能被 generic plan 严重拖慢。

如何确认是不是计划切换

可以在测试环境中显式控制计划模式,然后比较同一组参数:

SET plan_cache_mode = force_custom_plan;
EXPLAIN (ANALYZE, BUFFERS)
EXECUTE tenant_orders(7, 'open');

SET plan_cache_mode = force_generic_plan;
EXPLAIN (ANALYZE, BUFFERS)
EXECUTE tenant_orders(7, 'open');

如果两次输出的计划或耗时有明显差别,就说明参数敏感性值得重点调查。force_custom_planforce_generic_plan 更适合诊断或针对特定会话验证,不应未经测量就作为全局修复。应用连接池也需要纳入测试,因为 prepared statement 的生命周期可能由连接而不是请求决定。

在 PostgreSQL 中,还可以查看当前会话的 prepared statement 统计:

SELECT name, statement, generic_plans, custom_plans
FROM pg_prepared_statements;

这些计数帮助确认数据库是否确实在重复执行某个 prepared statement,以及 custom/generic 两种计划分别被使用了多少次。

处理方式要基于测量

可以按以下顺序处理:

  1. 用真实的慢参数和快参数分别采集 EXPLAIN (ANALYZE, BUFFERS)
  2. 比较 force_custom_planforce_generic_plan 的计划和延迟。
  3. 检查统计信息是否过期,必要时对相关列执行 ANALYZE,并确认索引与数据访问模式匹配。
  4. 如果 custom plan 稳定更快,再评估对相关会话设置 plan_cache_mode = force_custom_plan
  5. 重新检查连接池、数据库驱动和发布后的长连接行为。

强制 custom plan 不是免费的:每次执行都要规划,低延迟查询或高并发场景可能因此增加 CPU 消耗。反过来,generic plan 也不是天然错误;当参数对计划选择影响很小,复用它通常是合理的。

上线前检查清单

遇到“第几次执行后变慢”时,先确认三件事:是否使用了 prepared statement,慢点是否与第六次执行附近的计划切换重合,以及不同参数的数据分布是否足以改变最佳计划。只有在这些证据成立时,才应该调整计划缓存策略。把计划模式、参数类别和实际缓冲区读数一起记录下来,通常比单纯提高超时时间更能定位问题。


相关推荐