postgresql 执行器中readme的翻译

以下翻译来自AI

PostgreSQL 执行器

执行器处理由"计划节点"(plan nodes)组成的树。计划树本质上是一个元组处理操作的按需拉取管道。每个节点在被调用时,将产生其输出序列中的下一个元组,如果没有更多元组可用,则返回 NULL。如果该节点不是原始的关系扫描节点,它将拥有子节点,并依次调用这些子节点来获取输入元组。

在这个基本模型上的改进包括:

  • 扫描方向的选择(向前或向后)。注意:目前这个功能支持得不是很好。它对原始扫描节点有效,但对连接、聚合等节点效果不佳。

  • Rescan 命令用于重置节点并使其重新生成输出序列。

  • 可以改变节点结果的参数。在调整参数后,必须对该节点及其上方的所有节点应用 rescan 命令。有一个相当智能的方案来避免不必要的节点重新扫描(例如,如果输入的参数没有改变,Sort 不会重新扫描其输入,因为它可以只是重新读取其存储的已排序数据)。

对于 SELECT,只需要将顶层结果元组传递给客户端。对于 INSERT/UPDATE/DELETE/MERGE,实际的表修改操作发生在顶层 ModifyTable 计划节点中。如果查询包含 RETURNING 子句,ModifyTable 节点将计算出的 RETURNING 行作为输出传递,否则它不返回任何内容。处理 INSERT 相当直接:从 ModifyTable 下方计划树返回的元组被插入到正确的结果关系中。对于 UPDATE,计划树返回更新列的新值,加上识别要更新的表行的"垃圾"(隐藏)列。ModifyTable 节点必须获取该行以提取未更改列的值,将这些值组合成新行,并应用更新。(对于堆表,行标识垃圾列是 CTID,但对于其他表类型可能会使用其他东西。)对于 DELETE,计划树只需要提供垃圾行标识列,ModifyTable 节点访问每个这些行并将该行标记为已删除。MERGE 在下面描述。

XXX 这里需要编写更多的文档...

计划树和状态树

计划器传递的计划树包含一个 Plan 节点树(从 struct Plan 派生的结构类型)。在执行器启动期间,我们构建一个具有相同结构的并行树,包含执行器状态节点------通常,每个计划节点类型都有对应的执行器状态节点类型。状态树中的每个节点都有一个指向其计划树中对应节点的指针,以及实现该节点类型所需的执行器状态数据。这种安排允许计划树对执行器而言是完全只读的:执行期间修改的所有数据都在状态树中。只读计划树使计划缓存和重用更加简单。

如果在执行器启动期间执行器确定由于运行时分区剪枝确定在那里找不到匹配的记录,从而不需要整个子计划,则可能不会创建对应的执行器状态节点。目前这只发生在 Append 和 MergeAppend 节点上。在这种情况下,不需要的子计划被忽略,并且执行器状态的子节点数组将变得与计划的子计划列表不同步。

每个 Plan 节点可能有关联的表达式树,用于表示其目标列表、限定条件等。这些树对执行器也是只读的,但表达式求值的执行器状态不镜像 Plan 表达式树的形状,如下所述。相反,每个表达式树只有一个 ExprState 节点,尽管这可能有一些复杂表达式节点类型的子节点。

总共有四类节点用于这些树:Plan 节点、其对应的 PlanState 节点、Expr 节点和 ExprState 节点。(实际上,还有 List 节点,它们在所有三种基于树的表示中用作"胶水"。)

表达式树和 ExprState 节点

与计划树不同,表达式树不会被镜像到对应的状态节点树中。相反,每个可单独执行的表达式树(例如 Plan 的 qual 或 targetlist)由一个 ExprState 节点表示。ExprState 节点包含以紧凑的线性形式评估表达式所需的信息。这种紧凑形式存储为 ExprState->steps[] 中的扁平数组(ExprEvalStep 数组,而不是 ExprEvalStep *)。

