深入浅出openGauss的执行器基础

news2024/11/16 7:24:38

目录

火山模型

Tuple 数据结构设计

条件计算

Expr 和 Var

示例1 filter

示例2  join

示例3  index scan & index only scan


火山模型

执行器各个算子解耦合的基础。对于每个算子来说,只有三步:

1、向自己的孩子拿一个 tuple。即调用孩子节点的 Next 函数;

2、执行计算;

3、向上层返回一个 tuple。即当前节点 Next 函数的返回结果。

 所以整个执行器的内核可以用下面这个伪代码来表达。


ExecutePlan
{
for (;;)
slot = ExecProcNode(planstate);
------->if (node->chgParam != NULL)
           ExecReScan(node);
        result= g_execProcFuncTable[index](node)  // 表驱动,每个算子不同的执行函数
        return result;

if (TupIsNull(slot)) {
   ExecEarlyFreeBody(planstate);
   break;
}
// 返回给前端
}

这种模型的好处是:

  1. 设计简单,算子解耦,互相不依赖;

  2. 内存使用量小,没有物化的情况下,每次只消耗一个 tuple 的内存。

 

Tuple 数据结构设计

两个算子之间的传递的都是 tuple。所以 Tuple 数据结构是整个执行器的核心,也是执行器和存储引擎交互的数据结构。

先看下几个具体数据结构之间的关系(附上关键的变量,非全部)


// 存在磁盘上的数据结构,是 header + 数据 的一片连续内存。设计要求尽可能紧凑,节约存储空间。
// 只有事务信息等,其它比如长度、每一列起始地址等都不会直接存,而是算出来的。
HeapTupleHeaderData
    t_xmin; // 事务信息
    t_xmax;
    t_cid;
    t_ctid;
    ...
    
// 磁盘上的 Tuple 数据结构过于紧凑,不好用。所以设计了内存中一个 tuple 的对象,会存一些额外(冗余)的元数据信息方便处理。
// 数据和元数据可以不连续。这个数据结构一般是存储引擎内部用,比如可见性判断等。   
HeapTupleData
    t_len; // 数据长度
    tupTableType;// store 类型
    t_self; // ctid
    t_tableOid;
    ...
    HeapTupleHeader t_data;  // tuple header data 的地址

// 可以理解为 Tuple 槽位,执行器用。存储引擎不关心具体每一列的内容,只关心事务、长度等公共信息。
// 而执行器可能需要访问每一列,所以在这里把 tuple 进一步拆解开。
TupleTableSlot
    Tuple tts_tuple;   // 物理 tuple 位置(HeapTupleData);虚拟 tuple 的话为 null。
    TupleDesc tts_tupleDescriptor;
    Buffer tts_buffer;
    Datum* tts_values; // 数组,表示每一列的值(如果是 int 就是值,如果是 varchar 就是地址)
    bool* tts_isnull;  // 数组,表示每一列是否是 null
    TableAmType tts_tupslotTableAm  // 处理物理 tuple 的函数(astore 和 ustore 不同)

 以上图为例,最下层的 seqscan,会调用存储引擎的 heap_getnext 函数(astore)。


// 代码中用的函数指针,实际调用栈如下
ExecScanFetch
--->SeqNext
--------->seq_scan_getnext_template    // 存储引擎入口
                 tuple =  (Tuple)heap_getnext  // 拿到 HeapTuple
         
                 // 组装TupleTableSlot,但是 attr 的值还没置上,得在 heap_slot_getattr 里设置。
                 heap_slot_store_heap_tuple(htup, slot, scan->rs_cbuf, false, false); 
                 
heap_getnext
   ---> heapgettup
              HeapTuple tuple = &(scan->rs_ctup);   // tuple 是 scan 结构体中的变量。
              LockBuffer(scan->rs_base.rs_cbuf, BUFFER_LOCK_SHARE);  // 锁住 buffer
              dp = (Page)BufferGetPage(scan->rs_base.rs_cbuf);
              lpp = HeapPageGetItemId(dp, line_off);
              tuple->t_data = (HeapTupleHeader)PageGetItem((Page)dp, lpp);  // t_data 的地址就是 buffer 中的地址
              ItemPointerSet(&(tuple->t_self), page, line_off);  // 设置 tuple 中的一些变量
              valid = HeapTupleSatisfiesVisibility(tuple, snapshot, scan->rs_base.rs_cbuf);  // 用作可见性判断
              
        return  &(scan->rs_ctup);
        
