帮你快速理解、总结文档立即下载

tencentdb_spm

最近更新时间:2026-10-21 14:34:30
我的收藏

功能概述

功能简介

tencentdb_spm(SQL Plan Management,SQL 计划管理)是云数据库 PostgreSQL 的内核级执行计划管理插件,用于捕获、固化并管理 SQL 语句的执行计划基线(Plan Baseline),防止因统计信息变化、参数漂移、数据库升级等原因导致的计划回退(Plan Regression)。
插件核心能力包括:
能力
说明
计划捕获
手动(manual)或按执行次数阈值(automatic)捕获 SQL 执行计划为基线
计划应用
对已确认(Approved)的基线,通过 pg_hint_plan 强制固定执行计划
计划演进
评估候选计划与当前激活计划的性能差异(仅 advisory,不自动变更)
SQL Patch
不改应用代码,为指定 SQL 绑定 DBA 手写的 hint
生命周期管理
激活/回滚/启用/禁用/删除基线,全操作留有审计记录
可观测性
dba_plans 视图、决策计数器、单 SQL 应用链路诊断、参数敏感性诊断
技术实现上,插件通过 post_parse_analyze、planner_hook、ExecutorEnd、ProcessUtility 等 Hook 介入 SQL 的解析—规划—执行全链路。

支持版本

PostgreSQL 版本
内核版本
PostgreSQL 18
v18.6_r1.15及以上

工作原理

核心概念

概念
说明
sql_id
语句的64位查询标识(基于 queryId/Jumble 计算,常量归一化),不截断,不同 SQL 绝不共享行
plan_id
计划大纲(plan_outline)JSON 的64位哈希,标识一种计划形态
sql_fingerprint
归一化 SQL 文本的64位哈希,应用基线时做二次防碰撞校验
plan_outline
计划的规范化描述(jsonb):连接顺序、访问方式(SeqScan/IndexScan)、连接方式(HashJoin/NestLoop 等)
基线(baseline)
plan_baselines 表中的一行 = 某 SQL 的某一种计划形态
基线状态机:
status:Unapproved(候选)/ Approved(已确认)/ Rejected(已拒绝)。
enabled:true/false,可对基线做"掩码"而不改变 status,被禁用的基线不参与应用。
origin:AUTO(自动捕获)/ MANUAL / IMPORTED。
active_slot(单一激活):每个 sql_id 最多只有一行 Approved + enabled 的激活基线,由唯一索引 + 会话级咨询锁双重保证。
生命周期规则:某个 SQL 首次被捕获的计划直接成为 Approved 激活基线;此后出现的不同计划形态作为 Unapproved 候选记录,供 DBA 评估后切换。

工作流程



关键机制

Hint 注入 + 回环校验:应用时将存储的 outline 渲染为 pg_hint_plan hint 串,注入后重新规划并重提取 outline 计算 plan_id,与目标不一致则放弃 hint、回退自然计划(绝不让用户 SQL 失败)。
指纹防碰撞:应用前比对当前语句归一化指纹与基线存储指纹,不一致拒绝注入。
计划缓存交互:被 SPM 管理的语句命中计划缓存时,通过内核强制重新规划,保证 PREPARE/EXECUTE 语句同样受基线约束。
代数失效缓存:共享内存中维护 baseline_generation 计数器,任何基线写入都会使其递增,各 backend 的基线缓存据此自动刷新;对"确定无激活基线"的查询有负缓存,避免重复扫表。
共享内存执行计数器:automatic 模式按 sql_id 统计执行次数(跨 backend 共享),达到 plan_capture_threshold 触发捕获;计数表满时按 LFU 策略淘汰执行次数最少的条目。
数据落盘 + pg_dump 支持:基线与审计日志是用户数据,通过 pg_extension_config_dump 随 pg_dump/pg_restore 迁移。

版本与兼容性

项目
说明
插件版本
1.0
schema
tencentdb_spm(不可重定位)
前置依赖
必须加入 shared_preload_libraries 并重启实例;软依赖 pg_hint_plan(未预装时可正常安装插件、仅捕获模式可用,应用基线 / SQL Patch 需要 pg_hint_plan)
其他要求
compute_query_id = on(插件加载时会自动将 auto 提升为 on);TencentDB 环境需在 extension_whitelist 中放行 tencentdb_spm(当前内核已内置放行)

安装与启用

加载模块(需重启)

# postgresql.conf
shared_preload_libraries = 'pg_hint_plan,tencentdb_spm'
compute_query_id = on
# 可选:调整共享内存计数器容量(默认 4096)
# tencentdb_spm.max_shmem_entries = 4096
修改后重启实例。插件采用软加载约定(同 pg_stat_statements):未预加载时 CREATE EXTENSION 仍可成功,但所有管理函数调用会报错,且捕获/应用不生效。

创建扩展

-- 超级用户执行
CREATE EXTENSION tencentdb_spm;

开启功能

-- ① 总开关(默认 off,PGC_SUSET,管理员设置)
SET tencentdb_spm.enable_plan_management = on; -- 会话级
-- 或 ALTER SYSTEM SET tencentdb_spm.enable_plan_management = on; SELECT pg_reload_conf();

-- ② 捕获开关(默认 off,用户级可设)
SET tencentdb_spm.capture_plan_baselines = 'manual'; -- 或 'automatic'

-- ③ 应用开关(默认 off,用户级可设)
SET tencentdb_spm.use_plan_baselines = on;
说明:
enable_plan_management = off 时无论其他参数如何,捕获/应用路径全部跳过。

卸载

DROP EXTENSION tencentdb_spm; -- 删除 schema、表、函数
-- 再从 shared_preload_libraries 移除 tencentdb_spm 并重启

快速上手

以下示例演示“捕获 → 应用 → 切换”的完整闭环。
-- 1) 准备数据
CREATE TABLE spm_demo(id int primary key, v int, pad text);
INSERT INTO spm_demo SELECT g, g % 100, 'x' FROM generate_series(1, 100000) g;
ANALYZE spm_demo;

-- 2) 管理员会话开启总开关
SET tencentdb_spm.enable_plan_management = on;

-- 3) 开启手动捕获
SET tencentdb_spm.capture_plan_baselines = 'manual';
-- 也可以用便捷函数(对本会话生效):
-- SELECT tencentdb_spm.capture_plan();

-- 4) 执行目标 SQL(首捕获计划自动成为 Approved 激活基线)
SELECT count(*) FROM spm_demo WHERE id = 42;

-- 5) 查看捕获结果
SELECT sql_id, plan_id, status, enabled, origin, sql_text
FROM tencentdb_spm.dba_plans;
-- status | enabled | origin | sql_text
-- ----------+---------+--------+-------------------------------------
-- Approved | t | AUTO | SELECT count(*) FROM spm_demo WHERE id = $1

-- 6) 开启应用并验证(EXPLAIN 文本格式末尾会追加 SPM 决策行)
SET tencentdb_spm.use_plan_baselines = on;
EXPLAIN (COSTS OFF) SELECT count(*) FROM spm_demo WHERE id = 42;
-- QUERY PLAN
-- ------------------------------------------------------
-- Aggregate
-- -> Index Only Scan using spm_demo_pkey on spm_demo
-- Index Cond: (id = 42)
-- SPM: sql_id=617283... plan_id=-521096... (baseline forced)
-- (4 rows)
EXPLAIN 输出中的 SPM: 行含义:
决策
含义
baseline forced
命中激活基线,hint 注入且回环校验通过
captured
本次规划被捕获为新基线
(无 SPM 行)
语句未被管理,或应用被指纹/渲染/参数校验拒绝后回退自然计划

功能详解

计划捕获

捕获模式(tencentdb_spm.capture_plan_baselines)
模式
行为
off
不捕获(默认)
manual
本会话所有可捕获语句的每次执行都捕获(也可用 capture_plan() 函数一键武装)
automatic
跨会话共享内存计数器累计执行次数,达到 plan_capture_threshold(默认2)时捕获,随后计数清零
可捕获条件:
仅限可规划语句(SELECT/INSERT/UPDATE/DELETE 等,不含 UTILITY 命令);
查询树任何位置(含子查询)都不能引用 pg_catalog、tencentdb_spm、information_schema,避免反馈循环;
同一 sql_id 的基线数量受 max_plans_per_statement(默认10)限制;
capture_params = on(默认)时同时快照绑定参数(类型+值,jsonb),供后续受控重放使用。

计划应用

SET tencentdb_spm.use_plan_baselines = on; 后,规划器按 sql_id 查找激活基线(status='Approved' 并且 enabled):
1. 指纹校验:当前语句归一化指纹 ≠ 基线存储指纹 → 拒绝注入;
2. 参数类型校验:当前绑定参数类型与捕获时快照不兼容 → 拒绝注入;
3. 渲染 hint:outline → pg_hint_plan hint(如 /*+ IndexScan(t idx) Leading((a b)) */);自连接等别名冲突场景无法渲染 → 回退;
4. 回环校验:注入后重提取 outline,plan_id 不一致 → 记录 mismatch_fallback,回退自然计划。
任何一步失败都静默回退到 PostgreSQL 原生规划,不影响语句执行。被管理语句即使命中计划缓存也会被强制重新规划,保证基线生效。

生命周期管理 API(均为超级用户)

函数
说明
activate_plan(sql_id, plan_id [, note])
原子切换激活基线:当前激活行降级为 Unapproved,目标行升为唯一激活行;幂等;按 sql_id 串行化
rollback_plan(sql_id [, target_plan_id] [, note])
回滚到之前的激活计划;target_plan_id 缺省时沿审计链找最近一个仍有效的前任(引用的表/索引必须仍存在),失败则保持现状
set_plan_status(sql_id, plan_id, new_status [, note])
手工设置 status(Approved/Unapproved/Rejected)
set_plan_enabled(sql_id, plan_id, enabled [, note])
启用/禁用基线(掩码,不改 status),幂等
disable_plan / enable_plan(...)
上述函数的产品化别名
delete_plan(sql_id, plan_id [, note])
物理删除单行基线;返回是否删除
show_active_plan(sql_id)
查看某 SQL 当前激活基线(结果集函数)
show_plan_history(sql_id)
查看该 SQL 的切换/启停/审批审计历史(新→旧)
所有状态变更都会写入 tencentdb_spm.plan_evolve_log(action:evaluate / approve / reject / activate / rollback / enable / disable),rollback_plan 的默认目标即由该审计链推导。
-- 切换示例:将候选计划切换为激活计划
SELECT tencentdb_spm.activate_plan(617283..., -521096..., 'switch to seq scan plan');
-- 回滚到上一版激活计划
SELECT tencentdb_spm.rollback_plan(617283...);
-- 查看历史
SELECT * FROM tencentdb_spm.show_plan_history(617283...);

6.4 SQL Patch(create_patch)

不改应用代码、不改基线,直接为指定 sql_id 绑定一段 DBA 手写的 pg_hint_plan hint:
-- sql_id 可从 EXPLAIN 的 SPM 行或 dba_plans 中获取
SELECT tencentdb_spm.create_patch(
617283...,
'IndexScan(spm_demo spm_demo_pkey)',
'fix regression after upgrade');
行为要点:
创建后立即成为 Approved + enabled 的激活基线(custom_hint 字段存 hint 文本);
初始 plan_id = 0(未钉住),首次成功应用后写入真实 plan_id,此后进入回环校验;
与常规基线共用 activate/rollback/disable/delete 管理 API。

6.5 计划演进(advisory-only)

演进功能默认关闭(tencentdb_spm.evolve_enabled = off),开启后只做对比报告,绝不自动改变计划状态:
-- 管理员开启(PGC_SUSET)
SET tencentdb_spm.evolve_enabled = on;
函数
说明
evolve_plan_baselines(sql_id, plan_id [, min_speedup_factor=1.0] [, action='approve'] [, note])
将候选计划与当前最优 Approved 基线做耗时对比,返回观测结论;不改变任何状态
replay_compare(sql_id [, plan_a] [, plan_b] [, rounds=5] [, note])
受控重放:在捕获的绑定参数上交替强制激活计划与候选计划各执行若干轮(最多20轮),仅限纯 SELECT、每轮回滚、零副作用;单轮超时由 evolve_default_timeout_ms 控制
diagnose_param_sensitivity(sql_id)
基于 evolve_enabled=on 期间累计的结果行数直方图(rows_hist)判定语句是否参数敏感:sensitive-plan-risk(基数跨度 ≥ 100x 且多计划/双峰延迟——单一固化计划必然顾此失彼)/ sensitive / not-sensitive / insufficient-data(样本 < 20)
说明:
开启 evolve_enabled 后,执行统计还会维护多样本聚合(exec_count / EWMA / min / max / 64 桶延迟直方图),dba_plans 中的 exec_p50_ms、exec_p95_ms、rows_p05、rows_p95 由 hist_pctl() 从直方图插值计算。