选择这种表示的原因包括:

  • 评估单个 Expr 类型节点所需的工作量通常足够小,以至于在评估期间必须执行树遍历的开销是显著的。
  • 扁平表示可以在单个函数内非递归地评估,从而减少堆栈深度和函数调用开销。
  • 这种表示既可用于快速解释执行,也可用于编译为本机代码。

Plan 树的表达式表示由 ExecInitExpr() 编译成 ExprState 节点。尽可能多的复杂性应由 ExecInitExpr()(及其辅助函数)处理,而不是在执行时处理,因为解释版本和编译版本都需要处理这种复杂性。除了在执行方法之间重复工作外,运行时初始化检查在每次表达式求值时都有一个小但明显的成本。因此,我们允许 ExecInitExpr() 预先计算我们不期望在单个查询的执行期间发生变化的信息,例如要应用于域类型的 CHECK 约束表达式集。如果不大大增加需要计划失效的事件数量,就不可能在计划时完成这些操作。(以前,某些此类信息在每次表达式求值时都重新检查,但这似乎是不必要的开销。)

表达式初始化

在 ExecInitExpr() 和类似例程期间,Expr 树被转换为扁平表示。每个 Expr 节点可能由零个、一个或多个 ExprEvalSteps 表示。

每个 ExprEvalStep 的工作由其操作码(enum ExprEvalOp)确定,它将其工作结果存储到由 ExprEvalStep->resvalue/resnull 指向的 Datum 变量和布尔值 null 标志变量中。通过链接几个步骤来执行复杂表达式。例如,"a + b"(一个 OpExpr,带有两个 Var 表达式)将表示为两个获取 Var 值的步骤,和一个评估 + 运算符底层的函数的步骤。Var 的步骤将直接让它们的 resvalue/resnull 指向用于函数评估步骤的 FunctionCallInfoBaseData 结构中的适当 args[].value/.isnull 元素,从而避免复制结果值的额外工作。

完成的 ExprState->steps 数组中的最后一个条目始终是 EEOP_DONE 步骤;这消除了在迭代时测试数组末尾的需要。此外,如果表达式包含任何变量引用(对 ExprContext 的 INNER、OUTER 或 SCAN 元组的用户列),步骤数组以 EEOP_*_FETCHSOME 步骤开始,这些步骤确保相关元组已被解构,以使所需列直接可用(参见 slot_getsomeattrs())。这使得单个 Var 获取步骤几乎只是一个数组查找。

ExecInitExpr() 的大部分工作由递归函数 ExecInitExprRec() 及其子例程完成。ExecInitExprRec() 将一个 Expr 节点映射到执行所需的步骤,根据需要递归到子表达式。

每个 ExecInitExprRec() 调用必须指定该子表达式的结果存储位置(通过 resv/resnull 参数)。这允许上述将(子)表达式直接评估到 fcinfo->args[].value/.isnull 中的场景,但也需要一些注意:除非结果仅由在进一步使用那些目标 Datum/isnull 变量之前执行的步骤需要,否则目标 Datum/isnull 变量不得与另一个 ExecInitExprRec() 共享。由于 ExprEvalStep 表示的非递归性,这通常很容易保证。

ExecInitExprRec() 使用 ExprEvalPushStep() 将新操作推入 ExprState->steps 数组。为了使步骤保持为连续布局的数组,当没有足够空间时,ExprEvalPushStep() 必须重新分配整个数组。因此,在表达式初始化期间不允许直接指向任何步骤。因此,子表达式的 resv/resnull 通常指向与步骤数组分开 palloc 的存储。例如,函数调用步骤的 FunctionCallInfoBaseData 是单独分配的,而不是 ExprEvalStep 数组的一部分。完整表达式的整体结果通常返回到 ExprState 节点本身的 resvalue/resnull 字段中。

某些步骤,例如布尔表达式,允许跳过某些子表达式的评估。在扁平表示中,这相当于跳转到某个后面的步骤,而不是仅继续下一个步骤。此类跳转的目标由要执行的下一个步骤在 ExprState->steps 数组中的整数索引表示。(比较 execExprInterp.c 中的 EEO_NEXT 和 EEO_JUMP 宏。)

