From dbca18d6b1083a616412dbed989822b034e37c75 Mon Sep 17 00:00:00 2001 From: Johannes Edmeier Date: Fri, 25 Sep 2026 23:52:11 +0200 Subject: [PATCH v1 3/4] Share detoasted values with subplan and nestloop parameters. A correlated subplan, and the inner side of a nested loop, receive their outer row's columns as parameters, so a plan that detoasts a column once per row cannot hand that value to them: the parameter holds a copy of the stored datum, which says nothing about where the value came from. ParamExecData therefore carries a reference to the slot and attribute the value was taken from, filled in by EEOP_PARAM_SET for a plain Var argument of a correlated subplan and by NestLoop for its nestParams; value itself stays the stored datum. A varlena PARAM_EXEC parameter in an argument position compiles to EEOP_PARAM_EXEC_DETOAST, which follows the reference into the source slot, makes the copy there on first use, and reuses it for later references and later executions for the same outer row. The reference is followed only while that slot still holds the same datum. The address is recorded only for a plain Var argument, so forwarding reaches one level: a subplan nested inside another subplan takes its argument from a parameter rather than from a slot, and makes its own copy. That costs a detoast, never a wrong answer. A filter on the inner side of a lateral join, reading a wide outer column once and running ten times per outer row, reads 6013 buffers where it read 63013 before. --- src/backend/executor/README | 5 ++- src/backend/executor/execExpr.c | 35 ++++++++++++++++- src/backend/executor/execExprInterp.c | 55 +++++++++++++++++++++++++++ src/backend/executor/execParallel.c | 1 + src/backend/executor/nodeNestloop.c | 3 ++ src/backend/executor/nodeSubplan.c | 9 +++++ src/backend/jit/llvm/llvmjit_expr.c | 6 +++ src/backend/jit/llvm/llvmjit_types.c | 1 + src/include/executor/execExpr.h | 6 +++ src/include/nodes/params.h | 9 +++++ 10 files changed, 128 insertions(+), 2 deletions(-) diff --git a/src/backend/executor/README b/src/backend/executor/README index 76742bb053e..d12934eb241 100644 --- a/src/backend/executor/README +++ b/src/backend/executor/README @@ -326,7 +326,10 @@ copy into the result slot alongside the stored datum, so a parent node reading that column as an argument finds it. The copy and the stored datum are both borrowed from the source slot and so share its lifetime; materializing the result slot therefore drops the copy rather than taking ownership of it, since -a materialized slot may outlive its source. +a materialized slot may outlive its source. A PARAM_EXEC parameter carries a +reference to the slot and attribute its value came from rather than a pointer +to the copy, and that reference is followed only while the slot still holds +the same datum. The planner decides only where this pays, in set_plan_detoast_reuse() in setrefs.c, which records per node the attributes two or more argument diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 30305457b54..54cdf1c128e 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -1038,6 +1038,7 @@ ExecInitDetoastArg(Expr *arg, ExprState *state, Datum *resv, bool *resnull) PlanState *parent = state->parent; Expr *expr = arg; Plan *plan; + PlannedStmt *pstmt; /* * Expressions outside a plan tree, and the ones COPY compiles against a @@ -1049,10 +1050,13 @@ ExecInitDetoastArg(Expr *arg, ExprState *state, Datum *resv, bool *resnull) return; } plan = parent->plan; + pstmt = parent->state ? parent->state->es_plannedstmt : NULL; while (IsA(expr, RelabelType)) expr = ((RelabelType *) expr)->arg; - if (!((IsA(expr, Var) && plan != NULL && + if (!((IsA(expr, Param) && ((Param *) expr)->paramkind == PARAM_EXEC && + pstmt != NULL && pstmt->detoastReuse) || + (IsA(expr, Var) && plan != NULL && (plan->detoast_reuse_scan != NULL || plan->detoast_reuse_outer != NULL || plan->detoast_reuse_inner != NULL))) || @@ -1079,6 +1083,18 @@ ExecPushDetoastArgStep(Expr *expr, ExprState *state, Datum *resv, bool *resnull) { ExprEvalStep scratch = {0}; + if (IsA(expr, Param)) + { + Param *param = (Param *) expr; + + if (param->paramkind != PARAM_EXEC || + get_typlen(param->paramtype) != -1) + return false; + scratch.opcode = EEOP_PARAM_EXEC_DETOAST; + scratch.d.param.paramid = param->paramid; + scratch.d.param.paramtype = param->paramtype; + } + else { Var *var = (Var *) expr; Plan *plan = state->parent->plan; @@ -3074,6 +3090,7 @@ ExecInitSubPlanExpr(SubPlan *subplan, { int paramid = lfirst_int(l); Expr *arg = (Expr *) lfirst(pvar); + Expr *src = arg; ExecInitExprRec(arg, state, resv, resnull); @@ -3083,6 +3100,22 @@ ExecInitSubPlanExpr(SubPlan *subplan, scratch.d.param.paramid = paramid; /* paramtype's not actually used, but we might as well fill it */ scratch.d.param.paramtype = exprType((Node *) arg); + + /* + * A plain column value may have a detoasted copy beside its slot; + * tell the step where, so that argument positions in the subplan can + * use it (see ExecEvalParamExecDetoast). + */ + scratch.d.param.srcattnum = 0; + scratch.d.param.srcvarno = 0; + while (IsA(src, RelabelType)) + src = ((RelabelType *) src)->arg; + if (IsA(src, Var) && ((Var *) src)->varattno > 0 && + ((Var *) src)->varreturningtype == VAR_RETURNING_DEFAULT) + { + scratch.d.param.srcattnum = ((Var *) src)->varattno; + scratch.d.param.srcvarno = ((Var *) src)->varno; + } ExprEvalPushStep(state, &scratch); } diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 90236d68718..3538809b7e1 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -568,6 +568,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) &&CASE_EEOP_BOOLTEST_IS_FALSE, &&CASE_EEOP_BOOLTEST_IS_NOT_FALSE, &&CASE_EEOP_PARAM_EXEC, + &&CASE_EEOP_PARAM_EXEC_DETOAST, &&CASE_EEOP_PARAM_EXTERN, &&CASE_EEOP_PARAM_CALLBACK, &&CASE_EEOP_PARAM_SET, @@ -1393,6 +1394,12 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + EEO_CASE(EEOP_PARAM_EXEC_DETOAST) + { + ExecEvalParamExecDetoast(state, op, econtext); + + EEO_NEXT(); + } EEO_CASE(EEOP_PARAM_EXEC) { @@ -3210,6 +3217,25 @@ ExecEvalParamSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext) prm->value = *op->resvalue; prm->isnull = *op->resnull; + + /* remember where a detoasted copy of a plain Var's value may be kept */ + prm->detoast_slot = NULL; + if (op->d.param.srcattnum > 0) + { + switch (op->d.param.srcvarno) + { + case INNER_VAR: + prm->detoast_slot = econtext->ecxt_innertuple; + break; + case OUTER_VAR: + prm->detoast_slot = econtext->ecxt_outertuple; + break; + default: + prm->detoast_slot = econtext->ecxt_scantuple; + break; + } + prm->detoast_attnum = op->d.param.srcattnum; + } } /* @@ -5885,6 +5911,35 @@ ExecEvalAssignVarDetoast(ExprState *state, ExprEvalStep *op, } } +/* + * A PARAM_EXEC parameter in an argument position. The parameter carries a + * reference to the slot and attribute its value was taken from, so the copy + * can be made there and shared with the other references of that outer row. + * The reference is followed only while that slot still holds the same datum. + */ +void +ExecEvalParamExecDetoast(ExprState *state, ExprEvalStep *op, + ExprContext *econtext) +{ + ParamExecData *prm = &(econtext->ecxt_param_exec_vals[op->d.param.paramid]); + TupleTableSlot *slot; + int attnum; + varlena *attr; + + ExecEvalParamExec(state, op, econtext); + + slot = prm->detoast_slot; + if (prm->isnull || slot == NULL) + return; + attnum = prm->detoast_attnum - 1; + if (attnum < 0 || attnum >= slot->tts_nvalid || + slot->tts_isnull[attnum] || slot->tts_values[attnum] != prm->value) + return; + attr = (varlena *) DatumGetPointer(prm->value); + if (VARATT_IS_EXTERNAL_ONDISK(attr) || VARATT_IS_COMPRESSED(attr)) + *op->resvalue = slot_detoast_attr(slot, attnum, attr); +} + void ExecEvalSysVar(ExprState *state, ExprEvalStep *op, ExprContext *econtext, TupleTableSlot *slot) diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c index 6b85508a696..ecb135f71a4 100644 --- a/src/backend/executor/execParallel.c +++ b/src/backend/executor/execParallel.c @@ -468,6 +468,7 @@ RestoreParamExecParams(char *start_address, EState *estate) /* Read datum/isnull. */ prm->value = datumRestore(&start_address, &prm->isnull); + prm->detoast_slot = NULL; prm->execPlan = NULL; } } diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c index da48c9b204b..073bb6edc98 100644 --- a/src/backend/executor/nodeNestloop.c +++ b/src/backend/executor/nodeNestloop.c @@ -131,6 +131,9 @@ ExecNestLoop(PlanState *pstate) prm->value = slot_getattr(outerTupleSlot, nlp->paramval->varattno, &(prm->isnull)); + /* a detoasted copy may be kept beside the outer slot */ + prm->detoast_slot = outerTupleSlot; + prm->detoast_attnum = nlp->paramval->varattno; /* Flag parameter value as changed */ innerPlan->chgParam = bms_add_member(innerPlan->chgParam, paramno); diff --git a/src/backend/executor/nodeSubplan.c b/src/backend/executor/nodeSubplan.c index c6dd463c118..f045f62d81a 100644 --- a/src/backend/executor/nodeSubplan.c +++ b/src/backend/executor/nodeSubplan.c @@ -346,6 +346,7 @@ ExecScanSubPlan(SubPlanState *node, Assert(prmdata->execPlan == NULL); prmdata->value = heap_getattr(node->curTuple, col, tdesc, &(prmdata->isnull)); + prmdata->detoast_slot = NULL; col++; } @@ -390,6 +391,7 @@ ExecScanSubPlan(SubPlanState *node, prmdata = &(econtext->ecxt_param_exec_vals[paramid]); Assert(prmdata->execPlan == NULL); prmdata->value = slot_getattr(slot, col, &(prmdata->isnull)); + prmdata->detoast_slot = NULL; col++; } @@ -459,6 +461,7 @@ ExecScanSubPlan(SubPlanState *node, prmdata = &(econtext->ecxt_param_exec_vals[paramid]); Assert(prmdata->execPlan == NULL); prmdata->value = (Datum) 0; + prmdata->detoast_slot = NULL; prmdata->isnull = true; } } @@ -594,6 +597,7 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext) Assert(prmdata->execPlan == NULL); prmdata->value = slot_getattr(slot, col, &(prmdata->isnull)); + prmdata->detoast_slot = NULL; col++; } slot = ExecProject(node->projRight); @@ -1172,6 +1176,7 @@ ExecSetParamPlan(SubPlanState *node, ExprContext *econtext) prm->execPlan = NULL; prm->value = BoolGetDatum(true); prm->isnull = false; + prm->detoast_slot = NULL; found = true; break; } @@ -1222,6 +1227,7 @@ ExecSetParamPlan(SubPlanState *node, ExprContext *econtext) prm->execPlan = NULL; prm->value = heap_getattr(node->curTuple, i, tdesc, &(prm->isnull)); + prm->detoast_slot = NULL; i++; } } @@ -1245,6 +1251,7 @@ ExecSetParamPlan(SubPlanState *node, ExprContext *econtext) prm->execPlan = NULL; prm->value = node->curArray; prm->isnull = false; + prm->detoast_slot = NULL; } else if (!found) { @@ -1257,6 +1264,7 @@ ExecSetParamPlan(SubPlanState *node, ExprContext *econtext) prm->execPlan = NULL; prm->value = BoolGetDatum(false); prm->isnull = false; + prm->detoast_slot = NULL; } else { @@ -1269,6 +1277,7 @@ ExecSetParamPlan(SubPlanState *node, ExprContext *econtext) prm->execPlan = NULL; prm->value = (Datum) 0; prm->isnull = true; + prm->detoast_slot = NULL; } } } diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c index d172e7a42c3..92030a1fbbd 100644 --- a/src/backend/jit/llvm/llvmjit_expr.c +++ b/src/backend/jit/llvm/llvmjit_expr.c @@ -1224,6 +1224,12 @@ llvm_compile_expr(ExprState *state) break; } + case EEOP_PARAM_EXEC_DETOAST: + build_EvalXFunc(b, mod, "ExecEvalParamExecDetoast", + v_state, op, v_econtext); + LLVMBuildBr(b, opblocks[opno + 1]); + break; + case EEOP_PARAM_EXEC: build_EvalXFunc(b, mod, "ExecEvalParamExec", v_state, op, v_econtext); diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c index a825ff8e9a6..5f5c5749f84 100644 --- a/src/backend/jit/llvm/llvmjit_types.c +++ b/src/backend/jit/llvm/llvmjit_types.c @@ -171,6 +171,7 @@ void *referenced_functions[] = ExecEvalSysVar, ExecEvalVarDetoast, ExecEvalAssignVarDetoast, + ExecEvalParamExecDetoast, ExecEvalWholeRowVar, ExecEvalXmlExpr, ExecEvalJsonConstructor, diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index 98d7b30744e..fa988f59932 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -186,6 +186,7 @@ typedef enum ExprEvalOp /* evaluate PARAM_EXEC/EXTERN parameters */ EEOP_PARAM_EXEC, + EEOP_PARAM_EXEC_DETOAST, /* same, preferring the detoasted copy */ EEOP_PARAM_EXTERN, EEOP_PARAM_CALLBACK, /* set PARAM_EXEC value */ @@ -439,6 +440,9 @@ typedef struct ExprEvalStep { int paramid; /* numeric ID for parameter */ Oid paramtype; /* OID of parameter's datatype */ + /* EEOP_PARAM_SET: the Var the value comes from, if a plain one */ + int srcattnum; /* attribute number, or 0 */ + Index srcvarno; /* INNER_VAR, OUTER_VAR or a scan varno */ } param; /* for EEOP_PARAM_CALLBACK */ @@ -926,6 +930,8 @@ extern void ExecEvalVarDetoast(ExprState *state, ExprEvalStep *op, extern void ExecEvalAssignVarDetoast(ExprState *state, ExprEvalStep *op, ExprContext *econtext, TupleTableSlot *slot); +extern void ExecEvalParamExecDetoast(ExprState *state, ExprEvalStep *op, + ExprContext *econtext); extern void ExecAggInitGroup(AggState *aggstate, AggStatePerTrans pertrans, AggStatePerGroup pergroup, ExprContext *aggcontext); diff --git a/src/include/nodes/params.h b/src/include/nodes/params.h index 2a2f00c76de..106ae1eb041 100644 --- a/src/include/nodes/params.h +++ b/src/include/nodes/params.h @@ -147,6 +147,15 @@ typedef struct ParamExecData void *execPlan; /* should be "SubPlanState *" */ Datum value; bool isnull; + + /* + * Where a detoasted copy of value may live: the slot and attribute number + * of the Var the value was taken from, or NULL. The copy is read (and + * made) only by EEOP_PARAM_EXEC_DETOAST, see ExecEvalParamExecDetoast; + * value itself always stays the stored datum. + */ + struct TupleTableSlot *detoast_slot; + int detoast_attnum; } ParamExecData; /* type of argument for ParamsErrorCallback */ -- 2.55.0