维护

-- 预览/执行过期候选清理:仅删除 Unapproved 且超过保留期的行
-- Approved / Rejected 永不删除;dry_run 默认 true
SELECT tencentdb_spm.gc_plans(); -- 预览(按 plan_retention_days,默认 32 天)
SELECT tencentdb_spm.gc_plans(dry_run => false); -- 实删

-- 校验基线引用的表/索引是否仍存在
SELECT tencentdb_spm.validate_plans(sql_id, plan_id); -- 仅报告
SELECT tencentdb_spm.validate_plans(sql_id, plan_id, 'disable'); -- 失效行置 enabled=false
SELECT tencentdb_spm.validate_plans(sql_id, plan_id, 'delete'); -- 删除失效行

可观测性

入口
说明
tencentdb_spm.dba_plans
基线全景视图:状态、outline、耗时、使用计数、p50/p95 延迟、结果行数分位等
tencentdb_spm.get_stats()
集群级决策计数器(jsonb,自 postmaster 启动起):capture_attempts/skips/failures/evictions、apply_lookups/misses/fingerprint_refused/render_failures/param_mismatches/successes/mismatch_fallbacks、cache_reuse_rejected/cache_probe_reprobes
tencentdb_spm.diagnose_plan_usage(sql_id)
对单条 SQL 重放应用决策链,给出第一个阻断原因(未捕获 / 无激活 / outline 缺失 / hint 不可渲染 / 引用关系缺失)或确认可用;超级用户
tencentdb_spm.show_active_plan / show_plan_history
EXPLAIN SPM: 行
参见 快速上手 章节。
-- "为什么基线没生效"排查示例
SELECT tencentdb_spm.get_stats()->>'apply_misses' AS misses,
tencentdb_spm.get_stats()->>'apply_fingerprint_refused' AS fp_refused,
tencentdb_spm.get_stats()->>'apply_render_failures' AS render_fail;
SELECT tencentdb_spm.diagnose_plan_usage(617283...);

参数(GUC)说明

所有参数均在 tencentdb_spm.* 命名空间下(前缀已保留)。
参数
类型
默认值
上下文
说明
enable_plan_management
bool
off
SUSET
总开关,off 时捕获/应用全部跳过
capture_plan_baselines
enum
off
USERSET
捕获模式:off / manual / automatic
use_plan_baselines
bool
off
USERSET
on 时规划器强制应用激活基线
plan_capture_threshold
int
2
USERSET
automatic 模式下触发捕获的执行次数(≥1)
max_plans_per_statement
int
10
SUSET
每个 sql_id 保留的最大基线数(1 – 1024)
plan_retention_days
int
32
SUSET
Unapproved 基线保留天数(1 – 3650),供 gc_plans 使用
capture_params
bool
on
USERSET
捕获时是否快照绑定参数(受控重放必需)
evolve_enabled
bool
off
SUSET
演进功能总开关(advisory 对比 + 多样本统计聚合)
evolve_default_timeout_ms
int
30000
USERSET
演进/重放的单语句超时(毫秒,≥ 100)
auto_reject_regression_factor
real
0
USERSET
已废弃,无任何效果(单样本不足以自动拒绝)
debug_store_raw_sql
bool
off
SUSET
调试:存储原始 SQL 文本(可能泄露敏感数据,勿在生产开启)
max_shmem_entries
int
4096
POSTMASTER
共享内存执行计数器容量(128 – 1048576),需重启

数据对象与权限

对象清单

对象
类型
说明
tencentdb_spm.plan_baselines
表
基线主表,主键 (sql_id, plan_id);含 outline、状态、耗时统计、参数快照、直方图聚合、custom_hint 等共29列
tencentdb_spm.plan_evolve_log
表
全生命周期审计日志
tencentdb_spm.config
表
单行安装期元数据(格式版本等,不随 dump 迁移)
tencentdb_spm.dba_plans
视图
DBA 视角的基线全景(含延迟/基数分位)
tencentdb_spm.hist_pctl(hist, p)
函数
64桶 log2 直方图分位插值(通用工具函数)
tencentdb_spm.debug_render_hint(outline)
函数
调试:outline JSON → hint 串(仅测试用)
管理函数(set_plan_status、activate_plan、create_patch 等)均为 LANGUAGE C,仅内核 superuser() 可调用(TencentDB 的 tencentdb_superuser 不能绕过该校验,见内核 superuser.c 约定)。

只读授权:spm_role

安装时自动创建 spm_role(NOLOGIN),拥有对 plan_baselines、plan_evolve_log、config、dba_plans 的 SELECT 权限。给需要巡检的人员授予只读视图能力而无需超级用户:
GRANT spm_role TO analyst_role;

备份恢复

plan_baselines、plan_evolve_log 已注册 pg_extension_config_dump,pg_dump/pg_restore 正常迁移基线数据;config 为安装期元数据,由扩展脚本在恢复时重新播种。

使用限制与注意事项

1. 必须预加载并重启:shared_preload_libraries 未包含 tencentdb_spm 时,插件处于"惰性"状态——CREATE EXTENSION 成功但功能全部不可用,调用管理函数会报错。
2. pg_hint_plan 为应用前置:仅捕获(capture-only)场景可以不装 pg_hint_plan;应用基线、SQL Patch 均依赖它。推荐顺序 shared_preload_libraries = 'pg_hint_plan,tencentdb_spm'。
3. 不要手工 UPDATE plan_baselines:active_slot 由 C 代码在每次写入时维护,手工改 status/enabled 会破坏唯一激活约束,请始终走管理函数。
4. 自连接语句暂不可固化(v1):outline 渲染 hint 时同表别名冲突会导致渲染失败并回退自然计划;后续版本将通过别名后缀解决。
5. EXPLAIN 的 SPM 行仅文本格式输出:JSON/XML 等非文本 EXPLAIN 格式不追加该行。
6. 统计计数器重启即清零:get_stats() 的计数与执行计数器均位于共享内存,postmaster 重启后归零。
7. 演进永不自动变更:evolve_plan_baselines / replay_compare / diagnose_param_sensitivity 全部为 advisory-only;自动拒绝功能已被移除(auto_reject_regression_factor 仅为兼容保留、无效果)。
8. 参数敏感语句慎用固化:diagnose_param_sensitivity 判定为 sensitive-plan-risk 的语句,任何单一固化计划对部分参数区间都是次优的。
9. 捕获排除范围:引用系统目录/pg_catalog/information_schema 的查询不会被捕获;UTILITY 命令(DDL、EXPLAIN 本身等)不参与捕获。
10. 核内直读、无 SPI:基线查找/写入走内核 catalog API(systable 扫描),应用路径不产生执行器递归,内部错误一律降级为 WARNING/DEBUG,保证用户 SQL 不受影响。

FAQ

Q1:基线已存在,为什么 EXPLAIN 里没有 baseline forced?

A:按顺序排查:① enable_plan_management 与 use_plan_baselines 是否为 on;② 该 sql_id 是否有 Approved + enabled 行(show_active_plan);③ diagnose_plan_usage(sql_id) 给出第一个阻断原因;④ get_stats() 看指纹拒绝/渲染失败/参数不匹配计数是否增长。

Q2:manual 和 automatic 模式怎么选?

A:
manual:DBA 定点排查,某会话内执行的语句全部即时捕获(配合 capture_plan());
automatic:常态防回退,执行次数达到 plan_capture_threshold 的语句自动入基线,首个计划即激活基线。

Q3:捕获会不会拖慢业务?

A:捕获仅在计划形态变化时写一行基线(受 max_plans_per_statement 上限约束),执行统计回写为 best-effort;应用路径的查找有 per-sql 负缓存 + 代数失效,未管理语句开销接近于一次哈希探测。所有内部错误均静默降级,不影响语句执行。

Q4:升级/统计信息变化后计划变差,如何紧急止血?

A:用 EXPLAIN 拿到 sql_id → create_patch(sql_id, '...hint...') 立即固化正确计划;事后可用 rollback_plan 回退,或 activate_plan 切换到既有候选。

Q5:如何把测试环境验证过的基线迁到生产?

A:plan_baselines 随 pg_dump 迁移(origin 可标记 IMPORTED);迁移后建议先 validate_plans 校验引用对象存在性。

Q6:为什么 PREPARE 语句第二次 EXECUTE 才生效?

A:受计划缓存影响,未管理语句可复用缓存计划;一旦存在激活基线,内核会强制后续执行重新规划,从而应用基线。

Q7:drop extension 会丢基线吗?

A:会。DROP EXTENSION 删除全部基线与审计数据,请先 pg_dump 备份。