文章目录
- 每日一句正能量
- 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 Plan3.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=42000000Buffers:
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 Order5. 结果对比:从14.8秒波动到稳定800ms,真正有效的是“计划分区”
E0:强制Generic
普通:
12ms超级租户:
14.8sP99:
严重波动E1:force_custom_plan
普通:
13ms Index Scan超级租户:
1.9s Bitmap/Seq + Hash性能明显稳定。
代价:
每次多约1ms规划时间E2:统计修复 + auto
热点估算:
15000 →4000万更接近:
actual=4200万auto 模式:
更少出现灾难计划P95:
2.4sE3:接口分型
普通:
在线明细接口 P95=11ms超级租户长范围:
批量/汇总接口 P95=800ms这时已经不是:
让优化器在一条SQL里猜而是:
业务主动声明查询规模计划最稳定。
5.1 示例结果
| 实验 | 模式 | 普通参数 | 超级参数 | 结论 |
|---|---|---|---|---|
| E0 | Generic | 12ms | 14.8s | 波动巨大 |
| E1 | Custom | 13ms | 1.9s | 执行稳定,规划略增 |
| E2 | auto+统计修复 | 12ms | 2.4s | 可接受 |
| E3 | 接口分型 | 11ms | 0.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
欢迎 👍点赞✍评论⭐收藏,欢迎指正