通常,ExecInitExprRec() 必须将跳转步骤推入步骤数组,然后递归生成可能被跳过的子表达式的步骤,然后返回并使用现在已知的子表达式步骤长度修复跳转目标索引。这由 execExpr.c 中的 adjust_jumps 列表处理。

构造 ExprState 的最后一步是应用 ExecReadyExpr(),它使其准备好使用所选的任何执行方法进行执行。

表达式求值

为了允许不同的表达式求值方法,以及更好的分支/跳转目标预测,通过调用 ExprState->evalfunc(通过 ExecEvalExpr() 及其友元)来评估表达式。

ExecReadyExpr() 可以通过将 evalfunc 设置为适当的函数来选择解释方法。默认执行函数 ExecInterpExpr 在 execExprInterp.c 中实现;有关详细信息,请参阅其头注释。对于某些特别简单的表达式,使用特殊情况的 evalfunc。

注意,许多更复杂的表达式求值步骤,其性能关键性低于更简单的步骤,这些步骤在表达式执行的快速路径之外作为单独的函数实现,允许它们的实现在解释和编译表达式求值之间共享。这意味着这些辅助函数不允许自己执行表达式步骤调度,因为调度方法将基于调用者而变化。因此,辅助函数不能调用子表达式的执行;它们需要的所有子表达式结果必须由前面的步骤计算。并且必须在从辅助函数返回后执行对以下表达式步骤的调度。

目标列表求值

ExecBuildProjectionInfo 构建一个 ExprState,其效果是将目标列表评估到 ExprState->resultslot 中。通用目标列表表达式通过如上所述评估它(将结果存储到 ExprState 的 resvalue/resnull 字段中),然后使用 EEOP_ASSIGN_TMP 步骤将结果移动到结果槽的适当 tts_values[] 和 tts_isnull[] 数组元素中。有特殊的快速路径步骤类型(EEOP_ASSIGN_*_VAR)来处理仅使用一个步骤而不是两个步骤的简单 Var 目标列表条目。

MERGE

MERGE 是一个多表、多操作命令:它指定一个目标表和一个源关系,并且可以包含多个 WHEN MATCHED 和 WHEN NOT MATCHED 子句,每个子句指定一个 UPDATE、INSERT、DELETE 或 DO NOTHING 操作。目标表由 MERGE 修改,源关系为操作提供额外数据。每个操作可选地指定一个为每个元组评估的限定表达式。

在计划器中,transform_MERGE_to_join 在目标表和源关系之间构造一个连接,其中包含来自目标表的行标识垃圾列。如果 MERGE 命令包含任何 WHEN NOT MATCHED 子句,则此连接是外连接;ModifyTable 节点从该连接的计划树中获取元组。如果获取的元组中的行标识列为 NULL,则源关系包含一个不被目标表中的任何元组匹配的元组,因此给定计划返回的元组评估每个 WHEN NOT MATCHED 子句的限定表达式。如果表达式返回 true,则执行子句指示的操作,并且不评估进一步的子句。另一方面,如果行标识列不为 NULL,则可以获取目标表中的匹配元组;给定获取的元组和计划返回的元组,评估每个 WHEN MATCHED 子句的限定表达式。

如果不存在 WHEN NOT MATCHED 子句,则计划器构造的连接是内连接,并且行标识垃圾列始终不为 NULL。

如果 WHEN MATCHED 最终处理一个被并发更新或删除的行,则使用 EvalPlanQual(见下文)查找该行的最新版本,并重新获取它;如果存在,则从顶部开始搜索要使用的匹配 WHEN MATCHED 子句。

MERGE 不允许其自己的触发器类型,而是触发 UPDATE、DELETE 和 INSERT 触发器:当为某行执行操作时,为该行触发行触发器。语句触发器始终触发,无论是否有任何行匹配相应的子句。

内存管理

在 CreateExecutorState() 期间创建"每查询"(per query)内存上下文;在执行器调用期间分配的所有存储都在该上下文或子上下文中分配。这允许在执行器关闭期间容易地回收存储------而不是与零售的 pfree 和可能的存储泄漏纠缠,我们只是销毁内存上下文。

