From 1738020969778b3f10edc4c4b605c507b70e2416 Mon Sep 17 00:00:00 2001
Message-ID: <1738020969778b3f10edc4c4b605c507b70e2416.1785332874.git.darthunix@gmail.com>
In-Reply-To: <59baf8c1261da4518020e296480b4bc1dd71c31e.1785332874.git.darthunix@gmail.com>
References: <59baf8c1261da4518020e296480b4bc1dd71c31e.1785332874.git.darthunix@gmail.com>
From: Denis Smirnov <darthunix@gmail.com>
Date: Mon, 27 Jul 2026 21:56:05 +0700
Subject: [PATCH v4 3/3] Deform heap tuple batches in physical order

Fetching batch attributes one at a time makes tuple deformation depend
on the order of scan qualifications. Once offsets stop being cacheable,
accessing an earlier attribute can repeat physical tuple walks, while
reordering qualifications would change the planner's evaluation order.

Replace the per-attribute batch callback with getsomeattrs(), which
accepts a strictly increasing list of required attributes. Collect
distinct attributes during batch initialization and store their values
and NULL flags in compact attribute-major arrays sized by the number of
attributes rather than the highest attribute number.

Extend the common heap tuple deformation code with a selective output
path. For each batch row it walks the physical tuple once through the
highest requested attribute, jumps directly across fixed-offset regions,
and advances through variable-width gaps without storing unused values.
Keep the ordinary dense slot path separate so its existing loop remains
straightforward.

Preserve the qualification order chosen by the planner. Fixed offsets,
NULLs, missing attributes, and variable-width values continue to follow
the ordinary heap deformation rules.

This eagerly deforms through the last needed attribute even if an early
qualification rejects the batch. In exchange, each tuple is walked once,
memory usage depends on the number of requested attributes, and
performance no longer depends on qualification attribute order.
---
 src/backend/executor/Makefile            |   2 +
 src/backend/executor/execScan.c          |  48 ++-
 src/backend/executor/execScanBatch.h     |   5 +
 src/backend/executor/execTuples.c        | 417 ++++++++++++++++++-----
 src/include/executor/tuptable.h          |  21 +-
 src/test/regress/expected/scan_batch.out |  58 +++-
 src/test/regress/sql/scan_batch.sql      |  43 ++-
 7 files changed, 476 insertions(+), 118 deletions(-)

diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile
index 11118d0ce02..beefc89ff31 100644
--- a/src/backend/executor/Makefile
+++ b/src/backend/executor/Makefile
@@ -79,4 +79,6 @@ OBJS = \
 	tqueue.o \
 	tstoreReceiver.o
 
+execScan.o nodeSeqscan.o: execScanBatch.h
+
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/executor/execScan.c b/src/backend/executor/execScan.c
index 4dc6f52996b..e5a3c280851 100644
--- a/src/backend/executor/execScan.c
+++ b/src/backend/executor/execScan.c
@@ -25,6 +25,7 @@
 #include "executor/executor.h"
 #include "executor/execScan.h"
 #include "miscadmin.h"
+#include "nodes/bitmapset.h"
 #include "pgstat.h"
 #include "utils/memutils.h"
 #include "utils/syscache.h"
@@ -149,6 +150,7 @@ ExecInitScanBatch(ScanState *node, List *clauses)
 {
 	ExecScanBatchState *state;
 	ExprState  *qual;
+	Bitmapset  *attrs = NULL;
 	int			nquals = 0;
 	int			i = 0;
 
@@ -200,8 +202,34 @@ ExecInitScanBatch(ScanState *node, List *clauses)
 			break;
 		op = match_batch_qual(clause, &var_argno);
 		Assert(op != NULL);
-		init_batch_qual(&state->quals[i++], op, var_argno, &node->ps);
+		init_batch_qual(&state->quals[i], op, var_argno, &node->ps);
+		attrs = bms_add_member(attrs, state->quals[i].attnum - 1);
+		i++;
 	}
+	state->natts = bms_num_members(attrs);
+	state->attnums = palloc_array(AttrNumber, state->natts);
+	{
+		int			attnum = -1;
+
+		i = 0;
+		while ((attnum = bms_next_member(attrs, attnum)) >= 0)
+			state->attnums[i++] = attnum + 1;
+		Assert(i == state->natts);
+	}
+	for (i = 0; i < nquals; i++)
+	{
+		state->quals[i].attr_index =
+			bms_member_index(attrs, state->quals[i].attnum - 1);
+		Assert(state->quals[i].attr_index >= 0);
+	}
+	bms_free(attrs);
+
+	state->values =
+		palloc_array(Datum,
+					 (Size) state->natts * TUPLE_BATCH_MASK_BITS);
+	state->isnull =
+		palloc_array(bool,
+					 (Size) state->natts * TUPLE_BATCH_MASK_BITS);
 
 	/* Use the same scalar states when the complete qual must be evaluated. */
 	qual = makeNode(ExprState);
