From 992a5e21f7039825b12a6e800efb0265061bbe3a Mon Sep 17 00:00:00 2001
From: Amit Langote <amitlan@postgresql.org>
Date: Tue, 2 Sep 2025 23:46:34 +0900
Subject: [PATCH v1 6/8] WIP: Add EEOP_AGG_PLAIN_TRANS_BATCH_ROWLOOP

Introduce a batch EEOP that runs plain aggregate transitions by
looping over rows of a TupleBatch. This keeps transition logic in
the interpreter while amortizing per-row costs.

Gate with AggTransCanUseBatch(): plain, non-hashed, single-set
aggregates with no DISTINCT/ORDER/FILTER, and simple Var args.

Extend ExecBuildAggTrans() to prepare batch fetch/build steps and
to return whether a batch path is used.
---
 src/backend/executor/execExpr.c       | 228 ++++++++++++++++++++++++--
 src/backend/executor/execExprInterp.c | 103 ++++++++++++
 src/backend/executor/nodeAgg.c        |  17 +-
 src/backend/jit/llvm/llvmjit_expr.c   |   6 +
 src/backend/jit/llvm/llvmjit_types.c  |   1 +
 src/include/executor/execBatch.h      |   6 +
 src/include/executor/execExpr.h       |  14 ++
 src/include/executor/executor.h       |   3 +-
 src/include/executor/nodeAgg.h        |   2 +
 9 files changed, 363 insertions(+), 17 deletions(-)

diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index f1569879b52..af5ed8b6368 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -95,7 +95,9 @@ static void ExecBuildAggTransCall(ExprState *state, AggState *aggstate,
 								  ExprEvalStep *scratch,
 								  FunctionCallInfo fcinfo, AggStatePerTrans pertrans,
 								  int transno, int setno, int setoff, bool ishash,
-								  bool nullcheck);
+								  bool nullcheck, bool batch,
+								  BatchVector *bv);
+
 static void ExecInitJsonExpr(JsonExpr *jsexpr, ExprState *state,
 							 Datum *resv, bool *resnull,
 							 ExprEvalStep *scratch);
@@ -104,6 +106,10 @@ static void ExecInitJsonCoercion(ExprState *state, JsonReturning *returning,
 								 bool exists_coerce,
 								 Datum *resv, bool *resnull);
 
+static BatchVector *BatchVectorCreate(Bitmapset *attnos, AttrNumber last_var);
+static bool ExprListAllSimpleVars(const List *args, Bitmapset **allattnos);
+static BatchVectorSlice *BatchVectorSliceFromExprArgs(const List *args,
+													  const BatchVector *bv);
 
 /*
  * ExecInitExpr: prepare an expression tree for execution
@@ -3659,6 +3665,33 @@ ExecInitCoerceToDomain(ExprEvalStep *scratch, CoerceToDomain *ctest,
 	}
 }
 
+/* plain agg, single set, not hashed, no DISTINCT/ORDER/FILTER */
+static inline bool
+AggTransCanUseBatch(AggState *as, AggStatePerTrans pt)
+{
+	Agg *aggnode = (Agg *) as->ss.ps.plan;
+
+	if (!AggCanUsePlainBatch(as))
+		return false;
+	if (as->aggstrategy == AGG_HASHED)
+		return false;
+	if (aggnode->groupingSets != NIL)
+		return false;
+	if (as->phase == NULL || as->phase->numsets > 0)
+		return false;
+
+	/* per-aggregate complications */
+	if (pt->aggsortrequired)
+		return false;
+	if (pt->aggref &&
+		(pt->aggref->aggdistinct != NIL ||
+		 pt->aggref->aggorder != NIL ||
+		 pt->aggref->aggfilter != NULL))
+		return false;
+
+	return true;
+}
+
 /*
  * Build transition/combine function invocations for all aggregate transition
  * / combination function invocations in a grouping sets phase. This has to
@@ -3675,13 +3708,17 @@ ExecInitCoerceToDomain(ExprEvalStep *scratch, CoerceToDomain *ctest,
  */
 ExprState *
 ExecBuildAggTrans(AggState *aggstate, AggStatePerPhase phase,
-				  bool doSort, bool doHash, bool nullcheck)
+				  bool doSort, bool doHash, bool nullcheck,
+				  bool *batch_trans)
 {
 	ExprState  *state = makeNode(ExprState);
 	PlanState  *parent = &aggstate->ss.ps;
 	ExprEvalStep scratch = {0};
 	bool		isCombine = DO_AGGSPLIT_COMBINE(aggstate->aggsplit);
 	ExprSetupInfo deform = {0, 0, 0, 0, 0, NIL};
+	bool		batch = AggCanUsePlainBatch(aggstate);
+	Bitmapset  *allattnos = NULL;
+	BatchVector *bv = NULL;
 
 	state->expr = (Expr *) aggstate;
 	state->parent = parent;
@@ -3707,8 +3744,36 @@ ExecBuildAggTrans(AggState *aggstate, AggStatePerPhase phase,
 						  &deform);
 		expr_setup_walker((Node *) pertrans->aggref->aggfilter,
 						  &deform);
+
+		if (!AggTransCanUseBatch(aggstate, pertrans) ||
+			!ExprListAllSimpleVars(pertrans->aggref->args, &allattnos))
+			batch = false;
 	}