特别是,前面章节描述的计划状态树和表达式状态树在每查询内存上下文中分配。

为了避免查询内内存泄漏,查询运行时的大部分处理是在"每元组"(per tuple)内存上下文中完成的,它们之所以这样命名,是因为它们通常每个元组重置为空一次。每元组上下文通常与 ExprContexts 关联,并且通常每个 PlanState 节点都有自己的 ExprContext 来评估其 qual 和目标列表表达式。

查询处理控制流

这是完整查询处理的控制流草图:

复制代码
CreateQueryDesc

ExecutorStart
    CreateExecutorState
        创建每查询上下文
    切换到每查询上下文以运行 ExecInitNode
    AfterTriggerBeginQuery
    ExecInitNode --- 递归扫描计划树
        ExecInitNode
            递归到子节点
        CreateExprContext
            创建每元组上下文
        ExecInitExpr

ExecutorRun
    ExecProcNode --- 在每查询上下文中递归调用
        ExecEvalExpr --- 在每元组上下文中调用
        ResetExprContext --- 以释放内存

ExecutorFinish
    ExecPostprocessPlan --- 运行任何未完成的 ModifyTable 节点
    AfterTriggerEndQuery

ExecutorEnd
    ExecEndNode --- 递归释放资源
    FreeExecutorState
        释放每查询上下文和子上下文

FreeQueryDesc

根据前面的注释,ExecEndNode 释放任何内存并不是真的很关键;反正所有内容都将在 FreeExecutorState 中消失。但是,我们需要小心关闭关系、删除缓冲区 pin 等,所以我们确实需要扫描计划状态树来查找这类资源。

执行器也可用于评估没有任何计划树的简单表达式("简单"意味着"没有聚合和没有子选择",尽管这些可能隐藏在函数调用中)。这种情况有如下控制流:

复制代码
CreateExecutorState
    创建每查询上下文

CreateExprContext -- 或使用 GetPerTupleExprContext(estate)
    创建每元组上下文

ExecPrepareExpr
    临时切换到每查询上下文
    通过 expression_planner 运行表达式
    ExecInitExpr

重复做:
    ExecEvalExprSwitchContext
        ExecEvalExpr --- 在每元组上下文中调用
    ResetExprContext --- 以释放内存

FreeExecutorState
    释放每查询上下文,以及 ExprContext
    (不需要单独的 FreeExprContext 调用)

EvalPlanQual (READ COMMITTED 更新检查)

对于简单的 SELECT,执行器只需要注意根据当前事务看到的快照有效的元组(即,它们是由先前提交的事务插入的,并且没有被任何先前提交的事务删除)。但是,对于 UPDATE、DELETE 和 MERGE,修改或删除已被打开或并发提交的事务修改的元组是不可取的。如果我们以 SERIALIZABLE 隔离级别运行,那么当看到这种情况发生时,我们只是引发一个错误。在 READ COMMITTED 隔离级别中,我们需要更努力地工作。

READ COMMITTED 模式下的基本思想是获取由并发事务提交的修改元组(在需要时等待它提交之后),并重新评估查询限定,看看它是否仍然满足限定条件。如果是这样,我们从修改的元组重新生成更新的元组(如果我们正在执行 UPDATE),最后更新/删除修改的元组。SELECT FOR UPDATE/SHARE 的行为类似,除了它的操作只是锁定修改的元组并基于该元组版本返回结果。

为了实现这种检查,实际上我们从头开始为每个修改的元组(或元组集,对于 SELECT FOR UPDATE)重新运行查询,其中关系扫描节点被调整为仅返回当前元组------要么是原始元组,要么是修改的元组的更新(现在已锁定)版本。如果此查询返回一个元组,则修改的元组通过限定条件(如果我们正在执行 UPDATE,则查询输出是适当修改的更新元组)。如果不返回元组,则修改的元组未通过限定条件,因此我们忽略当前结果元组并继续原始查询。

在 UPDATE/DELETE/MERGE 中,只需要以这种方式处理目标关系。在 SELECT FOR UPDATE 中,可能标记了多个关系 FOR UPDATE,因此在执行重新检查之前,我们要获取每个此类关系中当前元组版本的锁定。

