plancache.c 设计与调用链说明
1. 简述
plancache用于PBE协议,prepare、execute语句,以及SPI、存储过程内sql的调用,通过缓存执行计划,优化sql多次执行时的时间(不需要重复parse和生成plan)
本文档归档 src/backend/utils/cache/plancache.c 的核心设计,补充以下内容:
- 模块职责与整体分层
- 关键数据结构含义
- 主要函数的职责划分
PREPARE/EXECUTE、extended query protocol、SPI、PL/pgSQL等调用链QueryEnvironment/ ENR 对plancache的影响- 失效机制、锁策略、内存策略
对应源码:
src/backend/utils/cache/plancache.csrc/include/utils/plancache.hsrc/include/nodes/plannodes.hsrc/backend/commands/prepare.csrc/backend/tcop/unvdb.csrc/backend/executor/spi.csrc/pl/plpgsql/src/pl_exec.csrc/backend/utils/misc/queryenvironment.c
2. 模块职责
plancache 不是一个单纯的"执行计划缓存器",而是一个组合模块,承担 4 类职责:
- 缓存查询源信息:保存 SQL 文本、原始语法树、重写后的
Query树、参数信息、结果描述等。 - 缓存可执行计划:在需要时生成
CachedPlan,并决定是否复用 generic plan。 - 跟踪依赖并处理失效:响应 relation/syscache invalidation,把过期对象标为无效。
- 在 generic plan 与 custom plan 之间动态选择:根据参数、环境和历史成本选择最合适的执行路径。
文件头注释已经明确指出两个主线:
- 选择 generic plan 还是 custom plan( generic plan 参数未知,生成的为根据统计信息的通用计划,custom plan 传入参数,生成对应参数优化后的计划)
- 跟踪 schema/object 变化导致的计划失效
2.1 总体架构图
下面这张图描述 plancache 在数据库内核中的位置。
SQL / 协议消息 / SPI / PL
Parser
raw parse tree
Analyzer + Rewriter
Query list
plancache
CachedPlanSource
Planner
plancache
CachedPlan
Portal / Executor
relcache / syscache invalidation
search_path / role / RLS / QueryEnvironment
从这张图可以看到:
plancache夹在 Query 语义层和执行计划层之间- 它既面向上层调用者,也面向失效系统
- 它不是 parser、planner、executor 的替代,而是三者之间的协调层
2.2 核心对象关系图
下面这张图描述 CachedPlanSource、CachedPlan、CachedExpression 的关系。
CachedPlanSource
raw_parse_tree
query_string
query_list + relationOids + invalItems
resultDesc
gplan
cost statistics
CachedPlan
stmt_list
refcount
saved_xmin
cached_expression_list
CachedExpression
planned expr
relationOids + invalItems
可以把三者理解成:
CachedPlanSource:查询源与 Query 层缓存CachedPlan:可执行计划对象CachedExpression:表达式级别的小型缓存
2.3 分层视图
这张图强调"谁负责哪一层"。
语义与执行
plancache
上层调用者
PREPARE / EXECUTE
extended protocol PBE协议 PREPARE/BIND/EXEC
SPI
PL/pgSQL
CachedPlanSource
SQL文本 + RawStmt + Query树
决策逻辑
generic vs custom
失效逻辑
sinval + 环境校验
CachedPlan
PlannedStmt + refcount
Analyze / Rewrite
Planner
Executor / Portal
2.4 失效与重建架构图
这张图强调为什么 invalidation 只是"打标记",不直接重建。
DDL / catalog change
sinval callback
mark plansource invalid
mark gplan invalid
等待下一次 GetCachedPlan
RevalidateCachedQuery
Analyze + Rewrite
BuildCachedPlan
返回新 plan
设计意图:
-
把失效广播做得尽量便宜
-
把真正昂贵的 analyze/rewrite/plan 延后到"下一次真的有人需要执行"时再做
cstatic dlist_head saved_plan_list = DLIST_STATIC_INIT(saved_plan_list); static dlist_head cached_expression_list = DLIST_STATIC_INIT(cached_expression_list); // 创建了两个全局静态list,用于保存所有创建的plansource和expression,在收到失效消息后,遍历链表对对应的cache做检查失效
2.5 生命周期与持有关系图
这张图强调 CachedPlan 为什么必须用引用计数。
generic 引用
临时引用
持有引用
登记引用
否
是
CachedPlanSource
CachedPlan
当前执行
Portal
ResourceOwner
refcount == 0 ?
继续存活
释放 plan->context
一句话理解:
CachedPlanSource可以消失- 但只要还有 Portal、执行器或 owner 在用,该
CachedPlan仍需存活
3. 核心数据结构
3.1 关键数据结构字段详解
CachedPlanSource 代表"一个可重复使用的查询源",它不是"执行计划",而是"生成执行计划的母体",主要保存:
c
typedef struct CachedPlanSource
{
int magic; /* 魔数,做对象类型与内存损坏检查 */
// 原始语句相关,不随上下文变化
struct RawStmt *raw_parse_tree; /* 原始语法树。失效后重新 analyze/rewrite 时要从这里重新开始 */
const char *query_string; /* 原始 SQL 文本。用于报错、日志、调试、重新分析 */
MemoryContext context; // 以上变量保存在该内存上下文中, 生命周期和PlanSource一致
// query相关,依赖其他对象,失效后需要重建
CommandTag commandTag; /* 语句类型标签,例如 SELECT / INSERT / UPDATE */
Oid *param_types;
int num_params; /* 固定参数类型定义。用于参数校验与重新分析 */
ParserSetupHook parserSetup;
void *parserSetupArg; /* 动态参数解析回调。SPI 等场景会使用 */
int cursor_options; /* 规划选项,例如强制 generic/custom、并行能力等 */
bool fixed_result; /* 结果结构是否必须固定。prepared statement 常设为 true */
TupleDesc resultDesc; /* 当前结果描述。Describe/EXECUTE/结果兼容检查要用它 */
List *query_list; /* analyze + rewrite 后的 Query 列表。失效后可整体丢弃重建 */
List *relationOids; /* Query 层 relation 依赖,供 relation invalidation 使用 */
List *invalItems; /* Query 层其他 catalog 依赖,主要是函数/类型等 syscache 对象 */
struct OverrideSearchPath *search_path; /* 建立 query_list 时使用的 search_path。环境变化时要重解析 */
MemoryContext query_context; /* 保存 query_list 与依赖信息的上下文。失效时可整体删除 */
Oid rewriteRoleId;
bool rewriteRowSecurity;
bool dependsOnRLS;
/* 记录 rewrite 时的角色与 row_security 状态,用于判断 RLS 是否要求重写 */
// plan相关
struct CachedPlan *gplan;
/* generic plan 的引用。存在时表示当前有可复用的通用计划 */
bool is_oneshot;
/* 是否为 one-shot。为 true 时不做长期失效跟踪与深拷贝语义 */
bool is_complete;
/* 是否已经完成 CompleteCachedPlan。GetCachedPlan 的前置条件 */
bool is_saved;
/* 是否已经迁入长期存储并加入全局 saved_plan_list */
bool is_valid;
/* Query 层缓存是否仍有效。sinval、search_path、RLS 变化会把它打成 false */
int generation;
/* 每次新建 CachedPlan 时递增,可视作 plansource 的计划代数 */
dlist_node node;
/* 挂入全局 saved_plan_list 的链表节点,仅 is_saved 时有效 */
double generic_cost;
/* generic plan 成本估计;-1 表示还没真正试算过 generic 成本 */
double total_custom_cost;
int64 num_custom_plans;
int64 num_generic_plans;
/* generic/custom 决策统计数据,用于 choose_custom_plan 的经验判断 */
} CachedPlanSource;
字段使用节奏可以概括为:
- 创建阶段主要写
raw_parse_tree、query_string、参数定义与状态位 CompleteCachedPlan主要写query_list、依赖信息、search_path、resultDesc- 执行阶段主要读
is_valid、gplan、成本统计 - 失效阶段主要改
is_valid和gplan->is_valid
CachedPlan 字段详解
c
typedef struct CachedPlan
{
int magic; /* 魔数,做对象完整性检查 */
List *stmt_list; /* 规划后的 PlannedStmt 列表。Executor、Portal、SPI 直接消费它 */
bool is_oneshot; /* 是否来自 one-shot plansource。决定释放时能否删除独占上下文 */
bool is_saved;
/* 是否已迁入长期上下文。若要交给 ResourceOwner 托管,通常要求为 true */
bool is_valid;
/* 计划当前是否仍可执行。失效回调、角色变化、saved_xmin 变化会把它打成 false */
Oid planRoleId;
bool dependsOnRole;
/* 角色依赖信息。若计划与角色绑定,则换用户后不能直接复用 */
TransactionId saved_xmin;
/* transient plan 的事务边界。TransactionXmin 变化后该计划要重建 */
int generation;
/* 对应父 plansource 的 generation 快照,用于调试与扩展安全检查 */
int refcount;
/* 引用计数。gplan、Portal、当前执行、ResourceOwner 都可能持有引用 */
MemoryContext context;
/* 计划自己的内存上下文。refcount 归零后通常整体删除 */
} CachedPlan;
字段使用节奏可以概括为:
BuildCachedPlan负责一次性填满绝大多数字段GetCachedPlan/ReleaseCachedPlan主要维护refcountCheckCachedPlan主要检查is_valid、dependsOnRole、saved_xmin- Portal 与 Executor 主要消费
stmt_list
CachedExpression 字段详解
c
typedef struct CachedExpression
{
int magic;
/* 魔数,做对象完整性检查 */
Node *expr;
/* 已经过 expression_planner 的表达式树,可直接供后续执行使用 */
bool is_valid;
/* 是否仍有效。plancache 只负责打失效标记,不负责自动重建 */
List *relationOids;
/* relation 依赖。表相关失效消息到达时用它判断是否过期 */
List *invalItems;
/* 其他 catalog 对象依赖,例如函数/类型等 */
MemoryContext context;
/* 表达式缓存对象的上下文 */
dlist_node node;
/* 挂入全局 cached_expression_list 的链表节点 */
} CachedExpression;
字段使用节奏可以概括为:
GetCachedExpression一次性填完结构- 失效回调只改
is_valid - 调用方发现
is_valid = false后必须自己重建
PlanInvalItem 字段详解
c
typedef struct PlanInvalItem
{
NodeTag type;
/* 节点类型标签 */
int cacheId;
/* syscache ID,标识依赖的是哪一类 catalog cache */
uint32 hashValue;
/* 该对象查找键对应的 hash,用于在失效回调中快速匹配 */
} PlanInvalItem;
它的作用非常单一但很关键:
- relation 用
relationOids跟踪 - 非 relation 的 catalog 依赖主要靠
PlanInvalItem
例如:
- 函数定义变化
- 类型定义变化
- 某些系统 catalog 对象变化
3.6 这些数据结构分别适合何时使用
这一节从"调用者视角"解释什么时候该用哪个对象。
适合使用 CachedPlanSource 的场景
- 你希望保存一条 SQL 的原始语义信息,而不是只执行一次
- 你需要在未来因为 DDL、RLS、
search_path变化而重新分析/重写 - 你希望让系统自动决定 generic/custom plan
- 你正在实现 prepared statement、SPI plan、可重用 Portal 背后的计划源
不适合的场景:
- 只执行一次,且生命周期严格受当前上下文控制
- 不需要 invalidation 跟踪
这时更适合 CreateOneShotCachedPlan。
oneshot plan主要在SPI_execute、SPI_execute_extended、SPI_execute_with_args 函数内调用调用 单次调用 计划执行后销毁

