参数敏感导致执行计划波动的处理——高频接口中的参数分布、计划缓存与典型参数压测实战
2026/8/24 14:50:05 网站建设 项目流程

文章目录

    • 每日一句正能量
    • 1. 背景与问题:为什么同一条SQL,普通租户12ms,超级租户却跑了15秒?
    • 2. 环境与数据:参数敏感测试不能只拿“典型平均值”
      • 2.1 参数至少分三组
      • 2.2 为什么“平均参数”没有意义?
      • 2.3 日期范围也是参数敏感的一部分
    • 3. 复现过程:强制Generic和Custom,直接证明是不是参数敏感
      • 3.1 使用 PREPARE 建立可复现实验
      • 3.2 force_generic_plan:复现“同一计划打天下”
      • 3.3 force_custom_plan:让参数参与规划
      • 3.4 为什么auto有时候仍会选到不理想计划?
    • 4. 方案实施:不要第一反应就全局 force_custom_plan
      • 4.1 方案一:先修统计信息
      • 4.2 方案二:观察 estimated vs actual
      • 4.3 方案三:针对高频接口使用Custom Plan
      • 4.4 为什么不建议全局 force_custom_plan?
      • 4.5 方案四:接口分型
      • 4.6 方案五:按日期范围分流
      • 4.7 方案六:SQL模板拆分
      • 4.8 方案七:清理缓存计划只能作为诊断/恢复动作
      • 4.9 应用连接池会放大问题
      • 4.10 使用 sys_prepared_statements 检查当前会话
      • 4.11 不要把“参数嗅探”当成唯一解释
      • 4.12 参数敏感还可能来自 Join 顺序
    • 5. 结果对比:从14.8秒波动到稳定800ms,真正有效的是“计划分区”
      • E0:强制Generic
      • E1:force_custom_plan
      • E2:统计修复 + auto
      • E3:接口分型
    • 5.1 示例结果
    • 5.2 不能只看Execution Time
    • 5.3 高频接口要看CPU
    • 5.4 P99比平均值更重要
    • 5.5 Buffer Read可以解释为什么计划错
    • 6. 风险与复盘:处理参数敏感最危险的,是把“局部救火参数”变成“全局策略”
      • 6.1 风险一:全局force_custom_plan
      • 6.2 风险二:全局force_generic_plan
      • 6.3 风险三:只测普通参数
      • 6.4 风险四:统计信息过期
      • 6.5 风险五:接口分型后语义不一致
      • 6.6 风险六:清计划缓存造成抖动
      • 6.7 风险七:连接池每个Session状态不同
  • 推荐诊断顺序
  • 回退方案
  • 最终复盘
  • 附录 A:检查当前计划缓存模式
  • 附录 B:检查准备语句
  • 附录 C:Custom/Generic对照
  • 附录 D:最低验收门禁

每日一句正能量

走在一起是缘分,一起在走是幸福,缘来时坦然接受,缘去时从不强留。
相遇是命运(缘),同行是选择(福)。不迎不拒,来去从容。有珍重当下的投入,也有放开执念的豁达。

主题:参数分布 / 参数敏感 / 高频接口
重点:Generic Plan、Custom Plan、plan_cache_mode、Prepared Statement、PBE、统计信息、数据倾斜、执行计划、P95/P99
适用场景:KingbaseES 上的订单查询、租户检索、客户明细、高频 API、报表接口等“同一条 SQL,不同参数表现天差地别”的生产问题。


1. 背景与问题:为什么同一条SQL,普通租户12ms,超级租户却跑了15秒?

生产系统里有一种非常折磨人的慢 SQL:

SQL文本完全一样 索引完全一样 数据库没有变更

但:

参数不同 性能相差几百倍

例如:

SELECTorder_id,customer_id,amount,created_atFROMapi_orderWHEREtenant_id=?ANDstatus=?ANDcreated_at>=?ANDcreated_at<?ORDERBYcreated_atDESCLIMIT100;

普通租户:

tenant_id=101 最近1天 命中120行 P95≈12ms

超级租户:

tenant_id=999999 最近1年 命中4200万行 P95≈14.8s

如果 SQL 使用 JDBC PreparedStatement,团队还可能观察到更加诡异的现象:

