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

FDW回调函数GetForeignRelSizeGetForeignPathsGetForeignPlanPlanForeignModifyGetForeignJoinPathsGetForeignUpperPaths以及PlanDirectModify必须适合PostgreSQL规划器的工作。这里有一些关于它们必须做什么的注记。

rootbaserel中的信息可以被用来减少必须从外部表获得的信息量(并且因此降低代价)。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域,并且可以生成被放置于ForeignPath计划节点中的fdw_exprsfdw_private列表。这两个列表必须被表示为一种copyObject可复制的形式。fdw_private列表没有任何其他限制并且不会被核心后端以任何形式解释。非 NIL 的fdw_exprs应该包含表达式树,该树会在运行时被执行。这些树将由规划器在后期处理,以便让它们变成完全可执行的。

GetForeignPlan中,通常被传入的目标列表可以被照样复制到计划节点中。被传入的scan_clauses 列表包含和baserel->baserestrictinfo相同的子句,但是可能为了更好的执行效率会被重新排序。在简单情况下,FDW可以只把RestrictInfo节点从scan_clauses 列表剥离(使用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值必须是一个包含表示返回列的 Var 或表达式的目标列表(TargetEntry的列表)。例如,这可以被用来显示 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)转化成计划。如果该查询的所有基本关系都来自于同一个 FDW,推荐在GetForeignUpperPaths回调函数中生成这种路径,该函数会为每一个上层关系被调用(即每一次扫描/连接后处理步骤)。

第 57.2.4 节中描述的PlanForeignModify以及其他回调的设计是建立在这样一个假设之上:外部表将以通常的方式被扫描并且行更新将被一个本地ModifyTable计划节点所驱动。这种方法对于更新需要读取本地表以及外部表的一般情况下是必要的。不过,如果操作可以完全由外部服务器执行,FDW 可以产生一个表示这种操作的计划并且把它插入到UPPERREL_FINAL上层关系中,在其中它会与ModifyTable方法竞争。这种方法还可以被用来实现远程SELECT FOR UPDATE,而不使用第 57.2.6 节中描述的行锁定回调。记住插入到UPPERREL_FINAL中的路径负责实现查询的所有行为。

在规划一个UPDATEDELETE时,PlanForeignModifyPlanDirectModify能为外部表查找RelOptInfo结构,并利用之前由扫描规划函数创建的baserel->fdw_private数据。但是,在INSERT中目标表不会被扫描,因此不会有它的RelOptInfo。由PlanForeignModify返回的List具有和ForeignScan计划节点的fdw_private列表相同的限制,即它必须只包含copyObject知道怎么拷贝的结构。

带有一个ON CONFLICT子句的INSERT不支持指定冲突目标,因为本地不知道远程表上的唯一约束和排除约束的情况。然后这也意味着ON CONFLICT DO UPDATE不被支持,因为该说明是强制性的。