// 把物理 tuple 中的数据,复制到 tupleTableSlot 中。这里是 astore 的解析函数,ustore 不一样。
heap_slot_getattr
    // 核心思想是利用 TupleDesc 知道表的定义,然后从0 开始一个 attr 一个 attr 地解析。
    // 对于定长字段,直接按地址取值;对于变长字段,长度会存在原始的数据头中。
    // 因此,之前的物理 tuple 不需要存长度这种信息;同时,也需要 tupleTableSlot 这样的变量来缓存具体数据的起始地址。

以上就是存储引擎和执行引擎之间 tuple 怎么交互的。总结下就是存储引擎会把实际数据所在的地址传给上层,最后执行引擎拿到的结构体为TupleTableSlot。 

所以,执行引擎只关心 TupleTableSlot 这一个结构体即可。

一个很自然的问题就是,如果算子之间的 Tuple 都是深拷贝传递,对于较大的 tuple 来说(包含 varchar 类型),性能很差。因此,PG 中的 tuple 分了4类(详见头文件tuptable.h):

  1. 第一类 tuple 是 Disk buffer page 上的 physical tuple,就是前文的 HeapTuple。Buffer 一定要 pin 住。这种 Tuple 可以直接根据头地址进行访问。

  2. 内存中的组装的tuple,格式和文件上 tuple 完全一样,也是进行过压缩。这种也算是 physical tuple,可以直接用地址。 

  3. Minimal physical tuple,也是内存中的,唯一区别在于没有系统列(xmin、xmax 等)。

  4.  virtual tuple,只记录每一个属性数据的地址,并没有深拷贝,而是直接通过地址来访问。现在约定的是,当一个算子向上层吐一个 tuple,直到它下次被调用时,该tuple所在的内存不会被释放。

对于查询来说,第一类和第四类最为常见。2、3两类会在物化的时候使用,比如 CTEScan、HashJoin 建立 hash 表的时候,相当于深拷贝。性能比较敏感的场合,尽量避免2、3类 tuple的使用。

slot 的创建一般通过 ExecAllocTableSlot、ExecSetSlotDescriptor 两个函数来分配内存和初始化信息。在执行器初始化阶段,每个算子会分配相应的 slot。

以上图为例,

  1. SeqScan 算子有一个 tupleTableSlot,是一个 physical tuple,指向的是 Buffer 中的地址。

    向上层返回自己的 tuple slot;

  2. HashJoin 一个一个拿到 内表的 tuple slot,需要建立 hash 表,所以创建了一个 minimal physical tuple,复制内表的 tuple 内容;

  3. Hash表建立后,HashJoin 算子然后一个一个拿到外表的 tuple slot,做 join 计算。

    HashJoin 自己有一个 tuple slot,如果碰到匹配,则把自己的 tuple slot 设置成 virtual tuple,其中的 tts_values 指向的是孩子节点的 tuple 中的地址。

    再向上返回。

    其中,外表的内容指向的是 Buffer 上的physical tuple, 内表的内容指向的是 hash 表中的 minimal physical tuple;

  4. 当 HashJoin 被再次调用时,它会重置 tuple slot。因为是 virtual tuple,所以没做任何事情。然后 HashJoin 会调用 SeqScan 拿下一条tuple;

  5. SeqScan 被再次调用时,也会重置 tuple slot。

    因为是 physical tuple,它需要释放之前的 Buffer。

    (当然,如果一直是同一个 Buffer 不会反复 pin/unpin,这是存储引擎的优化)。

条件计算

Expr 和 Var

执行器每个算子会对底下传上来的 tuple 进行计算和过滤。比如 NestLoop 需要计算内外表传上来的 tuple 是否满足 join 条件。