-	ExecPushExprSetupSteps(state, &deform);
+
+	if (batch)
+	{
+		if (deform.last_outer > 0)
+		{
+			Assert(!bms_is_empty(allattnos));
+			bv  = BatchVectorCreate(allattnos, deform.last_outer);
+
+			/*
+			 * Deform all tuples upto last_outer in batch
+			 */
+			scratch.opcode = EEOP_OUTER_FETCHSOME_BATCH;
+			scratch.d.fetch_batch.last_var = deform.last_outer;
+			ExprEvalPushStep(state, &scratch);
+
+			/*
+			 * Put all arg Vars into vectors once per batch slice
+			 */
+			scratch.opcode = EEOP_BUILD_OUTER_BATCH_VECTOR;
+			scratch.d.batch_vector.bv = bv;
+			ExprEvalPushStep(state, &scratch);
+		}
+	}
+	else
+		ExecPushExprSetupSteps(state, &deform);
 
 	/*
 	 * Emit instructions for each transition value / grouping set combination.
@@ -3746,7 +3811,7 @@ ExecBuildAggTrans(AggState *aggstate, AggStatePerPhase phase,
 		 * Evaluate arguments to aggregate/combine function.
 		 */
 		argno = 0;
-		if (isCombine)
+		if (isCombine && !batch)
 		{
 			/*
 			 * Combining two aggregate transition values. Instead of directly
@@ -3816,7 +3881,7 @@ ExecBuildAggTrans(AggState *aggstate, AggStatePerPhase phase,
 
 			Assert(pertrans->numInputs == argno);
 		}
-		else if (!pertrans->aggsortrequired)
+		else if (!pertrans->aggsortrequired && !batch)
 		{
 			ListCell   *arg;
 
@@ -3849,7 +3914,7 @@ ExecBuildAggTrans(AggState *aggstate, AggStatePerPhase phase,
 			}
 			Assert(pertrans->numTransInputs == argno);
 		}
-		else if (pertrans->numInputs == 1)
+		else if (pertrans->numInputs == 1 && !batch)
 		{
 			/*
 			 * Non-presorted DISTINCT and/or ORDER BY case, with a single
@@ -3868,7 +3933,7 @@ ExecBuildAggTrans(AggState *aggstate, AggStatePerPhase phase,
 
 			Assert(pertrans->numInputs == argno);
 		}
-		else
+		else if (!batch)
 		{
 			/*
 			 * Non-presorted DISTINCT and/or ORDER BY case, with multiple
@@ -3896,7 +3961,7 @@ ExecBuildAggTrans(AggState *aggstate, AggStatePerPhase phase,
 		 * just keep the prior transValue. This is true for both plain and
 		 * sorted/distinct aggregates.
 		 */
-		if (trans_fcinfo->flinfo->fn_strict && pertrans->numTransInputs > 0)
+		if (trans_fcinfo->flinfo->fn_strict && pertrans->numTransInputs > 0 && !batch)
 		{
 			if (strictnulls)
 				scratch.opcode = EEOP_AGG_STRICT_INPUT_CHECK_NULLS;
@@ -3914,7 +3979,7 @@ ExecBuildAggTrans(AggState *aggstate, AggStatePerPhase phase,
 		}
 
 		/* Handle DISTINCT aggregates which have pre-sorted input */
-		if (pertrans->numDistinctCols > 0 && !pertrans->aggsortrequired)
+		if (pertrans->numDistinctCols > 0 && !pertrans->aggsortrequired && !batch)
 		{
 			if (pertrans->numDistinctCols > 1)
 				scratch.opcode = EEOP_AGG_PRESORTED_DISTINCT_MULTI;
@@ -3942,7 +4007,7 @@ ExecBuildAggTrans(AggState *aggstate, AggStatePerPhase phase,
 			{
 				ExecBuildAggTransCall(state, aggstate, &scratch, trans_fcinfo,
 									  pertrans, transno, setno, setoff, false,
-									  nullcheck);
+									  nullcheck, batch, bv);
 				setoff++;
 			}
 		}
@@ -3962,7 +4027,7 @@ ExecBuildAggTrans(AggState *aggstate, AggStatePerPhase phase,
 			{
 				ExecBuildAggTransCall(state, aggstate, &scratch, trans_fcinfo,
 									  pertrans, transno, setno, setoff, true,
-									  nullcheck);
+									  nullcheck, false, NULL);
 				setoff++;
 			}
 		}
@@ -4007,6 +4072,9 @@ ExecBuildAggTrans(AggState *aggstate, AggStatePerPhase phase,
 
 	ExecReadyExpr(state);
 
+	if (batch_trans)
+		*batch_trans = batch;
+
 	return state;
 }
 
@@ -4020,10 +4088,11 @@ ExecBuildAggTransCall(ExprState *state, AggState *aggstate,
 					  ExprEvalStep *scratch,
 					  FunctionCallInfo fcinfo, AggStatePerTrans pertrans,
 					  int transno, int setno, int setoff, bool ishash,
-					  bool nullcheck)
+					  bool nullcheck, bool batch, BatchVector *bv)
 {
 	ExprContext *aggcontext;
 	int			adjust_jumpnull = -1;
+	BatchVectorSlice *bvs = NULL;
 
 	if (ishash)
 		aggcontext = aggstate->hashcontext;
@@ -4077,7 +4146,13 @@ ExecBuildAggTransCall(ExprState *state, AggState *aggstate,
 	 */
 	if (!pertrans->aggsortrequired)
 	{
-		if (pertrans->transtypeByVal)
+		if (batch)
+		{
+			if (bv)
+				bvs = BatchVectorSliceFromExprArgs(pertrans->aggref->args, bv);
+			scratch->opcode = EEOP_AGG_PLAIN_TRANS_BATCH_ROWLOOP;
+		}
+		else if (pertrans->transtypeByVal)
 		{
 			if (fcinfo->flinfo->fn_strict &&
 				pertrans->initValueIsNull)
@@ -4108,6 +4183,7 @@ ExecBuildAggTransCall(ExprState *state, AggState *aggstate,
 	scratch->d.agg_trans.setoff = setoff;
 	scratch->d.agg_trans.transno = transno;
 	scratch->d.agg_trans.aggcontext = aggcontext;
+	scratch->d.agg_trans.bvs = bvs;
 	ExprEvalPushStep(state, scratch);
 
 	/* fix up jumpnull */
@@ -5070,3 +5146,129 @@ ExecInitJsonCoercion(ExprState *state, JsonReturning *returning,
 		DomainHasConstraints(returning->typid);
 	ExprEvalPushStep(state, &scratch);
 }
+
+/* Is expr a Var node for a non-system attribute? */
+static bool
+expr_is_simple_var(Expr *expr, AttrNumber *out_attno)
+{
+	if (expr == NULL)
+		return false;
+
+	if (IsA(expr, TargetEntry))
+		return expr_is_simple_var((Expr *) ((TargetEntry *) expr)->expr,
+								  out_attno);
+	if (IsA(expr, RelabelType))
+		return expr_is_simple_var((Expr *) ((RelabelType *) expr)->arg,
+								  out_attno);
+
+	if (IsA(expr, Var) && ((Var *) expr)->varattno > 0)
+	{
+		*out_attno = ((Var *) expr)->varattno;
+		return true;
+	}
+
+	return false;
+}
+
+/* Are all inputs plain Vars (optionally allow RelabelType->Var)? Collect attnos. */
+static bool
+ExprListAllSimpleVars(const List *args, Bitmapset **allattnos)
+{
+	ListCell *lc;
+
+	foreach(lc, args)
+	{
+		TargetEntry *tle = lfirst_node(TargetEntry, lc);
+		Expr *arg = tle->expr;
+		AttrNumber attno;
+
+		if (!expr_is_simple_var(arg, &attno))
+			return false;
+
+		if (!IsA(arg, Var))
+			return false;
+
+		Assert(attno > 0);
+		*allattnos = bms_add_member(*allattnos, attno);
+	}
+
+	return true;
+}
+
+/* ---------- BatchVector stuff ------------- */
+
+static BatchVector *
+BatchVectorCreate(Bitmapset *attnos, AttrNumber last_var)
+{
+	int maxrows = EXEC_BATCH_ROWS;
+	BatchVector *bv;
+	AttrNumber	attno;
+	int			i;
+
+	bv = palloc(sizeof(BatchVector));
+	bv->ncols = bms_num_members(attnos);
+	bv->maxrows = maxrows;
+	bv->last_var = last_var;
+	bv->attnos = palloc(sizeof(AttrNumber) * bv->ncols);
+	attno = -1;
+	i = 0;
+	while ((attno = bms_next_member(attnos, attno)) > 0)
+		bv->attnos[i++] = attno;
+	bv->cols = palloc(sizeof(Datum *) * bv->ncols);
+	bv->nulls = palloc(sizeof(bool  *) * bv->ncols);
+
+	for (i =0; i < bv->ncols; i++)
+	{
+		bv->cols[i]  = palloc(sizeof(Datum) * maxrows);
+		bv->nulls[i] = palloc(sizeof(bool)  * maxrows);
+	}
+
+	bv->nrows = 0;
+	bv->hasnull = false;
+
+	return bv;
+}
+
+static int16
+BatchVectorFindAttColno(const BatchVector *bv, AttrNumber attno)
+{
+	for (int i = 0; i < bv->ncols; i++)
+		if (bv->attnos[i] == attno)
+			return i;
+
+	return -1;
+}
+
+/*
+ * BatchVectorSliceFromExprArgs
+ *		Build a BatchVectorSlice for a List of args.
+ *
+ * For Var args (possibly under RelabelType), store the col index.
+ * For non-Var args, store -1. Caller can handle Consts, etc.
+ */
+static BatchVectorSlice *
+BatchVectorSliceFromExprArgs(const List *args, const BatchVector *bv)
+{
+	BatchVectorSlice *bvs = palloc(sizeof(BatchVectorSlice));
+	int nargs = list_length(args);
+	int i = 0;
+	ListCell *lc;
+
+	Assert(bv);
+	bvs->bv = bv;
+	bvs->nargs = nargs;
+	bvs->argoffs = (int16 *) palloc(sizeof(int16) * nargs);
+
+	foreach (lc, args)
+	{
+		Expr *arg = (Expr *) lfirst(lc);
+		AttrNumber attno;
+
+		if (expr_is_simple_var(arg, &attno))
+			bvs->argoffs[i++] = BatchVectorFindAttColno(bv, attno);
+		else
+			bvs->argoffs[i++] = -1; /* non-Var */
+	}
+
+	return bvs;
+}
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 68629ad7991..3176679b346 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -606,6 +606,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 		&&CASE_EEOP_BUILD_INNER_BATCH_VECTOR,
 		&&CASE_EEOP_BUILD_OUTER_BATCH_VECTOR,
 		&&CASE_EEOP_BUILD_SCAN_BATCH_VECTOR,
+		&&CASE_EEOP_AGG_PLAIN_TRANS_BATCH_ROWLOOP,
 		&&CASE_EEOP_LAST
 	};
 
@@ -2336,6 +2337,14 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 			EEO_NEXT();
 		}
 
+		EEO_CASE(EEOP_AGG_PLAIN_TRANS_BATCH_ROWLOOP)
+		{
+			/* too complex for an inline implementation */
+			ExecAggPlainTransBatch(state, op, econtext);
+
+			EEO_NEXT();
+		}
+
 		EEO_CASE(EEOP_LAST)
 		{
 			/* unreachable */
@@ -6039,3 +6048,97 @@ ExecBuildBatchVector(ExprState *state, ExprEvalStep *op, ExprContext *econtext,
 	}
 	bv->nrows = i;
 }
+
+void
+ExecAggPlainTransBatch(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
+{
+	AggState   *aggstate = castNode(AggState, state->parent);
+	AggStatePerTrans	pertrans = op->d.agg_trans.pertrans;
+	AggStatePerGroup pergroup =
+		&aggstate->all_pergroups[op->d.agg_trans.setoff][op->d.agg_trans.transno];
+	BatchVectorSlice  *bvs = op->d.agg_trans.bvs;
+	FunctionCallInfo	fcinfo = pertrans->transfn_fcinfo;
+	FmgrInfo		   *finfo = fcinfo->flinfo;
+	Datum		newVal;
+	TupleBatch *batch = econtext->outer_batch;
+	int			batch_nrows = bvs ? bvs->bv->nrows : batch->nvalid;
+	int			start_row = 0;
+
+	if (finfo->fn_strict)
+	{
+		if (pergroup->noTransValue && bvs)
+		{
+			const BatchVector *bv = bvs->bv;
+			bool	found = false;
+
+			Assert(bv);
+			for (int i = 0; i < batch_nrows; i++)
+			{
+				for (int j = 0; j < bvs->nargs; j++)
+				{
+					if (!bv->nulls[bvs->argoffs[j]][i])
+					{
+						fcinfo->args[1].value = bv->cols[bvs->argoffs[j]][i];
+						fcinfo->args[1].isnull = false;
+						if (j == bvs->nargs - 1)
+						{
+							found = true;
+							break;
+						}
+					}
+				}
+				if (found)
+					break;
+			}
+			/* If transValue has not yet been initialized, do so now. */
+			ExecAggInitGroup(aggstate, pertrans, pergroup,
+							 op->d.agg_trans.aggcontext);
+			start_row = 1;
+		}
+		else if (pergroup->transValueIsNull)
+			return;
+	}
+
+	switch (ExecEvalStepOp(state, op))
+	{
+		case EEOP_AGG_PLAIN_TRANS_BATCH_ROWLOOP:
+			/* Loop rows, call the original transfn per element using vector cols. */
+			for (int i = start_row; i < batch_nrows; i++)
+			{
+				bool hasnull = false;
+
+				/* Set up fcinfo args 1..m from column vectors at row i. */
+				if (bvs)
+				{
+					const BatchVector *bv = bvs->bv;
+
+					for (int j = 0; j < bvs->nargs; j++)
+					{
+						int16	argoff = bvs->argoffs[j];
+
+						fcinfo->args[j+1].value = bv->cols[argoff][i];
+						fcinfo->args[j+1].isnull = bv->nulls[argoff][i];
+						if (!hasnull && bv->nulls[argoff][i])
+							hasnull = true;
+					}
+				}
+				/* fcinfo->args[0] is the existing transition state */
+				if (finfo->fn_strict && hasnull)
+					continue;
+				fcinfo->args[0].value = pergroup->transValue;
+				fcinfo->args[0].isnull = pergroup->transValueIsNull;
+				newVal = FunctionCallInvoke(fcinfo);
+				if (!pertrans->transtypeByVal &&
+					DatumGetPointer(newVal) != DatumGetPointer(pergroup->transValue))
+					newVal = ExecAggCopyTransValue(aggstate, pertrans,
+												   newVal, fcinfo->isnull,
+												   pergroup->transValue,
+												   pergroup->transValueIsNull);
+				pergroup->transValue = newVal;
+				pergroup->transValueIsNull = fcinfo->isnull;
+			}
+			break;
+		default:
+			elog(ERROR, "invalid ExprEvalOp in ExecAggPlainTransBatch()");
+	}
+}
diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index 3ace6363509..662d8bef43b 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -825,6 +825,16 @@ advance_aggregates_batch(AggState *aggstate, TupleBatch *b)
 {
 	ExprContext *tmpcontext = aggstate->tmpcontext;
 	ExprState *evaltrans = aggstate->phase->evaltrans;
+	bool		batch_trans = aggstate->phase->batch_trans;
+
+	if (batch_trans)
+	{
+		tmpcontext->ecxt_outertuple = TupleBatchGetSlot(b, 0);
+		tmpcontext->outer_batch = b;
+		ExecEvalExprNoReturnSwitchContext(evaltrans, tmpcontext);
+		TupleBatchConsumeAll(b);
+		return;
+	}
 
 	while (TupleBatchHasMore(b))
 	{
@@ -1800,7 +1810,8 @@ hashagg_recompile_expressions(AggState *aggstate, bool minslot, bool nullcheck)
 
 		phase->evaltrans_cache[i][j] = ExecBuildAggTrans(aggstate, phase,
 														 dosort, dohash,
-														 nullcheck);
+														 nullcheck,
+														 NULL);
 
 		/* change back */
 		aggstate->ss.ps.outerops = outerops;
@@ -3367,7 +3378,7 @@ hashagg_reset_spill_state(AggState *aggstate)
 	}
 }
 
-static bool
+bool
 AggCanUsePlainBatch(AggState *aggstate)
 {
 	const Agg *aggnode = (const Agg *) aggstate->ss.ps.plan;
@@ -4233,7 +4244,7 @@ ExecInitAgg(Agg *node, EState *estate, int eflags)
 			Assert(false);
 
 		phase->evaltrans = ExecBuildAggTrans(aggstate, phase, dosort, dohash,
-											 false);
+											 false, &phase->batch_trans);
 
 		/* cache compiled expression for outer slot without NULL check */
 		phase->evaltrans_cache[0][0] = phase->evaltrans;
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
index 848f0b52d6f..efb3ee639fc 100644
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -3026,6 +3026,12 @@ llvm_compile_expr(ExprState *state)
 				LLVMBuildBr(b, opblocks[opno + 1]);
 				break;
 
+			case EEOP_AGG_PLAIN_TRANS_BATCH_ROWLOOP:
+				build_EvalXFunc(b, mod, "ExecAggPlainTransBatch",
+								v_state, op, v_econtext);
+				LLVMBuildBr(b, opblocks[opno + 1]);
+				break;
+
 			case EEOP_LAST:
 				Assert(false);
 				break;
diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c
index 6bb527c3f6f..1b5e06f60cc 100644
--- a/src/backend/jit/llvm/llvmjit_types.c
+++ b/src/backend/jit/llvm/llvmjit_types.c
@@ -186,4 +186,5 @@ void	   *referenced_functions[] =
 	ExecBuildInnerBatchVector,
 	ExecBuildOuterBatchVector,
 	ExecBuildScanBatchVector,
+	ExecAggPlainTransBatch,
 };
diff --git a/src/include/executor/execBatch.h b/src/include/executor/execBatch.h
index 6f1a38d14bd..b50961fc0c9 100644
--- a/src/include/executor/execBatch.h
+++ b/src/include/executor/execBatch.h
@@ -99,4 +99,10 @@ TupleBatchMaterializeAll(TupleBatch *b)
 	TupleBatchUseInput(b, b->ntuples);
 }
 
+static inline void
+TupleBatchConsumeAll(TupleBatch *b)
+{
+	b->next = b->nvalid;
+}
+
 #endif	/* EXECBATCH_H */
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index 99c86bac702..1d33e084b69 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -302,6 +302,9 @@ typedef enum ExprEvalOp
 	EEOP_BUILD_OUTER_BATCH_VECTOR,
 	EEOP_BUILD_SCAN_BATCH_VECTOR,
 
+	/* Batched aggregate trans evaluation */
+	EEOP_AGG_PLAIN_TRANS_BATCH_ROWLOOP,	/* per-row fmgr calls */
+
 	/* non-existent operation, used e.g. to check array lengths */
 	EEOP_LAST
 } ExprEvalOp;
