本文整理于 HOW 2026 演讲内容,演讲者:杨向博,PostgreSQL ACE。
这是一个来自真实生产环境的案例。一条 SQL 在本地 PostgreSQL 实例上执行只需 17 毫秒,但通过 Postgres_fdw 访问远程数据库时,耗时长达 200 秒。两者相差超过一万倍。
初步排查时,第一反应往往是怀疑网络问题。但通过
EXPLAIN ANALYZE 查看执行计划后,问题的根源逐渐浮出水面。
在远程端实际执行的 SQL 中,原本的 Join 条件(如
A.sid = B.id)并未被正确下推。取而代之的是,本地的
WHERE 过滤条件(如
xt = 1)被错误地当成了 Join 条件下推到了远程端。最终效果是:远程端返回了大量中间结果数据,真正的 Join 关联却在本地完成,性能自然急剧恶化。
Postgres_fdw 并非无条件地将 Join 操作下推到远程端。优化器会综合评估代价,在四种 Join 路径(Nestloop、Hash Join、Merge Join,以及 Foreign Join)中选择最优方案。即使使用了 Foreign Join,也未必一定下推——仍需要通过代价模型比较才能决定。
具体到下推的条件,核心校验在
is_foreign_expr 函数中完成,需同时满足以下要求:
safety(安全)is_foreign_expr 必须返回 True本案例中的 SQL 存在一个看似无害的写法:
A.sid 和
B.id 两个字段都被显式进行了类型转换(转为
text)。这个类型转换节点触发了以下连锁反应:
is_foreign_expr 函数检测到类型转换节点,将其归入
"未知或不安全表达式" 分支(default 分支),返回
FalseA.sid = B.id)被拒绝下推WHERE xt = 1,于是该条件被强行"伪装"成 Join 条件下推到远程端xt = 1 作为 Join 条件执行,返回大量中间行,真正的表关联留在了本地完成这就是性能雪崩的根本原因。
直接修改 SQL 去除类型转换显然是最彻底的解决方式。但在实际场景中,这种写法往往涉及历史遗留系统或第三方封闭软件,SQL 不在应用方的可控范围内。修改 SQL 这条路走不通。
这时候需要从 FDW 内核层面寻找解决方案。
优化的关键在于:
在优化器生成 Foreign Join Path 的阶段进行干预。具体来说,在
add_foreign_join_paths 相关的路径生成逻辑中,嵌入一个 GUC 参数开关。
实现逻辑如下:
enable_fdw_join_pushdown),默认值为
on(允许 Join 下推)is_foreign_expr 校验之前,先检查该参数状态off,则直接阻止 Foreign Join 路径的生成,强制 Join 在本地执行当 GUC 参数关闭 Join 下推后,同样的 SQL 执行时间从
200 秒 骤降至
79 毫秒。执行计划显示,远程端仅执行了基础表的过滤扫描(
SELECT ... WHERE xt = 1),将数据拉回本地后再完成 Join 关联——虽然放弃了远程 Join 的优化机会,但在这个特定场景下,网络传输的数据量远小于错误下推导致的中间结果集,整体性能反而大幅提升。
性能提升约 3000 倍。
第一,排查 FDW 性能问题时,首要步骤是查看远程端实际执行的 SQL。 仅看本地的执行计划是不够的。使用
EXPLAIN (VERBOSE, ANALYZE) 或者查看远程端的日志,确认下推到远端的 SQL 是否符合预期。
第二,Join 键上的类型转换是 FDW 下推的常见陷阱。 类型转换节点会被
is_foreign_expr 判定为不安全表达式,导致原本合法的 Join 条件被拒绝下推。在编写跨库 SQL 时,应尽量避免在 Join 键上做显式类型转换。
第三,GUC 参数是快速止损的有效手段。 当 SQL 层面无法修改时,通过内核参数控制优化器行为,可以在不改变应用代码的前提下解决问题。对于云服务或 DBA 运维团队来说,这种"兜底"能力尤为重要——能在第一时间恢复业务,再从容规划长期方案。
本案例中新增的 GUC 参数虽只是一个简单开关,但背后的思路值得推广:很多 FDW 相关的性能问题,都可以通过类似的"干预点"设计,为用户提供更多的运行时控制能力,而不是将所有的优化决策都固化在代码逻辑中。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70043484/viewspace-3176318/ ,如需转载,请注明出处,否则将追究法律责任。