heapam: Add batch mode mvcc check and use it in page mode
authorAndres Freund <andres@anarazel.de>
Mon, 12 Jan 2026 18:14:58 +0000 (13:14 -0500)
committerAndres Freund <andres@anarazel.de>
Mon, 12 Jan 2026 18:22:04 +0000 (13:22 -0500)
There are two reasons for doing so:

1) It is generally faster to perform checks in a batched fashion and making
   sequential scans faster is nice.

2) We would like to stop setting hint bits while pages are being written
   out. The necessary locking becomes visible for page mode scans, if done for
   every tuple. With batching, the overhead can be amortized to only happen
   once per page.

There are substantial further optimization opportunities along these
lines:

- Right now HeapTupleSatisfiesMVCCBatch() simply uses the single-tuple
  HeapTupleSatisfiesMVCC(), relying on the compiler to inline it. We could
  instead write an explicitly optimized version that avoids repeated xid
  tests.

- Introduce batched version of the serializability test

- Introduce batched version of HeapTupleSatisfiesVacuum

Reviewed-by: Melanie Plageman <melanieplageman@gmail.com>
Discussion: https://postgr.es/m/6rgb2nvhyvnszz4ul3wfzlf5rheb2kkwrglthnna7qhe24onwr@vw27225tkyar

src/backend/access/heap/heapam.c
src/backend/access/heap/heapam_visibility.c
src/include/access/heapam.h
src/tools/pgindent/typedefs.list

index ad9d6338ec2b6954319d212a52375648e73bb270..f30a56ecf5539778645d42b75740595321b66714 100644 (file)
@@ -522,42 +522,86 @@ page_collect_tuples(HeapScanDesc scan, Snapshot snapshot,
                    BlockNumber block, int lines,
                    bool all_visible, bool check_serializable)
 {
+   Oid         relid = RelationGetRelid(scan->rs_base.rs_rd);
    int         ntup = 0;
-   OffsetNumber lineoff;
+   int         nvis = 0;
+   BatchMVCCState batchmvcc;
+
+   /* page at a time should have been disabled otherwise */
+   Assert(IsMVCCSnapshot(snapshot));
 
-   for (lineoff = FirstOffsetNumber; lineoff <= lines; lineoff++)
+   /* first find all tuples on the page */
+   for (OffsetNumber lineoff = FirstOffsetNumber; lineoff <= lines; lineoff++)
    {
        ItemId      lpp = PageGetItemId(page, lineoff);
-       HeapTupleData loctup;
-       bool        valid;
+       HeapTuple   tup;
 
-       if (!ItemIdIsNormal(lpp))
+       if (unlikely(!ItemIdIsNormal(lpp)))
            continue;
 
-       loctup.t_data = (HeapTupleHeader) PageGetItem(page, lpp);
-       loctup.t_len = ItemIdGetLength(lpp);
-       loctup.t_tableOid = RelationGetRelid(scan->rs_base.rs_rd);
-       ItemPointerSet(&(loctup.t_self), block, lineoff);
-
-       if (all_visible)
-           valid = true;
-       else
-           valid = HeapTupleSatisfiesVisibility(&loctup, snapshot, buffer);
+       /*
+        * If the page is not all-visible or we need to check serializability,
+        * maintain enough state to be able to refind the tuple efficiently,
+        * without again first needing to fetch the item and then via that the
+        * tuple.
+        */
+       if (!all_visible || check_serializable)
+       {
+           tup = &batchmvcc.tuples[ntup];
 
-       if (check_serializable)
-           HeapCheckForSerializableConflictOut(valid, scan->rs_base.rs_rd,
-                                               &loctup, buffer, snapshot);
+           tup->t_data = (HeapTupleHeader) PageGetItem(page, lpp);
+           tup->t_len = ItemIdGetLength(lpp);
+           tup->t_tableOid = relid;
+           ItemPointerSet(&(tup->t_self), block, lineoff);
+       }
 
-       if (valid)
+       /*
+        * If the page is all visible, these fields otherwise won't be
+        * populated in loop below.
+        */
+       if (all_visible)
        {
+           if (check_serializable)
+           {
+               batchmvcc.visible[ntup] = true;
+           }
            scan->rs_vistuples[ntup] = lineoff;
-           ntup++;
        }
+
+       ntup++;
    }
 
    Assert(ntup <= MaxHeapTuplesPerPage);
 
-   return ntup;
+   /*
+    * Unless the page is all visible, test visibility for all tuples one go.
+    * That is considerably more efficient than calling
+    * HeapTupleSatisfiesMVCC() one-by-one.
+    */
+   if (all_visible)
+       nvis = ntup;
+   else
+       nvis = HeapTupleSatisfiesMVCCBatch(snapshot, buffer,
+                                          ntup,
+                                          &batchmvcc,
+                                          scan->rs_vistuples);
+
+   /*
+    * So far we don't have batch API for testing serializabilty, so do so
+    * one-by-one.
+    */
+   if (check_serializable)
+   {
+       for (int i = 0; i < ntup; i++)
+       {
+           HeapCheckForSerializableConflictOut(batchmvcc.visible[i],
+                                               scan->rs_base.rs_rd,
+                                               &batchmvcc.tuples[i],
+                                               buffer, snapshot);
+       }
+   }
+
+   return nvis;
 }
 
 /*
index 9a034d5c9e8e47058b78253b91b96e90a0540e5f..75ae268d753c2800c2718950dcbcf7e9edb58106 100644 (file)
@@ -1598,6 +1598,49 @@ HeapTupleSatisfiesHistoricMVCC(HeapTuple htup, Snapshot snapshot,
        return true;
 }
 
+/*
+ * Perform HeaptupleSatisfiesMVCC() on each passed in tuple. This is more
+ * efficient than doing HeapTupleSatisfiesMVCC() one-by-one.
+ *
+ * To be checked tuples are passed via BatchMVCCState->tuples. Each tuple's
+ * visibility is stored in batchmvcc->visible[]. In addition,
+ * ->vistuples_dense is set to contain the offsets of visible tuples.
+ *
+ * The reason this is more efficient than HeapTupleSatisfiesMVCC() is that it
+ * avoids a cross-translation-unit function call for each tuple and allows the
+ * compiler to optimize across calls to HeapTupleSatisfiesMVCC. In the future
+ * it will also allow more efficient setting of hint bits.
+ *
+ * Returns the number of visible tuples.
+ */
+int
+HeapTupleSatisfiesMVCCBatch(Snapshot snapshot, Buffer buffer,
+                           int ntups,
+                           BatchMVCCState *batchmvcc,
+                           OffsetNumber *vistuples_dense)
+{
+   int         nvis = 0;
+
+   Assert(IsMVCCSnapshot(snapshot));
+
+   for (int i = 0; i < ntups; i++)
+   {
+       bool        valid;
+       HeapTuple   tup = &batchmvcc->tuples[i];
+
+       valid = HeapTupleSatisfiesMVCC(tup, snapshot, buffer);
+       batchmvcc->visible[i] = valid;
+
+       if (likely(valid))
+       {
+           vistuples_dense[nvis] = tup->t_self.ip_posid;
+           nvis++;
+       }
+   }
+
+   return nvis;
+}
+
 /*
  * HeapTupleSatisfiesVisibility
  *     True iff heap tuple satisfies a time qual.
index ce48fac42ba58dcf27c53091685e6e69c7b929f3..3c0961ab36ba7312976ffe5b968703f48d82700a 100644 (file)
@@ -449,6 +449,23 @@ extern bool HeapTupleHeaderIsOnlyLocked(HeapTupleHeader tuple);
 extern bool HeapTupleIsSurelyDead(HeapTuple htup,
                                  GlobalVisState *vistest);
 
+/*
+ * Some of the input/output to/from HeapTupleSatisfiesMVCCBatch() is passed
+ * via this struct, as otherwise the increased number of arguments to
+ * HeapTupleSatisfiesMVCCBatch() leads to on-stack argument passing on x86-64,
+ * which causes a small regression.
+ */
+typedef struct BatchMVCCState
+{
+   HeapTupleData tuples[MaxHeapTuplesPerPage];
+   bool        visible[MaxHeapTuplesPerPage];
+} BatchMVCCState;
+
+extern int HeapTupleSatisfiesMVCCBatch(Snapshot snapshot, Buffer buffer,
+                                       int ntups,
+                                       BatchMVCCState *batchmvcc,
+                                       OffsetNumber *vistuples_dense);
+
 /*
  * To avoid leaking too much knowledge about reorderbuffer implementation
  * details this is implemented in reorderbuffer.c not heapam_visibility.c
index 09e7f1d420ed3229e602e839456aaa216afd0b41..14dec2d49c103713212d9d4653d7f91e0e5171e4 100644 (file)
@@ -255,6 +255,7 @@ Barrier
 BaseBackupCmd
 BaseBackupTargetHandle
 BaseBackupTargetType
+BatchMVCCState
 BeginDirectModify_function
 BeginForeignInsert_function
 BeginForeignModify_function