@@ -252,7 +280,6 @@ ExecScanBatchPrepare(ExecScanBatchState *state, TupleTableSlot *slot,
 {
 	const TupleTableSlotBatch *batch = slot_getbatch(slot);
 	MemoryContext oldcontext;
-	Datum		values[TUPLE_BATCH_MASK_BITS];
 	TupleBatchMask mask;
 	int			first;
 	int			nrows;
@@ -277,6 +304,8 @@ ExecScanBatchPrepare(ExecScanBatchState *state, TupleTableSlot *slot,
 	if (nrows == 0)
 		return;
 	mask = PG_UINT64_MAX >> (TUPLE_BATCH_MASK_BITS - nrows);
+	batch->getsomeattrs(slot, state->attnums, state->natts, first, nrows,
+						state->values, state->isnull);
 
 	oldcontext = MemoryContextSwitchTo(econtext->ecxt_per_tuple_memory);
 	for (int q = 0; q < state->nquals && mask != 0; q++)
@@ -299,15 +328,6 @@ ExecScanBatchPrepare(ExecScanBatchState *state, TupleTableSlot *slot,
 			bool		track_function;
 			int			position;
 
-			/*
-			 * Fetch the first qual's attribute, and fetch again only when it
-			 * changes. Adjacent quals on the same attribute reuse its
-			 * values.
-			 */
-			if (q == 0 || qual->attnum != state->quals[q - 1].attnum)
-				mask &= ~batch->getattrs(slot, qual->attnum, first, nrows,
-										 mask, values);
-
 			track_function =
 				pgstat_track_functions > fcinfo->flinfo->fn_stats;
 			position = ScanDirectionIsForward(direction) ? 0 : nrows - 1;
@@ -317,10 +337,14 @@ ExecScanBatchPrepare(ExecScanBatchState *state, TupleTableSlot *slot,
 				 i++, position += direction)
 			{
 				TupleBatchMask bit = UINT64CONST(1) << position;
+				int			value_index =
+					qual->attr_index * TUPLE_BATCH_MASK_BITS + position;
 
 				if ((mask & bit) == 0)
 					continue;
-				if (!eval_batch_qual(qual, values[position], track_function))
+				if (state->isnull[value_index] ||
+					!eval_batch_qual(qual, state->values[value_index],
+									 track_function))
 					mask &= ~bit;
 			}
 			MemoryContextSwitchTo(oldoperatorcontext);
diff --git a/src/backend/executor/execScanBatch.h b/src/backend/executor/execScanBatch.h
index 8f85b6909aa..743b275a05b 100644
--- a/src/backend/executor/execScanBatch.h
+++ b/src/backend/executor/execScanBatch.h
@@ -23,12 +23,16 @@ struct ExecScanBatchQual
 	ExprState  *param_expr;
 	NullableDatum constant;
 	AttrNumber	attnum;
+	int			attr_index;
 	uint8		var_argno;
 };
 
 struct ExecScanBatchState
 {
 	struct ExecScanBatchQual *quals;
+	AttrNumber *attnums;
+	Datum	   *values;
+	bool	   *isnull;
 	ExprState  *prefix_qual;
 	ExprState  *rest_qual;
 	MemoryContext operator_context;
@@ -38,6 +42,7 @@ struct ExecScanBatchState
 	int			first;
 	int			nrows;
 	int			nquals;
+	int			natts;
 };
 
 extern void ExecScanBatchPrepare(ExecScanBatchState *state,
diff --git a/src/backend/executor/execTuples.c b/src/backend/executor/execTuples.c
index e0903c30aa0..c62f816697a 100644
--- a/src/backend/executor/execTuples.c
+++ b/src/backend/executor/execTuples.c
@@ -64,7 +64,6 @@
 #include "catalog/pg_type.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
-#include "port/pg_bitutils.h"
 #include "storage/bufmgr.h"
 #include "utils/builtins.h"
 #include "utils/expandeddatum.h"
@@ -73,6 +72,16 @@
 
 static TupleDesc ExecTypeFromTLInternal(List *targetList,
 										bool skipjunk);
+static pg_always_inline void deform_heap_tuple(TupleDesc tupleDesc,
+											   int first_nonguaranteed,
+											   HeapTuple tuple,
+											   uint32 *offp,
+											   AttrNumber *nvalidp,
+											   Datum *values, bool *isnull,
+											   int stride, int reqnatts,
+											   bool support_cstring,
+											   const AttrNumber *attnums,
+											   int nrequested);
 static pg_always_inline void slot_deform_heap_tuple(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp,
 													int reqnatts, bool support_cstring);
 static inline void tts_buffer_heap_store_tuple(TupleTableSlot *slot,
@@ -708,15 +717,16 @@ tts_minimal_store_tuple(TupleTableSlot *slot, MinimalTuple mtup, bool shouldFree
  * TupleTableSlotOps implementation for BufferHeapTupleTableSlot.
  */
 
-static TupleBatchMask
-tts_buffer_heap_getattrs(TupleTableSlot *slot, AttrNumber attnum,
-						 int first, int nrows, TupleBatchMask rows,
-						 Datum *values)
+static void
+tts_buffer_heap_batch_getsomeattrs(TupleTableSlot *slot,
+								   const AttrNumber *attnums, int natts,
+								   int first, int nrows,
+								   Datum *values, bool *isnull)
 {
 	BufferHeapTupleTableSlot *bslot;
 	HeapPageBatch *batch;
 	TupleDesc	tupledesc = slot->tts_tupleDescriptor;
-	TupleBatchMask nulls = 0;
+	AttrNumber	last_attnum;
 	Page		page;
 
 	Assert(TTS_IS_BUFFERTUPLE(slot));
@@ -727,37 +737,40 @@ tts_buffer_heap_getattrs(TupleTableSlot *slot, AttrNumber attnum,
 	Assert(slot_getbatch(slot) == &batch->batch);
 	Assert(batch->offsets != NULL);
 	Assert(BufferIsValid(bslot->buffer));
-	Assert(attnum > 0 && attnum <= tupledesc->natts);
+	Assert(attnums != NULL);
+	Assert(natts > 0);
 	Assert(first >= 0);
-	Assert(nrows >= 0 && nrows <= TUPLE_BATCH_MASK_BITS);
+	Assert(nrows > 0 && nrows <= TUPLE_BATCH_MASK_BITS);
 	Assert(nrows <= batch->batch.ntuples);
 	Assert(first <= batch->batch.ntuples - nrows);
-	Assert(nrows == TUPLE_BATCH_MASK_BITS || (rows >> nrows) == 0);
 	Assert(values != NULL);
+	Assert(isnull != NULL);
 
-	if (rows == 0)
-		return nulls;
+	last_attnum = attnums[natts - 1];
+	if (unlikely(last_attnum > tupledesc->natts))
+		elog(ERROR, "invalid attribute number %d", last_attnum);
+	Assert(attnums[0] > 0);
+	for (int i = 1; i < natts; i++)
+		Assert(attnums[i - 1] < attnums[i]);
 
 	page = BufferGetPage(bslot->buffer);
-	while (rows != 0)
+	for (int i = 0; i < nrows; i++)
 	{
-		int			i = pg_rightmost_one_pos64(rows);
 		OffsetNumber lineoff = batch->offsets[first + i];
 		ItemId		lpp = PageGetItemId(page, lineoff);
 		HeapTupleData tuple;
-		bool		isnull;
+		AttrNumber	nvalid = 0;
+		uint32		off = 0;
 
 		Assert(ItemIdIsNormal(lpp));
 		tuple.t_data = (HeapTupleHeader) PageGetItem(page, lpp);
 		tuple.t_len = ItemIdGetLength(lpp);
-		values[i] = heap_getattr(&tuple, attnum, tupledesc, &isnull);
-		if (isnull)
-			nulls |= UINT64CONST(1) << i;
-
-		rows &= rows - 1;
+		deform_heap_tuple(tupledesc, slot->tts_first_nonguaranteed,
+						  &tuple, &off, &nvalid,
+						  values + i, isnull + i,
+						  TUPLE_BATCH_MASK_BITS, last_attnum, false,
+						  attnums, natts);
 	}
-
-	return nulls;
 }
 
 static inline void
@@ -774,7 +787,8 @@ tts_buffer_heap_init(TupleTableSlot *slot)
 	Assert(slot->tts_batch == NULL);
 	if (slot->tts_flags & TTS_FLAG_SUPPORTS_BATCH)
 	{
-		bslot->batch.batch.getattrs = tts_buffer_heap_getattrs;
+		bslot->batch.batch.getsomeattrs =
+			tts_buffer_heap_batch_getsomeattrs;
 		slot->tts_batch = &bslot->batch.batch;
 	}
 }
@@ -1060,49 +1074,159 @@ tts_buffer_heap_store_tuple(TupleTableSlot *slot, HeapTuple tuple,
 	}
 }
 
+static pg_always_inline void
+getmissingattrs(TupleDesc tupleDesc, int startAttNum, int lastAttNum,
+				Datum *values, bool *isnull, int stride)
+{
+	AttrMissing *attrmiss = NULL;
+
+	if (unlikely(lastAttNum > tupleDesc->natts))
+		elog(ERROR, "invalid attribute number %d", lastAttNum);
+
+	if (tupleDesc->constr)
+		attrmiss = tupleDesc->constr->missing;
+
+	if (!attrmiss)
+	{
+		for (int attnum = startAttNum; attnum < lastAttNum; attnum++)
+		{
+			values[attnum * stride] = (Datum) 0;
+			isnull[attnum * stride] = true;
+		}
+	}
+	else
+	{
+		for (int attnum = startAttNum; attnum < lastAttNum; attnum++)
+		{
+			values[attnum * stride] = attrmiss[attnum].am_value;
+			isnull[attnum * stride] = !attrmiss[attnum].am_present;
+		}
+	}
+}
+
+static pg_always_inline void
+getmissingattrs_selected(TupleDesc tupleDesc, int startAttNum,
+						 const AttrNumber *attnums, int nrequested,
+						 int *next_requested,
+						 Datum *values, bool *isnull, int stride)
+{
+	AttrMissing *attrmiss = NULL;
+
+	if (unlikely(attnums[nrequested - 1] > tupleDesc->natts))
+		elog(ERROR, "invalid attribute number %d",
+			 attnums[nrequested - 1]);
+
+	if (tupleDesc->constr)
+		attrmiss = tupleDesc->constr->missing;
+
+	for (; *next_requested < nrequested; (*next_requested)++)
+	{
+		int			output_index = *next_requested;
+		int			attnum = attnums[output_index] - 1;
+
+		Assert(attnum >= startAttNum);
+		if (attrmiss && attrmiss[attnum].am_present)
+		{
+			values[output_index * stride] = attrmiss[attnum].am_value;
+			isnull[output_index * stride] = false;
+		}
+		else
+		{
+			values[output_index * stride] = (Datum) 0;
+			isnull[output_index * stride] = true;
+		}
+	}
+}
+
+static pg_always_inline Datum
+deform_heap_tuple_next_attr(CompactAttribute *cattrs, size_t attnum,
+							const char *tp, uint32 *off,
+							bool support_cstring)
+{
+	CompactAttribute *cattr = &cattrs[attnum];
+	int			attlen = cattr->attlen;
+
+	if (!support_cstring)
+		pg_assume(attlen > 0 || attlen == -1);
+	return align_fetch_then_add(tp, off, cattr->attbyval,
+								attlen, cattr->attalignby);
+}
+
+static pg_always_inline void
+deform_heap_tuple_skip_attrs(CompactAttribute *cattrs,
+							 size_t *attnum, size_t last_attnum,
+							 const char *tp, uint32 *off,
+							 bool support_cstring)
+{
+	for (; *attnum < last_attnum; (*attnum)++)
+		(void) deform_heap_tuple_next_attr(cattrs, *attnum, tp, off,
+										  support_cstring);
+}
+
+static pg_always_inline void
+deform_heap_tuple_skip_nullable_attrs(CompactAttribute *cattrs,
+									  size_t *attnum, size_t last_attnum,
+									  const char *tp, uint32 *off,
+									  const uint8 *bits,
+									  bool support_cstring)
+{
+	for (; *attnum < last_attnum; (*attnum)++)
+	{
+		if (!att_isnull(*attnum, bits))
+			(void) deform_heap_tuple_next_attr(cattrs, *attnum, tp, off,
+											  support_cstring);
+	}
+}
+
 /*
- * slot_deform_heap_tuple
- *		Given a TupleTableSlot, extract data from the slot's physical tuple
- *		into its Datum/isnull arrays.  Data is extracted up through the
- *		reqnatts'th column.  If there are insufficient attributes in the given
- *		tuple, then slot_getmissingattrs() is called to populate the
- *		remainder.  If reqnatts is above the number of attributes in the
- *		slot's TupleDesc, an error is raised.
+ * deform_heap_tuple
+ *		Extract data from a physical tuple into Datum/isnull arrays. Data is
+ *		extracted up through the reqnatts'th column. Array entries are
+ *		separated by stride. If attnums is NULL, every extracted attribute is
+ *		stored at its physical index. Otherwise, attnums must list nrequested
+ *		attributes in increasing order, and only those attributes are stored,
+ *		at their list indexes. Missing attributes are filled from the tuple
+ *		descriptor, and an invalid reqnatts raises an error.
+ *		Fixed-offset regions can skip directly to requested attributes.
+ *		Variable-offset regions advance through gaps separately, avoiding an
+ *		output check for every physical attribute.
  *
  *		This is essentially an incremental version of heap_deform_tuple:
  *		on each call we extract attributes up to the one needed, without
  *		re-computing information about previously extracted attributes.
- *		slot->tts_nvalid is the number of attributes already extracted.
+ *		*nvalidp is the number of attributes already extracted.
  *
- * This is marked as always inline, so the different offp for different types
- * of slots gets optimized away.
+ * This is marked as always inline, so stride and the different offp for
+ * different types of slots get optimized away.
  *
  * support_cstring should be passed as a const to allow the compiler only
  * emit code during inlining for cstring deforming when it's required.
  * cstrings can exist in MinimalTuples, but not in HeapTuples.
  */
 static pg_always_inline void
-slot_deform_heap_tuple(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp,
-					   int reqnatts, bool support_cstring)
+deform_heap_tuple(TupleDesc tupleDesc, int first_nonguaranteed,
+				  HeapTuple tuple, uint32 *offp, AttrNumber *nvalidp,
+				  Datum *values, bool *isnull, int stride,
+				  int reqnatts, bool support_cstring,
+				  const AttrNumber *attnums, int nrequested)
 {
 	CompactAttribute *cattrs;
 	CompactAttribute *cattr;
-	TupleDesc	tupleDesc = slot->tts_tupleDescriptor;
 	HeapTupleHeader tup = tuple->t_data;
 	size_t		attnum;
 	int			firstNonCacheOffsetAttr;
 	int			firstNonGuaranteedAttr;
 	int			firstNullAttr;
 	int			natts;
-	Datum	   *values;
-	bool	   *isnull;
+	int			next_requested = 0;
 	char	   *tp;				/* ptr to tuple data */
 	uint32		off;			/* offset in tuple data */
 
 	/* Did someone forget to call TupleDescFinalize()? */
 	Assert(tupleDesc->firstNonCachedOffsetAttr >= 0);
-
-	isnull = slot->tts_isnull;
+	Assert(attnums == NULL || *nvalidp == 0);
+	Assert(attnums == NULL || nrequested > 0);
+	Assert(attnums == NULL || attnums[nrequested - 1] == reqnatts);
 
 	/*
 	 * Some callers may form and deform tuples prior to NOT NULL constraints
@@ -1114,7 +1238,7 @@ slot_deform_heap_tuple(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp,
 	 * from the tuple and saves the additional cost of handling non-byval
 	 * attrs.
 	 */
-	firstNonGuaranteedAttr = Min(reqnatts, slot->tts_first_nonguaranteed);
+	firstNonGuaranteedAttr = Min(reqnatts, first_nonguaranteed);
 
 	firstNonCacheOffsetAttr = tupleDesc->firstNonCachedOffsetAttr;
 
@@ -1136,7 +1260,16 @@ slot_deform_heap_tuple(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp,
 			 * And populate the isnull array for all attributes being fetched
 			 * from the tuple.
 			 */
-			populate_isnull_array(bp, natts, isnull);
+			if (attnums == NULL)
+			{
+				if (stride == 1)
+					populate_isnull_array(bp, natts, isnull);
+				else
+				{
+					for (int i = 0; i < natts; i++)
+						isnull[i * stride] = att_isnull(i, bp);
+				}
+			}
 		}
 		else
 		{
@@ -1171,9 +1304,8 @@ slot_deform_heap_tuple(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp,
 		firstNullAttr = natts;
 	}
 
-	attnum = slot->tts_nvalid;
-	values = slot->tts_values;
-	slot->tts_nvalid = reqnatts;
+	attnum = *nvalidp;
+	*nvalidp = reqnatts;
 
 	/*
 	 * We store the tupleDesc's CompactAttribute array in 'cattrs' as gcc
@@ -1185,13 +1317,124 @@ slot_deform_heap_tuple(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp,
 	/* Ensure we calculated tp correctly */
 	Assert(tp == (char *) tup + tup->t_hoff);
 
+	if (attnums != NULL)
+	{
+		if (attnum < firstNonGuaranteedAttr)
+		{
+			int			attlen;
+
+			while (next_requested < nrequested &&
+				   attnums[next_requested] <= firstNonGuaranteedAttr)
+			{
+				int			output_index = next_requested++;
+
+				attnum = attnums[output_index] - 1;
+				cattr = &cattrs[attnum];
+				attlen = cattr->attlen;
+
+				pg_assume(attlen > 0);
+				Assert(cattr->attbyval == true);
+
+				off = cattr->attcacheoff;
+				isnull[output_index * stride] = false;
+				values[output_index * stride] =
+					fetch_att_noerr(tp + off, true, attlen);
+			}
+
+			attnum = firstNonGuaranteedAttr;
+			cattr = &cattrs[attnum - 1];
+			attlen = cattr->attlen;
+			off = cattr->attcacheoff + attlen;
+
+			if (attnum == reqnatts)
+				goto done;
+		}
+		else
+			off = *offp;
+
+		firstNonCacheOffsetAttr =
+			Min(firstNonCacheOffsetAttr, firstNullAttr);
+		if (attnum < firstNonCacheOffsetAttr)
+		{
+			int			attlen;
+
+			while (next_requested < nrequested &&
+				   attnums[next_requested] <= firstNonCacheOffsetAttr)
+			{
+				int			output_index = next_requested++;
+
+				attnum = attnums[output_index] - 1;
+				cattr = &cattrs[attnum];
+				attlen = cattr->attlen;
+				off = cattr->attcacheoff;
+				isnull[output_index * stride] = false;
+				values[output_index * stride] =
+					fetch_att_noerr(tp + off, cattr->attbyval, attlen);
+			}
+
+			attnum = firstNonCacheOffsetAttr;
+			cattr = &cattrs[attnum - 1];
+			attlen = cattr->attlen;
+			Assert(attlen > 0);
+			off = cattr->attcacheoff + attlen;
+		}
+
+		while (next_requested < nrequested &&
+			   attnums[next_requested] <= firstNullAttr)
+		{
+			size_t		requested_attnum = attnums[next_requested] - 1;
+			int			output_index;
+
+			deform_heap_tuple_skip_attrs(cattrs, &attnum, requested_attnum,
+										 tp, &off, support_cstring);
+			output_index = next_requested++;
+			isnull[output_index * stride] = false;
+			values[output_index * stride] =
+				deform_heap_tuple_next_attr(cattrs, attnum, tp, &off,
+											support_cstring);
+			attnum++;
+		}
+		deform_heap_tuple_skip_attrs(cattrs, &attnum, firstNullAttr,
+									 tp, &off, support_cstring);
+
+		while (next_requested < nrequested &&
+			   attnums[next_requested] <= natts)
+		{
+			size_t		requested_attnum = attnums[next_requested] - 1;
+			int			output_index;
+
+			deform_heap_tuple_skip_nullable_attrs(cattrs, &attnum,
+												  requested_attnum,
+												  tp, &off, tup->t_bits,
+												  support_cstring);
+			output_index = next_requested++;
+			if (att_isnull(attnum, tup->t_bits))
+			{
+				values[output_index * stride] = (Datum) 0;
+				isnull[output_index * stride] = true;
+			}
+			else
+			{
+				values[output_index * stride] =
+					deform_heap_tuple_next_attr(cattrs, attnum, tp, &off,
+												support_cstring);
+				isnull[output_index * stride] = false;
+			}
+			attnum++;
+		}
+		deform_heap_tuple_skip_nullable_attrs(cattrs, &attnum, natts,
+											  tp, &off, tup->t_bits,
+											  support_cstring);
+		goto deformation_done;
+	}
+
 	if (attnum < firstNonGuaranteedAttr)
 	{
 		int			attlen;
 
 		do
 		{
-			isnull[attnum] = false;
+			isnull[attnum * stride] = false;
 			cattr = &cattrs[attnum];
 			attlen = cattr->attlen;
 
@@ -1200,7 +1443,8 @@ slot_deform_heap_tuple(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp,
 			Assert(cattr->attbyval == true);
 
 			off = cattr->attcacheoff;
-			values[attnum] = fetch_att_noerr(tp + off, true, attlen);
+			values[attnum * stride] =
+				fetch_att_noerr(tp + off, true, attlen);
 			attnum++;
 		} while (attnum < firstNonGuaranteedAttr);
 
@@ -1236,13 +1480,12 @@ slot_deform_heap_tuple(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp,
 
 		do
 		{
-			isnull[attnum] = false;
+			isnull[attnum * stride] = false;
 			cattr = &cattrs[attnum];
 			attlen = cattr->attlen;
 			off = cattr->attcacheoff;
-			values[attnum] = fetch_att_noerr(tp + off,
-											 cattr->attbyval,
-											 attlen);
+			values[attnum * stride] =
+				fetch_att_noerr(tp + off, cattr->attbyval, attlen);
 			attnum++;
 		} while (attnum < firstNonCacheOffsetAttr);
 
@@ -1264,7 +1507,7 @@ slot_deform_heap_tuple(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp,
 	{
 		int			attlen;
 
-		isnull[attnum] = false;
+		isnull[attnum * stride] = false;
 		cattr = &cattrs[attnum];
 		attlen = cattr->attlen;
 
@@ -1277,11 +1520,9 @@ slot_deform_heap_tuple(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp,
 			pg_assume(attlen > 0 || attlen == -1);
 
 		/* align 'off', fetch the datum, and increment off beyond the datum */
-		values[attnum] = align_fetch_then_add(tp,
-											  &off,
-											  cattr->attbyval,
-											  attlen,
-											  cattr->attalignby);
+		values[attnum * stride] =
+			align_fetch_then_add(tp, &off, cattr->attbyval,
+								 attlen, cattr->attalignby);
 	}
 
 	/*
@@ -1293,9 +1534,9 @@ slot_deform_heap_tuple(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp,
 	{
 		int			attlen;
 
-		if (isnull[attnum])
+		if (isnull[attnum * stride])
 		{
-			values[attnum] = (Datum) 0;
+			values[attnum * stride] = (Datum) 0;
 			continue;
 		}
 
@@ -1307,13 +1548,12 @@ slot_deform_heap_tuple(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp,
 			pg_assume(attlen > 0 || attlen == -1);
 
 		/* align 'off', fetch the datum, and increment off beyond the datum */
-		values[attnum] = align_fetch_then_add(tp,
-											  &off,
-											  cattr->attbyval,
-											  attlen,
-											  cattr->attalignby);
+		values[attnum * stride] =
+			align_fetch_then_add(tp, &off, cattr->attbyval,
+								 attlen, cattr->attalignby);
 	}
 
+deformation_done:
 	/* Fetch any missing attrs and raise an error if reqnatts is invalid */
 	if (unlikely(attnum < reqnatts))
 	{
@@ -1322,15 +1562,35 @@ slot_deform_heap_tuple(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp,
 		 * to implement a tail-call optimization
 		 */
 		*offp = off;
-		slot_getmissingattrs(slot, attnum, reqnatts);
+		if (attnums == NULL)
+			getmissingattrs(tupleDesc, attnum, reqnatts,
+							values, isnull, stride);
+		else
+			getmissingattrs_selected(tupleDesc, attnum,
+									 attnums, nrequested, &next_requested,
+									 values, isnull, stride);
+		Assert(attnums == NULL || next_requested == nrequested);
 		return;
 	}
 done:
 
+	Assert(attnums == NULL || next_requested == nrequested);
+
 	/* Save current offset for next execution */
 	*offp = off;
 }
 
+static pg_always_inline void
+slot_deform_heap_tuple(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp,
+					   int reqnatts, bool support_cstring)
+{
+	deform_heap_tuple(slot->tts_tupleDescriptor,
+					  slot->tts_first_nonguaranteed,
+					  tuple, offp, &slot->tts_nvalid,
+					  slot->tts_values, slot->tts_isnull, 1,
+					  reqnatts, support_cstring, NULL, 0);
+}
+
 const TupleTableSlotOps TTSOpsVirtual = {
 	.base_slot_size = sizeof(VirtualTupleTableSlot),
 	.init = tts_virtual_init,
@@ -2204,33 +2464,8 @@ ExecInitNullTupleSlot(EState *estate, TupleDesc tupType,
 void
 slot_getmissingattrs(TupleTableSlot *slot, int startAttNum, int lastAttNum)
 {
-	AttrMissing *attrmiss = NULL;
-
-	/* Check for invalid attnums */
-	if (unlikely(lastAttNum > slot->tts_tupleDescriptor->natts))
-		elog(ERROR, "invalid attribute number %d", lastAttNum);
-
-	if (slot->tts_tupleDescriptor->constr)
-		attrmiss = slot->tts_tupleDescriptor->constr->missing;
-
-	if (!attrmiss)
-	{
-		/* no missing values array at all, so just fill everything in as NULL */
-		for (int attnum = startAttNum; attnum < lastAttNum; attnum++)
-		{
-			slot->tts_values[attnum] = (Datum) 0;
-			slot->tts_isnull[attnum] = true;
-		}
-	}
-	else
-	{
-		/* use attrmiss to set the missing values */
-		for (int attnum = startAttNum; attnum < lastAttNum; attnum++)
-		{
-			slot->tts_values[attnum] = attrmiss[attnum].am_value;
-			slot->tts_isnull[attnum] = !attrmiss[attnum].am_present;
-		}
-	}
+	getmissingattrs(slot->tts_tupleDescriptor, startAttNum, lastAttNum,
+					slot->tts_values, slot->tts_isnull, 1);
 }
 
 /*
diff --git a/src/include/executor/tuptable.h b/src/include/executor/tuptable.h
index d9117ade80c..4d87e552e6f 100644
--- a/src/include/executor/tuptable.h
+++ b/src/include/executor/tuptable.h
@@ -161,19 +161,21 @@ typedef uint64 TupleBatchMask;
  * A batch with zero tuples is inactive; otherwise, 0 <= current < ntuples and
  * current identifies the slot's tuple in the batch. The batch pointer and
  * generation together identify the batch contents. Attributes available
- * through getattrs must remain stable while a batch is active. Slots backed
+ * through getsomeattrs must remain stable while a batch is active. Slots backed
  * by storage that can change those attributes in place must leave tts_batch
  * NULL.
  *
- * getattrs requests at most TUPLE_BATCH_MASK_BITS tuples. Bit i in rows and
- * the returned NULL mask, and values[i], refer to tuple first + i in the
- * batch.
+ * getsomeattrs requests at most TUPLE_BATCH_MASK_BITS tuples and the listed
+ * attributes, which must be in strictly increasing order. For attribute
+ * attnums[j] and tuple first + i, the value and NULL flag are stored at
+ * j * TUPLE_BATCH_MASK_BITS + i.
  */
 struct TupleTableSlotBatch
 {
-	TupleBatchMask (*getattrs) (TupleTableSlot *slot,
-								AttrNumber attnum, int first, int nrows,
-								TupleBatchMask rows, Datum *values);
+	void		(*getsomeattrs) (TupleTableSlot *slot,
+								 const AttrNumber *attnums, int natts,
+								 int first, int nrows,
+								 Datum *values, bool *isnull);
 	uint64		generation;
 	int			ntuples;
 	int			current;
@@ -193,7 +195,7 @@ slot_getbatch(const TupleTableSlot *slot)
 	if (batch == NULL || batch->ntuples <= 0)
 		return NULL;
 
-	Assert(batch->getattrs != NULL);
+	Assert(batch->getsomeattrs != NULL);
 	Assert(batch->current >= 0);
 	Assert(batch->current < batch->ntuples);
 
@@ -339,13 +341,10 @@ typedef struct HeapTupleTableSlot
 	HeapTupleData tupdata;		/* optional workspace for storing tuple */
 } HeapTupleTableSlot;
 
-struct HeapScanDescData;
-
 typedef struct HeapPageBatch
 {
 	TupleTableSlotBatch batch;
 	const OffsetNumber *offsets;
-	struct HeapScanDescData *scan;
 } HeapPageBatch;
 
 /* heap tuple residing in a buffer */
diff --git a/src/test/regress/expected/scan_batch.out b/src/test/regress/expected/scan_batch.out
index 204dedb3485..4d6d58991a3 100644
--- a/src/test/regress/expected/scan_batch.out
+++ b/src/test/regress/expected/scan_batch.out
@@ -1,13 +1,16 @@
 --
 -- Tests for evaluating scan qualifications in tuple batches
 --
-CREATE TABLE scan_batch_test (id int, a int, t text)
+CREATE TABLE scan_batch_test (id int, a int, t text, b int, c int)
   WITH (autovacuum_enabled = false);
 INSERT INTO scan_batch_test
 SELECT g,
 	CASE WHEN g IN (1, 2) THEN NULL ELSE g END,
-	CASE WHEN g IN (3, 100) THEN NULL ELSE 'value-' || g END
+	CASE WHEN g IN (3, 100) THEN NULL ELSE 'value-' || g END,
+	CASE WHEN g % 13 = 0 THEN NULL ELSE g END,
+	CASE WHEN g % 7 = 0 THEN NULL ELSE g END
 FROM generate_series(1, 200) AS g;
+ALTER TABLE scan_batch_test ADD COLUMN d int DEFAULT 150;
 -- Multiple batch qualifications, both argument orders, and NULL attributes.
 SELECT count(*) FROM scan_batch_test
 WHERE 0 < a AND a < 201 AND t <> 'missing';
@@ -174,6 +177,26 @@ CREATE OPERATOR #<# (
 	LEFTARG = int,
 	RIGHTARG = int
 );
+/*
+ * Batch deformation should walk through the last needed column while storing
+ * only columns used by qualifications. The text and NULL columns make the
+ * offsets of b and c non-cacheable, while d is physically missing from the
+ * original tuples. Qualification order must not affect the extracted values.
+ */
+SELECT
+	(SELECT count(*) FROM scan_batch_test
+	 WHERE b #<# 180 AND c #<# 170 AND d #<# 151) AS increasing,
+	(SELECT count(*) FROM scan_batch_test
+	 WHERE d #<# 151 AND c #<# 170 AND b #<# 180) AS decreasing,
+	(SELECT count(*) FROM scan_batch_test
+	 WHERE c #<# 190 AND b #<# 180 AND d #<# 151) AS zigzag,
+	(SELECT count(*) FROM scan_batch_test
+	 WHERE c #<# 190 AND b #<# 180 AND c #<# 170 AND d #<# 151) AS repeated;
+ increasing | decreasing | zigzag | repeated 
+------------+------------+--------+----------
+        133 |        133 |    142 |      133
+(1 row)
+
 SELECT 'scan_batch_lt(integer,integer)'::regprocedure::oid AS batch_func_oid \gset
 SET track_functions = 'all';
 SELECT count(*) FROM scan_batch_test WHERE a #<# 201;
@@ -189,6 +212,22 @@ SELECT calls FROM pg_stat_user_functions WHERE funcid = :batch_func_oid;
    198
 (1 row)
 
+-- Batch evaluation must preserve the qualification order from the planner.
+SELECT pg_stat_reset_single_function_counters(:batch_func_oid) AS reset \gset
+SELECT count(*) FROM scan_batch_test
+WHERE d #<# 0 AND b #<# 201;
+ count 
+-------
+     0
+(1 row)
+
+SELECT pg_stat_force_next_flush() \gset
+SELECT calls FROM pg_stat_user_functions WHERE funcid = :batch_func_oid;
+ calls 
+-------
+   200
+(1 row)
+
 -- Updating returned rows must not cause a batch to be evaluated repeatedly.
 SELECT pg_stat_reset_single_function_counters(:batch_func_oid) AS reset \gset
 UPDATE scan_batch_test SET t = t WHERE a #<# 201;
@@ -236,6 +275,21 @@ SELECT calls FROM pg_stat_user_functions WHERE funcid = :batch_func_oid;
 (1 row)
 
 RESET track_functions;
+-- Repeat the attribute-order checks through the scalar path.
+SELECT
+	(SELECT count(*) FROM scan_batch_test
+	 WHERE b #<# 180 AND c #<# 170 AND d #<# 151) AS increasing,
+	(SELECT count(*) FROM scan_batch_test
+	 WHERE d #<# 151 AND c #<# 170 AND b #<# 180) AS decreasing,
+	(SELECT count(*) FROM scan_batch_test
+	 WHERE c #<# 190 AND b #<# 180 AND d #<# 151) AS zigzag,
+	(SELECT count(*) FROM scan_batch_test
+	 WHERE c #<# 190 AND b #<# 180 AND c #<# 170 AND d #<# 151) AS repeated;
+ increasing | decreasing | zigzag | repeated 
+------------+------------+--------+----------
+        133 |        133 |    142 |      133
+(1 row)
+
 DROP OPERATOR #<# (int, int);
 DROP FUNCTION scan_batch_lt(int, int);
 DROP TABLE scan_batch_test;
diff --git a/src/test/regress/sql/scan_batch.sql b/src/test/regress/sql/scan_batch.sql
index 44a2de64659..4ca5c8872cb 100644
--- a/src/test/regress/sql/scan_batch.sql
+++ b/src/test/regress/sql/scan_batch.sql
@@ -2,15 +2,19 @@
 -- Tests for evaluating scan qualifications in tuple batches
 --
 
-CREATE TABLE scan_batch_test (id int, a int, t text)
+CREATE TABLE scan_batch_test (id int, a int, t text, b int, c int)
   WITH (autovacuum_enabled = false);
 
 INSERT INTO scan_batch_test
 SELECT g,
 	CASE WHEN g IN (1, 2) THEN NULL ELSE g END,
-	CASE WHEN g IN (3, 100) THEN NULL ELSE 'value-' || g END
+	CASE WHEN g IN (3, 100) THEN NULL ELSE 'value-' || g END,
+	CASE WHEN g % 13 = 0 THEN NULL ELSE g END,
+	CASE WHEN g % 7 = 0 THEN NULL ELSE g END
 FROM generate_series(1, 200) AS g;
 
+ALTER TABLE scan_batch_test ADD COLUMN d int DEFAULT 150;
+
 -- Multiple batch qualifications, both argument orders, and NULL attributes.
 SELECT count(*) FROM scan_batch_test
 WHERE 0 < a AND a < 201 AND t <> 'missing';
@@ -110,6 +114,22 @@ CREATE OPERATOR #<# (
 	RIGHTARG = int
 );
 
+/*
+ * Batch deformation should walk through the last needed column while storing
+ * only columns used by qualifications. The text and NULL columns make the
+ * offsets of b and c non-cacheable, while d is physically missing from the
+ * original tuples. Qualification order must not affect the extracted values.
+ */
+SELECT
+	(SELECT count(*) FROM scan_batch_test
+	 WHERE b #<# 180 AND c #<# 170 AND d #<# 151) AS increasing,
+	(SELECT count(*) FROM scan_batch_test
+	 WHERE d #<# 151 AND c #<# 170 AND b #<# 180) AS decreasing,
+	(SELECT count(*) FROM scan_batch_test
+	 WHERE c #<# 190 AND b #<# 180 AND d #<# 151) AS zigzag,
+	(SELECT count(*) FROM scan_batch_test
+	 WHERE c #<# 190 AND b #<# 180 AND c #<# 170 AND d #<# 151) AS repeated;
+
 SELECT 'scan_batch_lt(integer,integer)'::regprocedure::oid AS batch_func_oid \gset
 SET track_functions = 'all';
 
@@ -117,6 +137,13 @@ SELECT count(*) FROM scan_batch_test WHERE a #<# 201;
 SELECT pg_stat_force_next_flush() \gset
 SELECT calls FROM pg_stat_user_functions WHERE funcid = :batch_func_oid;
 
+-- Batch evaluation must preserve the qualification order from the planner.
+SELECT pg_stat_reset_single_function_counters(:batch_func_oid) AS reset \gset
+SELECT count(*) FROM scan_batch_test
+WHERE d #<# 0 AND b #<# 201;
+SELECT pg_stat_force_next_flush() \gset
+SELECT calls FROM pg_stat_user_functions WHERE funcid = :batch_func_oid;
+
 -- Updating returned rows must not cause a batch to be evaluated repeatedly.
 SELECT pg_stat_reset_single_function_counters(:batch_func_oid) AS reset \gset
 UPDATE scan_batch_test SET t = t WHERE a #<# 201;
@@ -142,6 +169,18 @@ SELECT pg_stat_force_next_flush() \gset
 SELECT calls FROM pg_stat_user_functions WHERE funcid = :batch_func_oid;
 
 RESET track_functions;
+
+-- Repeat the attribute-order checks through the scalar path.
+SELECT
+	(SELECT count(*) FROM scan_batch_test
+	 WHERE b #<# 180 AND c #<# 170 AND d #<# 151) AS increasing,
+	(SELECT count(*) FROM scan_batch_test
+	 WHERE d #<# 151 AND c #<# 170 AND b #<# 180) AS decreasing,
+	(SELECT count(*) FROM scan_batch_test
+	 WHERE c #<# 190 AND b #<# 180 AND d #<# 151) AS zigzag,
+	(SELECT count(*) FROM scan_batch_test
+	 WHERE c #<# 190 AND b #<# 180 AND c #<# 170 AND d #<# 151) AS repeated;
+
 DROP OPERATOR #<# (int, int);
 DROP FUNCTION scan_batch_lt(int, int);
 DROP TABLE scan_batch_test;
-- 
2.54.0

