FDW 回调函数
GetForeignRelSize、
GetForeignPaths、
GetForeignPlan、
PlanForeignModify、
GetForeignJoinPaths、
GetForeignUpperPaths 和
PlanDirectModify
必须契合
PostgreSQL 规划器的工作方式。
以下是关于它们必须做什么的一些说明。
root 和
baserel 中的信息可以用来减少必须从外部表
取回的信息量(从而降低代价)。
baserel->baserestrictinfo 特别值得
关注,因为它包含应当用于过滤要取回行的限制限定符
(WHERE 子句)。
(FDW 自身无需强制这些限定符,因为核心执行器可以代为
检查。)
baserel->reltarget->exprs 可用于确定
需要取回哪些列;但注意它只列出必须由
ForeignScan 计划节点发出的列,
不列出用于限定符求值但查询不输出的列。
各种私有字段可供 FDW 规划函数保存信息。 一般来说,存储在 FDW 私有字段中的任何东西都应当是 palloc'd 的, 这样它会在规划结束时被回收。
baserel->fdw_private 是一个
void 指针,可供 FDW 规划函数存储与特定外部表相关的
信息。
核心规划器不会碰它,只在
RelOptInfo 节点被创建时把它初始化为
NULL。
它对于把信息从
GetForeignRelSize 向前传递给
GetForeignPaths、和/或从
GetForeignPaths 传递给
GetForeignPlan 很有用,
从而避免重新计算。
GetForeignPaths 可以通过把私有信息存储在
ForeignPath 节点的
fdw_private 字段中来标识不同访问路径的
含义。
fdw_private 被声明为一个
List 指针,但由于核心规划器不会碰它,它实际上可以
包含任何东西。
不过,最佳实践是使用
nodeToString 可转储的表示,以便配合后端
提供的调试支持。
GetForeignPlan 可以检查选定的
ForeignPath 节点的
fdw_private
字段,并可以生成
fdw_exprs 和
fdw_private 列表,放到
ForeignScan 计划节点中,在执行时它们
会可用。
这两个列表都必须以
copyObject 知道如何复制的形式表示。
fdw_private 列表没有其他限制,核心
后端不会以任何方式解释它。
fdw_exprs 列表(如果不是
NIL)预期包含打算在运行时执行的表达式树。
这些树会经过规划器的后处理,使它们完全可执行。
在
GetForeignPlan 中,通常可以把传入的目标
列表按原样复制到计划节点中。
传入的
scan_clauses 列表包含与
baserel->baserestrictinfo 相同的子句,
但为了更好的执行效率可能会被重新排序。
在简单情况下,FDW 可以直接从
scan_clauses 列表剥离
RestrictInfo 节点(使用
extract_actual_clauses),并把所有子句放入
计划节点的限定列表中,这意味着所有子句都会在运行时被执行器
检查。
更复杂的 FDW 或许能在内部检查其中一些子句,这种情况下这些
子句可以从计划节点的限定列表中移除,使执行器不必浪费时间去
重新检查它们。
举个例子,FDW 可能识别出一些形如
foreign_variable =
sub_expression 的限制子句,
并判定在给定
sub_expression 本地求值结果的
情况下,它们可以在远程服务器上执行。
对这类子句的实际识别应当发生在
GetForeignPaths 期间,因为它会影响该路径
的代价估算。
该路径的
fdw_private 字段可能包含指向被识别
子句的
RestrictInfo 节点的指针。
然后
GetForeignPlan 会把该子句从
scan_clauses 中移除,
但把
sub_expression 加入
fdw_exprs
以确保它被加工成可执行的形式。
它很可能还会把控制信息放入计划节点的
fdw_private 字段,告诉执行函数在
运行时要做什么。
传给远程服务器的查询会形如
WHERE ,参数值在运行时从对
foreign_variable =
$1fdw_exprs 表达式树的求值获得。
任何从计划节点限定列表中移除的子句都必须改为加入
fdw_recheck_quals,或由
RecheckForeignScan 重新检查,
以确保在
READ COMMITTED 隔离级别下的正确行为。
当查询涉及的某个其他表发生并发更新时,执行器可能需要验证
该元组是否仍然满足所有原始限定符,可能是针对一组不同的参数
值。
使用
fdw_recheck_quals 通常比在
RecheckForeignScan 内部实现检查更容易,
但当外连接被下推时这种方法会不够用,因为那种情况下连接元组的
某些字段可能变为 NULL 而不完全拒绝该元组。
FDW 可以填写的另一个
ForeignScan 字段是
fdw_scan_tlist,它描述 FDW 为这个
计划节点返回的元组。
对简单的外部表扫描,这可以设为
NIL,意味着返回的元组具有为该外部表声明的
行类型。
非
NIL 值必须是一个目标列表
(
TargetEntry 的列表),包含代表返回列的
Var 和/或表达式。
例如,这可以被用来说明 FDW 省略了它注意到该查询不需要的某些
列。
此外,如果 FDW 能比本地更廉价地计算查询使用的表达式,它可以把
那些表达式加入
fdw_scan_tlist。
注意,连接计划(由
GetForeignJoinPaths 生成的路径创建)必须
始终提供
fdw_scan_tlist 来描述它们将返回的
列集合。
FDW 应当始终构造至少一个只依赖表的限制子句的路径。
在连接查询中,它还可以选择构造依赖连接子句的路径,例如
foreign_variable =
local_variable。
这类子句不会出现在
baserel->baserestrictinfo 中,
而必须到该关系的连接列表中寻找。
使用这类子句的路径被称为
“参数化路径”。
它必须用一个合适的
param_info 值来标识所选连接子句中用到的其他
关系;使用
get_baserel_parampathinfo
计算该值。
在
GetForeignPlan 中,连接子句的
local_variable 部分会被加入
fdw_exprs,之后在运行时的处理与普通
限制子句相同。
如果 FDW 支持远程连接,GetForeignJoinPaths
应当用与
GetForeignPaths 处理基础表基本相同的方式,
为潜在的远程连接生成
ForeignPath。
关于预期连接的信息可以按上文描述的相同方式向前传给
GetForeignPlan。
不过,
baserestrictinfo 对连接关系并不相关;
相反,特定连接的相关连接子句会作为一个单独的参数
(
extra->restrictlist)传给
GetForeignJoinPaths。
此外,FDW 可能支持直接执行某些计划动作,
这些动作位于扫描和连接之上的层级,例如分组或聚集。
为了提供这类选项,FDW 应当生成路径并把它们插入相应的
上层关系。
例如,代表远程聚集的路径应当用
add_path 插入
UPPERREL_GROUP_AGG 关系。
这个路径会以代价为依据,与通过读取外部关系的一个简单扫描路径
执行的本地聚集进行比较
(注意,这样的路径也必须提供,否则在计划期会出错)。
如果远程聚集路径胜出(通常如此),它会以常规方式通过调用
GetForeignPlan 转换为一个计划。
生成这类路径的推荐位置是
GetForeignUpperPaths
回调函数;如果查询的所有基础关系都来自同一个 FDW,会为每个
上层关系(即每个扫描/连接后处理步骤)调用它。
PlanForeignModify 和
Section 58.2.4 中描述的其他回调是围绕
一个假设设计的:外部关系会以常规方式扫描,然后各行的更新由
本地
ModifyTable
计划节点驱动。
对更新需要同时读取本地表和外部表的一般情况,这种方法是必需
的。
不过,如果该操作可以完全由外部服务器执行,FDW 可以生成代表
这一点的路径,并把它插入
UPPERREL_FINAL 上层关系,在那里它与
ModifyTable 方法竞争。
这种方法也可以用来实现远程
SELECT FOR UPDATE,而不是使用
Section 58.2.6 中描述的行加锁
回调。
请记住,插入
UPPERREL_FINAL 的路径要负责实现查询的
全部行为。
在规划
UPDATE 或
DELETE 时,
PlanForeignModify 和
PlanDirectModify
可以查找外部表的
RelOptInfo
结构,并利用扫描规划函数之前创建的
baserel->fdw_private 数据。
不过,在
INSERT 中目标表不会被扫描,因此没有它的
RelOptInfo。
PlanForeignModify 返回的
List 具有与
ForeignScan 计划节点的
fdw_private 列表相同的限制,即它必须
只包含
copyObject 知道如何复制的结构。
带
ON CONFLICT 子句的
INSERT 不支持指定冲突目标,
因为远程表上的唯一约束或排除约束在本地是未知的。
这进而意味着
ON CONFLICT DO UPDATE 不受支持,
因为在那里指定冲突目标是强制性的。