这时候需要引出第二个重要的概念,表达式的抽象。个人理解,任何对数据的获取操作都可以认为是表达式。

Var/Const 也是表达式的一种。Var 表示直接从 tuple 中获取数据,Const 表示的是直接获取一个常数。

每一个表达式会对应一个 ExprContext,ExprContext 中会记录计算所需要的所有数据,一般是孩子节点返回的 tuple。

表达式本身,在执行器中用 ExprState 来表示。里面重点是表达式的计算函数


// 因为当前执行模型中,每个算子最多只有两个孩子节点,所以下面三个变量用的最多。
struct ExprContext {
    TupleTableSlot* ecxt_scantuple;  // scan 算子会用到
    TupleTableSlot* ecxt_innertuple;   // 非 scan 算子如 join
    TupleTableSlot* ecxt_outertuple;
    ...
}

// 表达式计算的结构体
struct ExprState {
    Expr* expr;  // 原始的表达式
    ExprStateEvalFunc evalfunc;  // 表达式对应的函数
}

// 执行器开始阶段,会通过优化器传过来的 Expr,初始化 ExprState 结构
ExecInitExpr {
    case T_Var:
        ExecInitExprVar
            state = (ExprState *)makeNode(ExprState);
            state->evalfunc = ExecEvalScalarVar;  // 内部实现是直接去取对应 tuple slot 上的 attr
            
    case T_OpExpr:
        FuncExprState* fstate = makeNode(FuncExprState);
        fstate->xprstate.evalfunc = (ExprStateEvalFunc)ExecEvalOper; // 内部实现是先递归调用 ExecEvalExpr, 获取参数列表,再调用 function
        fstate->args = (List*)ExecInitExpr((Expr*)opexpr->args, parent);
}

 

总结下,ExprState 结构体表示表达式计算的逻辑,ExprContext结构体表示的是表达式计算要用到的数据。

 从 OpExpr 可以看出,ExprState 本身也是一棵树。一直递归调用 ExecEvalExpr 来获取最终的结果。

需要注意的是,执行树中除了叶子节点上的扫描节点,其它节点的数据都来源于孩子节点。

所以,这些计算节点上的 Var,不能直接指向某个 table,而是需要指向的是内表还是外表的 tuple slot。

因此,在优化器最后的阶段,set_plan_refs 函数中,会把中间节点的 Var 的 varno 改写成特定的值。

而 Var 的表达式处理函数 ExecEvalScalarVar 也是根据这个信息决定去找 ExprContext 中的 哪个 tuple slot。

 表达式如下:

 

示例1 filter

 以 SeqScan 为例,在优化器阶段, SeqScan 上会有一个 qual,表示过滤条件。在执行器阶段会生成一个对应的 ExprState,用于计算。


// 执行器初始化阶段,会根据优化器里的 Expr 构造 ExprState
ExecInitSeqScan
    InitScanRelation
        node->ps.qual = (List*)ExecInitQualWithTryCodeGen((Expr*)node->ps.plan->qual, (PlanState*)&node->ps, false);
        
// 执行阶段
ExecSeqScan
    ExecScan
        qual = node->ps.qual;
        slot = ExecScanFetch(node, access_mtd, recheck_mtd);  // 从存储引擎那里拿到 slot
        econtext->ecxt_scantuple = slot;   // 设置好 ExprContext
        ExecNewQual(qual, econtext)   // 调用 ExprState 进行计算
            ret = ExecEvalExpr((ExprState*)qual, econtext, &isNull, NULL);
            return DatumGetBool(ret);

示例2  join

以 Nestloop 为例,优化器结束的时候,join 节点会有一个 joinqual 表示 join 条件。


ExecInitNestLoop
    nlstate->js.joinqual = (List*)ExecInitQualWithTryCodeGen((Expr*)node->join.joinqual, (PlanState*)nlstate, false);
    
ExecNestLoop
    // 拿到内外表的 tuple slot,设置在 ExprContext 上
    econtext->ecxt_innertuple =ExecProcNode(inner_plan); 
    econtext->ecxt_outertuple = ExecProcNode(outer_plan);
    List* joinqual = node->js.joinqual;
   
    // 调用 ExprState 中的函数,如果符合 join 条件,则向上层返回一个 tuple。
    if (ExecNewQual(joinqual, econtext)) {
        result = ExecProject(node->js.ps.ps_ProjInfo, &is_done);
        return result
    }

