FDW 回调函数GetForeignRelSize、GetForeignPaths、GetForeignPlan、PlanForeignModify、GetForeignJoinPaths、GetForeignUpperPaths以及PlanDirectModify必须适应PostgreSQL规划器的工作方式。下面给出一些关于它们必须做什么的说明。
root 和 baserel 中的信息可用于减少必须从外部表获取的信息量(从而降低代价)。baserel->baserestrictinfo 特别重要,因为它包含限制条件(WHERE 子句),这些条件应当用来过滤待获取的行。(FDW 本身并不一定非要强制这些条件,因为核心执行器也可以检查它们。)baserel->reltarget->exprs 可用于确定需要取回哪些列;但要注意,它只列出必须由 ForeignScan 计划节点输出的列,而不包括在条件求值中使用但不在查询结果中输出的列。
FDW 规划函数可以利用多个私有字段来保存信息。通常,存储在 FDW 私有字段中的任何内容都应通过 palloc 分配,这样它们会在规划结束时被回收。
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 57.2.4中描述的其他回调,都是围绕这样一个假设设计的:外部关系会以常规方式被扫描,然后单行更新将由本地的 ModifyTable 计划节点驱动。这种方法对于更新既需要读取本地表又需要读取外部表的一般情况是必需的。不过,如果某个操作可以完全由外部服务器执行,那么 FDW 就可以生成一个表示该操作的路径,并将其插入到 UPPERREL_FINAL 上层关系中,与 ModifyTable 方案竞争。这种方式也可用于实现远程 SELECT FOR UPDATE,而不是使用Section 57.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 也不受支持,因为在那里冲突目标是必需的。
如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。