应用重启后前几次很快 运行一段时间后突然变慢

或者:

先被普通参数调用 后续热点参数变慢

反过来也可能:

先跑超级租户 普通用户查询后来变慢

这就是典型:

Parameter Sensitive Plan

问题。

真正根因通常是两件事同时出现:

1. 参数分布高度不均匀 2. SQL计划被缓存/复用

KingbaseES 官方“缓存执行计划”文档说明,数据库会缓存执行计划以避免每次都重新规划。对于 PBE 扩展协议,执行计划保存在进程本地内存里;对于准备语句,plan_cache_mode可以控制通用计划与定制计划的选择。

官方文档对二者定义得很清楚:

Generic Plan: 不针对某一个具体参数值生成 多个参数共用一套计划 Custom Plan: 把本次参数值作为常量参与规划 可以根据具体参数选择计划

通用计划节省:

规划时间

但在数据分布高度倾斜时:

一套计划很难同时适合所有参数

因此本文的第一个核心结论是:

参数敏感不是“计划随机波动”,而是同一 SQL 横跨了多个完全不同的成本区间,而缓存机制试图用一套计划覆盖这些区间。


2. 环境与数据:参数敏感测试不能只拿“典型平均值”

示例表:

CREATETABLEapi_order(order_idBIGINTPRIMARYKEY,tenant_idBIGINTNOTNULL,customer_idBIGINTNOTNULL,statusINTNOTNULL,created_atTIMESTAMPNOTNULL,amountNUMERIC(18,2));

数据:

总订单: 2亿 普通租户: 每租户 1万~5万 大客户: 500万~1000万 超级租户: 4000万+

索引:

CREATEINDEXidx_api_order_tenant_createdONapi_order(tenant_id,created_atDESC);CREATEINDEXidx_api_order_tenant_status_createdONapi_order(tenant_id,status,created_atDESC);

2.1 参数至少分三组

不要只测试:

一个典型tenant_id

必须明确:

普通值 热点值 极端长尾值

例如:

普通: tenant=101 1天 120行 热点: tenant=880001 180天 1800万行 极端: tenant=999999 365天 4200万行


2.2 为什么“平均参数”没有意义?

假设:

99%的租户只有2万订单 1%的租户拥有60%的数据

平均值可能:

每租户20万

但生产上根本没有:

真正代表性的20万租户

优化器需要面对的是:

2万 和 4000万

两种完全不同的访问方式。

因此参数敏感测试一定要使用:

分位数 热点组 长尾组

而不是平均值。


2.3 日期范围也是参数敏感的一部分

同一个租户:

最近1天

可能适合:

Index Scan

最近2年:

返回全租户80%的数据

可能更适合:

Bitmap/Seq Scan

所以接口参数敏感常常不是单列:

tenant_id

而是:

tenant_id × date range × status

共同决定。


3. 复现过程:强制Generic和Custom,直接证明是不是参数敏感

3.1 使用 PREPARE 建立可复现实验

PREPAREq(bigint,int,timestamp,timestamp)ASSELECTorder_id,customer_id,amount,created_atFROMapi_orderWHEREtenant_id=$1ANDstatus=$2ANDcreated_at>=$3ANDcreated_at<$4ORDERBYcreated_atDESCLIMIT100;

官方 PREPARE 文档说明:

准备语句可以使用 generic plan 也可以使用 custom plan

并且:

EXPLAIN EXECUTE

可以检查实际使用的计划。

如果计划里仍显示:

$1 $2

通常代表:

Generic Plan

如果变成:

tenant_id = 999999

这样的具体值:

Custom Plan

3.2 force_generic_plan:复现“同一计划打天下”

SETplan_cache_mode='force_generic_plan';

普通参数:

EXPLAIN(ANALYZE,BUFFERS,VERBOSE)EXECUTEq(101,1,'2026-08-01','2026-08-02');

计划:

Index Scan estimated rows=15000 actual rows=120 P95=12ms

完全没问题。

再执行:

超级租户 + 365天

同一 Generic Plan:

Index Scan estimated rows=15000 actual rows=42000000

Buffers:

1500万

P95:

14.8s

