前回の Postgres テクノロジー インサイダー シリーズのライブ ブロードキャストでは、Postgres の射影演算子と式の計算の実装原理と基礎となる詳細を紹介しました。この記事は生放送の内容に基づいて構成されており、著者は現在 HashData カーネルの研究開発エンジニアです。
投影
A に含まれる属性を持つ列を関係 R から選択するために使用される関係代数の一種。
PG で追加の列 (計算式など) を導入するためにも使用されます。
簡単な質問と簡単な回答
create table t (int a, int b);
次の SQL のどれが投影ロジックをトリガーしますか?select * from t;
select a, b from t;
select b, a from t;
select a as b, b as a from t;
select a from t;
select a, b, b from t;
select a + 1, b from t;
select ctid, * from t;
メッセージを残して、答えを書き留めてください~
PG によるプロジェクションの実装
全体的な実装は、次の 3 つの主要な部分に分類できます。
1. 投影が必要かどうかを判断する
スキャンされた関係の記述子が計画ノードのターゲットリストと一致しない場合に限り、射影が必要となり、この関数はtlist_matches_tupdesc
関連する判断ロジックを実装するために使用されます。
static bool
tlist_matches_tupdesc(PlanState *ps, List *tlist, Index varno, TupleDesc tupdesc)
{
int numattrs = tupdesc->natts;
int attrno;
ListCell *tlist_item = list_head(tlist);
/* Check the tlist attributes */
for (attrno = 1; attrno <= numattrs; attrno++)
{
Form_pg_attribute att_tup = TupleDescAttr(tupdesc, attrno - 1);
Var *var;
if (tlist_item == NULL)
return false; /* tlist too short */
var = (Var *) ((TargetEntry *) lfirst(tlist_item))->expr;
if (!var || !IsA(var, Var))
return false; /* tlist item not a Var */
/* if these Asserts fail, planner messed up */
Assert(var->varno == varno);
Assert(var->varlevelsup == 0);
if (var->varattno != attrno)
return false; /* out of order */
if (att_tup->attisdropped)
return false; /* table contains dropped columns */
if (att_tup->atthasmissing)
return false; /* table contains cols with missing values */
/*
* Note: usually the Var's type should match the tupdesc exactly, but
* in situations involving unions of columns that have different
* typmods, the Var may have come from above the union and hence have
* typmod -1. This is a legitimate situation since the Var still
* describes the column, just not as exactly as the tupdesc does. We
* could change the planner to prevent it, but it'd then insert
* projection steps just to convert from specific typmod to typmod -1,
* which is pretty silly.
*/
if (var->vartype != att_tup->atttypid ||
(var->vartypmod != att_tup->atttypmod &&
var->vartypmod != -1))
return false; /* type mismatch */
tlist_item = lnext(tlist, tlist_item);
}
if (tlist_item)
return false; /* tlist too long */
return true;
}
その中心となるロジックは、ターゲットリストを走査し、それがスキャンテーブルの記述子と一致するかどうかを判断することですが、これは比較的単純なのでここでは説明しません。
2. 投影を構築するために必要な情報
ProjectionInfo *
ExecBuildProjectionInfo(List *targetList,
ExprContext *econtext,
TupleTableSlot *slot,
PlanState *parent,
TupleDesc inputDesc)
{
/* Insert EEOP_*_FETCHSOME steps as needed */
ExecInitExprSlots(state, (Node *) targetList);
/* Now compile each tlist column */
foreach(lc, targetList)
{
/* 考虑投影列为var的情况 */
if (tle->expr != NULL &&
IsA(tle->expr, Var) &&
((Var *) tle->expr)->varattno > 0)
{
if (inputDesc == NULL)
isSafeVar = true; /* can't check, just assume OK */
else if (attnum <= inputDesc->natts)
{
Form_pg_attribute attr = TupleDescAttr(inputDesc, attnum - 1);
/*
* If user attribute is dropped or has a type mismatch, don't
* use ASSIGN_*_VAR. Instead let the normal expression
* machinery handle it (which'll possibly error out).
*/
if (!attr->attisdropped && variable->vartype == attr->atttypid)
{
isSafeVar = true;
}
}
/* 对于简单的情况只需要 EEOP_ASSIGN_*_VAR 即可 */
if (isSafeVar)
{
/* Fast-path: just generate an EEOP_ASSIGN_*_VAR step */
switch (variable->varno)
{
case INNER_VAR:
/* get the tuple from the inner node */
scratch.opcode = EEOP_ASSIGN_INNER_VAR;
break;
case OUTER_VAR:
/* get the tuple from the outer node */
scratch.opcode = EEOP_ASSIGN_OUTER_VAR;
break;
/* INDEX_VAR is handled by default case */
default:
/* get the tuple from the relation being scanned */
scratch.opcode = EEOP_ASSIGN_SCAN_VAR;
break;
}
/*
* 这里是核心逻辑 构建了投影所需要的执行步骤 在执行过程中按照步骤依次执行即可
* 这么做的本质是为了降低函数递归调用的运行成本
*/
ExprEvalPushStep(state, &scratch);
}
else
{
/* 具体来说,包含表达式计算,或者系统变量等情况时,要按照常规方式处理表达式 */
/*
* Otherwise, compile the column expression normally.
*
* We can't tell the expression to evaluate directly into the
* result slot, as the result slot (and the exprstate for that
* matter) can change between executions. We instead evaluate
* into the ExprState's resvalue/resnull and then move.
*/
ExecInitExprRec(tle->expr, state,
&state->resvalue, &state->resnull);
// 投影求值计算的时候会用到 attnum 和 resultnum
scratch.d.assign_var.attnum = attnum - 1;
scratch.d.assign_var.resultnum = tle->resno - 1;
ExprEvalPushStep(state, &scratch);
}
}
}
}
このセクションでは、主に上記のコード内の高速パス関連のロジックに注釈を付けます。残りのロジックについては後で説明します。
このコードの中心となるロジックは、 ExprEvalPushStep
呼び出しによって配列で表されるプロジェクションの実行プロセスを構築し、オペコードを通じて各ステップのタイプを識別し、実行フェーズ中にオペコードに従って異なるプロセスを呼び出すことができるようにすることです。以下の実行プロセスを参照してください。
従来の式評価ロジックと比較して、この方法で記述することの利点は、関数の再帰呼び出しを削減できることです。
3. 射影演算子を実行する
ExecEvalExprSwitchContext
executor 射影演算子のエントリ関数。重要な関数は、PG の式の評価に関連するロジックがこの関数を通じて実装されていることがわかります。
#ifndef FRONTEND
static inline TupleTableSlot *
ExecProject(ProjectionInfo *projInfo)
{
ExprState *state = &projInfo->pi_state;
TupleTableSlot *slot = state->resultslot; // 投影之后的结果;目前还是未计算的状态
/* Run the expression, discarding scalar result from the last column. */
(void) ExecEvalExprSwitchContext(state, econtext, &isnull);
return slot;
}
まず PG 式評価のフレームワークを紹介します. 射影評価も式評価、つまり呼び出しによって実装され、ExecEvalExprSwitchContext
その基礎となる層は と呼ばれます ExecInterpExpr
。
実行プロセス全体は、マクロ定義によって実装される一連のディストリビュータ メカニズムに基づいており、以前に構築された式の評価ステップを順次実行するロジックを実装します。プロセスの実行中、ExprState を使用して中間計算結果やその他の実行状態を保存します。具体的なコードは次のとおりです。
// opcode对应步骤的实现逻辑的标识 用于goto
#define EEO_CASE(name) CASE_##name:
// 分发至步骤的执行逻辑
#define EEO_DISPATCH() goto *((void *) op->opcode)
//
#define EEO_OPCODE(opcode) ((intptr_t) dispatch_table[opcode])
// 当前步骤执行完毕时移动至下一个需要执行的步骤
#define EEO_NEXT() \
do { \
op++; \
EEO_DISPATCH(); \
} while (0)
ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
{
op = state->steps; // 存储所有的步骤,我们通过宏不断移动当前执行的步骤
resultslot = state->resultslot; // 用于存放最后返回的结果值
innerslot = econtext->ecxt_innertuple;
outerslot = econtext->ecxt_outertuple;
scanslot = econtext->ecxt_scantuple;
EEO_DISPATCH();
EEO_CASE()
EEO_CASE(EEOP_DONE)
{
goto out;
}
EEO_CASE(EEOP_SCAN_FETCHSOME)
{
CheckOpSlotCompatibility(op, scanslot);
slot_getsomeattrs(scanslot, op->d.fetch.last_var);
EEO_NEXT();
}
EEO_CASE(EEOP_ASSIGN_SCAN_VAR)
{
int resultnum = op->d.assign_var.resultnum;
int attnum = op->d.assign_var.attnum;
/*
* We do not need CheckVarSlotCompatibility here; that was taken
* care of at compilation time. But see EEOP_INNER_VAR comments.
*/
resultslot->tts_values[resultnum] = scanslot->tts_values[attnum];
resultslot->tts_isnull[resultnum] = scanslot->tts_isnull[attnum];
EEO_NEXT();
}
out:
*isnull = state->resnull
return state->resvalue
}
このようにして、射影列計算のロジックを実現し、最終的なタプルは state->resultslot
上位の演算子が使用できるようにそこに保存されます。
式の計算
以下に式計算の実装を紹介します。式の計算プロセスでは、前の投影列の評価プロセスと同じロジックが再利用されます。つまり、同じ分散メカニズムが評価に使用されます。
重要な違いは次のとおりです。
1) 計算ロジックは通常より複雑で、完了するには複数のステップが必要で、基本的に式ツリーを反復的に評価します。
2) 式を事前に計算し、定数部分をオプティマイザ段階で評価して、反復プロセス中の繰り返しの評価を回避できます。
例を使用して、式ツリーに対応する評価ステップがどのように構築されるかを調べてみましょう。まず、PG がメモリ内の式ツリーをどのように表現しているかを見てみましょう。上記のクエリを例にとると、次の FuncExpr は対応関係をexplain select (SQRT(POWER(i,i))) from generate_series(1,5) i;
非常に明確に見つけることができます。(SQRT(POWER(i,i)))
FuncExpr [funcid=1344 funcresulttype=701 funcretset=false funcvariadic=false funcformat=COERCE_EXPLICIT_CALL is_tablefunc=false]
FuncExpr [funcid=1368 funcresulttype=701 funcretset=false funcvariadic=false funcformat=COERCE_EXPLICIT_CALL is_tablefunc=false]
FuncExpr [funcid=316 funcresulttype=701 funcretset=false funcvariadic=false funcformat=COERCE_IMPLICIT_CAST is_tablefunc=false]
Var [varno=1 varattno=1 vartype=23 varnosyn=1 varattnosyn=1]
FuncExpr [funcid=316 funcresulttype=701 funcretset=false funcvariadic=false funcformat=COERCE_IMPLICIT_CAST is_tablefunc=false]
Var [varno=1 varattno=1 vartype=23 varnosyn=1 varattnosyn=1]
一般に、このような式ツリーを解くには、通常、再帰計算が使用されます。基になる式が最初に計算され、次に高レベルの式が計算されます。全体のプロセスは、ツリーを事後順序で走査するプロセスと似ています。
実行効率を向上させるために、PG は実行フェーズで射影評価の方法で反復実行することを選択するため、実行初期化フェーズではツリーの事後探索と同様の方法を採用し、追加する必要があります。各部分式を評価ステップの配列に追加します。
評価ステップを構築するためのコールスタックは次のとおりです。
-> ExecBuildProjectionInfo
-> ExecInitExprSlots // EEOP_*_FETCHSOME
->ExprEvalPushStep
for targetList:
-> ExecInitExprRec(tle->expr, )
scratch.resvalue = resv // 当前步骤的结果;上层通过指针传入我们应该存放的地址
case T_FuncExpr:
-> ExecInitFunc // 当前函数 [funcid=1344]
fcinfo = scratch->d.func.finfo
for args: // 这层有一个参数
-> ExecInitExprRec(arg, state, &fcinfo->args[argno].value) // resv - where to stor the result of the node into
case T_FuncExpr:
-> ExecInitFunc // 当前函数 [funcid=1368]
for args: // 这层有两个参数
-> ExecInitExprRec()
case T_FuncExpr:
-> ExecInitFunc // 当前函数 [funcid=316]
for args: // 这层有一个参数 Var [varno=1 varattno=1 vartype=23 varnosyn=1 varattnosyn=1]
-> ExecInitExprRec()
case T_Var:
// regular user column
scratch.d.var.attnum = variable->varattno - 1;
-> ExprEvalPushStep()
ExprEvalPushStep()
ExprEvalPushStep()
ExprEvalPushStep()
ExprEvalPushStep()
scratch.opcode = EEOP_DONE;
ExprEvalPushStep()
プロセス全体の本体は、式ツリーの再帰的走査です。式ツリーには通常、 T_FuncExpr
中間に複数または他のタイプの式ノードが含まれます。各ノードには複数のパラメータがあります。パラメータは式である場合もあります。すべての子ノードの評価ステップが生成された後、現在のステップが配列に生成されます。通常、葉ノードは T_Var または T_Const であり、処理方法は射影と一致します。
このセクションでは、T_FuncExpr 型のステップ構築プロセスと、前に説明されていない非 fast_path 式の評価ロジックに焦点を当てます。ExecInitExprRec
これに は主に と の 2 つの関数が含まれていますExecInitFunc
。
その中には ExecInitExprRec
式の評価という重要な機能があり、再帰呼び出しが行われる場所でもあります。コードはさまざまな式のタイプに応じて異なるロジックを呼び出し、特定の状況に応じて各ブランチが再帰的に呼び出され、現在の分岐がプッシュされます。ステップ ExecInitExprRec
配列 にステップインしますExprEvalPushStep
。その中には非常に重要なステップがあり scratch.resvalue = resv
、現在のステップで計算された値を上位の呼び出し元にポインタの形で渡すことができます(上位の式に相当し、その評価結果を取得できます)。部分式)、再帰計算プロセス全体が直列に接続されるようにします。
ExecInitFunc
関数式の型を計算する処理ですが、複雑なため独立した関数として記述します。その主なロジックは、現在の関数のパラメータを走査し、評価ステップを呼び出してExecInitExprRec
初期化すること です。部分式の評価の結果&fcinfo->args[argno].value
は次の方法で取得できます。完了後、現在の関数の評価ステップをステップ配列にプッシュします。
上記の例の実際の評価プロセスは次のとおりであり、前述の分散メカニズムで次のステップを順番に実行できます。
EEO_CASE(EEOP_SCAN_FETCHSOME)
EEO_CASE(EEOP_SCAN_VAR) // scan i
EEO_CASE(EEOP_FUNCEXPR_STRICT) // i4tod
EEO_CASE(EEOP_SCAN_VAR) // scan i
EEO_CASE(EEOP_FUNCEXPR_STRICT) // i4tod
EEO_CASE(EEOP_FUNCEXPR_STRICT) // dpow
EEO_CASE(EEOP_FUNCEXPR_STRICT) // dsprt
EEO_CASE(EEOP_ASSIGN_TMP) // 将计算结果赋值到resultslot
EEO_CASE(EEOP_DONE)
一定の事前計算の最適化
式ツリーの定数部分については、最適化段階で計算して、繰り返しの評価を避けることができます。この問題については、例を使用して説明します。次の例では、最適化段階で POWER(2,3) を 8 に置き換えることができれば、実行段階では明らかに POWER(2,3) の計算を 5 回繰り返すことを回避できます。
select i+POWER(2,3) from generate_series(1,5) i;
コールスタックは次のとおりです。
-> preprocess_expression
-> eval_const_expressions
-> eval_const_expressions_mutator
-> expression_tree_mutator (case T_List)
-> eval_const_expressions_mutator
-> expression_tree_mutator (case T_TargetEntry)
-> eval_const_expressions_mutator (case T_OpExpr)
-> simplify_function
// 对表达式列表递归调用 eval_const_expressions_mutator
-> args = expand_function_arguments()
-> args = (List *) expression_tree_mutator(args,eval_const_expressions_mutator)
-> evaluate_function // try to pre-evaluate a function call
-> evaluate_expr // pre-evaluate a constant expression
// 初始化表达式节点的执行状态信息
-> ExecInitExpr
-> ExecInitExprSlots() // Insert EEOP_*_FETCHSOME steps as needed
-> ExecInitExprRec() // 将执行步骤填入 ExprState->steps 数组
case T_FuncExpr:
-> ExecInitFunc // 主要工作是将 argument 求值;并放入 state 的 list 中
foreach(lc, args)
if IsA(arg, Const)
fcinfo->args[argno].value = con->constvalue
else
ExecInitExprRec() // 递归对表达式参数求值
-> ExprEvalPushStep
-> const_val = ExecEvalExprSwitchContext
-> evalfunc()
op = state->steps
resultslot = state->resultslot
outerslot = econtext->ecxt_outertuple
EEO_DISPATCH() // goto op->opcode
コードの中心となるロジックは、 sum 関数を通じて式ツリーをトラバースすることです。op_expr が見つかると、 単純化された部分式ツリー内の定数が呼び出されますeval_const_expressions_mutator
。 部分式が非定数であるかどうかがチェックされ、すべてが定数であれば、簡略化を続行できます。express_tree_mutator
simplify_function()->evaluate_function()
evaluate_function
簡素化されたプロセスの本質は、実行ステージの評価プロセスを最適化ステージに進めることです。最初にノードの実行ステータス情報と評価ステップの配列を生成し、次に呼び出して順番に実行し、最後に makeConst を通じて定数ノードを生成して、ノードの実行ステータスを置き換えます ExecEvalExprSwitchContext
。元の複雑な式の子ノード。
ここまで、PG における射影と式計算の実装ロジックを体系的に紹介してきました。ほとんどの場合、射影はほぼ必要な操作であり、数少ない最適化方法の 1 つは、上位レベルの演算子の射影をプッシュダウンすることである可能性があるため、ここでは説明しません。