学习PostgreSQL的FDW(#1)

实现一个FDW的核心是实现一组回调函数,有了这些回调函数的帮助, 在查询外部表对象的执行过程中就可以将运行逻辑切换至自定义的扩展代码中, 进而遵照PG的内部机制实现对外部数据源的访问。

目前PostgreSQL11 beta2,提供的FDW回调函数接口有39个。FDW的实现者需要根据外部数据源自身的能力(比如是否支持写操作,以及是否支持在外部数据源端执行join操作等等)对这些接口有选择地予以实现。

这些接口中, 最核心的接口有7个。无论外部数据源自身能力如何, 这7个接口是实现通过外部表对象访问该数据源的必须接口。它们的接口定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef void (*GetForeignRelSize_function) (PlannerInfo *root, RelOptInfo *baserel, Oid foreigntableid);

typedef void (*GetForeignPaths_function) (PlannerInfo *root, RelOptInfo *baserel, Oid foreigntableid);

typedef ForeignScan *(*GetForeignPlan_function) (PlannerInfo *root, RelOptInfo *baserel,Oid foreigntableid, ForeignPath *best_path, List *tlist, List *scan_clauses, Plan *outer_plan);

typedef void (*BeginForeignScan_function) (ForeignScanState *node, int eflags);

typedef TupleTableSlot *(*IterateForeignScan_function) (ForeignScanState *node);

typedef void (*ReScanForeignScan_function) (ForeignScanState *node);

typedef void (*EndForeignScan_function) (ForeignScanState *node);

在PG中,查询语句经过以下5个子系统处理:

  1. Parser
    用于将文本式的SQL命令转换成解析树

  2. Analyzer/Analyser
    用于执行解析树的语义分析并生成查询树

  3. Rewriter
    重写器用于根据规则系统中已存在的规则转换查询树
  4. Planner
    规划器生成可以从查询树中最有效地执行的计划树
  5. Executor
    执行器通过按计划树创建的顺序访问表和索引来执行查询

可以整合成三个大阶段:

  • Parser: 包含对SQL的语法解析,语义校验,查询重写
  • Optimizer:生成查询计划
  • Executor:按照火山模型执行查询计划的算子并向上返回数据

PG的FDW所需的7个回调函数主要是在Optimizer和Executor阶段进行“介入”:

这7个回调函数详细的调用时机以及作用:

回调函数 在PG中的调用时机 作用 详细描述
GetForeignRelSize 优化器生成访问路径的过程中对外部表估算访问代价时 提供外部表对于计算访问代价所需的基础数据,如表的元组数以及元组的平均长度,并将这些数据保存在输入参数baserel的字段”rows”以及”width”中 void GetForeignRelSize (PlannerInfo *root, RelOptInfo *baserel, Oid foreigntableid);
root是规划器的关于该查询的全局信息;baserel是规划器的关于该表的信息;foreigntableid是外部表在pg_class中的 OID (foreigntableid可以从规划器的数据结构中获得,但是为了减少工作量,这里直接显式地将它传递给函数);
这个函数应该更新baserel->rows为表扫描根据限制条件完成了过滤后将返回的预期行数。baserel->rows的初始值只是一个常数的默认估计值,应该尽可能把它替换掉。如果该函数能够计算出一个平均结果行宽度的更好的估计值,该函数也可能选择更新baserel->width。
GetForeignPaths 生成对外部表的访问路径时 生成对目标外部表的访问路径(通过PG中的接口createforeignscanpath()生成) void GetForeignPaths (PlannerInfo *root, RelOptInfo *baserel, Oid foreigntableid);
参数和GetForeignRelSize相同;
这个函数必须为外部表上的扫描生成至少一个访问路径(ForeignPath节点),并且必须调用add_path把每一个这样的路径加入到baserel->pathlist中。我们推荐使用create_foreignscan_path来建立ForeignPath节点。该函数可以生成多个访问路径,例如一个具有合法pathkeys的路径表示一个预排序好的结果。每一个访问路径必须包含代价估计,并且能包含任何FDW的私有信息,这种信息被用来标识想要使用的指定扫描方法。
GetForeignPlan 优化器生成扫描外部表的查询计划节点时 生成访问目标外部表的ForeignScan计划节点(通过PG中的接口make_foreignscan()) ForeignScan * GetForeignPlan (PlannerInfo *root, RelOptInfo *baserel, Oid foreigntableid, ForeignPath *best_path, List *tlist, List *scan_clauses, Plan *outer_plan);
参数和GetForeignRelSize的一样,外加选中的ForeignPath(在前面由GetForeignPaths、GetForeignJoinPaths或者GetForeignUpperPaths产生)、被计划节点发出的目标列表以及计划节点强制的限制子句以及被RecheckForeignScan执行的复查所使用的ForeignScan的外子计划(如果该路径是用于一个连接而非基本关系,则foreigntableid是InvalidOid);
这个函数必须创建并返回一个ForeignScan计划节点,推荐使用make_foreignscan来建立ForeignScan节点。
BeginForeignScan 执行器即将开始执行ForeignScan算子,进行该算子相关的初始化时 获取执行ForeignScan算子所需的信息,并将它们组织并保存在ForeignScanState中 void BeginForeignScan (ForeignScanState *node, int eflags);
它应该执行任何在扫描能够开始之前需要完成的初始化工作,但是并不开始执行真正的扫描(会在第一次调用IterateForeignScan时完成)。ForeignScanState节点已经被创建好了,但是它的fdw_state属性仍然为 NULL。关于要被扫描的表的信息可以通过ForeignScanState节点访问(特殊地,从底层的ForeignScan计划节点,它包含任何由GetForeignPlan提供的FDW私有信息)。eflags包含描述执行器对该计划节点操作模式的标志位。
注意当(eflags & EXEC_FLAG_EXPLAIN_ONLY)为真时,这个函数不应该执行任何外部可见的动作;它应当只做最少的事情来创建对ExplainForeignScan 和EndForeignScan有效的节点状态
IterateForeignScan 执行ForeignScan算子过程中需要获取下一元组时 读取外部数据源的一行数据,并将它组织为PG中的Tuple(即TupleTableSlot). 当该回调函数返回一个空的TupleTableSlot结构时, 迭代器停止迭代 TupleTableSlot * IterateForeignScan (ForeignScanState *node);
从外部源获得一行,将它放在一个元组表槽中返回(节点的ScanTupleSlot应当被用于此目的)。如果没有更多的行可用则返回 NULL。元组表槽设施允许一个物理的或者虚拟的元组被返回;在大部分情况下出于性能的考虑会倾向于选择后者。注意这是在一个短期存在的内存上下文中被调用的,该内存上下文会在调用之间被重置。如果需要长期存在的存储,需要在BeginForeignScan中创建内存上下文,或者使用节点的EState中的es_query_cxt。
如果提供了fdw_scan_tlist目标列表,被返回的行必须匹配它,如果没有提供则它们必须匹配被扫描的外部表的行类型。如果选择优化掉不需要的列,你应该在那些列的位置上插入控制或者生成一个忽略了那些列的fdw_scan_tlist列表。
注意PostgreSQL的执行器并不在乎被返回的行是否违背了定义在该外部表上的任何约束 — 但是规划器会在乎这一点,并且如果在外部表中有可见行不满足一个约束,规划器可能会错误地优化查询。如果当用户已经声明一个约束应该为真时它却被违背,最合适的处理可能是产生一个错误(就像在数据类型失配的情况下所作的那样)
ReScanForeignScan 执行Nested Loop过程中需要重置Inner Scan时(即Outter Scan需要向前推进一行时) 将外部数据源的读取位置重置回最初的起始位置 void ReScanForeignScan (ForeignScanState *node);
注意扫描所依赖的任何参数可能已经改变了值,因此新扫描不一定会返回完全相同的行。
EndForeignScan ForeignScan算子执行完成时 释放整个ForeignScan算子执行过程中占用的外部资源或FDW中的资源 void EndForeignScan (ForeignScanState *node);
通常释放palloc过的内存并不重要,但是打开的文件和到远程服务器的连接等应该被清理。

以上是必须实现的扫描相关的回调函数




参考:

http://www.interdb.jp/pg/pgsql03.html
https://xiaowing.github.io/post/20180513_write_pgfdw_in_golang_part02/