此时已经可以证明:

不是SQL写法突然变化

而是:

通用计划对普通参数合理 对超级参数严重失配

3.3 force_custom_plan:让参数参与规划

SETplan_cache_mode='force_custom_plan';

普通参数:

Index Scan actual=120 P95=13ms

热点参数:

Bitmap / Seq Scan 或 不同Join计划 actual=4200万 P95=1.9s

同一 SQL:

不同参数 →不同执行计划

而且两边都合理。

这就是参数敏感最直接的证据。


3.4 为什么auto有时候仍会选到不理想计划?

KingbaseES 官方缓存执行计划文档说明,plan_cache_mode=auto是默认模式。

官方描述的策略是:

前五次执行先生成定制计划 计算这些定制计划的平均估算代价 之后再生成通用计划 比较Generic代价与Custom平均代价 决定是否值得缓存复用

这个机制的出发点非常合理:

平衡规划成本与执行成本

但它有一个工程前提:

前几次参数必须能代表后续真实分布。

假设应用启动后前五次请求恰好都是:

普通小租户

它们都:

Index Scan非常便宜

随后:

超级租户

进入。

如果最终复用的 Generic Plan 对热点值估算很差:

参数敏感问题就会出现

因此线上诊断时必须问:

慢SQL第一次由什么参数执行? PreparedStatement生命周期多久? 连接池是否长期复用同一个后端Session?

4. 方案实施:不要第一反应就全局 force_custom_plan

4.1 方案一:先修统计信息

参数敏感首先是:

参数值代表的数据量不同

如果统计信息又不准确:

问题会被进一步放大

执行:

ANALYZEapi_order;

KingbaseES 官方运行时统计文档说明,自动统计机制会在 DML 变化达到阈值时执行 ANALYZE,帮助优化器使用更新后的统计信息。

仍需检查:

热点值是否进入统计 统计目标是否足够 多列条件是否存在相关性

4.2 方案二:观察 estimated vs actual

普通:

estimated=130 actual=120

很好。

热点:

estimated=15000 actual=42000000

差:

2800倍

此时不能只说:

Generic Plan不好

还要问:

为什么优化器对热点分布认知这么差?

所以统计修复是基础。


4.3 方案三:针对高频接口使用Custom Plan

如果接口:

单SQL执行成本高 参数差异巨大 规划时间只有1ms左右

那么:

每次重新规划

可能是值得的。

例如:

Planning Time: 1.2ms Execution差异: 14.8s vs 1.9s

为节省:

1ms

而承受:

十几秒

显然不划算。

此时可以评估:

会话级 角色级 特定连接池

使用:

force_custom_plan

而不是全局。


4.4 为什么不建议全局 force_custom_plan?

因为系统还有很多:

简单点查 短SQL 高QPS

它们本身执行:

0.5ms

规划:

0.3ms

如果每次都重新规划:

CPU成本比例很高

所以:

Custom Plan应该只给真正参数敏感、且执行成本远高于规划成本的接口。


4.5 方案四:接口分型

如果业务已经清楚存在:

普通租户 超级租户

可以直接拆:

普通查询接口 大客户查询接口

普通:

tenant_id=? 短日期LIMIT100

目标:

Index Scan

超级租户:

大时间范围 统计/导出

走:

批量查询 汇总表 异步任务

而不是强迫:

一个SQL

覆盖完全不同的业务量级。


4.6 方案五:按日期范围分流

例如:

<=7天

走在线实时索引查询。

>90天

走:

报表库 汇总表 离线任务

这比让:

同一个PreparedStatement

同时覆盖:

1天 365天

更稳定。


4.7 方案六:SQL模板拆分

如果:

普通参数

和:

热点参数

永远需要不同计划。

可以让 SQL 文本本身不同:

Q_NORMAL Q_HOT

从而:

分别缓存计划

这是一种非常实用的:

Plan Segmentation

思想。

不要把所有流量放到:

同一个SQL fingerprint

里。


4.8 方案七:清理缓存计划只能作为诊断/恢复动作

PBE 计划缓存情况下:

DISCARDPLANS;

可以清除当前会话缓存计划。

官方文档也说明:

当表定义 函数定义 统计信息

变化时,对应缓存计划会自动失效。

但:

不断手工DISCARD PLANS

不是长期治理。

如果每隔半小时:

清缓存才能恢复

说明真正问题仍然存在。


4.9 应用连接池会放大问题

KingbaseES PBE 缓存计划:

保存在进程本地内存

Java HikariCP:

长连接

意味着:

同一个后端Session 可能长时间持有PreparedStatement/计划行为

所以 DBA 在:

新ksql Session

里复现不到:

应用慢计划

并不奇怪。

必须:

用真实JDBC调用 真实PreparedStatement 真实连接池

测试。


4.10 使用 sys_prepared_statements 检查当前会话

KingbaseES 官方sys_prepared_statements视图可以查看当前会话中可用的准备语句,包括:

name statement prepare_time parameter_types from_sql

这对验证:

当前session到底准备了什么SQL

非常有帮助。


4.11 不要把“参数嗅探”当成唯一解释

如果:

每次强制custom

仍然都选择:

同一个坏计划

说明:

不是缓存问题

更可能是:

统计失真 索引不合理 SQL本身不佳 成本参数错误

所以诊断必须同时做:

Generic vs Custom

对照。


4.12 参数敏感还可能来自 Join 顺序

例如:

tenant=普通

订单120行。

最优:

订单小集 →Nested Loop →客户表

超级租户:

订单4200万

最优可能变:

Hash Join

如果缓存:

普通参数生成的Nested Loop

再给超级租户:

Inner loops

可能爆炸。

因此不要只检查:

Scan

还要比较:

Join Method Join Order

5. 结果对比:从14.8秒波动到稳定800ms,真正有效的是“计划分区”

E0:强制Generic

普通:

12ms

超级租户:

14.8s

P99:

严重波动

E1:force_custom_plan

普通:

13ms Index Scan

超级租户:

1.9s Bitmap/Seq + Hash

性能明显稳定。

代价:

每次多约1ms规划时间

E2:统计修复 + auto

热点估算:

15000 →4000万

更接近:

actual=4200万

auto 模式:

更少出现灾难计划

P95:

2.4s

E3:接口分型

普通:

在线明细接口 P95=11ms

超级租户长范围:

批量/汇总接口 P95=800ms

这时已经不是:

让优化器在一条SQL里猜

而是:

业务主动声明查询规模

计划最稳定。


5.1 示例结果

实验模式普通参数超级参数结论
E0Generic12ms14.8s波动巨大
E1Custom13ms1.9s执行稳定,规划略增
E2auto+统计修复12ms2.4s可接受
E3接口分型11ms0.8s最稳定

以上为方法演示数据,不是生产实测。


5.2 不能只看Execution Time

Custom Plan:

执行更快

但要记录:

Planning Time

例如:

Generic: Planning 0.2ms Custom: Planning 1.6ms

如果 SQL 本身:

执行10秒

无所谓。

如果:

执行0.4ms

就很重要。

所以需要比较:

Planning + Execution

总成本。


5.3 高频接口要看CPU

假设:

1万QPS

每次多:

0.5ms规划CPU

累计成本非常高。

这就是为什么:

全局force_custom

通常不是好方案。


5.4 P99比平均值更重要

参数敏感接口经常:

平均值很好 P99灾难

因为:

99%普通用户快 1%超级租户极慢

平均:

看起来只有100ms

但 VIP 客户:

15秒

所以验收重点:

参数组P95/P99

而不是总体平均值。


5.5 Buffer Read可以解释为什么计划错

普通 Index Scan:

Buffer Read=2400

超级租户错误 Index Scan:

1500万

Custom 热点计划:

320万

说明热点参数真正需要:

不同访问路径

不是单纯:

CPU偶尔抖了一下

6. 风险与复盘:处理参数敏感最危险的,是把“局部救火参数”变成“全局策略”

6.1 风险一:全局force_custom_plan

好处:

参数敏感SQL改善

坏处:

所有PreparedStatement都增加规划成本

应该优先:

会话 角色 特定连接池

控制范围。


6.2 风险二:全局force_generic_plan

对于:

高度均匀 简单SQL

可能很好。

但热点分布接口:

风险极大

