Postgresql源码(147)Nestloop流程与Instrumentation简单分析
nestloop流程
- 外层循环控制外表元组,内层循环控制内表元组。
- 每对外表/内表元组都要先过ON条件,再过WHERE条件。
- 支持参数化内表,outtertuple拿到后,把将外表的某些字段值传递给内表。
- 支持LEFT JOIN/SEMI JOIN/ANTI JOIN等多种连接类型。
- 通过InstrCountFiltered1/2统计被ON/WHERE条件过滤的元组数。
- explain analyze中下面数据来源:
- Rows Removed by Join Filter: 1234
- Rows Removed by … Cond: 5678
- 每次返回一个符合条件的连接结果元组,直到所有外表元组处理完毕。
explain analyze打点位置:
1 nestloop流程简化
注意这套流程每次执行返回一条数据
ExecNestLoopfor (;;)if (需要新外表元组)取外表元组if (外表元组为空) return NULL处理参数,重扫内表取内表元组if (内表元组为空)标记需要新外表元组if (当前外表元组匹配了所有内标元组但没有匹配 && 是LEFT JOIN或ANTI JOIN)构造内表全NULL的连接结果if (通过WHERE条件)返回这个结果else统计被WHERE过滤if (ON连接条件通过)外表已匹配if (ANTI JOIN) 需要新外表,continueif (SEMI JOIN) 需要新外表if (WHERE通过) 返回否则计数else计数继续下一轮循环
2 nestloop源码分析
关键变量
- NestLoopState *node:当前NestLoop节点的执行状态。
- NestLoop *nl:计划树中的NestLoop节点。
- PlanState *outerPlan, *innerPlan:外表和内表的执行节点。
- TupleTableSlot *outerTupleSlot, *innerTupleSlot:外表和内表的当前元组。
- ExprState *joinqual:连接条件(ON条件)。
- ExprState *otherqual:其他条件(WHERE条件)。
- ExprContext *econtext:表达式求值上下文。
源码:
static TupleTableSlot *
ExecNestLoop(PlanState *pstate)
{......for (;;){
获取外表元组,outer tuple
if (node->nl_NeedNewOuter){outerTupleSlot = ExecProcNode(outerPlan);if (TupIsNull(outerTupleSlot)){// 外表没数据,结束。return NULL;}// 处理NestLoop参数foreach(lc, nl->nestParams){......
将外表的某些字段值传递给内表
innerPlan->chgParam = bms_add_member(innerPlan->chgParam,paramno);}/** now rescan the inner plan*/
重新扫描内表
ExecReScan(innerPlan);}
获取内表元组(inner tuple)
innerTupleSlot = ExecProcNode(innerPlan);econtext->ecxt_innertuple = innerTupleSlot;
内表元组为空的处理
if (TupIsNull(innerTupleSlot)){
需要新外表元组
node->nl_NeedNewOuter = true;
处理LEFT JOIN/ANTI JOIN的特殊情况
到这里如果想进去,需要三个条件
- 外表这一行,内表循环了多次,但是没有一次能连接上的,nl_MatchedOuter保持false
- 连接是left join 或 anti join
这两中连接方式,在连不上的时候需要特殊处理下:
if (!node->nl_MatchedOuter &&(node->js.jointype == JOIN_LEFT ||node->js.jointype == JOIN_ANTI)){
没有匹配的内表元组,生成一个全NULL的内表元组
econtext->ecxt_innertuple = node->nl_NullInnerTupleSlot;
检查WHERE条件
if (otherqual == NULL || ExecQual(otherqual, econtext)){
连上一条,
return ExecProject(node->js.ps.ps_ProjInfo);}elseInstrCountFiltered2(node, 1);}continue;}
内外表元组都不为空,判断连接条件
if (ExecQual(joinqual, econtext)){node->nl_MatchedOuter = true;
ANTI JOIN特殊处理:匹配到就不返回
if (node->js.jointype == JOIN_ANTI){node->nl_NeedNewOuter = true;continue;}
SEMI JOIN(single_match)只要第一个匹配
if (node->js.single_match)node->nl_NeedNewOuter = true;
检查WHERE条件,满足即可返回
if (otherqual == NULL || ExecQual(otherqual, econtext)){return ExecProject(node->js.ps.ps_ProjInfo);}else
计数,被WHERE条件过滤
InstrCountFiltered2(node, 1);}else
计数,被ON连接条件过滤
InstrCountFiltered1(node, 1);// 每次循环结束,重置表达式上下文,释放内存ResetExprContext(econtext);}
}
3 explain analyze记录逻辑
惯用法:
InstrStartNode
执行算子
InstrStopNode
源码:
void
InstrStartNode(Instrumentation *instr)
{...
记录启动时间
INSTR_TIME_SET_CURRENT_LAZY(instr->starttime); 记录启动时间...
}/* Exit from a plan node */
void
InstrStopNode(Instrumentation *instr, double nTuples)
{double save_tuplecount = instr->tuplecount;instr_time endtime;/* count the returned tuples */instr->tuplecount += nTuples;
记录本次算子执行时间
if (instr->need_timer){INSTR_TIME_IS_ZERO(instr->starttime);INSTR_TIME_SET_CURRENT(endtime);INSTR_TIME_ACCUM_DIFF(instr->counter, endtime, instr->starttime);INSTR_TIME_SET_ZERO(instr->starttime);}...if (!instr->running){instr->running = true;
记录第一次算子执行完的时间,无论算子是否返回数据
instr->firsttuple = INSTR_TIME_GET_DOUBLE(instr->counter);}else{if (instr->async_mode && save_tuplecount < 1.0)instr->firsttuple = INSTR_TIME_GET_DOUBLE(instr->counter);}
}