timescaledb RegisterCustomScanMethods
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] │ ├─ 创建 ChunkAppendPathCustomPath └─ 设置 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 PlanPostgreSQL 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 字段回调函数PathChunkAppendPathcpath.methodsts_chunk_append_plan_createPath→Plan 转换PlanCustomScanmethodsts_chunk_append_state_createPlan→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 的 planPostgreSQL 可以根据CustomName字符串重建methods指针。直接赋值cscan-methods xxx只在同一进程内、不跨越序列化边界时有效。为什么 ChunkAppend 需要ChunkDispatch 不需要ChunkAppendchunk_append/planner.c:50用于SELECT查询在 planner.c:706-717 可以看到ChunkAppend也会出现在partial_pathlist并行查询路径中foreach (lc, rel-partial_pathlist) { ... *pathptr ts_chunk_append_path_create(root, rel, ht, *pathptr, true, false, NIL);这里的第4个参数true表示is_partial说明 ChunkAppend 可以用于并行查询并行查询需要把 plan序列化后发给 worker 进程worker 端需要反序列化重建 CustomScan如果不注册worker 收到 ChunkAppend 的 CustomScan 时找不到对应的methods会报错ChunkDispatchchunk_dispatch_plan.c用于INSERT查询嵌入在ModifyTable内部PostgreSQL 的ModifyTable执行是单进程的不参与并行查询INSERT 的 plan 不会被序列化传给其他进程plan 在同一个 backend 里完成 planning → executionmethods指针一直有效所以光靠cscan-methods chunk_dispatch_plan_methods;就够了同理HypertableInsert同属 INSERT 路径也没有调用RegisterCustomScanMethods原因完全一样。总结CustomScan用于并行查询需要序列化RegisterCustomScanMethods?ChunkAppendSELECT✅ 可能partial pathlist✅ 需要传递给 worker调用ChunkDispatchINSERT (ModifyTable)❌ 不支持❌ 在同一进程内不调用HypertableInsertINSERT (ModifyTable)❌ 不支持❌ 在同一进程内不调用本质上就是只在需要跨进程传递 plan并行查询 / prepared statement plan 缓存时才需要注册。INSERT 路径上的 CustomScan 一直留在同一个进程里直接赋值指针就够了。注册的核心是CreateCustomScanStateplan → state用于反序列化之后的 executor 重建不是PlanCustomPathpath → plan。CustomPathMethodsvsCustomScanMethods这两个是不同阶段、不同生命周期的东西阶段结构体函数指针注册planning: path → planCustomPathMethodsPlanCustomPath❌ 不用注册随CustomPath指针传递规划在同一进程内完成execution: plan → stateCustomScanMethodsCreateCustomScanState✅ 需要RegisterCustomScanMethods因为 plan 可能被序列化再反序列化ChunkDispatch 在 chunk_dispatch_plan.c:82-85 只定义了CustomPathMethodsstatic CustomPathMethods chunk_dispatch_path_methods { .CustomName ChunkDispatchPath, .PlanCustomPath chunk_dispatch_plan_create, // ← path → plan };而在 chunk_dispatch_plan.c:33-36 定义了CustomScanMethodsstatic CustomScanMethods chunk_dispatch_plan_methods { .CustomName ChunkDispatch, .CreateCustomScanState create_chunk_dispatch_state, // ← plan → state };但这两个都不应该被等价对待—— 关键问题为什么 ChunkDispatch 不需要RegisterCustomScanMethods前面说因为不支持并行——这个答案依然成立但更精确的表述是RegisterCustomScanMethods解决的场景PostgreSQL 的_readCustomScannodeRead反序列化在读到custom_private和custom_name之后会调用FindCustomScanMethods(name)来查找注册过的CustomScanMethods。也就是说只要这个 plan 节点有可能被序列化→反序列化就必须注册CreateCustomScanState否则重建 executor state 时会找不到方法。哪些场景会序列化 plan并行查询— 主进程_writeCustomScan→ 文本 → worker_readCustomScan→ 需要找到CreateCustomScanStateEXPLAIN (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直接赋值指针就够了不需要全局注册。