@@ -750,6 +753,7 @@ typedef struct ExprEvalStep
 
 		/* for EEOP_AGG_PLAIN_TRANS_[INIT_][STRICT_]{BYVAL,BYREF} */
 		/* for EEOP_AGG_ORDERED_TRANS_{DATUM,TUPLE} */
+		/* for EEOP_AGG_PLAIN_TRANS_{BATCH,BATCH_ROWLOOP}*/
 		struct
 		{
 			AggStatePerTrans pertrans;
@@ -757,6 +761,7 @@ typedef struct ExprEvalStep
 			int			setno;
 			int			transno;
 			int			setoff;
+			struct BatchVectorSlice *bvs;
 		}			agg_trans;
 
 		/* for EEOP_IS_JSON */
@@ -956,8 +961,17 @@ typedef struct BatchVector
 	int		nrows;			/* #rows loaded into cols/nulls */
 } BatchVector;
 
+/* A slice of BatchVector that maps caller args to BatchVector columns. */
+typedef struct BatchVectorSlice
+{
+	const BatchVector *bv;
+	int			nargs;		/* number of args covered */
+	int16	   *argoffs;	/* length nargs, -1 for non-Var entries */
+} BatchVectorSlice;
+
 extern void ExecBuildInnerBatchVector(ExprState *state, ExprEvalStep *op, ExprContext *econtext);
 extern void ExecBuildOuterBatchVector(ExprState *state, ExprEvalStep *op, ExprContext *econtext);
 extern void ExecBuildScanBatchVector(ExprState *state, ExprEvalStep *op, ExprContext *econtext);
 