适合使用 CachedPlan 的场景
- 你已经要开始执行一条语句
- 你需要把
PlannedStmt挂到 Portal 或 Executor - 你需要一个带 refcount 的可执行计划对象
不适合的场景:
- 仅想查看 SQL 的参数定义、结果结构或 Query 语义信息
这时应该看 CachedPlanSource,而不是直接追 CachedPlan。
适合使用 CachedExpression 的场景
- 你缓存的是标量表达式,不是完整 SQL
- 调用方能够接受"失效后自己重建"
- 你只需要 expression planner 结果
典型场景:
- PL/pgSQL 内部 cast/简单表达式缓存
适合使用 one-shot 计划的场景
- 语句只执行一次
- 希望减少树复制和失效跟踪开销
- 执行链较短,且不要求长期缓存
典型场景:
SPI_execute()一次性语句- 某些内部临时执行路径
适合使用 generic plan 的场景
- 语句会被频繁执行
- 参数值分布对计划影响不大
- 规划成本相对执行成本占比高
适合使用 custom plan 的场景
- 参数值强烈影响索引选择、分区裁剪或 join 路径
- 同一语句不同参数会导致很不一样的最优计划
- 规划额外成本能被执行收益抵消
4. 核心设计思路
4.1 分层缓存
模块把缓存拆成两层:
CachedPlanSource缓存 Query 语义层CachedPlan缓存 PlannedStmt 执行层
这样做的原因是:
- Query 层和 Plan 层的依赖不同
- Query 层和 Plan 层的失效条件不同
- Query 层和 Plan 层的生命周期不同
4.2 懒失效、懒重建
收到 invalidation 时不立即重建,而是:
- 把
plansource->is_valid = false - 如果有 generic plan,则把
gplan->is_valid = false
等下次真正调用 GetCachedPlan() 时,再重做:
- parse analyze
- rewrite
- planning
4.3 环境参与缓存键
SQL 文本一样,不代表查询语义一致。以下环境都会影响缓存合法性:
search_path- 当前用户
row_securityTransactionXminQueryEnvironment
其中 search_path 和 RLS 直接在 plancache.c 中检查;QueryEnvironment 则通过上层调用路径传入分析与规划流程。
4.4 generic/custom 自适应决策
如果语句带参数,系统不会总是固定用 generic 或 custom,而是:
- 先做若干次 custom plan(默认5次)
- 累积 custom 平均成本
- 生成 generic plan 并估算其成本 (在做完5次custom plan后,生成)
- 比较
generic_cost与平均 custom 成本,(对比的是之前生成的平均 custom 成本,并不直接重新生成custom plan,只有比generic_cost更优时才再次生成计划) - 选择更优路径
4.5 内存上下文分治
plancache 的内存布局非常重要:
plansource->context:保存 SQL 文本、raw tree、参数、结果描述等稳定信息plansource->query_context:保存当前有效的Query树和依赖,可整体删除重建plan->context:保存PlannedStmt等执行计划对象
这种设计让"失效后整体丢弃 Query 层"变得非常便宜。
5. 关键函数分组
5.1 创建与保存
CreateCachedPlanCreateOneShotCachedPlanCompleteCachedPlanSaveCachedPlanDropCachedPlanCopyCachedPlan
职责:
- 构造
CachedPlanSource - 填充分析/重写结果
- 在需要时迁入长期内存
- 支持复制和销毁
5.2 重验证与建计划
RevalidateCachedQueryCheckCachedPlanBuildCachedPlanGetCachedPlanReleaseCachedPlan
职责:
- 检查缓存是否仍然合法
- 在需要时重做分析/重写
- 生成 generic 或 custom plan
- 管理计划引用计数
5.3 generic/custom 决策
choose_custom_plancached_plan_cost
职责:
- 根据参数、GUC、cursor options、历史统计,判断当前应使用哪一类计划
5.4 快速有效性检查
CachedPlanAllowsSimpleValidityCheckCachedPlanIsSimplyValid
职责:
- 给 PL/pgSQL 等高频简单表达式提供快路径
- 避免每次都走完整 revalidate + lock + replan
5.5 锁与结果辅助
AcquirePlannerLocksAcquireExecutorLocksScanQueryForLocksScanQueryWalkerPlanCacheComputeResultDescQueryListGetPrimaryStmt
职责:
- 获取 planning/execution 所需锁
- 遍历 Query 树中的子查询与子链接
- 计算结果 tuple descriptor
5.6 失效回调
InitPlanCachePlanCacheRelCallbackPlanCacheObjectCallbackPlanCacheSysCallbackResetPlanCache
职责:
- 向 inval 系统注册回调
- 响应 relation / syscache 失效
- 在必要时精准或全量标记缓存无效
5.7 表达式缓存
GetCachedExpressionFreeCachedExpression
职责:
- 缓存独立标量表达式的 planned form
6. 主流程
6.1 查询从 SQL 到 CachedPlan
text
SQL
-> raw_parser / pg_parse_query
-> CreateCachedPlan / CreateOneShotCachedPlan
-> analyze + rewrite
-> CompleteCachedPlan
-> (可选) SaveCachedPlan
-> GetCachedPlan
-> RevalidateCachedQuery
-> choose_custom_plan
-> CheckCachedPlan / BuildCachedPlan
-> 执行
-> ReleaseCachedPlan
6.2 invalidation 到重建
text
DDL / catalog 变化
-> sinval 回调
-> PlanCacheRelCallback / PlanCacheObjectCallback / PlanCacheSysCallback
-> plansource/gplan 标记 invalid
-> 下一次 GetCachedPlan()
-> RevalidateCachedQuery()
-> analyze + rewrite + plan 重建
6.3 关键代码伪代码
本节不是源码逐行翻译,而是抽取核心控制逻辑,帮助快速建立执行模型。
RevalidateCachedQuery 伪代码
适合阅读时机:
- 你想理解"为什么计划失效后不是立即重建"
- 你想理解
search_path、RLS、锁与 race condition 的关系
text
function RevalidateCachedQuery(plansource, queryEnv):
if plansource is oneshot or statement does not require revalidation:
assert plansource is valid
return NIL
if plansource is currently valid:
if saved search_path != current search_path:
invalidate plansource and gplan
if plansource is currently valid and dependsOnRLS:
if rewrite role or row_security changed:
invalidate plansource
if plansource is still valid:
acquire planner locks on query_list
if plansource is still valid:
return NIL
else:
release planner locks
discard old query_list, dependency info, search_path, query_context
release generic plan if any
ensure snapshot if needed
copy raw_parse_tree
analyze + rewrite again using fixed params or parserSetup callback
recompute resultDesc
if resultDesc changed and fixed_result is true:
ERROR
else if resultDesc changed:
update plansource->resultDesc
copy new querytree into new query_context
extract dependencies
save current RLS info and search_path
mark plansource valid
return transient querytree list
BuildCachedPlan 伪代码
适合阅读时机:
- 你想理解 Query 层对象是如何变成
PlannedStmt - 你想理解 transient plan、role dependency、
saved_xmin的来源
text
function BuildCachedPlan(plansource, qlist, boundParams, queryEnv):
if plansource is not valid:
qlist = RevalidateCachedQuery(plansource, queryEnv)
if qlist is NIL:
if not oneshot:
qlist = deep copy of plansource->query_list
else:
qlist = plansource->query_list
ensure snapshot if planning requires one
plist = pg_plan_queries(qlist, query_string, cursor_options, boundParams)
if not oneshot:
create plan_context
copy plist into plan_context
plan = new CachedPlan
plan->stmt_list = plist
plan->planRoleId = current user
plan->dependsOnRole = plansource->dependsOnRLS or any plannedstmt dependsOnRole
plan->saved_xmin = TransactionXmin if any plannedstmt is transient
plan->refcount = 0
plan->is_valid = true
plan->generation = ++plansource->generation
return plan
choose_custom_plan 伪代码
适合阅读时机:
- 你想理解 generic/custom 到底怎么选
- 你在分析为什么某条 prepared statement 总是 custom 或总是 generic
text
function choose_custom_plan(plansource, boundParams):
if oneshot:
return custom
if boundParams is NULL:
return generic
if statement does not require replanning:
return generic
if GUC forces generic/custom:
obey GUC
if cursor option forces generic/custom:
obey cursor option
if num_custom_plans < 5:
return custom
avg_custom_cost = total_custom_cost / num_custom_plans
if generic_cost < avg_custom_cost:
return generic
else:
return custom
GetCachedPlan 伪代码
适合阅读时机:
- 你想知道外部调用
GetCachedPlan()后内部到底做了多少事 - 你在排查"为什么本次执行拿到的是 generic/custom"
text
function GetCachedPlan(plansource, boundParams, owner, queryEnv):
ensure plansource complete
if owner is given, plansource must be saved
qlist = RevalidateCachedQuery(plansource, queryEnv)
customplan = choose_custom_plan(plansource, boundParams)
if customplan is false:
if generic plan exists and passes CheckCachedPlan:
plan = gplan
else:
plan = BuildCachedPlan(plansource, qlist, NULL, queryEnv)
replace old gplan with new generic plan
generic_cost = cached_plan_cost(plan, include_planner = false)
customplan = choose_custom_plan(plansource, boundParams)
qlist = NIL
if customplan is true:
plan = BuildCachedPlan(plansource, qlist, boundParams, queryEnv)
total_custom_cost += cached_plan_cost(plan, include_planner = true)
num_custom_plans++
else:
num_generic_plans++
increase refcount
if owner exists:
remember plan in ResourceOwner
if customplan and plansource is saved:
move plan to CacheMemoryContext
return plan
CachedPlanIsSimplyValid 伪代码
适合阅读时机:
- 你在看 PL/pgSQL simple expression 快路径
- 你想区分"完整重验证"和"简单有效性检查"
text
function CachedPlanIsSimplyValid(plansource, plan, owner):
if plansource is invalid:
return false
if plan is NULL:
return false
if plan != plansource->gplan:
return false
if plan->is_valid is false:
return false
if saved search_path != current search_path:
return false
if owner is provided:
increase refcount and record owner
return true
6.4 哪些接口适合何时调用
这一节回答"该代码何时适合调用"。
| 接口 | 适合调用时机 | 不适合调用时机 |
|---|---|---|
CreateCachedPlan |
需要构造可重用计划源,且后续还要 CompleteCachedPlan |
只执行一次且不想复制数据 |
CreateOneShotCachedPlan |
一次性执行路径,想避免复制和失效跟踪 | 需要长期缓存、需要 SaveCachedPlan |
CompleteCachedPlan |
已经拿到 analyze/rewrite 后的 Query 列表 |
还没做 analyze/rewrite |
SaveCachedPlan |
准备把计划源挂入长期结构或全局可重用容器 | one-shot 计划、尚未 complete |
GetCachedPlan |
真正即将执行,需要可执行 PlannedStmt |
仅想查看计划源信息,不准备执行 |
ReleaseCachedPlan |
执行结束或不再需要该 plan 引用时 | 还在被 Portal/owner 使用时贸然提前释放 |
CachedPlanAllowsSimpleValidityCheck |
已拿到 generic plan,想为简单表达式建立快路径资格 | plan 不是 generic,或不是"极简单"查询 |
CachedPlanIsSimplyValid |
已经确认该 plan 允许 simple validity check | 从未经过 CachedPlanAllowsSimpleValidityCheck 认证 |
GetCachedExpression |
想缓存标量表达式的 planned form | 你需要完整 SQL 级缓存与自动重建 |
ResetPlanCache |
全局环境变化后需要批量失效计划 | 普通执行路径里频繁调用 |
7. 调用链补充
本节补充谁在调用 plancache,以及为什么这样调用。
7.1 SQL PREPARE / EXECUTE 调用链
入口文件:
src/backend/commands/prepare.c
PREPARE 路径
PrepareQuery() 的主链路:
text
PrepareQuery
-> CreateCachedPlan
-> pg_analyze_and_rewrite_varparams
-> CompleteCachedPlan
-> StorePreparedStatement
-> SaveCachedPlan
设计原因:
PREPARE必须保存原始语句和参数类型,供后续多次执行复用fixed_result = true,因为 SQL 层 prepared statement 默认不允许结果结构悄悄变化StorePreparedStatement()最后才SaveCachedPlan(),这样中途报错不会把半成品挂进长期存储
EXECUTE 路径
ExecuteQuery() 的主链路:
text
ExecuteQuery
-> FetchPreparedStatement
-> EvaluateParams
-> CreateNewPortal
-> GetCachedPlan
-> PortalDefineQuery
-> PortalStart
-> PortalRun
-> PortalDrop
这里 GetCachedPlan() 负责:
- 检查计划是否过期
- 必要时重规划
- 按参数决定 generic/custom
注意源码中特意强调:
GetCachedPlan()与PortalDefineQuery()之间不要放会抛错的逻辑
原因是:
GetCachedPlan()已经增加了 plan refcount- 如果中间抛错,可能导致 plan 引用泄漏
7.2 extended query protocol 调用链
入口文件:
src/backend/tcop/unvdb.c
主要包括 Parse/Bind 两段。
Parse message 路径
exec_parse_message() 的关键链路:
text
Parse message
-> pg_parse_query
-> CreateCachedPlan
-> pg_analyze_and_rewrite_varparams
-> CompleteCachedPlan
-> StorePreparedStatement 或 SaveCachedPlan
说明:
- named statement:进入 prepared statement 哈希表
- unnamed statement:保存到
unnamed_stmt_psrc
这条路径和 SQL PREPARE 类似,但入口来自前后端协议,而不是 SQL utility。
Bind message 路径
exec_bind_message() 的关键链路:
text
Bind message
-> 找到 CachedPlanSource
-> 解析并转换参数
-> CreatePortal
-> GetCachedPlan
-> PortalDefineQuery
-> PortalStart
这里有几个值得注意的设计点:
- 在调用
GetCachedPlan()之前,先把 query string、stmt name、参数值等复制到 portal 上下文。 - 如果参数输入或重新规划可能需要快照,就提前
PushActiveSnapshot()。 - 参数都被标记为
PARAM_FLAG_CONST,这样 custom plan 可以充分利用参数常量。
这和 plancache 的设计是吻合的:
- generic plan 用
boundParams = NULL - custom plan 用真实参数值驱动优化器
7.3 SPI 调用链
入口文件:
src/backend/executor/spi.c
SPI 是存储过程语言、触发器、内部执行框架的重要基础设施,因此它是 plancache 的大调用者。
SPI_prepare 路径
对应内部函数 _SPI_prepare_plan():
text
_SPI_prepare_plan
-> raw_parser
-> 对每个 RawStmt:
CreateCachedPlan
pg_analyze_and_rewrite_withcb / fixedparams
CompleteCachedPlan
-> plan->plancache_list
特点:
- 生成的是 unsaved
CachedPlanSource SPIPlan可以包含多个CachedPlanSourcequeryEnv会透传到 analyze/rewrite 过程
SPI one-shot 路径
对应 _SPI_prepare_oneshot_plan():
text
_SPI_prepare_oneshot_plan
-> raw_parser
-> 对每个 RawStmt:
CreateOneShotCachedPlan
-> 延迟到执行时再 CompleteCachedPlan
为什么要有 one-shot:
- 减少拷贝和 invalidation 开销
- 适合一次性执行
- 避免前一条 DDL 改写后一条语句时过早分析带来的问题
SPI_execute_plan 路径
对应 _SPI_execute_plan():
text
_SPI_execute_plan
-> 遍历 plan->plancache_list
-> 如果是 oneshot:
analyze + rewrite
CompleteCachedPlan
-> GetCachedPlan(plansource, options->params, plan_owner, _SPI_current->queryEnv)
-> 执行 stmt_list
-> ReleaseCachedPlan
设计重点:
plan_owner只对 saved plan 有意义queryEnv会继续传给GetCachedPlan()- one-shot 计划直到执行点才真正补全 Query 层缓存
SPI_cursor_open 路径
SPI 打开游标时也会走 plancache:
text
SPI_cursor_open_internal
-> GetCachedPlan
-> 如果 plan 未保存:
copy stmt_list 到 portal 上下文
ReleaseCachedPlan
-> PortalDefineQuery
-> PortalStart
设计原因:
- portal 不应依赖一个短生命周期的 unsaved
CachedPlanSource - 对 unsaved plan,宁可复制
stmt_list,也不要让 portal 持有不安全依赖
SPI_plan_get_cached_plan
这是一个专门给 PL/pgSQL 等上层使用的桥接函数:
text
SPI_plan_get_cached_plan
-> 如果不是 oneshot 且仅有一个 CachedPlanSource
-> GetCachedPlan(plansource, NULL, owner, _SPI_current->queryEnv)
-> 返回 generic plan
它的用途是:
- 上层语言拿到一个稳定的 generic plan 引用
- 再结合
CachedPlanAllowsSimpleValidityCheck()走简单表达式快路径
7.4 PL/pgSQL 调用链
入口文件:
src/pl/plpgsql/src/pl_exec.c
PL/pgSQL 和 plancache 结合最紧的部分,不是普通 SQL 执行,而是"简单表达式优化"。
首次识别 simple expression
exec_simple_check_plan() 的关键链路:
text
exec_simple_check_plan
-> SPI_plan_get_plan_sources
-> exec_is_simple_query
-> SPI_plan_get_cached_plan
-> CachedPlanAllowsSimpleValidityCheck
-> 保存 expr_simple_plansource / expr_simple_plan
含义:
- 先判定查询是不是足够简单
- 再确认
plancache也认可这个 plan 可以走快路径
后续重用 simple expression
后续执行时:
text
exec_eval_simple_expr / related path
-> EnsurePortalSnapshotExists
-> CachedPlanIsSimplyValid
-> 若有效则直接复用
-> 若无效:
SPI_plan_get_cached_plan
CachedPlanAllowsSimpleValidityCheck
exec_save_simple_expr
这条路径依赖 plancache 提供的两个快路径函数:
CachedPlanAllowsSimpleValidityCheckCachedPlanIsSimplyValid
因此可以把 plancache 看成 PL/pgSQL 简单表达式优化的核心支点之一。
7.5 QueryEnvironment / ENR 调用链
相关文件:
src/backend/utils/misc/queryenvironment.csrc/backend/executor/spi.c
QueryEnvironment 是一个"查询环境容器",目前主要用于保存 ENR(Ephemeral Named Relation,临时命名关系)信息。
基本能力:
text
create_queryEnv
-> 创建 QueryEnvironment
register_ENR / unregister_ENR
-> 注册或注销 ENR
get_ENR / get_visible_ENR_metadata
-> 查询 ENR 元数据
在 SPI 里的链路:
text
SPI_register_relation
-> 如果 _SPI_current->queryEnv 为空则 create_queryEnv
-> register_ENR
SPI 执行 / 准备
-> pg_analyze_and_rewrite_* (..., _SPI_current->queryEnv)
-> GetCachedPlan(..., _SPI_current->queryEnv)
-> portal->queryEnv = _SPI_current->queryEnv
这意味着:
QueryEnvironment不是plancache自己维护的- 但它会参与 analyze/rewrite/plan 阶段
- 所以
plancache的重规划逻辑必须接受queryEnv
换句话说,plancache 缓存的不是"完全脱离上下文的抽象 SQL",而是"在某个查询环境中分析和规划过的 SQL"
8. generic plan 与 custom plan 选择
核心函数:
choose_custom_plancached_plan_cost
决策规则可概括为:
- oneshot 总是 custom
- 无参数时没有 custom 的意义
plan_cache_mode或 cursor options 可强制决策- 前 5 次先走 custom,积累统计
- 再比较
generic_cost和平均 custom 成本
注意:
- custom 成本包含规划开销
- generic 成本通常只算执行成本
因此 generic plan 只要"执行成本低于 custom 的平均总成本"就值得复用。
9. transient plan 说明
9.1 transient plan 是什么
transient plan 指的是:
- 当前可以生成并执行
- 但它的正确性还依赖当前事务可见性边界
- 一旦
TransactionXmin变化,就应该重新规划
对应字段:
PlannedStmt.transientPlanPlannerGlobal.transientPlanCachedPlan.saved_xmin
可以把它理解成:
- 普通 plan:依赖 schema/object/search_path/RLS 等环境
- transient plan:除了上面这些,还额外依赖当前事务快照边界
9.2 plancache 如何处理 transient plan
planner 生成 PlannedStmt 后,会把 glob->transientPlan 写入 PlannedStmt.transientPlan。
之后 BuildCachedPlan() 会扫描所有 PlannedStmt:
- 如果发现任一
plannedstmt->transientPlan = true - 则整个
CachedPlan被视为 transient - 并记录当前
TransactionXmin到plan->saved_xmin
后续 CheckCachedPlan() 在复用 generic plan 前会检查:
saved_xmin是否有效- 是否仍等于当前
TransactionXmin
如果不相等,就把 plan 标记失效并触发重建。
9.3 planner 通常在什么场景下置位 transientPlan
在当前代码中,planner 的明确置位点主要位于:
src/backend/optimizer/util/plancat.c
核心场景是:
- 某个索引
indisvalid = true - 但是
indcheckxmin = true - 并且该索引对应 catalog tuple 的
xmin还没有早于当前TransactionXmin
此时 planner 会:
- 不使用这个索引
- 同时把
root->glob->transientPlan = true
也就是说,当前规划结果之所以成立,依赖于"这个索引此刻还不能安全使用"这个事务可见性事实。
一旦事务边界推进,这个索引可能变得可用,那么原计划就不再是最合适或最安全的复用对象,因此需要重规划。
可以把这段逻辑概括成:
text
if index is valid but still behind indcheckxmin horizon:
mark current plan as transient
ignore this index for current planning
9.4 为什么会出现 indcheckxmin 这种情况
这通常和索引可见性、HOT 链、以及某些索引构建/状态切换阶段有关。
从 planner 的角度,不需要理解所有底层细节,只需要记住:
- 某些索引虽然已经"有效"
- 但对当前事务快照来说还"不能安全用于查询"
- 如果 planner 因此跳过了该索引,那么生成的计划就不应被长期稳定复用
源码注释也直接提示参考:
src/backend/access/heap/README.HOT
9.5 一个直观例子
假设某张表上新出现了一个"对未来事务会可用、但对当前事务还不安全"的索引。
当前事务做规划时:
- planner 发现这个索引不能安全使用
- 因此选择 seqscan 或其他 index path
- 同时把 plan 标记为 transient
后面换到新的事务边界后:
- 该索引可能已经进入可安全使用状态
- planner 此时可能会选择完全不同的访问路径
所以旧 plan 不能被盲目复用。
9.6 伪代码
text
planner build relation paths:
for each index on relation:
if index is invalid:
skip it
else if index->indcheckxmin is true
and index xmin is not older than TransactionXmin:
mark root->glob->transientPlan = true
skip this index for current planning
else:
index can participate in path generation
BuildCachedPlan:
if any plannedstmt.transientPlan is true:
plan.saved_xmin = TransactionXmin
else:
plan.saved_xmin = InvalidTransactionId
CheckCachedPlan:
if saved_xmin is valid and saved_xmin != current TransactionXmin:
invalidate plan
9.7 目前代码层面的结论
如果严格按当前代码追踪,planner 里 transientPlan 的明确置位点主要就是这一类:
- 因
indcheckxmin导致某些索引当前事务不可安全使用
所以可以说:
- "通常"是这个场景
- "在当前代码实现里,核心且明确的置位场景也是这个场景"
10. 失效机制
10.1 relation 级失效
PlanCacheRelCallback 负责处理 relation invalidation:
- 检查
plansource->relationOids - 再检查
gplan->stmt_list中的plannedstmt->relationOids
这样做的原因是:
- planner 阶段的依赖可能比 Query 层更多
10.2 syscache 对象失效
PlanCacheObjectCallback 处理 PROCOID、TYPEOID 等对象:
- 通过
PlanInvalItem(cacheId, hashValue)精准匹配
10.3 全量失效
PlanCacheSysCallback 和某些外部调用会直接触发:
ResetPlanCache
典型入口:
DISCARD PLANSDISCARD ALLsession_replication_role变更
10.4 失效回调伪代码
这一节帮助理解 invalidation 回调"为什么只是打标,不直接重建"。
PlanCacheRelCallback 伪代码
适合阅读时机:
- 你在分析某张表 DDL 后,为什么某些计划失效、某些没有
- 你想理解 Query 层依赖和 Plan 层依赖为什么都要检查
text
function PlanCacheRelCallback(relid):
for each saved plansource:
if plansource already invalid:
continue
if statement does not require revalidation:
continue
if relid matches plansource->relationOids:
invalidate plansource
invalidate gplan if any
if gplan still valid:
scan gplan->stmt_list relation dependencies
if relid matches any plannedstmt dependency:
invalidate gplan only
for each cached expression:
if expression already invalid:
continue
if relid matches expression relation dependencies:
invalidate expression
PlanCacheObjectCallback 伪代码
适合阅读时机:
- 你在分析函数替换、类型变化为什么会导致计划失效
text
function PlanCacheObjectCallback(cacheid, hashvalue):
for each saved plansource:
if plansource already invalid:
continue
if statement does not require revalidation:
continue
if any plansource->invalItems match (cacheid, hashvalue):
invalidate plansource
invalidate gplan if any
if gplan still valid:
if any plannedstmt->invalItems match (cacheid, hashvalue):
invalidate gplan only
for each cached expression:
if any expression->invalItems match (cacheid, hashvalue):
invalidate expression
ResetPlanCache 伪代码
适合阅读时机:
- 你在看
DISCARD PLANS、DISCARD ALL、全局环境切换
text
function ResetPlanCache():
for each saved plansource:
if already invalid:
continue
if statement does not require revalidation:
continue
invalidate plansource
invalidate gplan if any
for each cached expression:
invalidate expression
关键点:
- 回调只打失效标记,不做 analyze/rewrite/plan
- 真正重建延迟到下一次
GetCachedPlan - 这样做能把失效广播的成本压到最低
11. 锁策略
11.1 planning 锁
AcquirePlannerLocks / ScanQueryForLocks 负责在 Query 层递归获取锁:
- relation
- subquery
- CTE
SubLink
11.2 execution 锁
AcquireExecutorLocks 负责对 PlannedStmt 的 rtable 获取执行锁。
11.3 为什么要"加锁后再检查一次 is_valid"
因为存在竞争窗口:
text
先检查有效
-> 还没拿到锁
-> 期间收到 invalidation
-> 如果不复查,就可能使用过期计划
所以代码采用:
text
检查看起来有效
-> 获取锁
-> 再检查有效位
这正是 RevalidateCachedQuery() 和 CheckCachedPlan() 里的关键防线。
12. 内存与生命周期
12.1 CachedPlanSource 生命周期
- 刚创建时,通常挂在当前工作内存上下文下
SaveCachedPlan()后迁入CacheMemoryContextDropCachedPlan()删除plansource本体
12.2 CachedPlan 生命周期
- 由
refcount决定 plansource->gplan会持有一个引用- portal / ResourceOwner / 当前执行也可能持有引用
ReleaseCachedPlan()降到 0 后释放plan->context
12.3 one-shot 生命周期
- 不复制底层树
- 不加入全局失效链表
- 不支持保存和复制
- 生命周期绑定调用方上下文
12.4 引用持有关系矩阵
这一节专门回答一个常见问题:谁在持有 CachedPlan 的引用?
| 持有者 | 持有方式 | 何时建立 | 何时释放 |
|---|---|---|---|
CachedPlanSource->gplan |
永久引用 | 建立 generic plan 时 | ReleaseGenericPlan / DropCachedPlan |
| 当前执行者 | 临时引用 | GetCachedPlan 返回前 |
ReleaseCachedPlan |
Portal |
通过 PortalDefineQuery 关联 |
获取 plan 后定义 portal | portal 销毁时 |
ResourceOwner |
资源拥有者登记 | GetCachedPlan(owner != NULL) |
owner 清理或显式 ReleaseCachedPlan |
| simple expression resowner | 专用快路径持有 | CachedPlanAllowsSimpleValidityCheck |
事务结束或重规划 |
可以把 CachedPlan 想成"多方共享的只读执行计划对象":
plansource持有它,是为了 generic 复用Portal持有它,是为了延迟执行与游标执行ResourceOwner持有它,是为了异常路径也能兜底释放
12.5 生命周期常见组合
组合 1:长期 prepared statement
text
CreateCachedPlan
-> CompleteCachedPlan
-> SaveCachedPlan
-> 多次 GetCachedPlan / ReleaseCachedPlan
-> 最终 DropCachedPlan
适合场景:
- PREPARE/EXECUTE
- 协议级 named statement
组合 2:SPI 非持久化计划
text
CreateCachedPlan
-> CompleteCachedPlan
-> 不 SaveCachedPlan
-> GetCachedPlan
-> 执行
-> ReleaseCachedPlan
适合场景:
- SPI 内部普通计划
- 生命周期不需要跨更长范围
组合 3:one-shot 执行
text
CreateOneShotCachedPlan
-> 执行前 CompleteCachedPlan
-> GetCachedPlan
-> 执行
-> 不做长期保存
适合场景:
- 一次性动态执行
- 不值得做长期 invalidation 跟踪
13. 典型调用模板
这一节从"如果你自己要写调用代码,该怎么接"来总结。
13.1 模板一:实现一个可重用 prepared statement
适合场景:
- 你要把 SQL 语句保存起来,后续重复执行
text
raw = raw_parser(sql)
plansource = CreateCachedPlan(raw, sql, commandTag)
querytree = analyze_and_rewrite(raw, params, queryEnv)
CompleteCachedPlan(plansource,
querytree,
querytree_context_or_NULL,
param_types,
num_params,
parserSetup,
parserSetupArg,
cursor_options,
fixed_result = true)
SaveCachedPlan(plansource)
for each execution:
cplan = GetCachedPlan(plansource, boundParams, owner, queryEnv)
execute(cplan->stmt_list)
ReleaseCachedPlan(cplan, owner)
调用要点:
- 如果结果结构不允许变化,
fixed_result应设为true - 如果打算让
ResourceOwner托管,必须先SaveCachedPlan
13.2 模板二:实现一次性 SPI 风格执行
适合场景:
- 只执行一次,且不希望额外复制和 invalidation 跟踪
text
raw = raw_parser(sql)
plansource = CreateOneShotCachedPlan(raw, sql, commandTag)
querytree = analyze_and_rewrite(raw, params, queryEnv)
CompleteCachedPlan(plansource,
querytree,
NULL,
param_types,
num_params,
parserSetup,
parserSetupArg,
cursor_options,
fixed_result = false)
cplan = GetCachedPlan(plansource, boundParams, NULL, queryEnv)
execute(cplan->stmt_list)
ReleaseCachedPlan(cplan, NULL)
调用要点:
- 不要对 one-shot 调用
SaveCachedPlan - 不要指望 one-shot 能自动处理后续 invalidation
13.3 模板三:实现简单表达式快路径
适合场景:
- 查询足够简单,想用
CachedPlanIsSimplyValid降低重复检查成本
text
cplan = SPI_plan_get_cached_plan(spiplan)
if cplan exists and CachedPlanAllowsSimpleValidityCheck(plansource, cplan, simple_resowner):
save plansource and cplan for fast path
else:
fallback to normal execution path
on later execution:
if CachedPlanIsSimplyValid(plansource, cplan, simple_resowner):
reuse directly
else:
rebuild through normal GetCachedPlan path
调用要点:
- 只能对已确认"足够简单"的 generic plan 用
- 不能跳过第一次资格判定,直接调用
CachedPlanIsSimplyValid
14. 常见误用与风险点
14.1 在 one-shot 计划上调用 SaveCachedPlan
这是错误用法。
原因:
- one-shot 不保证原始树和分析树可长期安全保存
- 代码中会直接报错
14.2 在 GetCachedPlan 和 PortalDefineQuery 之间插入可能报错的逻辑
这是很危险的用法。
原因:
GetCachedPlan已经增加了 plan refcount- 中间报错会导致引用泄漏
14.3 把 CachedPlan 当成可随意修改的对象
这也是错误用法。
原因:
- generic plan 是可复用对象
- 多处可能共享同一
CachedPlan - 调用方应把它视为只读
14.4 忘记 ReleaseCachedPlan
这是最常见资源泄漏点之一。
后果:
refcount无法归零plan->context无法释放- 长期运行可能累积内存泄漏
14.5 用 CachedPlanIsValid 代替完整重验证
这通常不够安全。
原因:
CachedPlanIsValid只读plansource->is_valid- 它不处理并发窗口,不获取锁,不保证 race-free
14.6 把 QueryEnvironment 变化当成无关因素
这在 SPI/ENR 场景下容易出错。
原因:
queryEnv会影响 analyze/rewrite/plan 过程- 尤其是 ENR 可见性会改变名字解析与对象绑定结果
15. 读代码建议
建议按下面顺序读源码:
src/include/utils/plancache.hsrc/backend/utils/cache/plancache.c文件头与结构化注释RevalidateCachedQueryBuildCachedPlanchoose_custom_planGetCachedPlanprepare.c、unvdb.c、spi.c的调用点pl_exec.c中 simple expression 快路径
16. 结论
plancache 的核心不是"把计划存起来"这么简单,而是:
- 按层缓存 Query 与 Plan
- 把运行环境纳入合法性判断
- 依赖 invalidation 做懒失效
- 在执行前重验证并补锁
- 自适应选择 generic/custom
- 给上层语言提供快路径能力
如果要继续深入,下一步建议顺着下面两条继续读:
-
extract_query_dependencies()如何抽取 relation/syscache 依赖 -
Portal生命周期如何承接CachedPlan的 refcount -
plancache的具体使用场景:prepare、spi、plsql以及PBE协议中的使用