post_analyze_hook的作用
post_analyze_hook 是 TimescaleDB loader 模块中的一个静态函数(定义在 loader.c:356),其核心作用是作为 延迟加载(lazy loading)机制 的入口点。
整体架构背景
TimescaleDB 分为两层:
- loader — 通过
shared_preload_libraries在 PostgreSQL 启动时预加载(loader.c) - versioned extension — 实际的 TimescaleDB 功能,安装在每个数据库中的扩展
因为 loader 在服务器启动时、分配数据库之前就被加载了,此时无法查询扩展是否存在,所以不能直接加载完整的扩展。于是采取了懒加载策略。
具体工作流程(loader.c:37-71)
阶段 1 — 注册钩子(_PG_init,第 535 行)
post_parse_analyze_hook = post_analyze_hook;
将 post_analyze_hook 注册为 PostgreSQL 的 post_parse_analyze_hook。这是 PostgreSQL 定义的一个钩子,每条 SQL 语句被解析后都会调用它。
阶段 2 — 懒加载(hook 被调用时) 当用户第一次执行 SQL 语句时,post_analyze_hook 被触发:
- 检查当前数据库是否安装了 TimescaleDB 扩展(通过
extension_check()) - 如果是,调用
do_load()加载对应版本的.so文件 - 加载后,捕获版本化扩展自己设置的
post_parse_analyze_hook(保存在extension_post_parse_analyze_hook) - 将
post_parse_analyze_hook恢复回 loader 自己的post_analyze_hook,这样后续的 SQL 语句仍然会先经过 loader - 通过
call_extension_post_parse_analyze_hook()委托给版本化扩展的实际钩子做真正的处理
阶段 3 — 钩子链 loader 还保存了之前的钩子(prev_post_parse_analyze_hook),确保与其他也使用该钩子的扩展兼容。
函数本身的功能
post_analyze_hook 自身(从第 356 行开始)主要处理实用命令(Utility Command),例如:
ALTER DATABASE SET TABLESPACE— 在执行前停止该数据库的后台工作进程,使表空间移动成功- 其他需要拦截的 DDL 操作
总结
简而言之:post_analyze_hook 是 TimescaleDB loader 的钩子入口函数,充当代理/转发器的角色——既负责在首次使用时延迟加载真正扩展,也负责在每次 SQL 解析后将控制权转交给已加载的版本化扩展,同时还要维护多个扩展之间的钩子链完整性。
timescaledb_planner 的作用
timescaledb_planner 是 TimescaleDB 注册到 PostgreSQL planner_hook 的钩子函数(注册在 planner.c:1184),用于拦截和修改查询计划过程,使 TimescaleDB 能够对超表(hypertable)上的查询进行优化。
工作流程
static PlannedStmt *
timescaledb_planner(Query *parse, int cursor_opts, ParamListInfo bound_params)
它围绕 PostgreSQL 的标准规划器做三件事情:
1. 预处理查询(第 288-289 行)
if (ts_extension_is_loaded())
preprocess_query((Node *) parse, parse);
在规划之前,对查询树进行预处理。preprocess_query 函数会遍历查询树,对涉及超表的查询进行转换,例如将针对父表的查询重写为针对子表的查询,插入必要的约束条件等。
2. 调用原始规划链(第 291-296 行)
if (prev_planner_hook != NULL)
stmt = (prev_planner_hook)(parse, cursor_opts, bound_params);
else
stmt = standard_planner(parse, cursor_opts, bound_params);
维护规划器钩子链:如果其他扩展也注册了规划器钩子,则先调用它们的钩子;否则调用 PostgreSQL 标准的 standard_planner。这确保了与其他扩展的兼容性。
3. 后处理修复(第 298-316 行)
if (ts_extension_is_loaded()) {
ts_hypertable_insert_fixup_tlist(stmt->planTree);
foreach (lc, stmt->subplans) {
Plan *subplan = (Plan *) lfirst(lc);
ts_hypertable_insert_fixup_tlist(subplan);
}
}
在标准规划完成后,对生成的计划树进行后处理:
- 修复
HypertableInsert的目标列表(target list):TimescaleDB 自定义的HypertableInsert计划节点包装了 PostgreSQL 的ModifyTable节点。ModifyTable的最终目标列表只有在set_plan_references()运行之后才能确定,所以需要在这里做一次修复,确保HypertableInsert节点的目标列表与内部的ModifyTable节点一致。
4. 缓存管理(第 284, 327 行)
planner_hcache_push(); // 规划前推送新的缓存栈帧
// ...
planner_hcache_pop(true); // 规划后弹出并释放缓存
使用 PG_TRY/PG_CATCH 确保异常情况下也能正确释放缓存。
总结
timescaledb_planner 是 TimescaleDB 查询优化扩展的核心入口。它在 PostgreSQL 的查询规划过程中"插入"了自己的逻辑:规划前对超表查询进行改写,规划后对包含超表插入的计划进行目标列表修复。通过这种方式,TimescaleDB 能够在不修改 PostgreSQL 内核代码的前提下,实现对分布式超表查询的透明优化
timescaledb_set_rel_pathlist 的作用
timescaledb_set_rel_pathlist 是 TimescaleDB 注册到 PostgreSQL set_rel_pathlist_hook 的钩子函数(注册在 planner.c:1186),其作用是在 PostgreSQL 规划器为每个关系(表)生成访问路径时进行干预,为超表相关的关系添加自定义的优化路径。
背景
在 PostgreSQL 的查询规划过程中,规划器会为每个基表(relation)生成一组可能的访问路径(paths),如顺序扫描、索引扫描等,然后从中选择代价最低的。set_rel_pathlist_hook 允许扩展在这个阶段插入自定义逻辑。
函数流程(planner.c:742-787)
1. 快速退出(第 749-754 行)
if (!valid_hook_call() || !OidIsValid(rte->relid) || IS_DUMMY_REL(rel))
如果不是有效的钩子调用、关系 OID 无效、或者是虚拟关系(dummy rel),则直接调用前一个钩子并返回。
2. 分类关系(第 756 行)
reltype = classify_relation(root, rel, &ht);
判断当前关系是哪种类型:超表(TS_REL_HYPERTABLE)、普通表、超表子表、chunk、chunk 子表等。
3. 调用其他扩展的钩子(第 765-766 行)
if (prev_set_rel_pathlist_hook != NULL)
(*prev_set_rel_pathlist_hook)(root, rel, rti, rte);
维护钩子链,确保与其他扩展兼容。
4. 按关系类型处理(第 768-786 行)
TS_REL_HYPERTABLE_CHILD — 直接跳过,不需要特殊处理。
TS_REL_CHUNK / TS_REL_CHUNK_CHILD — 对于 chunk 关系:
- 如果是 UPDATE/DELETE 操作且涉及压缩 chunk,调用
ts_cm_functions->set_rel_pathlist_dml,由压缩模块(compression module)为 DML 操作设置访问路径,确保能正确修改压缩数据。 - 其他情况(包括非 DML 或未压缩)则向下执行
apply_optimizations。
apply_optimizations(第 784 行)— 默认处理分支,对超表、chunk 等应用 TimescaleDB 的各种优化,例如:
- 启用约束排除(constraint exclusion),根据查询条件排除不需要扫描的 chunk
- 应用排序优化
- 处理空间分区相关的优化
- 其他针对分布式超表的时间/空间维度剪枝
总结
timescaledb_set_rel_pathlist 是 TimescaleDB 查询优化的关键细粒度钩子。与 timescaledb_planner 在整体规划层面干预不同,它深入到每个关系生成访问路径的环节,让 TimescaleDB 能针对不同的关系类型(超表、chunk、压缩表等)注入专门的路径生成逻辑,是实现 chunk 剪枝、压缩透明访问等核心功能的基石之一。
timescaledb_get_relation_info_hook 的作用
timescaledb_get_relation_info_hook 是 TimescaleDB 注册到 PostgreSQL get_relation_info_hook 的钩子函数(注册在 planner.c:1189)。
它的注释已经清楚地说明了意图(planner.c:789-792):
"这个钩子用于编辑(editorialize)规划器获取的关于关系的信息。我们用它来附加自己的元数据到超表和 chunk 关系的 RelOptInfo 上,这些元数据在规划阶段是必需的。我们也在这里展开超表。"
具体工作流程
get_relation_info_hook 是 PostgreSQL 在规划器收集关系信息时调用的钩子,timescaledb_get_relation_info_hook 按关系类型做不同处理:
1. 调用前一个钩子(第 799-800 行)
if (prev_get_relation_info_hook != NULL)
prev_get_relation_info_hook(root, relation_objectid, inhparent, rel);
维护钩子链。
2. 处理超表(TS_REL_HYPERTABLE,第 807-848 行)
做了两件事:
标记超表 RTE 用于自主展开(PG12+,第 828-833 行):
if (!ts_guc_disable_optimizations && ts_guc_enable_constraint_exclusion && inhparent &&
rte->ctename == NULL && !IS_UPDL_CMD(query) && query->resultRelation == 0 &&
query->rowMarks == NIL && (rte->requiredPerms & (ACL_UPDATE | ACL_DELETE)) == 0)
{
rte_mark_for_expansion(rte);
}
在特定条件下(优化未禁用、约束排除启用、非 UPDATE/DELETE 等),将超表的 RTE 标记为"需要展开"。这样 TimescaleDB 后续可以自主控制超表展开为 chunk 集合,而不是依赖 PostgreSQL 默认的继承展开逻辑。对于 UPDATE/DELETE 会特别避开,因为 PG 对它们有特殊的二次规划流程。
创建私有 RelOptInfo(第 835 行):
ts_create_private_reloptinfo(rel);
在 RelOptInfo 上挂载 TimescaleDB 私有的元数据,用于后续规划阶段使用。
时间桶注解(PG12+,第 838 行):
ts_plan_expand_timebucket_annotate(root, rel);
对时间桶表达式(如 time_bucket())进行预处理注解。
PG11 及以下直接展开(第 840-846 行):
ts_plan_expand_hypertable_chunks(ht, root, rel);
PG11 及以下版本由于继承展开时机不同,直接在钩子里展开超表的 chunk。
3. 处理 Chunk(TS_REL_CHUNK / TS_REL_CHUNK_CHILD,第 850-874 行)
创建私有 RelOptInfo(第 853 行)。
透明解压缩支持(第 855-872 行):
if (ts_guc_enable_transparent_decompression && TS_HYPERTABLE_HAS_COMPRESSION(ht))
{
Chunk *chunk = ts_chunk_get_by_relid(chunk_rte->relid, true);
if (chunk->fd.compressed_chunk_id > 0)
{
ts_get_private_reloptinfo(rel)->compressed = true;
rel->indexlist = NIL; // 清空索引列表
}
}
对于已压缩的 chunk:
- 标记该关系为"已压缩"
- 清空索引列表:因为压缩 chunk 中的所有数据实际上存储在压缩文件中,未压缩版本上的索引毫无意义,清除索引列表可以避免规划器浪费大量时间来生成无用的 IndexPath,显著提升规划性能
总结
timescaledb_get_relation_info_hook 在规划器收集关系元数据的阶段介入,为超表和 chunk 附加私有元数据,控制超表展开策略,并为压缩 chunk 优化访问路径。它是后续 timescaledb_set_rel_pathlist 能够做出正确优化决策的数据准备基础。
timescale_create_upper_paths_hook 的作用
timescale_create_upper_paths_hook 是 TimescaleDB 注册到 PostgreSQL create_upper_paths_hook 的钩子函数(注册在 planner.c:1192)。它在规划器的上层路径生成阶段被调用,用于处理和优化超表上的 INSERT 和聚合查询。
函数流程(planner.c:991-1045)
1. 调用前一个钩子(第 999-1000 / 1008-1009 行)
维护钩子链。
2. 委托给压缩模块(第 1015-1016 行)
if (ts_cm_functions->create_upper_paths_hook != NULL)
ts_cm_functions->create_upper_paths_hook(root, stage, input_rel, output_rel);
将控制权转交给压缩模块的自定义钩子(如果存在),用于处理压缩相关的上层路径生成。
3. 替换超表上的 INSERT 路径(第 1020-1022 行)
if (output_rel->pathlist != NIL)
output_rel->pathlist = replace_hypertable_insert_paths(root, output_rel->pathlist);
这是最核心的功能之一。调用 replace_hypertable_insert_paths 对 INSERT 路径进行改造,其工作原理在注释中详细说明(planner.c:908-964):
原始计划:
[ ModifyTable ] → resultRelation
↑ Tuple
[ subplan ]
改造后的计划:
[ HypertableInsert ]
↑
[ ModifyTable ] → resultRelation
↑ Tuple ↑
[ ChunkDispatch ] ── 将 resultRelation 设置为匹配的 chunk 表
↑ Tuple
[ subplan ]
关键点:
ModifyTablePath是 PostgreSQL 用于 INSERT/UPDATE/DELETE 的标准路径replace_hypertable_insert_paths检查每个ModifyTablePath的目标是否是超表- 如果是超表,则调用
ts_hypertable_insert_path_create将其包装为HypertableInsert路径节点 - 在
HypertableInsert内部,插入一个ChunkDispatch节点,它在执行时根据元组数据动态查找目标 chunk,并设置正确的resultRelation,实现将 INSERT 路由到正确的 chunk
4. 处理部分聚合(第 1023-1029 行)
if (parse->hasAggs && stage == UPPERREL_GROUP_AGG)
{
partials_found = ts_plan_process_partialize_agg(root, input_rel, output_rel);
}
如果查询包含聚合函数且当前阶段是分组聚合阶段(UPPERREL_GROUP_AGG),则处理部分聚合(partial aggregation),支持在 chunk 级别先做部分聚合,然后在上层合并结果。
5. 添加 HashAgg 优化(第 1037-1043 行)
if (stage == UPPERREL_GROUP_AGG && output_rel != NULL)
{
if (!partials_found)
ts_plan_add_hashagg(root, input_rel, output_rel);
if (parse->hasAggs)
ts_preprocess_first_last_aggregates(root, root->processed_tlist);
}
- 如果没有部分聚合被处理,添加 HashAgg 路径作为额外的聚合选项,为规划器提供更多选择
- 预处理
first()/last()聚合函数,这些是 TimescaleDB 自定义的聚合,需要特殊处理
总结
timescale_create_upper_paths_hook 在 PostgreSQL 规划器的上层路径生成阶段介入,主要做三件事:
| 功能 | 说明 |
|---|---|
| INSERT 路由 | 将超表上的 INSERT 计划改造为 HypertableInsert → ChunkDispatch 结构,实现元组到正确 chunk 的透明路由 |
| 部分聚合 | 在分组聚合阶段处理 chunk 级别的部分聚合,支持跨 chunk 的高效聚合 |
| 聚合优化 | 添加 HashAgg 路径选项,处理自定义聚合函数(如 first/last) |
这是 TimescaleDB 透明分布式写入和查询 的关键基础设施之一,让用户无需关心数据具体落在哪个 chunk 上,系统自动处理路由。
PostgreSQL 查询处理全流程
SQL 语句
│
1. Parser(解析器)──── 生成解析树(parse tree)
│
2. Analyzer(分析器)── 生成查询树(Query tree)
│ └─ post_parse_analyze_hook ← ★ 第一个钩子
│
3. Rewriter(重写器)── 应用规则重写
│
4. Planner(规划器)── 生成执行计划(PlannedStmt)
│ ├─ planner_hook ← ★ 第二个钩子(整体规划入口)
│ │
│ ├─ get_relation_info()(每个关系)
│ │ └─ get_relation_info_hook ← ★ 第三个钩子(收集关系信息)
│ │
│ ├─ set_rel_pathlist()(每个关系)
│ │ └─ set_rel_pathlist_hook ← ★ 第四个钩子(生成访问路径)
│ │
│ └─ create_upper_paths()(上层路径)
│ └─ create_upper_paths_hook ← ★ 第五个钩子(聚合/排序/INSERT 改造)
│
5. Executor(执行器)── 执行计划
关键理解:第 2 步是为每个 SQL 语句调用一次;第 4 步中,
get_relation_info_hook→set_rel_pathlist_hook→create_upper_paths_hook是为**每个关系(表)**调用的,如果查询涉及多个表,会重复执行多次。
场景一:SELECT 查询超表
以 SELECT * FROM conditions WHERE time > '2020-01-01' 为例(conditions 是超表):
钩子调用链
① post_parse_analyze_hook → post_analyze_hook(loader.c:356)
时机:SQL 语句语义分析完成后
做了什么:
- 首次调用时,
extension_check()→do_load()→ 延迟加载真正的 TimescaleDB 扩展.so - 加载后,捕获版本化扩展的钩子,保存到
extension_post_parse_analyze_hook - 然后调用版本化扩展的钩子(即实际的
post_analyze_hook在 init.c 中) - 后续调用时:直接
call_extension_post_parse_analyze_hook()转交控制权
本质:这是 TimescaleDB 懒加载机制的入口,确保实际扩展只在需要时才加载。
② planner_hook → timescaledb_planner(planner.c:279)
时机:SQL 进入规划器时(最外层入口)
做了什么:
- 推入规划器缓存栈(
planner_hcache_push) - 调用
preprocess_query()— 遍历查询树:- 识别涉及超表的范围表条目(RTE)
- 如果启用了约束排除且不是 UPDATE/DELETE,标记超表 RTE 用于展开(
rte_mark_for_expansion) - 对时间分区等做预处理
- 调用
standard_planner()(或前一个规划器钩子)— 真正进入 PostgreSQL 标准规划流程 - 规划完成后,修复
HypertableInsert的目标列表
③ get_relation_info_hook → timescaledb_get_relation_info_hook(planner.c:793)
时机:规划器为每个表收集基本信息时(在 set_rel_pathlist 之前)
发生了什么 — 对于超表:
- PG12+:再次尝试标记 RTE 展开(处理内联函数中未被 preprocess_query 捕获的情况)
- 创建私有 RelOptInfo — 挂载 TimescaleDB 自己的元数据结构
- PG12+:
ts_plan_expand_timebucket_annotate()— 注解时间桶表达式 - PG11 及以下:直接调用
ts_plan_expand_hypertable_chunks()— 展开超表为 chunk 集合
对于 chunk 且已压缩:
- 标记
compressed = true - 清空索引列表 — 因为压缩 chunk 上的索引无用,避免规划器浪费时间生成索引扫描路径
④ set_rel_pathlist_hook → timescaledb_set_rel_pathlist(planner.c:742)
时机:规划器为每个表生成所有可能的访问路径时
发生了什么:
根据关系类型做不同处理:
-
超表(
TS_REL_HYPERTABLE) — 调用apply_optimizations():- 约束排除:根据 WHERE 条件(如
time > '2020-01-01')排除不需要扫描的 chunk,只保留相关 chunk - 排序优化、空间分区剪枝等
- 约束排除:根据 WHERE 条件(如
-
已压缩的 chunk(
TS_REL_CHUNK) — 如果是 UPDATE/DELETE 涉及压缩 chunk:- 调用
set_rel_pathlist_dml处理压缩数据的 DML 路径 - 否则也通过
apply_optimizations优化
- 调用
结果:规划器只收到需要扫描的 chunk 的访问路径,不需要的 chunk 被剪枝掉了。这就是 TimescaleDB chunk 剪枝(约束排除) 的实现。
⑤ create_upper_paths_hook → timescale_create_upper_paths_hook(planner.c:991)
时机:规划器生成上层路径(聚合、排序、分组等)时
发生了什么(SELECT 查询):
- 如果查询有聚合(
GROUP BY time_bucket(...)):- 调用
ts_plan_process_partialize_agg()— 处理部分聚合,在每个 chunk 上先做部分聚合,再合并结果 - 调用
ts_plan_add_hashagg()— 添加 HashAgg 路径作为候选 - 预处理
first()/last()自定义聚合
- 调用
场景二:INSERT 插入超表
以 INSERT INTO conditions VALUES ('2020-01-01', 25.0, ...) 为例:
①-③ 与 SELECT 完全一样
post_parse_analyze_hook— 确保扩展已加载timescaledb_planner—preprocess_query预处理get_relation_info_hook— 附加元数据
④ set_rel_pathlist_hook
有所不同:对于 INSERT,超表不会被标记展开(因为有 IS_UPDL_CMD 检查),chunk 路径统一由 apply_optimizations 处理。
⑤ create_upper_paths_hook — 重点在此
这是 INSERT 跟 SELECT 最不同的地方:
if (output_rel->pathlist != NIL)
output_rel->pathlist = replace_hypertable_insert_paths(root, output_rel->pathlist);
replace_hypertable_insert_paths() 遍历所有路径,遇到 ModifyTablePath(INSERT 用)且目标为超表时:
标准 PostgreSQL INSERT 计划:
[ ModifyTable ] ──→ resultRelation = conditions (超表)
↑
[ ValuesScan ]
改造后的计划:
[ HypertableInsert ]
↑
[ ModifyTable ] ──→ resultRelation = conditions (超表)
↑ ↗
[ ChunkDispatch ] ─ 根据元组时间戳,动态将 resultRelation 设置对正确的 chunk
↑
[ ValuesScan ]
这就是 TimescaleDB INSERT 路由的机制 — 用户 INSERT 到超表,系统自动将数据分发到正确的 chunk。
完整调用顺序总结表
| 顺序 | 钩子 | 函数 | 阶段 | SELECT 作用 | INSERT 作用 |
|---|---|---|---|---|---|
| 1 | post_parse_analyze_hook | post_analyze_hook | 分析后 | 懒加载扩展,确保就绪 | 同上 |
| 2 | planner_hook | timescaledb_planner | 规划入口 | preprocess_query → 标记展开 | preprocess_query |
| 3 | get_relation_info_hook | timescaledb_get_relation_info_hook | 收集信息 | 创建私有元数据、标记压缩、展开超表 | 同上(但不标记展开) |
| 4 | set_rel_pathlist_hook | timescaledb_set_rel_pathlist | 生成路径 | 约束排除、剪枝 chunk | apply_optimizations |
| 5 | create_upper_paths_hook | timescale_create_upper_paths_hook | 上层路径 | 部分聚合、HashAgg、first/last | INSERT 路径改造(插入 ChunkDispatch) |
一句话概括
扩展懒加载 → 查询预处理 → 关系元数据准备 → 约束排除/路径生成 → 上层路径优化(聚合改造或 INSERT 路由)
每个钩子在前一个的基础上深化处理,层层递进,最终由规划器选择代价最低的执行计划。

937

被折叠的 条评论
为什么被折叠?