也有可能查询中有一些关系不需要被锁定(它们既不是 UPDATE/DELETE/MERGE 目标,也不是在 SELECT FOR UPDATE/SHARE 中指定要锁定的)。当重新运行测试查询时,我们要使用与锁定行连接的相同行。对于普通关系,这可以通过在连接输出中包含行 TID 并重新获取该 TID 来相对便宜地实现。(重新获取很昂贵,但我们正在优化不需要重新测试的正常情况。)我们还必须考虑非表关系,如 ValuesScan 或 FunctionScan。对于这些,由于没有等效的 TID,唯一的实际解决方案似乎是在连接输出行中包含整个行值。

我们在 SELECT FOR UPDATE 的目标列表中不允许返回集函数,以确保对于任何特定的扫描元组集最多可以返回一个元组。否则,由于原始查询多次返回相同的扫描元组集,我们会得到重复项。同样,在 UPDATE 的目标列表中不允许使用 SRF。在那里,它们的效果是同一行被更新多次,这不是很有用------而且第一次之后的更新无论如何都不会产生任何效果。

异步执行

在节点正在等待数据库系统外部的事件(例如等待网络 I/O 的 ForeignScan)的情况下,节点指示它无法立即返回任何元组但可能在稍后这样做是可取的。发现这种类型情况的进程可以简单地通过阻塞来处理它,但这可能会浪费本可以用于执行计划树的某些其他部分(在那里可以立即取得进展)的时间。当计划树包含 Append 节点时,这种情况特别可能发生。异步执行并发地而不是串行地运行 Append 节点的多个部分以提高性能。

对于异步执行,Append 节点必须首先使用 ExecAsyncRequest 从能够异步的子节点请求元组。接下来,它必须使用 ExecAppendAsyncEventWait 执行异步事件循环。最终,当发出异步请求的子节点产生元组时,Append 节点将通过 ExecAsyncResponse 从事件循环接收它。在异步执行的当前实现中,唯一从能够异步的子节点请求元组的节点类型是 Append,而唯一可能能够异步的节点类型是 ForeignScan。

通常,ExecAsyncResponse 回调是希望异步请求元组的节点唯一需要的。另一方面,能够异步的节点通常需要实现三种方法:

  1. 当发出异步请求时,将调用节点的 ExecAsyncRequest 回调;它应该使用 ExecAsyncRequestPending 来指示请求对于下面描述的回调是挂起的。或者,它可以改为使用 ExecAsyncRequestDone,如果立即可用结果。

  2. 当事件循环希望等待或轮询文件描述符事件时,将调用节点的 ExecAsyncConfigureWait 回调来配置节点希望等待的文件描述符事件。

  3. 当文件描述符准备好时,将调用节点的 ExecAsyncNotify 回调;与 #1 一样,它应该使用 ExecAsyncRequestPending 进行另一个回调或 ExecAsyncRequestDone 以立即返回结果。

相关推荐
萧曵 丶2 小时前
覆盖索引与回表(MySQL 索引核心概念,性能优化关键)
数据库·mysql·性能优化·索引·聚簇索引
霖霖总总2 小时前
[小技巧24]MySQL 命令行提示符(Prompt)自定义:从入门到精通
数据库·mysql
风送雨2 小时前
FastAPI 学习教程 · 第2部分
学习·fastapi
石像鬼₧魂石2 小时前
3306 端口(MySQL 数据库)渗透测试全流程学习总结
数据库·学习·mysql
float_六七2 小时前
数据库三级模式:逻辑与物理的完美架构
数据库·oracle
xj7573065332 小时前
《精通Django》第6章 Django表单
数据库·django·sqlite
Main. 242 小时前
从0到1学习Qt -- Qt3D入门
开发语言·qt·学习
weixin_462446232 小时前
Python+React 专为儿童打造的汉字学习平台:从学前到小学的智能汉字教育解决方案
python·学习·react.js
星火开发设计2 小时前
C++ deque 全面解析与实战指南
java·开发语言·数据结构·c++·学习·知识