+extern void ExecAggPlainTransBatch(ExprState *state, ExprEvalStep *op, ExprContext *econtext);
 #endif							/* EXEC_EXPR_H */
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index cf5b0c7e05c..5ba9a523970 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -336,7 +336,8 @@ extern ExprState *ExecInitQual(List *qual, PlanState *parent);
 extern ExprState *ExecInitCheck(List *qual, PlanState *parent);
 extern List *ExecInitExprList(List *nodes, PlanState *parent);
 extern ExprState *ExecBuildAggTrans(AggState *aggstate, struct AggStatePerPhaseData *phase,
-									bool doSort, bool doHash, bool nullcheck);
+									bool doSort, bool doHash, bool nullcheck,
+									bool *batch_trans);
 extern ExprState *ExecBuildHash32FromAttrs(TupleDesc desc,
 										   const TupleTableSlotOps *ops,
 										   FmgrInfo *hashfunctions,
diff --git a/src/include/executor/nodeAgg.h b/src/include/executor/nodeAgg.h
index 6c4891bbaeb..5c5ebfc73f2 100644
--- a/src/include/executor/nodeAgg.h
+++ b/src/include/executor/nodeAgg.h
@@ -289,6 +289,7 @@ typedef struct AggStatePerPhaseData
 	Sort	   *sortnode;		/* Sort node for input ordering for phase */
 
 	ExprState  *evaltrans;		/* evaluation of transition functions  */
+	bool		batch_trans;	/* true if evaltrans contains batch EEOPs */
 
 	/*----------
 	 * Cached variants of the compiled expression.
@@ -338,4 +339,5 @@ extern void ExecAggInitializeDSM(AggState *node, ParallelContext *pcxt);
 extern void ExecAggInitializeWorker(AggState *node, ParallelWorkerContext *pwcxt);
 extern void ExecAggRetrieveInstrumentation(AggState *node);
 
+extern bool AggCanUsePlainBatch(AggState *aggstate);
 #endif							/* NODEAGG_H */
-- 
2.43.0

