• Postgresql源码(64)查询执行——子模块Executor(2)执行前的数据结构和执行过程


    上一篇说明了执行的框架,本篇深入分析执行细节。测试用例不变,还是分析之前的case。

    0 总结

    • 下图中的planstate有四类:控制节点、扫描节点、连接节点、物化节点
      • 扫描节点公共父类:Scan
      • 连接节点公共父类:Join
    • Plan的子节点通过Plan的lefttree和righttree指针连接,构成计划树
    • 执行时,Planstate用于记录各节点的执行状态,estate中的es_tupleTable在节点间传递元组。
    • Excutor输入QueryDesc,包含plannedstmt树形结构;执行前用plannedstmt初始化节点状态树planstate,同时初始化全局状态信息estate。然后执行planstate根节点的函数指针,进入根节点业务处理函数(例如nestloop),pull模型向下层取数据拉动整个计划树的执行。

    在这里插入图片描述

    1 ExecutorRun执行前数据结构

    执行计划:

    1. teach_course和teacher走hash连接,生成outer表(驱动表)
    2. course表做inner表
    3. 循环嵌套连接:course.no是连接键,位于course表的第一列。
    explain select t.name, c.name, stu_num
    from course as c, teach_course as tc, teacher as t
    where c.no = tc.cno and tc.tno = t.no and c.name = 'Database System' and t.name = 'Jennifer';
                                        QUERY PLAN                                     
    -----------------------------------------------------------------------------------
     Nested Loop  (cost=24.10..74.58 rows=1 width=68)
       ->  Hash Join  (cost=23.95..62.61 rows=61 width=40)
             Hash Cond: (tc.tno = t.no)
             ->  Seq Scan on teach_course tc  (cost=0.00..30.40 rows=2040 width=12)
             ->  Hash  (cost=23.88..23.88 rows=6 width=36)
                   ->  Seq Scan on teacher t  (cost=0.00..23.88 rows=6 width=36)
                         Filter: ((name)::text = 'Jennifer'::text)
       ->  Index Scan using course_pkey on course c  (cost=0.15..0.20 rows=1 width=36)
             Index Cond: (no = tc.cno)
             Filter: ((name)::text = 'Database System'::text)
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    执行前的数据结构:
    在这里插入图片描述

    2 ExecutorRun执行过程

    测试SQL

    select t.name, c.name, stu_num
    from course as c, teach_course as tc, teacher as t
    where c.no = tc.cno and tc.tno = t.no and c.name = 'Database System' and t.name = 'Jennifer';
    
    • 1
    • 2
    • 3

    调试起点:b ExecutorRun

    我们再看下这张图:
    在这里插入图片描述

    2.1 ExecNestLoop

    nestloop需要从外表(outer表)中(驱动表)顺序扫描拿一条,在从内表(inner表)中找这条能连上的。具体在这个执行计划中:

    • 从hashjoin的结果中按顺序那一条(outer表)
    • 用这一条去indexscan找能连上的(去inner表上索引扫描)
    • 返回一条结果
      在这里插入图片描述

    执行过程

    1. 用Outerplan从驱动表里面拿一条
    2. 用这一条去innerplan里面找一条能连上的
    3. 返回
    ExecNestLoop
      ...
      outerPlan = outerPlanState(node)    // left node
      innerPlan = innerPlanState(node)    // right node
      // loop until we return a qualifying join tuple
      for (;;)
        // 从outer表的计划上找到一条
        outerTupleSlot = ExecProcNode(outerPlan)
      // 完了去inner表里面拿一条(要能连得上的一条)
      innerTupleSlot = ExecProcNode(innerPlan)
        ExecIndexScan
          for (;;)
            // 【重要】注意这个函数,ExecScanFetch没有具体业务逻辑,这是个框架函数
            // ExecScanFetch用accessMtd拿到元组,在用recheckMtd检查元组是否符合要求
            // 这里用的是两个通用函数 IndexNext 和 IndexRecheck
            slot = ExecScanFetch(node, accessMtd, recheckMtd)
              // 进入bt堆栈
              IndexNext
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    2.2 ExecHashJoin

    ExecHashJoin的逻辑是一个小型switch状态机,通过流转状态走完业务逻辑,读起来比较顺畅。

    hashjoin会seqscan扫左表,同时把右表创建成一个哈希表(会带着过滤条件,并不是把所有元组都建到哈希表里面)

    • 从左表中拿一条
    • 用这一条去哈希表里面查询,如果能连上就返回一条

    在这里插入图片描述
    执行过程:

    1. 创建右节点的哈希表
    2. 从左节点拿一个元组
    3. 去哈希表中匹配
    4. 匹配上返回,匹配不上goto 2
    ExecHashJoinImpl
      ExecHashTableCreate
      // 拿一条左表中的数据
      ExecHashJoinOuterGetTuple
        // T_SeqScanState
        slot = ExecProcNode(outerNode)
      // 找到能连上的tuple所在的bucket
      ExecHashGetBucketAndBatch
      ExecHashGetSkewBucket
      // 查找bucket
      ExecScanHashBucket
      // 找到连接元组
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2.3 ExecSeqScan

    seqscan为什么那么费CPU又效率低。
    答:无等待无IO连续下面几件事情:

    1. 找buffer(没在需要IO上来)
    2. 在buffer找到合适的位置,用tuple->t_data指上去
    3. 拼tupleslot
    4. 返回
    5. 继续循环

    上面5步不涉及IO、没有任何会sleep的逻辑,基本就是连续的函数调用、变量赋值,所以持续做会把单核打满。效率低是应为没条件一行一行全部都要扫一遍,慢是必然的。

    在这里插入图片描述

    SeqNext
      scandesc = table_beginscan
      table_scan_getnextslot
        slot->tts_tableOid = RelationGetRelid(sscan->rs_rd)
        heap_getnextslot
          heapgettup_pagemode
            heapgetpage
            BufferGetPage
            tuple->t_data = (HeapTupleHeader) PageGetItem((Page) dp, lpp)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  • 相关阅读:
    LeetCode-946-验证栈序列
    巧妙使用多个旧路由器无线中继提升网络速度
    【云原生 | Kubernetes 系列】----K8s持续集成与部署
    100天精通Andriod逆向——第6天:Andriod 开发入门
    OLED透明屏在智慧零售场景的应用
    weblogic任意文件上传漏洞操作(CVE-2018-2894)
    fdbus之CBaseClient类和CBaseServer类
    小咖啡馆也能撬动大生意
    k8s 中 Pod 的控制器
    暑假算法训练day3
  • 原文地址:https://blog.csdn.net/jackgo73/article/details/125791106