不能只为了:

减少Planning CPU

全局强制。


6.3 风险三:只测普通参数

测试环境:

每个租户数据都差不多

生产:

几个超级租户占60%

参数敏感问题根本不会在测试暴露。

测试数据必须保留:

真实倾斜

6.4 风险四:统计信息过期

数据分布改变后:

热点租户

可能从:

100万 →4000万

而统计信息还停留在旧分布。

自动 ANALYZE 有帮助,但超大表、热点列仍应该建立专门统计质量监控。


6.5 风险五:接口分型后语义不一致

普通接口:

实时明细

超级租户:

汇总路径

必须明确:

一致性 延迟 排序 分页

差异。

不能为了快偷偷改变业务含义。


6.6 风险六:清计划缓存造成抖动

大范围:

DISCARD PLANS

会让大量 SQL:

重新规划

可能产生短期 CPU 抖动。

只在:

明确范围 维护/诊断窗口

使用。


6.7 风险七:连接池每个Session状态不同

PBE 计划缓存是:

进程本地

不同连接:

可能持有不同计划状态

于是应用表现为:

同一个接口 有的连接快 有的连接慢

这正是生产里最难定位的一类抖动。

所以压测必须覆盖:

多连接 长期复用 连接池生命周期

推荐诊断顺序

1. 固定SQL指纹 2. 参数分桶 3. EXPLAIN EXECUTE普通/热点/极端参数 4. force_generic_plan 5. force_custom_plan 6. 比较Plan/estimated/actual 7. ANALYZE与统计修复 8. 评估auto 9. 必要时接口/SQL分型 10. JDBC连接池并发验收

回退方案

如果新的缓存策略或接口分型上线后出现:

CPU升高 Planning Time激增 普通用户P99恶化 热点接口错误

执行:

1. 停止扩大新策略 2. 恢复原SQL/接口路由 3. 恢复原plan_cache_mode 4. 必要时在受控Session DISCARD PLANS 5. 保存Custom/Generic执行计划 6. 重测普通/热点/极端参数 7. 保留新统计信息,除非有证据说明它本身造成回归

不要:

一出问题就回滚ANALYZE

统计信息本身通常应该保留,真正应回退的是:

不合适的SQL或缓存策略

最终复盘

参数敏感 SQL 的本质可以概括为:

同一查询模板 + 不同参数选择性 + 不同最优计划 + 计划缓存复用 = 性能波动

真正的解决方案不是:

永远Custom

也不是:

永远Generic

而是先判断:

参数分布是否跨越多个成本区间

如果普通参数:

120行

热点参数:

4200万行

那么要求:

一套计划同时最优

本身就不现实。

这时更成熟的策略是:

准确统计 + Custom/Generic对照 + 合理缓存 + 参数分组 + 必要时业务接口分型

如果只记住一句话:

参数敏感问题的核心不是“缓存计划好不好”,而是同一 SQL 的不同参数是否本来就需要不同计划;当参数分布跨越多个数量级时,应该让统计、计划缓存和接口设计共同承认这种差异,而不是强行用一套计划覆盖所有用户。


附录 A:检查当前计划缓存模式

SHOWplan_cache_mode;

附录 B:检查准备语句

SELECTname,statement,prepare_time,parameter_types,from_sqlFROMsys_prepared_statements;

附录 C:Custom/Generic对照

SETplan_cache_mode='force_custom_plan';EXPLAINEXECUTEq(...);SETplan_cache_mode='force_generic_plan';EXPLAINEXECUTEq(...);RESET plan_cache_mode;

附录 D:最低验收门禁

[ ] 普通参数已测试 [ ] 热点参数已测试 [ ] 极端长尾参数已测试 [ ] Generic/Custom计划差异已确认 [ ] estimated/actual无重大未解释偏差 [ ] Planning Time已量化 [ ] P95/P99达到SLA [ ] CPU规划开销在预算内 [ ] JDBC/PBE已测试 [ ] 多连接池Session已测试 [ ] 统计信息新鲜 [ ] 回退策略已准备

转载自:https://blog.csdn.net/u014727709/article/details/163950047
欢迎 👍点赞✍评论⭐收藏,欢迎指正

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询