示例3  index scan & index only scan


# Index on t(a,b,c)
# select a,b,c from t where a = 1 and c = 1;
 Index Only Scan using t_a_b_c_idx on t  (cost=0.15..8.26 rows=1 width=12)
   Index Cond: ((a = 1) AND (c = 1))
   
# select a,b,c from t where a = 1 and c <> 1;
 Index Only Scan using t_a_b_c_idx on t  (cost=0.15..32.35 rows=10 width=12)
   Index Cond: (a = 1)
   Filter: (c <> 1)
   
# select * from t where a = 1 and c <> 1 and d = 1;
 Index Scan using t_a_b_c_idx on t  (cost=0.15..32.36 rows=1 width=16)
   Index Cond: (a = 1)
   Filter: ((c <> 1) AND (d = 1))

之前很多人搞不清楚这里面 index cond/ filter 是什么关系。但是,通过执行器源码很容易得知它们的用处。先看 IndexOnlyScan:


## 首先,通过 Explain 可以看出来,Index Cond 显示的是  plan->indexqual,  Filter 显示的是 plan->qual。
optutil_explain_proc_node:
    caesT_IndexOnlyScan:
       optutil_explain_show_scan_qual(((IndexOnlyScan *)plan)->indexqual, "Index Cond", planstate, ancestors, es);
       optutil_explain_show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
       
