chunk_append_plan_methods的注册与调用链条
注册:在set_rel_pathlist_hook之前完成
_PG_init() [init.c:80] └─ _planner_init() [init.c:80] ← 注册所有 planner 钩子 ├─ planner_hook = timescaledb_planner ├─ set_rel_pathlist_hook = timescaledb_set_rel_pathlist ├─ get_relation_info_hook = timescaledb_get_relation_info_hook └─ create_upper_paths_hook = timescale_create_upper_paths_hook └─ _chunk_append_init() [init.c:82] ← 独立于钩子,在扩展初始化时 └─ RegisterCustomScanMethods(&chunk_append_plan_methods) [planner.c:50]这一步不是任何钩子触发的,而是 TimescaleDB 扩展加载时(_PG_init)直接注册的。RegisterCustomScanMethods告诉 PostgreSQL 系统:有一个名为"ChunkAppend"的自定义扫描类型存在,PostgreSQL 在序列化/反序列化计划时可以识别它。
调用:两个阶段,通过两层 methods 串联
阶段 1 — 计划生成阶段(Creating Plan)
用户执行 SELECT 查询超表 │ 1. post_parse_analyze_hook ─── 确保扩展已加载 2. planner_hook ─── preprocess_query 预处理 3. get_relation_info_hook ─── 附加元数据,展开超表 4. set_rel_pathlist_hook ─── ★ 关键入口 │ └─ timescaledb_set_rel_pathlist() [planner.c:742] └─ apply_optimizations() [planner.c:638] │ └─ 对超表,遍历 rel->pathlist: 遇到 AppendPath / MergeAppendPath └─ ts_chunk_append_path_create() [chunk_append.c:44] │ ├─ 创建 ChunkAppendPath(CustomPath) └─ 设置 cpath.methods = &chunk_append_path_methods [chunk_append.c:72-73]// chunk_append_path_methods 定义在 chunk_append.c:32 static CustomPathMethods chunk_append_path_methods = { .CustomName = "ChunkAppend", .PlanCustomPath = ts_chunk_append_plan_create, // ← 回调 };PostgreSQL 的标准规划流程会调用PlanCustomPath回调来将CustomPath转换为Plan节点:
│ 5. PostgreSQL planner → PlanCustomPath │ └─ ts_chunk_append_plan_create() [planner.c:89] │ ├─ 创建 CustomScan 节点 └─ 设置 cscan->methods = &chunk_append_plan_methods [planner.c:103]// chunk_append_plan_methods 定义在 planner.c:42 static CustomScanMethods chunk_append_plan_methods = { .CustomName = "ChunkAppend", .CreateCustomScanState = ts_chunk_append_state_create, // ← 执行时回调 };同时create_upper_paths_hook还会做后处理(修复HypertableInsert的目标列表等)。
阶段 2 — 执行阶段(Executing Plan)
PostgreSQL Executor │ └─ CreateCustomScanState │ └─ ts_chunk_append_state_create(chunk_append/exec.c) │ └─ 创建 ChunkAppendState 执行状态 负责在运行时:按顺序或并行方式扫描多个 chunk完整调用链一览
注册阶段(扩展加载时): _PG_init └─ _chunk_append_init() → RegisterCustomScanMethods(&chunk_append_plan_methods) 计划阶段(set_rel_pathlist_hook 内): timescaledb_set_rel_pathlist ← set_rel_pathlist_hook └─ apply_optimizations() └─ ts_chunk_append_path_create() └─ 创建 CustomPath + chunk_append_path_methods PostgreSQL 标准规划器: └─ PlanCustomPath(回调) └─ ts_chunk_append_plan_create() └─ 创建 CustomScan + chunk_append_plan_methods 执行阶段(executor 启动): ExecInitCustomScan └─ CreateCustomScanState(回调) └─ ts_chunk_append_state_create()关键技术点
| 层级 | 数据结构 | methods 字段 | 回调函数 |
|---|---|---|---|
| Path | ChunkAppendPath | cpath.methods | ts_chunk_append_plan_create(Path→Plan 转换) |
| Plan | CustomScan | methods | ts_chunk_append_state_create(Plan→State 执行) |
双层 methods 设计是 PostgreSQLCustomScan接口的标准模式:第一层把自定义路径转为计划节点,第二层把计划节点转为执行状态。chunk_append_plan_methods就是第二层的那个。它本身不通过任何 hook 调用,而是通过set_rel_pathlist_hook生成的ChunkAppendPath一路携带,最终由 PostgreSQL 执行器框架回调触发的。
chunkdispatch为什么不调用RegisterCustomScanMethods函数呢?
关键区别在于ChunkAppend 用于 SELECT 查询(可能会参与并行查询),而ChunkDispatch 用于 INSERT(在 ModifyTable 内部,永远不会被序列化)。
RegisterCustomScanMethods的作用
RegisterCustomScanMethods把CustomScanMethods注册到 PostgreSQL 的全局哈希表中,这样当 plan 被序列化再反序列化时(比如传给并行 worker,或者缓存 prepared statement 的 plan),PostgreSQL 可以根据CustomName字符串重建methods指针。
直接赋值cscan->methods = &xxx只在同一进程内、不跨越序列化边界时有效。
为什么 ChunkAppend 需要,ChunkDispatch 不需要
ChunkAppend(chunk_append/planner.c:50):
- 用于SELECT查询
- 在 planner.c:706-717 可以看到,ChunkAppend也会出现在
partial_pathlist(并行查询路径)中:
这里的第4个参数foreach (lc, rel->partial_pathlist) { ... *pathptr = ts_chunk_append_path_create(root, rel, ht, *pathptr, true, false, NIL);true表示is_partial,说明 ChunkAppend 可以用于并行查询 - 并行查询需要把 plan序列化后发给 worker 进程,worker 端需要反序列化重建 CustomScan
- 如果不注册,worker 收到 "ChunkAppend" 的 CustomScan 时找不到对应的
methods,会报错
ChunkDispatch(chunk_dispatch_plan.c):
- 用于INSERT查询,嵌入在
ModifyTable内部 - PostgreSQL 的
ModifyTable执行是单进程的,不参与并行查询 - INSERT 的 plan 不会被序列化传给其他进程
- plan 在同一个 backend 里完成 planning → execution,
methods指针一直有效 - 所以光靠
cscan->methods = &chunk_dispatch_plan_methods;就够了
同理
HypertableInsert(同属 INSERT 路径)也没有调用RegisterCustomScanMethods,原因完全一样。
总结
| CustomScan | 用于 | 并行查询? | 需要序列化? | RegisterCustomScanMethods? |
|---|---|---|---|---|
| ChunkAppend | SELECT | ✅ 可能(partial pathlist) | ✅ 需要传递给 worker | 调用 |
| ChunkDispatch | INSERT (ModifyTable) | ❌ 不支持 | ❌ 在同一进程内 | 不调用 |
| HypertableInsert | INSERT (ModifyTable) | ❌ 不支持 | ❌ 在同一进程内 | 不调用 |
本质上就是:只在需要跨进程传递 plan(并行查询 / prepared statement plan 缓存)时,才需要注册。INSERT 路径上的 CustomScan 一直留在同一个进程里,直接赋值指针就够了。
注册的核心是CreateCustomScanState(plan → state,用于反序列化之后的 executor 重建),不是PlanCustomPath(path → plan)。
CustomPathMethodsvsCustomScanMethods
这两个是不同阶段、不同生命周期的东西:
| 阶段 | 结构体 | 函数指针 | 注册? |
|---|---|---|---|
| planning: path → plan | CustomPathMethods | PlanCustomPath | ❌ 不用注册,随CustomPath指针传递,规划在同一进程内完成 |
| execution: plan → state | CustomScanMethods | CreateCustomScanState | ✅ 需要RegisterCustomScanMethods,因为 plan 可能被序列化再反序列化 |
ChunkDispatch 在 chunk_dispatch_plan.c:82-85 只定义了CustomPathMethods:
static CustomPathMethods chunk_dispatch_path_methods = { .CustomName = "ChunkDispatchPath", .PlanCustomPath = chunk_dispatch_plan_create, // ← path → plan };而在 chunk_dispatch_plan.c:33-36 定义了CustomScanMethods:
static CustomScanMethods chunk_dispatch_plan_methods = { .CustomName = "ChunkDispatch", .CreateCustomScanState = create_chunk_dispatch_state, // ← plan → state };但这两个都不应该被等价对待—— 关键问题:为什么 ChunkDispatch 不需要RegisterCustomScanMethods?
前面说"因为不支持并行"——这个答案依然成立,但更精确的表述是:
RegisterCustomScanMethods解决的场景
PostgreSQL 的_readCustomScan(nodeRead反序列化)在读到custom_private和custom_name之后,会调用FindCustomScanMethods(name)来查找注册过的CustomScanMethods。也就是说,只要这个 plan 节点有可能被序列化→反序列化,就必须注册CreateCustomScanState,否则重建 executor state 时会找不到方法。
哪些场景会序列化 plan?
- 并行查询— 主进程
_writeCustomScan→ 文本 → worker_readCustomScan→ 需要找到CreateCustomScanState - EXPLAIN (FORMAT JSON)等输出格式 — 这些只
_write不_read,不需要注册 - plan 日志 /
debug_print_plan— 只写不读,也不需要 - prepared statement 跨 session— 实际上 PG 的 CachedPlan 是纯内存的,不跨进程序列化
总结
注册的是CreateCustomScanState(反序列化后重建执行状态),不是PlanCustomPath(路径转计划阶段)。
ChunkAppend 注册是因为它可能出现在partial_pathlist中,plan 会被发给并行 worker 反序列化执行。ChunkDispatch/HypertableInsert 的 plan 只在主进程的 ModifyTable 内部使用,永远不会被序列化传给其他进程,所以cscan->methods = &chunk_dispatch_plan_methods直接赋值指针就够了,不需要全局注册。