PostgreSQL 中文参考文档
🌐 英文原版 官方 ↗

58.4. 外部数据包装器查询规划 #

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 = $1,参数值在运行时从对 fdw_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 不受支持, 因为在那里指定冲突目标是强制性的。