## 通过 Init 函数,发现 这两个 qual 分别被初始化为 Expr,赋给 ss.ps.qual 和 indexqual 上面。
ExecInitIndexOnlyScan
    indexstate->ss.ps.qual = ExecInitExpr((Expr *) node->scan.plan.qual,  (PlanState *) indexstate);
    indexstate->indexqual = (List *) = ExecInitExpr((Expr *) node->indexqual, 
   
    // 用 indexqual 来做Scan 的key
    ExecIndexBuildScanKeys((PlanState *) indexstate,
                           indexstate->ioss_RelationDesc,
                           node->indexqual.... 

## 在执行的时候发现,indexqual 在索引扫描内部使用, ps.qual 是用在扫描之后的 check 中。
ExecIndexOnlyScan
--->ExecScan
         qual = node->ps.qual;  // 注意,这里用的是 qual。
         slot = ExecScanFetch(node, accessMtd, recheckMtd);
                   ----> IndexOnlyNext
                                tid = index_getnext_tid(scandesc, direction)  // 调用 btree 相关函数
                                // 用 IndexTuple 来填充 scan  的 slot (IndexScan是根据 tid 回表去 heap 上读取)
                                StoreIndexTuple(slot, scandesc->xs_itup, scandesc->xs_itupdesc);
        // 用 qual 做过滤条件
         if (!qual || ExecQual(qual, econtext, false))
                返回 slot
                
btgettuple
    _bt_steppage
        _bt_readpage  
            _bt_checkkeys  // 每个页面会挨个检查一下 ScanKey 的条件是否满足

总结:

  • Index Cond 是用来做 btree 扫描的 key,定位到第一个 IndexTuple。存储引擎中用

  • 之后索引扫描会顺着 btree 的链表扫描所有的叶子页面,对叶子页面上的每一个 tuple 用 ScanKey 检查是否满足条件,满足再返回

  • Filter 是在 执行器层用,针对 HeapTuple 再做一次过滤

  • IndexOnlyScan 和 Index Scan的唯一区别是,IndexOnlyScan 的HeapTuple是根据IndexTuple直接构建的,不需要回表,其它逻辑是一样。

  • 所以理论上讲 IndexOnlyScan 不应该出现 filter,上述场景可能有改进空间。

 

openGauss: 一款高性能、高安全、高可靠的企业级开源关系型数据库。

🍒如果您觉得博主的文章还不错或者有帮助的话,请关注一下博主,如果三连收藏支持就更好啦!谢谢各位大佬给予的鼓励!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/442684.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++初阶之缺省参数

目录 前言 缺省参数 1.缺省参数的概念 2.缺省参数的分类 全缺省参数 半缺省参数 前言 今天小编继续给大家带来C的内容&#xff0c;那么今天小编给大家讲解的就是有关C中缺省参数的介绍。 缺省参数 1.缺省参数的概念 缺省参数是声明或定义函数时为函数的参数指定一个缺省…

分布式互联网——Web3.0

文章目录 前言一、什么是 Web3.0?二、Web3.0 技术1.分布式账本技术(DLT)2. 区块链3. 智能合约4. 加密算法2.操作 三、Web3.0 的未来发展 前言 Web3.0&#xff0c;也被称为“分布式互联网”&#xff0c;是当前互联网的下一代版本。它是一种未来的互联网&#xff0c;它没有中心…

ROS学习第十一节——常用命令

1.概述 机器人系统中启动的节点少则几个&#xff0c;多则十几个、几十个&#xff0c;不同的节点名称各异&#xff0c;通信时使用话题、服务、消息、参数等等都各不相同&#xff0c;一个显而易见的问题是: 当需要自定义节点和其他某个已经存在的节点通信时&#xff0c;如何获取…

实践分享:如何在自己的App 中引入AI 画图

最近AIGC 简直是杀疯了&#xff0c;领导动不动就让我们在APP 里引入大语言模型&#xff0c;引入AI画图……说搞就搞&#xff01;本期基于最近在app 里引入AI画图小程序的操作&#xff0c;给大家做一波实践分享。 Scribble Diffusion 是一个简单的在线服务&#xff0c;它使用 A…

定制自己的文档问答机器人

近期ChatGPT很火爆&#xff0c;功能很强大&#xff0c;其具有强大的逻辑推理能力和数据背景。但是如果我们想要使用ChatGPT聊一些它没有训练过的知识&#xff0c;或者我们自己的一些数据时&#xff0c;由于ChatGPT没有学习过这些知识&#xff0c;所以回答结果不准确。 下文就介…

十、切分织物起球和非起球区域以便于计算毛球对比度

一、通过训练的模型可以将织物的起球区域进行识别区分 原图&#xff1a; 模型识别&#xff1a; 二、对比度的计算 为了对织物起球等级进行评定&#xff0c;需要这边不同的参数特征来构建模型的评级系统 通过查阅相关文献&#xff0c;确定最终的特征参数为&#xff1a;织物起…

11.面向对象概述,类的创建,对象的创建

一.面向对象程序设计概述 1.知识点面向对象程序设计的目的 &#xff08;1&#xff09;从程序设计的角度来看&#xff0c;事物的属性就可以用变量来表示&#xff0c;行为则可以用方法来反映。 &#xff08;2&#xff09;客观世界中事物的属性和行为可以进行传递&#xff0c;当…

汇编与内联 x86-64

机器字长 x86是32位系统 64是64位系统 这里的32和64&#xff0c;指的都是机器字长 机器字长是 能直接进行整数/位运算的大小指针的大小(索引内存的范围) 8位机 由于空间大小限制&#xff0c;想要把集成电路做到个人主机里&#xff0c;只能用8位字长的 16位机 8086 IBMP…

如何破除增长的未知性?火山引擎交出了答卷

4月18日&#xff0c;由火山引擎主办的2023春季火山引擎“FORCE原动力”大会在上海召开。本次大会主要围绕云计算和数字化领域&#xff0c;全方位地展示火山引擎在云技术、云服务和云场景方面的最新探索、应用与实践&#xff0c;呈现创新发展的战略蓝图。 曾经&#xff0c;增长是…

Flink高手之路5-Table API SQL

文章目录 Flink 中的Table API & SQL一、Table API & SQL 介绍1. 为什么要Table API和SQL2. Table API & SQL的特点3. Table API& SQL发展历程3.1 架构升级3.2 查询处理器的选择3.3 了解-Blink planner和Flink Planner具体区别如下&#xff1a;3.4 了解-Blink …

神采PromeAI 2.0版本上线,助你释放创作超能力

上个月&#xff0c;我们推出神采PromeAI 1.0版本&#xff0c;让用户可以免费体验AI草图渲染功能。神采作为设计师的提效工具和灵感源泉&#xff0c;深受用户的广大好评。于是&#xff0c;在经过算法优化后&#xff0c;神采PromeAI 2.0版本终于在本周上线了&#xff01; 我们提供…

【Vulnhub】之Symfonos2

一、 部署方法 在官网上下载靶机ova环境&#xff1a;https://download.vulnhub.com/symfonos/symfonos2.7z使用VMware搭建靶机环境攻击机使用VMware上搭建的kali靶机和攻击机之间使用NAT模式&#xff0c;保证靶机和攻击机放置于同一网段中。 二、 靶机下载安装 靶机下载与安…

ETCD(四)读请求处理过程

客户端通过etcdctl执行get命令 etcdctl get name --endpoints localhost:12379,192.158.00.32:12379client端 首先是client会解析这条命令&#xff0c;包括其中的get API方法&#xff0c;key值&#xff0c;请求server地址。解析完之后etcdctl会创建一个clientv3库对象&#xf…

Ubantu docker学习笔记(七)容器网络

文章目录 一、容器网络管理1.1查看容器网络1.2创建容器网络1.3 删除容器网络1.4 容器网络详细信息1.5 配置容器网络1.6 断开容器网络连接 二、none网络三、host网络四、bridge网络五、container网络六、容器连接外部网络6.1创建Overlay网络6.2创建Macvlan网络 一、容器网络管理…

研0进阶式学习---数据库配置

目录 最开始的问题&#xff1a;不同的连接名下面的数据库信息完全一样尝试新建用户名和密码&#xff0c;以此来建立新的连接 但这样建立的连接下面的数据库仍然是和之前的一模一样尝试改变xampp端口号&#xff0c;以此来建立新的连接 结论MySQL实例的数据库文件是与实例绑定的&…

完美解决丨+# TypeError: ‘dict_keys‘ object does not support indexing

结构 - 标题 - 问题描述 - 代码栗子 - 总结 目录 TypeError: dict_keys object does not support indexing 如何实现&#xff1f; python a {a: 1} b a.keys() c b[0] 异常描述 TypeError Traceback (most recent call last) <ipython-input-9-9dceb06f3f…

信号完整性分析基本概念之Retimer和Redriver

一两句话讲清楚版&#xff1a; Retimer 通过 其 Rx 端 CTLE/DFE (连续时间线性均衡/判断反馈均衡) 、CDR (时钟数据恢复) 及 Tx 端 EQ (均衡)&#xff0c;来够补偿信道损耗&#xff0c;消除信号抖动&#xff0c;提升信号完整性&#xff0c;从而增加传输距离。 Redriver 是放大…

多线程拉取+kafka推送

多线程拉取kafka推送 1 多线程 在本次需求中&#xff0c;多线程部分我主要考虑了一个点&#xff0c;就是线程池的配置如何最优。因为数据量级比较大&#xff0c;所以这个点要着重处理&#xff0c;否则拉取的时间会非常长或者是任务失败会比较频繁&#xff1b; 因为数据的量级…

Spring Security OAuth2.0(一)-----前言-授权码模式及代码实例

什么是 OAuth2 OAuth 是一个开放标准&#xff0c;该标准允许用户让第三方应用访问该用户在某一网站上存储的私密资源&#xff08;如头像、照片、视频等&#xff09;&#xff0c;而在这个过程中无需将用户名和密码提供给第三方应用。实现这一功能是通过提供一个令牌&#xff08…

如何治理“网络暴力” 在人类文明不断发展向前的进程中,大数据时代应运而来。数学建模解题步骤,愚见而已,欢迎指错和探讨呀~

题目可见文章&#xff1a;(20条消息) 如何治理“网络暴力” 在人类文明不断发展向前的进程中&#xff0c;大数据时代应运而来。 数学建模&#xff0c;90%成品论文&#xff0c;附附件、原题、代码 注&#xff0c;水平有限&#xff0c;非广告&#xff0c;仅供交流参考&#xff0c…