相关
《Postgresql源码(61)查询执行——最外层Portal模块》
《Postgresql源码(62)查询执行——子模块ProcessUtility》
《Postgresql源码(63)查询执行——子模块Executor(1)》
《Postgresql源码(64)查询执行——子模块Executor(2)执行前的数据结构和执行过程》
《Postgresql源码(85)查询执行——表达式解析器分析(select 1+1如何执行)》
《Postgresql查询执行模块README笔记》
上一篇说明了执行的框架,本篇深入分析执行细节。测试用例不变,还是分析之前的case。
0 总结
- 下图中的planstate有四类:控制节点、扫描节点、连接节点、物化节点
- 扫描节点公共父类:Scan
- 连接节点公共父类:Join
- Plan的子节点通过Plan的lefttree和righttree指针连接,构成计划树
- 执行时,Planstate用于记录各节点的执行状态,estate中的es_tupleTable在节点间传递元组。
- Excutor输入QueryDesc,包含plannedstmt树形结构;执行前用plannedstmt初始化节点状态树planstate,同时初始化全局状态信息estate。然后执行planstate根节点的函数指针,进入根节点业务处理函数(例如nestloop),pull模型向下层取数据拉动整个计划树的执行。
1 ExecutorRun执行前数据结构
执行计划:
- teach_course和teacher走hash连接,生成outer表(驱动表)
- course表做inner表
- 循环嵌套连接: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)
执行前的数据结构:
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';
调试起点:b ExecutorRun
我们再看下这张图:
2.1 ExecNestLoop
nestloop需要从外表(outer表)中(驱动表)顺序扫描拿一条,在从内表(inner表)中找这条能连上的。具体在这个执行计划中:
- 从hashjoin的结果中按顺序那一条(outer表)
- 用这一条去indexscan找能连上的(去inner表上索引扫描)
- 返回一条结果
执行过程
- 用Outerplan从驱动表里面拿一条
- 用这一条去innerplan里面找一条能连上的
- 返回
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
2.2 ExecHashJoin
ExecHashJoin的逻辑是一个小型switch状态机,通过流转状态走完业务逻辑,读起来比较顺畅。
hashjoin会seqscan扫左表,同时把右表创建成一个哈希表(会带着过滤条件,并不是把所有元组都建到哈希表里面)
- 从左表中拿一条
- 用这一条去哈希表里面查询,如果能连上就返回一条
执行过程:
- 创建右节点的哈希表
- 从左节点拿一个元组
- 去哈希表中匹配
- 匹配上返回,匹配不上goto 2
ExecHashJoinImpl
ExecHashTableCreate
// 拿一条左表中的数据
ExecHashJoinOuterGetTuple
// T_SeqScanState
slot = ExecProcNode(outerNode)
// 找到能连上的tuple所在的bucket
ExecHashGetBucketAndBatch
ExecHashGetSkewBucket
// 查找bucket
ExecScanHashBucket
// 找到连接元组
2.3 ExecSeqScan
seqscan为什么那么费CPU又效率低。
答:连续函数调用无等待、低频IO连续下面几件事情:
- 找buffer(没在需要IO上来)
- 在buffer找到合适的位置,用tuple->t_data指上去
- 拼tupleslot
- 返回
- 继续循环
上面5步低频率IO、没有任何会sleep的逻辑,基本就是连续的函数调用、变量赋值,所以持续做会把单核打满。效率低是应为没条件一行一行全部都要扫一遍,慢是必然的。
Ps. 火山Pull模型的缺点在这里就体现一些了,一次返回一行,深层的函数调用,CPU Cache低效工作(所以现在有的DB在推push模型,从底层往上层批量推数据,一次处理一批(向量化?)代价是控制变复杂了)
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)