Postgresql源码(137)执行器参数传递与使用
参考
《Postgresql源码(127)投影ExecProject的表达式执行分析》
0 总结速查
prepare p_04(int,int) as select b from tbl_01 where a = $1 and b = $2
为例。
- custom计划中,在表达式计算中使用参数的值,因为custom计划会带参数值,所以表达式计算时不需要从参数列表中取值,直接EEO_CASE(EEOP_FUNCEXPR_STRICT)计算即可。
- generic计划中,在表达式计算中使用参数的值,generic计划不带参数值,所以需要多走一步EEO_CASE(EEOP_PARAM_EXTERN)、EEO_CASE(EEOP_FUNCEXPR_STRICT)。具体调用ExecEvalParamExtern函数拿到参数值。
- generic计划中,使用参数的节点会放一个Param。
1 执行器如何使用参数?
在下面例子中,execute语句执行时带了两个参数,执行器是在哪里带入参数执行的?下面做一些分析。
prepare p_04(int,int) as select b from tbl_01 where a = $1 and b = $2;
execute p_04(2,200);
带入参数的位置是PortalStart函数。
extern void PortalStart(Portal portal, ParamListInfo params, int eflags, Snapshot snapshot);
PortalStart的第二个参数可以接受参数列表ParamListInfo,从代码上看,PortalStart有三处调用位置:
exec_simple_query
PortalStart(portal, NULL, 0, InvalidSnapshot)
exec_bind_message
PortalStart(portal, params, 0, InvalidSnapshot)
ExecuteQuery
PortalStart(portal, paramLI, eflags, GetActiveSnapshot())
SPI_cursor_open_internal
PortalStart(portal, paramLI, 0, snapshot)
- exec_simple_query:执行简单SQL语句,不需要传参。
- exec_bind_message:PEB协议中的B阶段,绑定参数。
- ExecuteQuery:SQL语法提供的PBE协议,在execute阶段,绑定参数。
- SPI_cursor_open_internal:PL中打开游标时,绑定参数。
下面对3、4两种情况展开分析。
2 execute执行时执行器的传参
栗子:
create table tbl_01(a int, b int);
insert into tbl_01 values (1,100);
insert into tbl_01 values (2,200);
prepare p_04(int,int) as select b from tbl_01 where a = $1 and b = $2;
execute p_04(2,200);
execute p_04(2,200);
执行时,PortalStart会带入ParamListInfo。
2.1 问题一:ParamListInfo的来源?
ExecuteQuery
EvaluateParams
// 准备计算参数表达式的值
exprstates = ExecPrepareExprList(params, estate)
makeParamList
retval->parserSetup = paramlist_parser_setup
// paramlist_parser_setup
// pstate->p_paramref_hook = paramlist_param_ref
//
// paramlist_param_ref
// param = makeNode(Param)
// param->paramkind = PARAM_EXTERN
// param->paramid = paramno
// ...
// 钩子函数中生成Param,该实例不会走这个钩子,注意Param结构是不带值的,只用于中间过程。
// 走表达式计算参数的值
// 注意值都在paramLI->params[i]->value中
i = 0;
foreach(l, exprstates)
{
ExprState *n = (ExprState *) lfirst(l);
ParamExternData *prm = ¶mLI->params[i];
prm->ptype = param_types[i];
prm->pflags = PARAM_FLAG_CONST;
prm->value = ExecEvalExprSwitchContext(n,
GetPerTupleExprContext(estate),
&prm->isnull);
i++;
}
生成ParamListInfo(包含参数的全部信息,包括值)
paramLI =
{paramFetch = 0x0,
paramFetchArg = 0x0,
paramCompile = 0x0,
paramCompileArg = 0x0,
parserSetup = 0x8357c1 <paramlist_parser_setup>,
parserSetupArg = 0x3238da8,
paramValuesStr = 0x0,
numParams = 2,
params = 0x3238de8}
2.2 问题二:ParamListInfo的使用位置?
custom plan
前五次执行会生成customplan,customplan在GetCachedPlan中生成,customplan计划是带着参数生成的(按参数定制的计划,更准确,但每次参数变了都要重新生成),所以计划中会有参数的具体值,执行时不会使用执行器带入的ParamListInfo。
构造customplan时,会带入参数生成计划:
ExecQual调用表达式框架计算where a = $1 and b = $2
,ExecQual函数进入表达计算。
ExecScan
for (;;)
slot = ExecScanFetch
...
if (qual == NULL || ExecQual(qual, econtext))
...
return slot
ExecQual表达式计算流程:
- EEO_CASE(EEOP_SCAN_FETCHSOME)
- 从econtext->ecxt_scantuple读取到scanslot(当前要处理的一行数据)slot_getsomeattrs函数确保这一行数据中,至少有op->d.fetch.last_var个列是可以直接访问的。《Postgresql源码(127)投影ExecProject的表达式执行分析》
- EEO_CASE(EEOP_SCAN_VAR)
- 从行中拿到需要列的值。《Postgresql源码(127)投影ExecProject的表达式执行分析》
- EEO_CASE(EEOP_FUNCEXPR_STRICT)
- 计算where中第一个条件
- fcinfo : int4eq
- arg1 : 2
- arg2 : 2
- EEO_CASE(EEOP_QUAL)
- EEO_CASE(EEOP_FUNCEXPR_STRICT)
- 计算where中第二个条件
- fcinfo : int4eq
- arg1 : 200
- arg2 : 200
- EEO_CASE(EEOP_QUAL)
generic plan
第六次执行时,choose_custom_plan返回false,BuildCachedPlan时没有传入任何参数构造generic plan(通用执行计划,可以接受任何参数,不用每次都生成计划,但没有customplan精确,毕竟这里没有给参数)
执行execute p_04(2,200);给的两个参数明显是用于过滤条件的,所以在ExecScan→ExecQual中寻找使用的位置:
在ExecEvalParamExtern函数中,从econtext->ecxt_param_list_info中拿到参数值,返回给表达式框架:
ExecEvalParamExtern(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
{
ParamListInfo paramInfo = econtext->ecxt_param_list_info;
int paramId = op->d.param.paramid;
if (likely(paramInfo &&
paramId > 0 && paramId <= paramInfo->numParams))
{
ParamExternData *prm;
ParamExternData prmdata;
...
...
prm = ¶mInfo->params[paramId - 1];
...
...
*op->resvalue = prm->value;
*op->resnull = prm->isnull;
return;
}
}
...
...
}
最终ExecScan在loop中,拿到的slot经过ExecQual的计算,决定是否保留该元组。
ExecQual调用表达式框架计算where a = $1 and b = $2
,通过ExecEvalParamExtern
函数拿到$1
、$2
的值,完成计算,放回true OR false。
ExecScan
for (;;)
slot = ExecScanFetch
...
if (qual == NULL || ExecQual(qual, econtext))
...
return slot
表达式计算流程:
- EEO_CASE(EEOP_SCAN_FETCHSOME)
- EEO_CASE(EEOP_SCAN_VAR)
- EEO_CASE(EEOP_PARAM_EXTERN)
- EEO_CASE(EEOP_FUNCEXPR_STRICT)
- int4eq
- arg1 : 2
- arg2 : 2
- EEO_CASE(EEOP_QUAL)
- EEO_CASE(EEOP_SCAN_VAR)
- EEO_CASE(EEOP_PARAM_EXTERN)
- EEO_CASE(EEOP_FUNCEXPR_STRICT)
- int4eq
- arg1 : 200
- arg2 : 200
从计划上来看,参数的位置是两个Param。
3 游标打开时执行器的传参
create table tbl_01(a int, b int);
insert into tbl_01 values (1,100);
insert into tbl_01 values (2,200);
do $$
declare
res int;
c_04 CURSOR (p1 int, p2 int) FOR select b from tbl_01 where a = p1 and b = p2;
begin
open c_04(2,200);
fetch c_04 into res;
raise notice '%',res;
end; $$;
PL的参数在transform阶段已经转成const了,所以执行类似customplan:
fetch堆栈: