agora inbox for pgsql-hackers@postgresql.org
help / color / mirror / Atom feed[PATCH v4 14/19] Vacuum second pass emits XLOG_HEAP2_PRUNE record
10+ messages / 4 participants
[nested] [flat]
* [PATCH v4 14/19] Vacuum second pass emits XLOG_HEAP2_PRUNE record
@ 2024-03-19 22:50 Melanie Plageman <melanieplageman@gmail.com>
0 siblings, 0 replies; 10+ messages in thread
From: Melanie Plageman @ 2024-03-19 22:50 UTC (permalink / raw)
Remove the XLOG_HEAP2_VACUUM record and update vacuum's second pass to
emit a XLOG_HEAP2_PRUNE record. This temporarily wastes some space but a
future commit will streamline xl_heap_prune and ensure that no unused
members are included in the WAL record.
---
src/backend/access/heap/heapam.c | 94 ++++--------------------
src/backend/access/heap/pruneheap.c | 67 ++++++++++-------
src/backend/access/heap/vacuumlazy.c | 12 ++-
src/backend/access/rmgrdesc/heapdesc.c | 20 -----
src/backend/replication/logical/decode.c | 1 -
src/include/access/heapam.h | 2 +-
src/include/access/heapam_xlog.h | 33 +++++----
7 files changed, 85 insertions(+), 144 deletions(-)
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 532868039d5..16bab55ba02 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -8717,23 +8717,34 @@ heap_xlog_prune(XLogReaderState *record)
BlockNumber blkno;
XLogRedoAction action;
bool get_cleanup_lock;
+ bool lp_truncate_only;
XLogRecGetBlockTag(record, 0, &rlocator, NULL, &blkno);
+ lp_truncate_only = xlrec->flags & XLHP_LP_TRUNCATE_ONLY;
+
/*
* If there are dead, redirected, or unused items set unused by
* heap_page_prune_and_freeze(), heap_page_prune_execute() will call
* PageRepairFragementation() which expects a full cleanup lock.
*/
get_cleanup_lock = xlrec->nredirected > 0 ||
- xlrec->ndead > 0 || xlrec->nunused > 0;
+ xlrec->ndead > 0 ||
+ (xlrec->nunused > 0 && !lp_truncate_only);
+
+ if (lp_truncate_only)
+ {
+ Assert(xlrec->nredirected == 0);
+ Assert(xlrec->ndead == 0);
+ Assert(xlrec->nunused > 0);
+ }
/*
* We are either about to remove tuples or freeze them. In Hot Standby
* mode, ensure that there's no queries running for which any removed
* tuples are still visible or which consider the frozen xids as running.
*/
- if (InHotStandby)
+ if (xlrec->flags & XLHP_HAS_CONFLICT_HORIZON && InHotStandby)
ResolveRecoveryConflictWithSnapshot(xlrec->snapshotConflictHorizon,
xlrec->isCatalogRel,
rlocator);
@@ -8772,7 +8783,7 @@ heap_xlog_prune(XLogReaderState *record)
/* Update all line pointers per the record, and repair fragmentation */
if (nredirected > 0 || ndead > 0 || nunused > 0)
- heap_page_prune_execute(buffer,
+ heap_page_prune_execute(buffer, lp_truncate_only,
redirected, nredirected,
nowdead, ndead,
nowunused, nunused);
@@ -8819,7 +8830,7 @@ heap_xlog_prune(XLogReaderState *record)
UnlockReleaseBuffer(buffer);
/*
- * After pruning records from a page, it's useful to update the FSM
+ * After modifying records on a page, it's useful to update the FSM
* about it, as it may cause the page become target for insertions
* later even if vacuum decides not to visit it (which is possible if
* gets marked all-visible.)
@@ -8831,78 +8842,6 @@ heap_xlog_prune(XLogReaderState *record)
}
}
-/*
- * Handles XLOG_HEAP2_VACUUM record type.
- *
- * Acquires an ordinary exclusive lock only.
- */
-static void
-heap_xlog_vacuum(XLogReaderState *record)
-{
- XLogRecPtr lsn = record->EndRecPtr;
- xl_heap_vacuum *xlrec = (xl_heap_vacuum *) XLogRecGetData(record);
- Buffer buffer;
- BlockNumber blkno;
- XLogRedoAction action;
-
- /*
- * If we have a full-page image, restore it (without using a cleanup lock)
- * and we're done.
- */
- action = XLogReadBufferForRedoExtended(record, 0, RBM_NORMAL, false,
- &buffer);
- if (action == BLK_NEEDS_REDO)
- {
- Page page = (Page) BufferGetPage(buffer);
- OffsetNumber *nowunused;
- Size datalen;
- OffsetNumber *offnum;
-
- nowunused = (OffsetNumber *) XLogRecGetBlockData(record, 0, &datalen);
-
- /* Shouldn't be a record unless there's something to do */
- Assert(xlrec->nunused > 0);
-
- /* Update all now-unused line pointers */
- offnum = nowunused;
- for (int i = 0; i < xlrec->nunused; i++)
- {
- OffsetNumber off = *offnum++;
- ItemId lp = PageGetItemId(page, off);
-
- Assert(ItemIdIsDead(lp) && !ItemIdHasStorage(lp));
- ItemIdSetUnused(lp);
- }
-
- /* Attempt to truncate line pointer array now */
- PageTruncateLinePointerArray(page);
-
- PageSetLSN(page, lsn);
- MarkBufferDirty(buffer);
- }
-
- if (BufferIsValid(buffer))
- {
- Size freespace = PageGetHeapFreeSpace(BufferGetPage(buffer));
- RelFileLocator rlocator;
-
- XLogRecGetBlockTag(record, 0, &rlocator, NULL, &blkno);
-
- UnlockReleaseBuffer(buffer);
-
- /*
- * After vacuuming LP_DEAD items from a page, it's useful to update
- * the FSM about it, as it may cause the page become target for
- * insertions later even if vacuum decides not to visit it (which is
- * possible if gets marked all-visible.)
- *
- * Do this regardless of a full-page image being applied, since the
- * FSM data is not in the page anyway.
- */
- XLogRecordPageWithFreeSpace(rlocator, blkno, freespace);
- }
-}
-
/*
* Replay XLOG_HEAP2_VISIBLE record.
*
@@ -9943,9 +9882,6 @@ heap2_redo(XLogReaderState *record)
case XLOG_HEAP2_PRUNE:
heap_xlog_prune(record);
break;
- case XLOG_HEAP2_VACUUM:
- heap_xlog_vacuum(record);
- break;
case XLOG_HEAP2_VISIBLE:
heap_xlog_visible(record);
break;
diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c
index 19b50931b90..135fe2dba3e 100644
--- a/src/backend/access/heap/pruneheap.c
+++ b/src/backend/access/heap/pruneheap.c
@@ -601,7 +601,7 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
*/
if (do_prune)
{
- heap_page_prune_execute(buffer,
+ heap_page_prune_execute(buffer, false,
prstate.redirected, prstate.nredirected,
prstate.nowdead, prstate.ndead,
prstate.nowunused, prstate.nunused);
@@ -668,12 +668,16 @@ log_heap_prune_and_freeze(Relation relation, Buffer buffer,
OffsetNumber offsets[MaxHeapTuplesPerPage];
bool do_freeze = presult->nfrozen > 0;
+ xlrec.flags = 0;
+
xlrec.isCatalogRel = RelationIsAccessibleInLogicalDecoding(relation);
xlrec.nredirected = prstate->nredirected;
xlrec.ndead = prstate->ndead;
xlrec.nunused = prstate->nunused;
xlrec.nplans = 0;
+ xlrec.flags |= XLHP_HAS_CONFLICT_HORIZON;
+
/*
* The snapshotConflictHorizon for the whole record should be the most
* conservative of all the horizons calculated for any of the possible
@@ -1149,7 +1153,7 @@ heap_prune_record_unused(PruneState *prstate, OffsetNumber offnum)
* cleanup lock on the buffer.
*/
void
-heap_page_prune_execute(Buffer buffer,
+heap_page_prune_execute(Buffer buffer, bool lp_truncate_only,
OffsetNumber *redirected, int nredirected,
OffsetNumber *nowdead, int ndead,
OffsetNumber *nowunused, int nunused)
@@ -1171,6 +1175,7 @@ heap_page_prune_execute(Buffer buffer,
ItemId tolp PG_USED_FOR_ASSERTS_ONLY;
#ifdef USE_ASSERT_CHECKING
+ Assert(!lp_truncate_only);
/*
* Any existing item that we set as an LP_REDIRECT (any 'from' item)
@@ -1226,6 +1231,7 @@ heap_page_prune_execute(Buffer buffer,
ItemId lp = PageGetItemId(page, off);
#ifdef USE_ASSERT_CHECKING
+ Assert(!lp_truncate_only);
/*
* An LP_DEAD line pointer must be left behind when the original item
@@ -1259,23 +1265,29 @@ heap_page_prune_execute(Buffer buffer,
#ifdef USE_ASSERT_CHECKING
- /*
- * When heap_page_prune_and_freeze() was called, mark_unused_now may
- * have been passed as true, which allows would-be LP_DEAD items to be
- * made LP_UNUSED instead. This is only possible if the relation has
- * no indexes. If there are any dead items, then mark_unused_now was
- * not true and every item being marked LP_UNUSED must refer to a
- * heap-only tuple.
- */
- if (ndead > 0)
+ if (lp_truncate_only)
{
- Assert(ItemIdHasStorage(lp) && ItemIdIsNormal(lp));
- htup = (HeapTupleHeader) PageGetItem(page, lp);
- Assert(HeapTupleHeaderIsHeapOnly(htup));
+ /* Setting LP_DEAD to LP_UNUSED in vacuum's second pass */
+ Assert(ItemIdIsDead(lp) && !ItemIdHasStorage(lp));
}
else
{
- Assert(ItemIdIsUsed(lp));
+ /*
+ * When heap_page_prune_and_freeze() was called, mark_unused_now
+ * may have been passed as true, which allows would-be LP_DEAD
+ * items to be made LP_UNUSED instead. This is only possible if
+ * the relation has no indexes. If there are any dead items, then
+ * mark_unused_now was not true and every item being marked
+ * LP_UNUSED must refer to a heap-only tuple.
+ */
+ if (ndead > 0)
+ {
+ Assert(ItemIdHasStorage(lp) && ItemIdIsNormal(lp));
+ htup = (HeapTupleHeader) PageGetItem(page, lp);
+ Assert(HeapTupleHeaderIsHeapOnly(htup));
+ }
+ else
+ Assert(ItemIdIsUsed(lp));
}
#endif
@@ -1283,17 +1295,22 @@ heap_page_prune_execute(Buffer buffer,
ItemIdSetUnused(lp);
}
- /*
- * Finally, repair any fragmentation, and update the page's hint bit about
- * whether it has free pointers.
- */
- PageRepairFragmentation(page);
+ if (lp_truncate_only)
+ PageTruncateLinePointerArray(page);
+ else
+ {
+ /*
+ * Finally, repair any fragmentation, and update the page's hint bit
+ * about whether it has free pointers.
+ */
+ PageRepairFragmentation(page);
- /*
- * Now that the page has been modified, assert that redirect items still
- * point to valid targets.
- */
- page_verify_redirects(page);
+ /*
+ * Now that the page has been modified, assert that redirect items
+ * still point to valid targets.
+ */
+ page_verify_redirects(page);
+ }
}
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index c4553a4159c..9dfb56475cf 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -2394,18 +2394,24 @@ lazy_vacuum_heap_page(LVRelState *vacrel, BlockNumber blkno, Buffer buffer,
/* XLOG stuff */
if (RelationNeedsWAL(vacrel->rel))
{
- xl_heap_vacuum xlrec;
+ xl_heap_prune xlrec;
XLogRecPtr recptr;
+ xlrec.flags = XLHP_LP_TRUNCATE_ONLY;
+ xlrec.snapshotConflictHorizon = InvalidTransactionId;
+ xlrec.nplans = 0;
+ xlrec.nredirected = 0;
+ xlrec.ndead = 0;
xlrec.nunused = nunused;
+ xlrec.isCatalogRel = RelationIsAccessibleInLogicalDecoding(vacrel->rel);
XLogBeginInsert();
- XLogRegisterData((char *) &xlrec, SizeOfHeapVacuum);
+ XLogRegisterData((char *) &xlrec, SizeOfHeapPrune);
XLogRegisterBuffer(0, buffer, REGBUF_STANDARD);
XLogRegisterBufData(0, (char *) unused, nunused * sizeof(OffsetNumber));
- recptr = XLogInsert(RM_HEAP2_ID, XLOG_HEAP2_VACUUM);
+ recptr = XLogInsert(RM_HEAP2_ID, XLOG_HEAP2_PRUNE);
PageSetLSN(page, recptr);
}
diff --git a/src/backend/access/rmgrdesc/heapdesc.c b/src/backend/access/rmgrdesc/heapdesc.c
index 9f0a0341d40..ea03f902fc4 100644
--- a/src/backend/access/rmgrdesc/heapdesc.c
+++ b/src/backend/access/rmgrdesc/heapdesc.c
@@ -242,23 +242,6 @@ heap2_desc(StringInfo buf, XLogReaderState *record)
}
}
}
- else if (info == XLOG_HEAP2_VACUUM)
- {
- xl_heap_vacuum *xlrec = (xl_heap_vacuum *) rec;
-
- appendStringInfo(buf, "nunused: %u", xlrec->nunused);
-
- if (XLogRecHasBlockData(record, 0))
- {
- OffsetNumber *nowunused;
-
- nowunused = (OffsetNumber *) XLogRecGetBlockData(record, 0, NULL);
-
- appendStringInfoString(buf, ", unused:");
- array_desc(buf, nowunused, sizeof(OffsetNumber), xlrec->nunused,
- &offset_elem_desc, NULL);
- }
- }
else if (info == XLOG_HEAP2_VISIBLE)
{
xl_heap_visible *xlrec = (xl_heap_visible *) rec;
@@ -360,9 +343,6 @@ heap2_identify(uint8 info)
case XLOG_HEAP2_PRUNE:
id = "PRUNE";
break;
- case XLOG_HEAP2_VACUUM:
- id = "VACUUM";
- break;
case XLOG_HEAP2_VISIBLE:
id = "VISIBLE";
break;
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index f77051572fd..38d1bdd825e 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -446,7 +446,6 @@ heap2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
* interested in.
*/
case XLOG_HEAP2_PRUNE:
- case XLOG_HEAP2_VACUUM:
case XLOG_HEAP2_VISIBLE:
case XLOG_HEAP2_LOCK_UPDATED:
break;
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 321a46185e1..d5cb8f99cac 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -349,7 +349,7 @@ extern void heap_page_prune_and_freeze(Relation relation, Buffer buffer,
HeapPageFreeze *pagefrz,
PruneFreezeResult *presult,
OffsetNumber *off_loc);
-extern void heap_page_prune_execute(Buffer buffer,
+extern void heap_page_prune_execute(Buffer buffer, bool lp_truncate_only,
OffsetNumber *redirected, int nredirected,
OffsetNumber *nowdead, int ndead,
OffsetNumber *nowunused, int nunused);
diff --git a/src/include/access/heapam_xlog.h b/src/include/access/heapam_xlog.h
index fe4a8ff0620..2393540cf68 100644
--- a/src/include/access/heapam_xlog.h
+++ b/src/include/access/heapam_xlog.h
@@ -52,11 +52,10 @@
*/
#define XLOG_HEAP2_REWRITE 0x00
#define XLOG_HEAP2_PRUNE 0x10
-#define XLOG_HEAP2_VACUUM 0x20
-#define XLOG_HEAP2_VISIBLE 0x30
-#define XLOG_HEAP2_MULTI_INSERT 0x40
-#define XLOG_HEAP2_LOCK_UPDATED 0x50
-#define XLOG_HEAP2_NEW_CID 0x60
+#define XLOG_HEAP2_VISIBLE 0x20
+#define XLOG_HEAP2_MULTI_INSERT 0x30
+#define XLOG_HEAP2_LOCK_UPDATED 0x40
+#define XLOG_HEAP2_NEW_CID 0x50
/*
* xl_heap_insert/xl_heap_multi_insert flag values, 8 bits are available.
@@ -266,6 +265,7 @@ typedef struct xl_heap_freeze_plan
*/
typedef struct xl_heap_prune
{
+ uint8 flags;
TransactionId snapshotConflictHorizon;
uint16 nplans;
uint16 nredirected;
@@ -288,19 +288,22 @@ typedef struct xl_heap_prune
#define SizeOfHeapPrune (offsetof(xl_heap_prune, isCatalogRel) + sizeof(bool))
+/* Flags for xl_heap_prune */
+
/*
- * The vacuum page record is similar to the prune record, but can only mark
- * already LP_DEAD items LP_UNUSED (during VACUUM's second heap pass)
- *
- * Acquires an ordinary exclusive lock only.
+ * During vacuum's second pass which sets LP_DEAD items LP_UNUSED, we will only
+ * truncate the line pointer array, not call PageRepairFragmentation. We need
+ * this flag to differentiate what kind of lock (exclusive or cleanup) to take
+ * on the buffer and whether to call PageTruncateLinePointerArray() or
+ * PageRepairFragementation().
*/
-typedef struct xl_heap_vacuum
-{
- uint16 nunused;
- /* OFFSET NUMBERS are in the block reference 0 */
-} xl_heap_vacuum;
+#define XLHP_LP_TRUNCATE_ONLY (1 << 1)
-#define SizeOfHeapVacuum (offsetof(xl_heap_vacuum, nunused) + sizeof(uint16))
+/*
+ * Vacuum's first pass and on-access pruning may need to include a snapshot
+ * conflict horizon.
+ */
+#define XLHP_HAS_CONFLICT_HORIZON (1 << 2)
/* flags for infobits_set */
#define XLHL_XMAX_IS_MULTI 0x01
--
2.40.1
--tez7m2a73jtztiij
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
filename="v4-0015-Set-hastup-in-heap_page_prune.patch"
^ permalink raw reply [nested|flat] 10+ messages in thread
* [PATCH v4 14/19] Vacuum second pass emits XLOG_HEAP2_PRUNE record
@ 2024-03-19 22:50 Melanie Plageman <melanieplageman@gmail.com>
0 siblings, 0 replies; 10+ messages in thread
From: Melanie Plageman @ 2024-03-19 22:50 UTC (permalink / raw)
Remove the XLOG_HEAP2_VACUUM record and update vacuum's second pass to
emit a XLOG_HEAP2_PRUNE record. This temporarily wastes some space but a
future commit will streamline xl_heap_prune and ensure that no unused
members are included in the WAL record.
---
src/backend/access/heap/heapam.c | 94 ++++--------------------
src/backend/access/heap/pruneheap.c | 67 ++++++++++-------
src/backend/access/heap/vacuumlazy.c | 12 ++-
src/backend/access/rmgrdesc/heapdesc.c | 20 -----
src/backend/replication/logical/decode.c | 1 -
src/include/access/heapam.h | 2 +-
src/include/access/heapam_xlog.h | 33 +++++----
7 files changed, 85 insertions(+), 144 deletions(-)
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 532868039d5..16bab55ba02 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -8717,23 +8717,34 @@ heap_xlog_prune(XLogReaderState *record)
BlockNumber blkno;
XLogRedoAction action;
bool get_cleanup_lock;
+ bool lp_truncate_only;
XLogRecGetBlockTag(record, 0, &rlocator, NULL, &blkno);
+ lp_truncate_only = xlrec->flags & XLHP_LP_TRUNCATE_ONLY;
+
/*
* If there are dead, redirected, or unused items set unused by
* heap_page_prune_and_freeze(), heap_page_prune_execute() will call
* PageRepairFragementation() which expects a full cleanup lock.
*/
get_cleanup_lock = xlrec->nredirected > 0 ||
- xlrec->ndead > 0 || xlrec->nunused > 0;
+ xlrec->ndead > 0 ||
+ (xlrec->nunused > 0 && !lp_truncate_only);
+
+ if (lp_truncate_only)
+ {
+ Assert(xlrec->nredirected == 0);
+ Assert(xlrec->ndead == 0);
+ Assert(xlrec->nunused > 0);
+ }
/*
* We are either about to remove tuples or freeze them. In Hot Standby
* mode, ensure that there's no queries running for which any removed
* tuples are still visible or which consider the frozen xids as running.
*/
- if (InHotStandby)
+ if (xlrec->flags & XLHP_HAS_CONFLICT_HORIZON && InHotStandby)
ResolveRecoveryConflictWithSnapshot(xlrec->snapshotConflictHorizon,
xlrec->isCatalogRel,
rlocator);
@@ -8772,7 +8783,7 @@ heap_xlog_prune(XLogReaderState *record)
/* Update all line pointers per the record, and repair fragmentation */
if (nredirected > 0 || ndead > 0 || nunused > 0)
- heap_page_prune_execute(buffer,
+ heap_page_prune_execute(buffer, lp_truncate_only,
redirected, nredirected,
nowdead, ndead,
nowunused, nunused);
@@ -8819,7 +8830,7 @@ heap_xlog_prune(XLogReaderState *record)
UnlockReleaseBuffer(buffer);
/*
- * After pruning records from a page, it's useful to update the FSM
+ * After modifying records on a page, it's useful to update the FSM
* about it, as it may cause the page become target for insertions
* later even if vacuum decides not to visit it (which is possible if
* gets marked all-visible.)
@@ -8831,78 +8842,6 @@ heap_xlog_prune(XLogReaderState *record)
}
}
-/*
- * Handles XLOG_HEAP2_VACUUM record type.
- *
- * Acquires an ordinary exclusive lock only.
- */
-static void
-heap_xlog_vacuum(XLogReaderState *record)
-{
- XLogRecPtr lsn = record->EndRecPtr;
- xl_heap_vacuum *xlrec = (xl_heap_vacuum *) XLogRecGetData(record);
- Buffer buffer;
- BlockNumber blkno;
- XLogRedoAction action;
-
- /*
- * If we have a full-page image, restore it (without using a cleanup lock)
- * and we're done.
- */
- action = XLogReadBufferForRedoExtended(record, 0, RBM_NORMAL, false,
- &buffer);
- if (action == BLK_NEEDS_REDO)
- {
- Page page = (Page) BufferGetPage(buffer);
- OffsetNumber *nowunused;
- Size datalen;
- OffsetNumber *offnum;
-
- nowunused = (OffsetNumber *) XLogRecGetBlockData(record, 0, &datalen);
-
- /* Shouldn't be a record unless there's something to do */
- Assert(xlrec->nunused > 0);
-
- /* Update all now-unused line pointers */
- offnum = nowunused;
- for (int i = 0; i < xlrec->nunused; i++)
- {
- OffsetNumber off = *offnum++;
- ItemId lp = PageGetItemId(page, off);
-
- Assert(ItemIdIsDead(lp) && !ItemIdHasStorage(lp));
- ItemIdSetUnused(lp);
- }
-
- /* Attempt to truncate line pointer array now */
- PageTruncateLinePointerArray(page);
-
- PageSetLSN(page, lsn);
- MarkBufferDirty(buffer);
- }
-
- if (BufferIsValid(buffer))
- {
- Size freespace = PageGetHeapFreeSpace(BufferGetPage(buffer));
- RelFileLocator rlocator;
-
- XLogRecGetBlockTag(record, 0, &rlocator, NULL, &blkno);
-
- UnlockReleaseBuffer(buffer);
-
- /*
- * After vacuuming LP_DEAD items from a page, it's useful to update
- * the FSM about it, as it may cause the page become target for
- * insertions later even if vacuum decides not to visit it (which is
- * possible if gets marked all-visible.)
- *
- * Do this regardless of a full-page image being applied, since the
- * FSM data is not in the page anyway.
- */
- XLogRecordPageWithFreeSpace(rlocator, blkno, freespace);
- }
-}
-
/*
* Replay XLOG_HEAP2_VISIBLE record.
*
@@ -9943,9 +9882,6 @@ heap2_redo(XLogReaderState *record)
case XLOG_HEAP2_PRUNE:
heap_xlog_prune(record);
break;
- case XLOG_HEAP2_VACUUM:
- heap_xlog_vacuum(record);
- break;
case XLOG_HEAP2_VISIBLE:
heap_xlog_visible(record);
break;
diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c
index 19b50931b90..135fe2dba3e 100644
--- a/src/backend/access/heap/pruneheap.c
+++ b/src/backend/access/heap/pruneheap.c
@@ -601,7 +601,7 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
*/
if (do_prune)
{
- heap_page_prune_execute(buffer,
+ heap_page_prune_execute(buffer, false,
prstate.redirected, prstate.nredirected,
prstate.nowdead, prstate.ndead,
prstate.nowunused, prstate.nunused);
@@ -668,12 +668,16 @@ log_heap_prune_and_freeze(Relation relation, Buffer buffer,
OffsetNumber offsets[MaxHeapTuplesPerPage];
bool do_freeze = presult->nfrozen > 0;
+ xlrec.flags = 0;
+
xlrec.isCatalogRel = RelationIsAccessibleInLogicalDecoding(relation);
xlrec.nredirected = prstate->nredirected;
xlrec.ndead = prstate->ndead;
xlrec.nunused = prstate->nunused;
xlrec.nplans = 0;
+ xlrec.flags |= XLHP_HAS_CONFLICT_HORIZON;
+
/*
* The snapshotConflictHorizon for the whole record should be the most
* conservative of all the horizons calculated for any of the possible
@@ -1149,7 +1153,7 @@ heap_prune_record_unused(PruneState *prstate, OffsetNumber offnum)
* cleanup lock on the buffer.
*/
void
-heap_page_prune_execute(Buffer buffer,
+heap_page_prune_execute(Buffer buffer, bool lp_truncate_only,
OffsetNumber *redirected, int nredirected,
OffsetNumber *nowdead, int ndead,
OffsetNumber *nowunused, int nunused)
@@ -1171,6 +1175,7 @@ heap_page_prune_execute(Buffer buffer,
ItemId tolp PG_USED_FOR_ASSERTS_ONLY;
#ifdef USE_ASSERT_CHECKING
+ Assert(!lp_truncate_only);
/*
* Any existing item that we set as an LP_REDIRECT (any 'from' item)
@@ -1226,6 +1231,7 @@ heap_page_prune_execute(Buffer buffer,
ItemId lp = PageGetItemId(page, off);
#ifdef USE_ASSERT_CHECKING
+ Assert(!lp_truncate_only);
/*
* An LP_DEAD line pointer must be left behind when the original item
@@ -1259,23 +1265,29 @@ heap_page_prune_execute(Buffer buffer,
#ifdef USE_ASSERT_CHECKING
- /*
- * When heap_page_prune_and_freeze() was called, mark_unused_now may
- * have been passed as true, which allows would-be LP_DEAD items to be
- * made LP_UNUSED instead. This is only possible if the relation has
- * no indexes. If there are any dead items, then mark_unused_now was
- * not true and every item being marked LP_UNUSED must refer to a
- * heap-only tuple.
- */
- if (ndead > 0)
+ if (lp_truncate_only)
{
- Assert(ItemIdHasStorage(lp) && ItemIdIsNormal(lp));
- htup = (HeapTupleHeader) PageGetItem(page, lp);
- Assert(HeapTupleHeaderIsHeapOnly(htup));
+ /* Setting LP_DEAD to LP_UNUSED in vacuum's second pass */
+ Assert(ItemIdIsDead(lp) && !ItemIdHasStorage(lp));
}
else
{
- Assert(ItemIdIsUsed(lp));
+ /*
+ * When heap_page_prune_and_freeze() was called, mark_unused_now
+ * may have been passed as true, which allows would-be LP_DEAD
+ * items to be made LP_UNUSED instead. This is only possible if
+ * the relation has no indexes. If there are any dead items, then
+ * mark_unused_now was not true and every item being marked
+ * LP_UNUSED must refer to a heap-only tuple.
+ */
+ if (ndead > 0)
+ {
+ Assert(ItemIdHasStorage(lp) && ItemIdIsNormal(lp));
+ htup = (HeapTupleHeader) PageGetItem(page, lp);
+ Assert(HeapTupleHeaderIsHeapOnly(htup));
+ }
+ else
+ Assert(ItemIdIsUsed(lp));
}
#endif
@@ -1283,17 +1295,22 @@ heap_page_prune_execute(Buffer buffer,
ItemIdSetUnused(lp);
}
- /*
- * Finally, repair any fragmentation, and update the page's hint bit about
- * whether it has free pointers.
- */
- PageRepairFragmentation(page);
+ if (lp_truncate_only)
+ PageTruncateLinePointerArray(page);
+ else
+ {
+ /*
+ * Finally, repair any fragmentation, and update the page's hint bit
+ * about whether it has free pointers.
+ */
+ PageRepairFragmentation(page);
- /*
- * Now that the page has been modified, assert that redirect items still
- * point to valid targets.
- */
- page_verify_redirects(page);
+ /*
+ * Now that the page has been modified, assert that redirect items
+ * still point to valid targets.
+ */
+ page_verify_redirects(page);
+ }
}
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index c4553a4159c..9dfb56475cf 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -2394,18 +2394,24 @@ lazy_vacuum_heap_page(LVRelState *vacrel, BlockNumber blkno, Buffer buffer,
/* XLOG stuff */
if (RelationNeedsWAL(vacrel->rel))
{
- xl_heap_vacuum xlrec;
+ xl_heap_prune xlrec;
XLogRecPtr recptr;
+ xlrec.flags = XLHP_LP_TRUNCATE_ONLY;
+ xlrec.snapshotConflictHorizon = InvalidTransactionId;
+ xlrec.nplans = 0;
+ xlrec.nredirected = 0;
+ xlrec.ndead = 0;
xlrec.nunused = nunused;
+ xlrec.isCatalogRel = RelationIsAccessibleInLogicalDecoding(vacrel->rel);
XLogBeginInsert();
- XLogRegisterData((char *) &xlrec, SizeOfHeapVacuum);
+ XLogRegisterData((char *) &xlrec, SizeOfHeapPrune);
XLogRegisterBuffer(0, buffer, REGBUF_STANDARD);
XLogRegisterBufData(0, (char *) unused, nunused * sizeof(OffsetNumber));
- recptr = XLogInsert(RM_HEAP2_ID, XLOG_HEAP2_VACUUM);
+ recptr = XLogInsert(RM_HEAP2_ID, XLOG_HEAP2_PRUNE);
PageSetLSN(page, recptr);
}
diff --git a/src/backend/access/rmgrdesc/heapdesc.c b/src/backend/access/rmgrdesc/heapdesc.c
index 9f0a0341d40..ea03f902fc4 100644
--- a/src/backend/access/rmgrdesc/heapdesc.c
+++ b/src/backend/access/rmgrdesc/heapdesc.c
@@ -242,23 +242,6 @@ heap2_desc(StringInfo buf, XLogReaderState *record)
}
}
}
- else if (info == XLOG_HEAP2_VACUUM)
- {
- xl_heap_vacuum *xlrec = (xl_heap_vacuum *) rec;
-
- appendStringInfo(buf, "nunused: %u", xlrec->nunused);
-
- if (XLogRecHasBlockData(record, 0))
- {
- OffsetNumber *nowunused;
-
- nowunused = (OffsetNumber *) XLogRecGetBlockData(record, 0, NULL);
-
- appendStringInfoString(buf, ", unused:");
- array_desc(buf, nowunused, sizeof(OffsetNumber), xlrec->nunused,
- &offset_elem_desc, NULL);
- }
- }
else if (info == XLOG_HEAP2_VISIBLE)
{
xl_heap_visible *xlrec = (xl_heap_visible *) rec;
@@ -360,9 +343,6 @@ heap2_identify(uint8 info)
case XLOG_HEAP2_PRUNE:
id = "PRUNE";
break;
- case XLOG_HEAP2_VACUUM:
- id = "VACUUM";
- break;
case XLOG_HEAP2_VISIBLE:
id = "VISIBLE";
break;
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index f77051572fd..38d1bdd825e 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -446,7 +446,6 @@ heap2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
* interested in.
*/
case XLOG_HEAP2_PRUNE:
- case XLOG_HEAP2_VACUUM:
case XLOG_HEAP2_VISIBLE:
case XLOG_HEAP2_LOCK_UPDATED:
break;
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 321a46185e1..d5cb8f99cac 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -349,7 +349,7 @@ extern void heap_page_prune_and_freeze(Relation relation, Buffer buffer,
HeapPageFreeze *pagefrz,
PruneFreezeResult *presult,
OffsetNumber *off_loc);
-extern void heap_page_prune_execute(Buffer buffer,
+extern void heap_page_prune_execute(Buffer buffer, bool lp_truncate_only,
OffsetNumber *redirected, int nredirected,
OffsetNumber *nowdead, int ndead,
OffsetNumber *nowunused, int nunused);
diff --git a/src/include/access/heapam_xlog.h b/src/include/access/heapam_xlog.h
index fe4a8ff0620..2393540cf68 100644
--- a/src/include/access/heapam_xlog.h
+++ b/src/include/access/heapam_xlog.h
@@ -52,11 +52,10 @@
*/
#define XLOG_HEAP2_REWRITE 0x00
#define XLOG_HEAP2_PRUNE 0x10
-#define XLOG_HEAP2_VACUUM 0x20
-#define XLOG_HEAP2_VISIBLE 0x30
-#define XLOG_HEAP2_MULTI_INSERT 0x40
-#define XLOG_HEAP2_LOCK_UPDATED 0x50
-#define XLOG_HEAP2_NEW_CID 0x60
+#define XLOG_HEAP2_VISIBLE 0x20
+#define XLOG_HEAP2_MULTI_INSERT 0x30
+#define XLOG_HEAP2_LOCK_UPDATED 0x40
+#define XLOG_HEAP2_NEW_CID 0x50
/*
* xl_heap_insert/xl_heap_multi_insert flag values, 8 bits are available.
@@ -266,6 +265,7 @@ typedef struct xl_heap_freeze_plan
*/
typedef struct xl_heap_prune
{
+ uint8 flags;
TransactionId snapshotConflictHorizon;
uint16 nplans;
uint16 nredirected;
@@ -288,19 +288,22 @@ typedef struct xl_heap_prune
#define SizeOfHeapPrune (offsetof(xl_heap_prune, isCatalogRel) + sizeof(bool))
+/* Flags for xl_heap_prune */
+
/*
- * The vacuum page record is similar to the prune record, but can only mark
- * already LP_DEAD items LP_UNUSED (during VACUUM's second heap pass)
- *
- * Acquires an ordinary exclusive lock only.
+ * During vacuum's second pass which sets LP_DEAD items LP_UNUSED, we will only
+ * truncate the line pointer array, not call PageRepairFragmentation. We need
+ * this flag to differentiate what kind of lock (exclusive or cleanup) to take
+ * on the buffer and whether to call PageTruncateLinePointerArray() or
+ * PageRepairFragementation().
*/
-typedef struct xl_heap_vacuum
-{
- uint16 nunused;
- /* OFFSET NUMBERS are in the block reference 0 */
-} xl_heap_vacuum;
+#define XLHP_LP_TRUNCATE_ONLY (1 << 1)
-#define SizeOfHeapVacuum (offsetof(xl_heap_vacuum, nunused) + sizeof(uint16))
+/*
+ * Vacuum's first pass and on-access pruning may need to include a snapshot
+ * conflict horizon.
+ */
+#define XLHP_HAS_CONFLICT_HORIZON (1 << 2)
/* flags for infobits_set */
#define XLHL_XMAX_IS_MULTI 0x01
--
2.40.1
--tez7m2a73jtztiij
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
filename="v4-0015-Set-hastup-in-heap_page_prune.patch"
^ permalink raw reply [nested|flat] 10+ messages in thread
* [PATCH v4 14/19] Vacuum second pass emits XLOG_HEAP2_PRUNE record
@ 2024-03-19 22:50 Melanie Plageman <melanieplageman@gmail.com>
0 siblings, 0 replies; 10+ messages in thread
From: Melanie Plageman @ 2024-03-19 22:50 UTC (permalink / raw)
Remove the XLOG_HEAP2_VACUUM record and update vacuum's second pass to
emit a XLOG_HEAP2_PRUNE record. This temporarily wastes some space but a
future commit will streamline xl_heap_prune and ensure that no unused
members are included in the WAL record.
---
src/backend/access/heap/heapam.c | 94 ++++--------------------
src/backend/access/heap/pruneheap.c | 67 ++++++++++-------
src/backend/access/heap/vacuumlazy.c | 12 ++-
src/backend/access/rmgrdesc/heapdesc.c | 20 -----
src/backend/replication/logical/decode.c | 1 -
src/include/access/heapam.h | 2 +-
src/include/access/heapam_xlog.h | 33 +++++----
7 files changed, 85 insertions(+), 144 deletions(-)
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 532868039d5..16bab55ba02 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -8717,23 +8717,34 @@ heap_xlog_prune(XLogReaderState *record)
BlockNumber blkno;
XLogRedoAction action;
bool get_cleanup_lock;
+ bool lp_truncate_only;
XLogRecGetBlockTag(record, 0, &rlocator, NULL, &blkno);
+ lp_truncate_only = xlrec->flags & XLHP_LP_TRUNCATE_ONLY;
+
/*
* If there are dead, redirected, or unused items set unused by
* heap_page_prune_and_freeze(), heap_page_prune_execute() will call
* PageRepairFragementation() which expects a full cleanup lock.
*/
get_cleanup_lock = xlrec->nredirected > 0 ||
- xlrec->ndead > 0 || xlrec->nunused > 0;
+ xlrec->ndead > 0 ||
+ (xlrec->nunused > 0 && !lp_truncate_only);
+
+ if (lp_truncate_only)
+ {
+ Assert(xlrec->nredirected == 0);
+ Assert(xlrec->ndead == 0);
+ Assert(xlrec->nunused > 0);
+ }
/*
* We are either about to remove tuples or freeze them. In Hot Standby
* mode, ensure that there's no queries running for which any removed
* tuples are still visible or which consider the frozen xids as running.
*/
- if (InHotStandby)
+ if (xlrec->flags & XLHP_HAS_CONFLICT_HORIZON && InHotStandby)
ResolveRecoveryConflictWithSnapshot(xlrec->snapshotConflictHorizon,
xlrec->isCatalogRel,
rlocator);
@@ -8772,7 +8783,7 @@ heap_xlog_prune(XLogReaderState *record)
/* Update all line pointers per the record, and repair fragmentation */
if (nredirected > 0 || ndead > 0 || nunused > 0)
- heap_page_prune_execute(buffer,
+ heap_page_prune_execute(buffer, lp_truncate_only,
redirected, nredirected,
nowdead, ndead,
nowunused, nunused);
@@ -8819,7 +8830,7 @@ heap_xlog_prune(XLogReaderState *record)
UnlockReleaseBuffer(buffer);
/*
- * After pruning records from a page, it's useful to update the FSM
+ * After modifying records on a page, it's useful to update the FSM
* about it, as it may cause the page become target for insertions
* later even if vacuum decides not to visit it (which is possible if
* gets marked all-visible.)
@@ -8831,78 +8842,6 @@ heap_xlog_prune(XLogReaderState *record)
}
}
-/*
- * Handles XLOG_HEAP2_VACUUM record type.
- *
- * Acquires an ordinary exclusive lock only.
- */
-static void
-heap_xlog_vacuum(XLogReaderState *record)
-{
- XLogRecPtr lsn = record->EndRecPtr;
- xl_heap_vacuum *xlrec = (xl_heap_vacuum *) XLogRecGetData(record);
- Buffer buffer;
- BlockNumber blkno;
- XLogRedoAction action;
-
- /*
- * If we have a full-page image, restore it (without using a cleanup lock)
- * and we're done.
- */
- action = XLogReadBufferForRedoExtended(record, 0, RBM_NORMAL, false,
- &buffer);
- if (action == BLK_NEEDS_REDO)
- {
- Page page = (Page) BufferGetPage(buffer);
- OffsetNumber *nowunused;
- Size datalen;
- OffsetNumber *offnum;
-
- nowunused = (OffsetNumber *) XLogRecGetBlockData(record, 0, &datalen);
-
- /* Shouldn't be a record unless there's something to do */
- Assert(xlrec->nunused > 0);
-
- /* Update all now-unused line pointers */
- offnum = nowunused;
- for (int i = 0; i < xlrec->nunused; i++)
- {
- OffsetNumber off = *offnum++;
- ItemId lp = PageGetItemId(page, off);
-
- Assert(ItemIdIsDead(lp) && !ItemIdHasStorage(lp));
- ItemIdSetUnused(lp);
- }
-
- /* Attempt to truncate line pointer array now */
- PageTruncateLinePointerArray(page);
-
- PageSetLSN(page, lsn);
- MarkBufferDirty(buffer);
- }
-
- if (BufferIsValid(buffer))
- {
- Size freespace = PageGetHeapFreeSpace(BufferGetPage(buffer));
- RelFileLocator rlocator;
-
- XLogRecGetBlockTag(record, 0, &rlocator, NULL, &blkno);
-
- UnlockReleaseBuffer(buffer);
-
- /*
- * After vacuuming LP_DEAD items from a page, it's useful to update
- * the FSM about it, as it may cause the page become target for
- * insertions later even if vacuum decides not to visit it (which is
- * possible if gets marked all-visible.)
- *
- * Do this regardless of a full-page image being applied, since the
- * FSM data is not in the page anyway.
- */
- XLogRecordPageWithFreeSpace(rlocator, blkno, freespace);
- }
-}
-
/*
* Replay XLOG_HEAP2_VISIBLE record.
*
@@ -9943,9 +9882,6 @@ heap2_redo(XLogReaderState *record)
case XLOG_HEAP2_PRUNE:
heap_xlog_prune(record);
break;
- case XLOG_HEAP2_VACUUM:
- heap_xlog_vacuum(record);
- break;
case XLOG_HEAP2_VISIBLE:
heap_xlog_visible(record);
break;
diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c
index 19b50931b90..135fe2dba3e 100644
--- a/src/backend/access/heap/pruneheap.c
+++ b/src/backend/access/heap/pruneheap.c
@@ -601,7 +601,7 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
*/
if (do_prune)
{
- heap_page_prune_execute(buffer,
+ heap_page_prune_execute(buffer, false,
prstate.redirected, prstate.nredirected,
prstate.nowdead, prstate.ndead,
prstate.nowunused, prstate.nunused);
@@ -668,12 +668,16 @@ log_heap_prune_and_freeze(Relation relation, Buffer buffer,
OffsetNumber offsets[MaxHeapTuplesPerPage];
bool do_freeze = presult->nfrozen > 0;
+ xlrec.flags = 0;
+
xlrec.isCatalogRel = RelationIsAccessibleInLogicalDecoding(relation);
xlrec.nredirected = prstate->nredirected;
xlrec.ndead = prstate->ndead;
xlrec.nunused = prstate->nunused;
xlrec.nplans = 0;
+ xlrec.flags |= XLHP_HAS_CONFLICT_HORIZON;
+
/*
* The snapshotConflictHorizon for the whole record should be the most
* conservative of all the horizons calculated for any of the possible
@@ -1149,7 +1153,7 @@ heap_prune_record_unused(PruneState *prstate, OffsetNumber offnum)
* cleanup lock on the buffer.
*/
void
-heap_page_prune_execute(Buffer buffer,
+heap_page_prune_execute(Buffer buffer, bool lp_truncate_only,
OffsetNumber *redirected, int nredirected,
OffsetNumber *nowdead, int ndead,
OffsetNumber *nowunused, int nunused)
@@ -1171,6 +1175,7 @@ heap_page_prune_execute(Buffer buffer,
ItemId tolp PG_USED_FOR_ASSERTS_ONLY;
#ifdef USE_ASSERT_CHECKING
+ Assert(!lp_truncate_only);
/*
* Any existing item that we set as an LP_REDIRECT (any 'from' item)
@@ -1226,6 +1231,7 @@ heap_page_prune_execute(Buffer buffer,
ItemId lp = PageGetItemId(page, off);
#ifdef USE_ASSERT_CHECKING
+ Assert(!lp_truncate_only);
/*
* An LP_DEAD line pointer must be left behind when the original item
@@ -1259,23 +1265,29 @@ heap_page_prune_execute(Buffer buffer,
#ifdef USE_ASSERT_CHECKING
- /*
- * When heap_page_prune_and_freeze() was called, mark_unused_now may
- * have been passed as true, which allows would-be LP_DEAD items to be
- * made LP_UNUSED instead. This is only possible if the relation has
- * no indexes. If there are any dead items, then mark_unused_now was
- * not true and every item being marked LP_UNUSED must refer to a
- * heap-only tuple.
- */
- if (ndead > 0)
+ if (lp_truncate_only)
{
- Assert(ItemIdHasStorage(lp) && ItemIdIsNormal(lp));
- htup = (HeapTupleHeader) PageGetItem(page, lp);
- Assert(HeapTupleHeaderIsHeapOnly(htup));
+ /* Setting LP_DEAD to LP_UNUSED in vacuum's second pass */
+ Assert(ItemIdIsDead(lp) && !ItemIdHasStorage(lp));
}
else
{
- Assert(ItemIdIsUsed(lp));
+ /*
+ * When heap_page_prune_and_freeze() was called, mark_unused_now
+ * may have been passed as true, which allows would-be LP_DEAD
+ * items to be made LP_UNUSED instead. This is only possible if
+ * the relation has no indexes. If there are any dead items, then
+ * mark_unused_now was not true and every item being marked
+ * LP_UNUSED must refer to a heap-only tuple.
+ */
+ if (ndead > 0)
+ {
+ Assert(ItemIdHasStorage(lp) && ItemIdIsNormal(lp));
+ htup = (HeapTupleHeader) PageGetItem(page, lp);
+ Assert(HeapTupleHeaderIsHeapOnly(htup));
+ }
+ else
+ Assert(ItemIdIsUsed(lp));
}
#endif
@@ -1283,17 +1295,22 @@ heap_page_prune_execute(Buffer buffer,
ItemIdSetUnused(lp);
}
- /*
- * Finally, repair any fragmentation, and update the page's hint bit about
- * whether it has free pointers.
- */
- PageRepairFragmentation(page);
+ if (lp_truncate_only)
+ PageTruncateLinePointerArray(page);
+ else
+ {
+ /*
+ * Finally, repair any fragmentation, and update the page's hint bit
+ * about whether it has free pointers.
+ */
+ PageRepairFragmentation(page);
- /*
- * Now that the page has been modified, assert that redirect items still
- * point to valid targets.
- */
- page_verify_redirects(page);
+ /*
+ * Now that the page has been modified, assert that redirect items
+ * still point to valid targets.
+ */
+ page_verify_redirects(page);
+ }
}
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index c4553a4159c..9dfb56475cf 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -2394,18 +2394,24 @@ lazy_vacuum_heap_page(LVRelState *vacrel, BlockNumber blkno, Buffer buffer,
/* XLOG stuff */
if (RelationNeedsWAL(vacrel->rel))
{
- xl_heap_vacuum xlrec;
+ xl_heap_prune xlrec;
XLogRecPtr recptr;
+ xlrec.flags = XLHP_LP_TRUNCATE_ONLY;
+ xlrec.snapshotConflictHorizon = InvalidTransactionId;
+ xlrec.nplans = 0;
+ xlrec.nredirected = 0;
+ xlrec.ndead = 0;
xlrec.nunused = nunused;
+ xlrec.isCatalogRel = RelationIsAccessibleInLogicalDecoding(vacrel->rel);
XLogBeginInsert();
- XLogRegisterData((char *) &xlrec, SizeOfHeapVacuum);
+ XLogRegisterData((char *) &xlrec, SizeOfHeapPrune);
XLogRegisterBuffer(0, buffer, REGBUF_STANDARD);
XLogRegisterBufData(0, (char *) unused, nunused * sizeof(OffsetNumber));
- recptr = XLogInsert(RM_HEAP2_ID, XLOG_HEAP2_VACUUM);
+ recptr = XLogInsert(RM_HEAP2_ID, XLOG_HEAP2_PRUNE);
PageSetLSN(page, recptr);
}
diff --git a/src/backend/access/rmgrdesc/heapdesc.c b/src/backend/access/rmgrdesc/heapdesc.c
index 9f0a0341d40..ea03f902fc4 100644
--- a/src/backend/access/rmgrdesc/heapdesc.c
+++ b/src/backend/access/rmgrdesc/heapdesc.c
@@ -242,23 +242,6 @@ heap2_desc(StringInfo buf, XLogReaderState *record)
}
}
}
- else if (info == XLOG_HEAP2_VACUUM)
- {
- xl_heap_vacuum *xlrec = (xl_heap_vacuum *) rec;
-
- appendStringInfo(buf, "nunused: %u", xlrec->nunused);
-
- if (XLogRecHasBlockData(record, 0))
- {
- OffsetNumber *nowunused;
-
- nowunused = (OffsetNumber *) XLogRecGetBlockData(record, 0, NULL);
-
- appendStringInfoString(buf, ", unused:");
- array_desc(buf, nowunused, sizeof(OffsetNumber), xlrec->nunused,
- &offset_elem_desc, NULL);
- }
- }
else if (info == XLOG_HEAP2_VISIBLE)
{
xl_heap_visible *xlrec = (xl_heap_visible *) rec;
@@ -360,9 +343,6 @@ heap2_identify(uint8 info)
case XLOG_HEAP2_PRUNE:
id = "PRUNE";
break;
- case XLOG_HEAP2_VACUUM:
- id = "VACUUM";
- break;
case XLOG_HEAP2_VISIBLE:
id = "VISIBLE";
break;
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index f77051572fd..38d1bdd825e 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -446,7 +446,6 @@ heap2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
* interested in.
*/
case XLOG_HEAP2_PRUNE:
- case XLOG_HEAP2_VACUUM:
case XLOG_HEAP2_VISIBLE:
case XLOG_HEAP2_LOCK_UPDATED:
break;
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 321a46185e1..d5cb8f99cac 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -349,7 +349,7 @@ extern void heap_page_prune_and_freeze(Relation relation, Buffer buffer,
HeapPageFreeze *pagefrz,
PruneFreezeResult *presult,
OffsetNumber *off_loc);
-extern void heap_page_prune_execute(Buffer buffer,
+extern void heap_page_prune_execute(Buffer buffer, bool lp_truncate_only,
OffsetNumber *redirected, int nredirected,
OffsetNumber *nowdead, int ndead,
OffsetNumber *nowunused, int nunused);
diff --git a/src/include/access/heapam_xlog.h b/src/include/access/heapam_xlog.h
index fe4a8ff0620..2393540cf68 100644
--- a/src/include/access/heapam_xlog.h
+++ b/src/include/access/heapam_xlog.h
@@ -52,11 +52,10 @@
*/
#define XLOG_HEAP2_REWRITE 0x00
#define XLOG_HEAP2_PRUNE 0x10
-#define XLOG_HEAP2_VACUUM 0x20
-#define XLOG_HEAP2_VISIBLE 0x30
-#define XLOG_HEAP2_MULTI_INSERT 0x40
-#define XLOG_HEAP2_LOCK_UPDATED 0x50
-#define XLOG_HEAP2_NEW_CID 0x60
+#define XLOG_HEAP2_VISIBLE 0x20
+#define XLOG_HEAP2_MULTI_INSERT 0x30
+#define XLOG_HEAP2_LOCK_UPDATED 0x40
+#define XLOG_HEAP2_NEW_CID 0x50
/*
* xl_heap_insert/xl_heap_multi_insert flag values, 8 bits are available.
@@ -266,6 +265,7 @@ typedef struct xl_heap_freeze_plan
*/
typedef struct xl_heap_prune
{
+ uint8 flags;
TransactionId snapshotConflictHorizon;
uint16 nplans;
uint16 nredirected;
@@ -288,19 +288,22 @@ typedef struct xl_heap_prune
#define SizeOfHeapPrune (offsetof(xl_heap_prune, isCatalogRel) + sizeof(bool))
+/* Flags for xl_heap_prune */
+
/*
- * The vacuum page record is similar to the prune record, but can only mark
- * already LP_DEAD items LP_UNUSED (during VACUUM's second heap pass)
- *
- * Acquires an ordinary exclusive lock only.
+ * During vacuum's second pass which sets LP_DEAD items LP_UNUSED, we will only
+ * truncate the line pointer array, not call PageRepairFragmentation. We need
+ * this flag to differentiate what kind of lock (exclusive or cleanup) to take
+ * on the buffer and whether to call PageTruncateLinePointerArray() or
+ * PageRepairFragementation().
*/
-typedef struct xl_heap_vacuum
-{
- uint16 nunused;
- /* OFFSET NUMBERS are in the block reference 0 */
-} xl_heap_vacuum;
+#define XLHP_LP_TRUNCATE_ONLY (1 << 1)
-#define SizeOfHeapVacuum (offsetof(xl_heap_vacuum, nunused) + sizeof(uint16))
+/*
+ * Vacuum's first pass and on-access pruning may need to include a snapshot
+ * conflict horizon.
+ */
+#define XLHP_HAS_CONFLICT_HORIZON (1 << 2)
/* flags for infobits_set */
#define XLHL_XMAX_IS_MULTI 0x01
--
2.40.1
--tez7m2a73jtztiij
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
filename="v4-0015-Set-hastup-in-heap_page_prune.patch"
^ permalink raw reply [nested|flat] 10+ messages in thread
* MERGE behavior with REPEATABLE READ isolation level
@ 2026-02-15 01:20 Alexander Korotkov <aekorotkov@gmail.com>
2026-02-15 01:22 ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
0 siblings, 1 reply; 10+ messages in thread
From: Alexander Korotkov @ 2026-02-15 01:20 UTC (permalink / raw)
To: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
Hi hackers,
I found it strange that ExecMergeMatched() checks for
IsolationUsesXactSnapshot() in the TM_Deleted case, but not in the
TM_Updated case. Indeed, EPQ works on the repeatable read isolation level!
s1# create table test (id int primary key, val int);
s1# insert into test values (1,0);
s2# begin;
s2# update test set val = val + 100;
s1# MERGE INTO test t USING (VALUES (1, 100)) AS s (id, inc)
ON t.id = s.id
WHEN MATCHED THEN
UPDATE SET val = t.val + s.inc
WHEN NOT MATCHED THEN
INSERT (id, val) VALUES (s.id, s.inc);
(waiting ...)
s2# commit;
s1# MERGE 1
s1# select * from test;
id | val
----+-----
1 | 200
(1 row)
Quick search didn't give an explanation of this to me. I also don't see
this covered by an isolation test. Is it an intended behavior or a bug?
(Sorry for buzz if this was discussed before)
------
Regards,
Alexander Korotkov
Supabase
^ permalink raw reply [nested|flat] 10+ messages in thread
* Re: MERGE behavior with REPEATABLE READ isolation level
2026-02-15 01:20 MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
@ 2026-02-15 01:22 ` Alexander Korotkov <aekorotkov@gmail.com>
2026-02-24 03:12 ` Re: MERGE behavior with REPEATABLE READ isolation level Tender Wang <tndrwang@gmail.com>
0 siblings, 1 reply; 10+ messages in thread
From: Alexander Korotkov @ 2026-02-15 01:22 UTC (permalink / raw)
To: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
On Sun, Feb 15, 2026 at 3:20 AM Alexander Korotkov <aekorotkov@gmail.com>
wrote:
> Hi hackers,
>
> I found it strange that ExecMergeMatched() checks for
> IsolationUsesXactSnapshot() in the TM_Deleted case, but not in the
> TM_Updated case. Indeed, EPQ works on the repeatable read isolation level!
>
> s1# create table test (id int primary key, val int);
> s1# insert into test values (1,0);
>
> s2# begin;
> s2# update test set val = val + 100;
>
> s1# MERGE INTO test t USING (VALUES (1, 100)) AS s (id, inc)
> ON t.id = s.id
> WHEN MATCHED THEN
> UPDATE SET val = t.val + s.inc
> WHEN NOT MATCHED THEN
> INSERT (id, val) VALUES (s.id, s.inc);
> (waiting ...)
>
> s2# commit;
> s1# MERGE 1
> s1# select * from test;
> id | val
> ----+-----
> 1 | 200
> (1 row)
>
Oh, sorry I missed the begin statement for s1. The complete case should
look like this.
s1# create table test (id int primary key, val int);
s1# insert into test values (1,0);
s2# begin;
s2# update test set val = val + 100;
s1# begin isolation level repeatable read;
s1# MERGE INTO test t USING (VALUES (1, 100)) AS s (id, inc)
ON t.id = s.id
WHEN MATCHED THEN
UPDATE SET val = t.val + s.inc
WHEN NOT MATCHED THEN
INSERT (id, val) VALUES (s.id, s.inc);
(waiting ...)
s2# commit;
s1# MERGE 1
s1# select * from test;
id | val
----+-----
1 | 200
(1 row)
------
Regards,
Alexander Korotkov
Supabase
^ permalink raw reply [nested|flat] 10+ messages in thread
* Re: MERGE behavior with REPEATABLE READ isolation level
2026-02-15 01:20 MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
2026-02-15 01:22 ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
@ 2026-02-24 03:12 ` Tender Wang <tndrwang@gmail.com>
2026-02-24 09:09 ` Re: MERGE behavior with REPEATABLE READ isolation level Dean Rasheed <dean.a.rasheed@gmail.com>
2026-02-26 17:03 ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
0 siblings, 2 replies; 10+ messages in thread
From: Tender Wang @ 2026-02-24 03:12 UTC (permalink / raw)
To: Alexander Korotkov <aekorotkov@gmail.com>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
Hi Alexander,
Alexander Korotkov <aekorotkov@gmail.com> 于2026年2月15日周日 09:23写道:
> Oh, sorry I missed the begin statement for s1. The complete case should look like this.
>
> s1# create table test (id int primary key, val int);
> s1# insert into test values (1,0);
>
> s2# begin;
> s2# update test set val = val + 100;
>
> s1# begin isolation level repeatable read;
> s1# MERGE INTO test t USING (VALUES (1, 100)) AS s (id, inc)
> ON t.id = s.id
> WHEN MATCHED THEN
> UPDATE SET val = t.val + s.inc
> WHEN NOT MATCHED THEN
> INSERT (id, val) VALUES (s.id, s.inc);
> (waiting ...)
>
> s2# commit;
>
> s1# MERGE 1
> s1# select * from test;
> id | val
> ----+-----
> 1 | 200
> (1 row)
>
I tried "update test set val = val + 100;" but the SQL reported a
"could not serialize access due to concurrent update" error.
It seems that the MERGE command should behave identically to UPDATE
when performing a match action.
I wrote a fix patch and attached it, and added your test case, too.
--
Thanks,
Tender Wang
From 9f7267cdc3bf5b34eb95686f288d39b5766142ac Mon Sep 17 00:00:00 2001
From: Tender Wang <tndrwang@gmail.com>
Date: Tue, 24 Feb 2026 11:04:26 +0800
Subject: [PATCH] Fix MERGE match do update in RR isolation level.
---
src/backend/executor/nodeModifyTable.c | 4 ++
.../expected/merge-match-do-update.out | 26 +++++++++++
src/test/isolation/isolation_schedule | 1 +
.../specs/merge-match-do-update.spec | 43 +++++++++++++++++++
4 files changed, 74 insertions(+)
create mode 100644 src/test/isolation/expected/merge-match-do-update.out
create mode 100644 src/test/isolation/specs/merge-match-do-update.spec
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 793c76d4f82..8d8c9fb54e3 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -3547,6 +3547,10 @@ lmerge_matched:
*inputslot;
LockTupleMode lockmode;
+ if (IsolationUsesXactSnapshot())
+ ereport(ERROR,
+ (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
+ errmsg("could not serialize access due to concurrent update")));
/*
* The target tuple was concurrently updated by some other
* transaction. If we are currently processing a MATCHED
diff --git a/src/test/isolation/expected/merge-match-do-update.out b/src/test/isolation/expected/merge-match-do-update.out
new file mode 100644
index 00000000000..c2cde0de606
--- /dev/null
+++ b/src/test/isolation/expected/merge-match-do-update.out
@@ -0,0 +1,26 @@
+Parsed test spec with 2 sessions
+
+starting permutation: b2 update2 b1 merge1 c2 select1 c1
+step b2: BEGIN;
+step update2:
+ UPDATE test SET val = val + 100;
+
+step b1:
+ BEGIN ISOLATION LEVEL REPEATABLE READ;
+
+step merge1:
+ MERGE INTO test t USING (VALUES (1, 100)) AS s (id, inc)
+ ON t.id = s.id
+ WHEN MATCHED THEN
+ UPDATE SET val = t.val + s.inc
+ WHEN NOT MATCHED THEN
+ INSERT (id, val) VALUES (s.id, s.inc);
+ <waiting ...>
+step c2: COMMIT;
+step merge1: <... completed>
+ERROR: could not serialize access due to concurrent update
+step select1:
+ SELECT * FROM test;
+
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+step c1: COMMIT;
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 4e466580cd4..95d8597e204 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -58,6 +58,7 @@ test: insert-conflict-do-select
test: merge-insert-update
test: merge-delete
test: merge-update
+test: merge-match-do-update
test: merge-match-recheck
test: merge-join
test: delete-abort-savept
diff --git a/src/test/isolation/specs/merge-match-do-update.spec b/src/test/isolation/specs/merge-match-do-update.spec
new file mode 100644
index 00000000000..046c78826b9
--- /dev/null
+++ b/src/test/isolation/specs/merge-match-do-update.spec
@@ -0,0 +1,43 @@
+# MERGE UPDATE
+
+setup
+{
+ CREATE TABLE test (id int primary key, val int);
+ INSERT INTO test VALUES (1,0);
+}
+
+teardown
+{
+ DROP TABLE test;
+}
+
+session "s1"
+step "b1"
+{
+ BEGIN ISOLATION LEVEL REPEATABLE READ;
+}
+step "merge1"
+{
+ MERGE INTO test t USING (VALUES (1, 100)) AS s (id, inc)
+ ON t.id = s.id
+ WHEN MATCHED THEN
+ UPDATE SET val = t.val + s.inc
+ WHEN NOT MATCHED THEN
+ INSERT (id, val) VALUES (s.id, s.inc);
+}
+step "select1"
+{
+ SELECT * FROM test;
+}
+step "c1" { COMMIT; }
+
+session "s2"
+step "b2" { BEGIN; }
+step "update2"
+{
+ UPDATE test SET val = val + 100;
+}
+step "c2" { COMMIT; }
+
+# Now with concurrency
+permutation "b2" "update2" "b1" "merge1" "c2" "select1" "c1"
\ No newline at end of file
--
2.34.1
Attachments:
[text/plain] 0001-Fix-MERGE-match-do-update-in-RR-isolation-level.patch (3.7K, ../../CAHewXNmtg=-WRczRUmd0tyNaP57WYOg74=O2pMssq9hWS3YoOw@mail.gmail.com/2-0001-Fix-MERGE-match-do-update-in-RR-isolation-level.patch)
download | inline diff:
From 9f7267cdc3bf5b34eb95686f288d39b5766142ac Mon Sep 17 00:00:00 2001
From: Tender Wang <tndrwang@gmail.com>
Date: Tue, 24 Feb 2026 11:04:26 +0800
Subject: [PATCH] Fix MERGE match do update in RR isolation level.
---
src/backend/executor/nodeModifyTable.c | 4 ++
.../expected/merge-match-do-update.out | 26 +++++++++++
src/test/isolation/isolation_schedule | 1 +
.../specs/merge-match-do-update.spec | 43 +++++++++++++++++++
4 files changed, 74 insertions(+)
create mode 100644 src/test/isolation/expected/merge-match-do-update.out
create mode 100644 src/test/isolation/specs/merge-match-do-update.spec
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 793c76d4f82..8d8c9fb54e3 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -3547,6 +3547,10 @@ lmerge_matched:
*inputslot;
LockTupleMode lockmode;
+ if (IsolationUsesXactSnapshot())
+ ereport(ERROR,
+ (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
+ errmsg("could not serialize access due to concurrent update")));
/*
* The target tuple was concurrently updated by some other
* transaction. If we are currently processing a MATCHED
diff --git a/src/test/isolation/expected/merge-match-do-update.out b/src/test/isolation/expected/merge-match-do-update.out
new file mode 100644
index 00000000000..c2cde0de606
--- /dev/null
+++ b/src/test/isolation/expected/merge-match-do-update.out
@@ -0,0 +1,26 @@
+Parsed test spec with 2 sessions
+
+starting permutation: b2 update2 b1 merge1 c2 select1 c1
+step b2: BEGIN;
+step update2:
+ UPDATE test SET val = val + 100;
+
+step b1:
+ BEGIN ISOLATION LEVEL REPEATABLE READ;
+
+step merge1:
+ MERGE INTO test t USING (VALUES (1, 100)) AS s (id, inc)
+ ON t.id = s.id
+ WHEN MATCHED THEN
+ UPDATE SET val = t.val + s.inc
+ WHEN NOT MATCHED THEN
+ INSERT (id, val) VALUES (s.id, s.inc);
+ <waiting ...>
+step c2: COMMIT;
+step merge1: <... completed>
+ERROR: could not serialize access due to concurrent update
+step select1:
+ SELECT * FROM test;
+
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+step c1: COMMIT;
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 4e466580cd4..95d8597e204 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -58,6 +58,7 @@ test: insert-conflict-do-select
test: merge-insert-update
test: merge-delete
test: merge-update
+test: merge-match-do-update
test: merge-match-recheck
test: merge-join
test: delete-abort-savept
diff --git a/src/test/isolation/specs/merge-match-do-update.spec b/src/test/isolation/specs/merge-match-do-update.spec
new file mode 100644
index 00000000000..046c78826b9
--- /dev/null
+++ b/src/test/isolation/specs/merge-match-do-update.spec
@@ -0,0 +1,43 @@
+# MERGE UPDATE
+
+setup
+{
+ CREATE TABLE test (id int primary key, val int);
+ INSERT INTO test VALUES (1,0);
+}
+
+teardown
+{
+ DROP TABLE test;
+}
+
+session "s1"
+step "b1"
+{
+ BEGIN ISOLATION LEVEL REPEATABLE READ;
+}
+step "merge1"
+{
+ MERGE INTO test t USING (VALUES (1, 100)) AS s (id, inc)
+ ON t.id = s.id
+ WHEN MATCHED THEN
+ UPDATE SET val = t.val + s.inc
+ WHEN NOT MATCHED THEN
+ INSERT (id, val) VALUES (s.id, s.inc);
+}
+step "select1"
+{
+ SELECT * FROM test;
+}
+step "c1" { COMMIT; }
+
+session "s2"
+step "b2" { BEGIN; }
+step "update2"
+{
+ UPDATE test SET val = val + 100;
+}
+step "c2" { COMMIT; }
+
+# Now with concurrency
+permutation "b2" "update2" "b1" "merge1" "c2" "select1" "c1"
\ No newline at end of file
--
2.34.1
^ permalink raw reply [nested|flat] 10+ messages in thread
* Re: MERGE behavior with REPEATABLE READ isolation level
2026-02-15 01:20 MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
2026-02-15 01:22 ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
2026-02-24 03:12 ` Re: MERGE behavior with REPEATABLE READ isolation level Tender Wang <tndrwang@gmail.com>
@ 2026-02-24 09:09 ` Dean Rasheed <dean.a.rasheed@gmail.com>
1 sibling, 0 replies; 10+ messages in thread
From: Dean Rasheed @ 2026-02-24 09:09 UTC (permalink / raw)
To: Tender Wang <tndrwang@gmail.com>; +Cc: Alexander Korotkov <aekorotkov@gmail.com>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
On Tue, 24 Feb 2026 at 03:13, Tender Wang <tndrwang@gmail.com> wrote:
>
> Alexander Korotkov <aekorotkov@gmail.com> 于2026年2月15日周日 09:23写道:
> >
> > I found it strange that ExecMergeMatched() checks for IsolationUsesXactSnapshot() in the TM_Deleted case, but not in the TM_Updated case.
>
> I tried "update test set val = val + 100;" but the SQL reported a
> "could not serialize access due to concurrent update" error.
> It seems that the MERGE command should behave identically to UPDATE
> when performing a match action.
>
Yes, I agree. I think this is a bug (probably just an oversight in the
original MERGE commit).
Regards,
Dean
^ permalink raw reply [nested|flat] 10+ messages in thread
* Re: MERGE behavior with REPEATABLE READ isolation level
2026-02-15 01:20 MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
2026-02-15 01:22 ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
2026-02-24 03:12 ` Re: MERGE behavior with REPEATABLE READ isolation level Tender Wang <tndrwang@gmail.com>
@ 2026-02-26 17:03 ` Alexander Korotkov <aekorotkov@gmail.com>
2026-02-27 12:54 ` Re: MERGE behavior with REPEATABLE READ isolation level Tender Wang <tndrwang@gmail.com>
1 sibling, 1 reply; 10+ messages in thread
From: Alexander Korotkov @ 2026-02-26 17:03 UTC (permalink / raw)
To: Tender Wang <tndrwang@gmail.com>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
On Tue, Feb 24, 2026 at 5:13 AM Tender Wang <tndrwang@gmail.com> wrote:
> Alexander Korotkov <aekorotkov@gmail.com> 于2026年2月15日周日 09:23写道:
> > Oh, sorry I missed the begin statement for s1. The complete case should look like this.
> >
> > s1# create table test (id int primary key, val int);
> > s1# insert into test values (1,0);
> >
> > s2# begin;
> > s2# update test set val = val + 100;
> >
> > s1# begin isolation level repeatable read;
> > s1# MERGE INTO test t USING (VALUES (1, 100)) AS s (id, inc)
> > ON t.id = s.id
> > WHEN MATCHED THEN
> > UPDATE SET val = t.val + s.inc
> > WHEN NOT MATCHED THEN
> > INSERT (id, val) VALUES (s.id, s.inc);
> > (waiting ...)
> >
> > s2# commit;
> >
> > s1# MERGE 1
> > s1# select * from test;
> > id | val
> > ----+-----
> > 1 | 200
> > (1 row)
> >
>
> I tried "update test set val = val + 100;" but the SQL reported a
> "could not serialize access due to concurrent update" error.
> It seems that the MERGE command should behave identically to UPDATE
> when performing a match action.
>
> I wrote a fix patch and attached it, and added your test case, too.
Thank you for the confirmation and for the patch. Regarding the test
case, can we handle this without introducing a new .spec file? I
think we can add 1-2 permutations to merge-update.spec. Even existing
step should work, you only need one new which begins transaction in RR
isolation mode.
------
Regards,
Alexander Korotkov
Supabase
^ permalink raw reply [nested|flat] 10+ messages in thread
* Re: MERGE behavior with REPEATABLE READ isolation level
2026-02-15 01:20 MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
2026-02-15 01:22 ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
2026-02-24 03:12 ` Re: MERGE behavior with REPEATABLE READ isolation level Tender Wang <tndrwang@gmail.com>
2026-02-26 17:03 ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
@ 2026-02-27 12:54 ` Tender Wang <tndrwang@gmail.com>
2026-03-01 22:32 ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
0 siblings, 1 reply; 10+ messages in thread
From: Tender Wang @ 2026-02-27 12:54 UTC (permalink / raw)
To: Alexander Korotkov <aekorotkov@gmail.com>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Dean Rasheed <dean.a.rasheed@gmail.com>
Hi all,
Alexander Korotkov <aekorotkov@gmail.com> 于2026年2月27日周五 01:03写道:
> Thank you for the confirmation and for the patch. Regarding the test
> case, can we handle this without introducing a new .spec file? I
> think we can add 1-2 permutations to merge-update.spec. Even existing
> step should work, you only need one new which begins transaction in RR
> isolation mode.
Done
Please see the v2 patch.
--
Thanks,
Tender Wang
From 6a8dadb846338648e289ae3ac7e90eba53e35564 Mon Sep 17 00:00:00 2001
From: Tender Wang <tndrwang@gmail.com>
Date: Tue, 24 Feb 2026 11:04:26 +0800
Subject: [PATCH v2] Fix MERGE match do update in RR isolation level.
---
src/backend/executor/nodeModifyTable.c | 4 +++
src/test/isolation/expected/merge-update.out | 33 ++++++++++++++++++++
src/test/isolation/specs/merge-update.spec | 2 ++
3 files changed, 39 insertions(+)
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 793c76d4f82..8d8c9fb54e3 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -3547,6 +3547,10 @@ lmerge_matched:
*inputslot;
LockTupleMode lockmode;
+ if (IsolationUsesXactSnapshot())
+ ereport(ERROR,
+ (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
+ errmsg("could not serialize access due to concurrent update")));
/*
* The target tuple was concurrently updated by some other
* transaction. If we are currently processing a MATCHED
diff --git a/src/test/isolation/expected/merge-update.out b/src/test/isolation/expected/merge-update.out
index feceacf4818..821565b4303 100644
--- a/src/test/isolation/expected/merge-update.out
+++ b/src/test/isolation/expected/merge-update.out
@@ -549,3 +549,36 @@ step c1: COMMIT;
step pa_merge2c_dup: <... completed>
ERROR: MERGE command cannot affect row a second time
step a2: ABORT;
+
+starting permutation: merge2a c1 s1beginrr merge1 c2
+step merge2a:
+ MERGE INTO target t
+ USING (SELECT 1 as key, 'merge2a' as val) s
+ ON s.key = t.key
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED THEN
+ UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val
+ WHEN NOT MATCHED BY SOURCE THEN
+ UPDATE set key = t.key + 1, val = t.val || ' source not matched by merge2a'
+ RETURNING merge_action(), old, new, t.*;
+
+merge_action|old |new |key|val
+------------+----------+-------------------------------+---+-------------------------
+UPDATE |(1,setup1)|(2,"setup1 updated by merge2a")| 2|setup1 updated by merge2a
+(1 row)
+
+step c1: COMMIT;
+step s1beginrr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step merge1:
+ MERGE INTO target t
+ USING (SELECT 1 as key, 'merge1' as val) s
+ ON s.key = t.key
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED THEN
+ UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+ <waiting ...>
+step c2: COMMIT;
+step merge1: <... completed>
+ERROR: could not serialize access due to concurrent update
diff --git a/src/test/isolation/specs/merge-update.spec b/src/test/isolation/specs/merge-update.spec
index 771ee5b70cf..b902779edd6 100644
--- a/src/test/isolation/specs/merge-update.spec
+++ b/src/test/isolation/specs/merge-update.spec
@@ -93,6 +93,7 @@ step "pa_merge3"
}
step "c1" { COMMIT; }
step "a1" { ABORT; }
+step "s1beginrr" { BEGIN ISOLATION LEVEL REPEATABLE READ; }
session "s2"
setup
@@ -223,3 +224,4 @@ permutation "pa_merge2" "c1" "pa_merge2a" "pa_select2" "c2" # succeeds
permutation "pa_merge3" "pa_merge2b_when" "c1" "pa_select2" "c2" # WHEN not satisfied by updated tuple
permutation "pa_merge1" "pa_merge2b_when" "c1" "pa_select2" "c2" # WHEN satisfied by updated tuple
permutation "pa_merge1" "pa_merge2c_dup" "c1" "a2"
+permutation "merge2a" "c1" "s1beginrr" "merge1" "c2"
--
2.34.1
Attachments:
[text/plain] v2-0001-Fix-MERGE-match-do-update-in-RR-isolation-level.patch (3.4K, ../../CAHewXNk+LhHOUE7EhugnkanEJABp8sg4ViXr4L80LZrQvyuBqg@mail.gmail.com/2-v2-0001-Fix-MERGE-match-do-update-in-RR-isolation-level.patch)
download | inline diff:
From 6a8dadb846338648e289ae3ac7e90eba53e35564 Mon Sep 17 00:00:00 2001
From: Tender Wang <tndrwang@gmail.com>
Date: Tue, 24 Feb 2026 11:04:26 +0800
Subject: [PATCH v2] Fix MERGE match do update in RR isolation level.
---
src/backend/executor/nodeModifyTable.c | 4 +++
src/test/isolation/expected/merge-update.out | 33 ++++++++++++++++++++
src/test/isolation/specs/merge-update.spec | 2 ++
3 files changed, 39 insertions(+)
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 793c76d4f82..8d8c9fb54e3 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -3547,6 +3547,10 @@ lmerge_matched:
*inputslot;
LockTupleMode lockmode;
+ if (IsolationUsesXactSnapshot())
+ ereport(ERROR,
+ (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
+ errmsg("could not serialize access due to concurrent update")));
/*
* The target tuple was concurrently updated by some other
* transaction. If we are currently processing a MATCHED
diff --git a/src/test/isolation/expected/merge-update.out b/src/test/isolation/expected/merge-update.out
index feceacf4818..821565b4303 100644
--- a/src/test/isolation/expected/merge-update.out
+++ b/src/test/isolation/expected/merge-update.out
@@ -549,3 +549,36 @@ step c1: COMMIT;
step pa_merge2c_dup: <... completed>
ERROR: MERGE command cannot affect row a second time
step a2: ABORT;
+
+starting permutation: merge2a c1 s1beginrr merge1 c2
+step merge2a:
+ MERGE INTO target t
+ USING (SELECT 1 as key, 'merge2a' as val) s
+ ON s.key = t.key
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED THEN
+ UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val
+ WHEN NOT MATCHED BY SOURCE THEN
+ UPDATE set key = t.key + 1, val = t.val || ' source not matched by merge2a'
+ RETURNING merge_action(), old, new, t.*;
+
+merge_action|old |new |key|val
+------------+----------+-------------------------------+---+-------------------------
+UPDATE |(1,setup1)|(2,"setup1 updated by merge2a")| 2|setup1 updated by merge2a
+(1 row)
+
+step c1: COMMIT;
+step s1beginrr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step merge1:
+ MERGE INTO target t
+ USING (SELECT 1 as key, 'merge1' as val) s
+ ON s.key = t.key
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED THEN
+ UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+ <waiting ...>
+step c2: COMMIT;
+step merge1: <... completed>
+ERROR: could not serialize access due to concurrent update
diff --git a/src/test/isolation/specs/merge-update.spec b/src/test/isolation/specs/merge-update.spec
index 771ee5b70cf..b902779edd6 100644
--- a/src/test/isolation/specs/merge-update.spec
+++ b/src/test/isolation/specs/merge-update.spec
@@ -93,6 +93,7 @@ step "pa_merge3"
}
step "c1" { COMMIT; }
step "a1" { ABORT; }
+step "s1beginrr" { BEGIN ISOLATION LEVEL REPEATABLE READ; }
session "s2"
setup
@@ -223,3 +224,4 @@ permutation "pa_merge2" "c1" "pa_merge2a" "pa_select2" "c2" # succeeds
permutation "pa_merge3" "pa_merge2b_when" "c1" "pa_select2" "c2" # WHEN not satisfied by updated tuple
permutation "pa_merge1" "pa_merge2b_when" "c1" "pa_select2" "c2" # WHEN satisfied by updated tuple
permutation "pa_merge1" "pa_merge2c_dup" "c1" "a2"
+permutation "merge2a" "c1" "s1beginrr" "merge1" "c2"
--
2.34.1
^ permalink raw reply [nested|flat] 10+ messages in thread
* Re: MERGE behavior with REPEATABLE READ isolation level
2026-02-15 01:20 MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
2026-02-15 01:22 ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
2026-02-24 03:12 ` Re: MERGE behavior with REPEATABLE READ isolation level Tender Wang <tndrwang@gmail.com>
2026-02-26 17:03 ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
2026-02-27 12:54 ` Re: MERGE behavior with REPEATABLE READ isolation level Tender Wang <tndrwang@gmail.com>
@ 2026-03-01 22:32 ` Alexander Korotkov <aekorotkov@gmail.com>
0 siblings, 0 replies; 10+ messages in thread
From: Alexander Korotkov @ 2026-03-01 22:32 UTC (permalink / raw)
To: Tender Wang <tndrwang@gmail.com>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Dean Rasheed <dean.a.rasheed@gmail.com>
On Fri, Feb 27, 2026 at 2:54 PM Tender Wang <tndrwang@gmail.com> wrote:
> Alexander Korotkov <aekorotkov@gmail.com> 于2026年2月27日周五 01:03写道:
> > Thank you for the confirmation and for the patch. Regarding the test
> > case, can we handle this without introducing a new .spec file? I
> > think we can add 1-2 permutations to merge-update.spec. Even existing
> > step should work, you only need one new which begins transaction in RR
> > isolation mode.
>
> Done
> Please see the v2 patch.
Looks good to me. I'll push it if no objections.
------
Regards,
Alexander Korotkov
Supabase
^ permalink raw reply [nested|flat] 10+ messages in thread
end of thread, other threads:[~2026-03-01 22:32 UTC | newest]
Thread overview: 10+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2024-03-19 22:50 [PATCH v4 14/19] Vacuum second pass emits XLOG_HEAP2_PRUNE record Melanie Plageman <melanieplageman@gmail.com>
2024-03-19 22:50 [PATCH v4 14/19] Vacuum second pass emits XLOG_HEAP2_PRUNE record Melanie Plageman <melanieplageman@gmail.com>
2024-03-19 22:50 [PATCH v4 14/19] Vacuum second pass emits XLOG_HEAP2_PRUNE record Melanie Plageman <melanieplageman@gmail.com>
2026-02-15 01:20 MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
2026-02-15 01:22 ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
2026-02-24 03:12 ` Re: MERGE behavior with REPEATABLE READ isolation level Tender Wang <tndrwang@gmail.com>
2026-02-24 09:09 ` Re: MERGE behavior with REPEATABLE READ isolation level Dean Rasheed <dean.a.rasheed@gmail.com>
2026-02-26 17:03 ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
2026-02-27 12:54 ` Re: MERGE behavior with REPEATABLE READ isolation level Tender Wang <tndrwang@gmail.com>
2026-03-01 22:32 ` Re: MERGE behavior with REPEATABLE READ isolation level Alexander Korotkov <aekorotkov@gmail.com>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox