pg.ddx.io  pgsql-hackers@postgresql.org mailing list archive  
help / color / mirror / Atom feed
[PATCH] Batched clock sweep to reduce cross-socket atomic contention
21+ messages / 3 participants
[nested] [flat]

* [PATCH] Batched clock sweep to reduce cross-socket atomic contention
@ 2026-04-25 20:08 Greg Burd <greg@burd.me>
  2026-04-27 13:26 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-04-27 14:15 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  0 siblings, 2 replies; 21+ messages in thread

From: Greg Burd @ 2026-04-25 20:08 UTC (permalink / raw)
  To: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; +Cc: Andres Freund <andres@anarazel.de>; Tomas Vondra <tomas@vondra.me>; Nathan Bossart <nathandbossart@gmail.com>

Hello hackers,

A colleague of mine, Jim Mlodgenski, has been poking at NUMA behavior on some of the newer AWS bare-metal instance types (r8i in particular, which exposes 6 NUMA nodes via SNC3 on a 2-socket box), and in the process landed on a very small change to freelist.c that I think is worth showing around.  His patch is attached with some tweaks of my own.

Full disclosure: the exploration that led Jim to this patch idea was done with help from an AI assistant (Kiro); the idea, the benchmarking, and the final shape of the patch are human-driven, but I wanted to be up front about how his investigation started.  Happy to discuss that separately if people want to.

The one-line summary: instead of advancing nextVictimBuffer one buffer at a time via pg_atomic_fetch_add_u32, each backend claims a batch of 64 consecutive buffer IDs from the shared hand and then iterates them privately.  Global sweep order is preserved -- every buffer is still
visited exactly once per complete pass -- but the atomic contention on that one cache line drops by roughly the batch size.


Why this matters
----------------

On multi-socket boxes under eviction pressure, every backend that needs a victim buffer ends up CAS'ing the same cache line.  On a single socket, a locked RMW on that cache line stays warm in L1/L2 and completes in ~20ns.  On 2+ sockets, the line bounces over QPI/UPI at ~100-200ns per op, and with hundreds of backends running StrategyGetBuffer() concurrently, the line ping-pongs constantly.  It's a textbook NUMA scalability bottleneck, and once shared_buffers is smaller than the working set and the sweep is running continuously, that single atomic is what you hit in a perf profile (elevated bus-cycles, cache-misses on the cache line holding nextVictimBuffer).

Andres pointed at the same spot in his pgconf.eu 2024 talk, and Tomas called it out in the "Adding basic NUMA awareness" thread [1] -- so this isn't news to anyone who's been looking at this area.  What I think is new is a fix that's just this, without any of the surrounding architectural change.

The framing (credit to Jim): the clock hand is doing two jobs.  It *coordinates* backends so they don't redundantly decrement usage_count on the same buffers and so they eventually visit every buffer in the pool exactly once per pass.  It also *serializes* access to the counter.  Coordination is the part we want.  Serialization is the part that's killing us on bigger NUMA boxes.  Batching keeps the coordination and thins out the serialization.


How it works
------------

Two per-backend statics, MyBatchPos and MyBatchEnd.  When a backend calls ClockSweepTick() and its local batch is exhausted, it does a single fetch-add of CLOCK_SWEEP_BATCH_SIZE (64) against nextVictimBuffer and now owns that range.  Subsequent ticks just bump the local counter.

Wraparound got a small rewrite.  The original code had the backend that crossed NBuffers drive completePasses++ under the spinlock via a CAS loop.  With batching, multiple backends can each land a fetch-add that returns a value >= NBuffers in the same pass, so the logic now is: whoever sees a start >= NBuffers takes the spinlock, re-reads the counter, and if it's still out of range does a single CAS to wrap it and bumps completePasses.  If somebody else already wrapped, we just release and move on.  StrategySyncStart() still sees a consistent (nextVictimBuffer, completePasses) pair.

The batch size is gated on whether we actually have multiple NUMA nodes.  On a single-socket box the atomic is already socket-local, batching just makes backends skip further ahead than they need to, so we fall back to batch size 1 -- which is bit-for-bit the original behavior.  The guard:

    if (pg_numa_init() != -1 && pg_numa_get_max_node() >= 1)
        ClockSweepBatchSize = Min(CLOCK_SWEEP_BATCH_SIZE, (uint32) NBuffers);
    else
        ClockSweepBatchSize = 1;

Min() against NBuffers covers the small-shared_buffers corner so a batch never wraps the pool multiple times in one claim.


Does batching mess up the meaning of usage_count?
--------------------------------------------------

Short answer: no.  I want to walk through this because it was my first concern too, and I think it's the question that will come up most on review.

The clock sweep's usage_count is an access-frequency approximation measured in units of *complete passes*.  A buffer with usage_count = N survives N passes without a re-pin.  The semantic meaning lives at pass granularity, not at individual-buffer granularity.

What batching changes: intra-pass temporal ordering.  Without batching, with N backends sweeping, decrements are interleaved -- backend A hits B[0], backend B hits B[1], backend C hits B[2].  With batching, backend A hits B[0..63] in a tight local burst, then backend B hits B[64..127], etc.  The 64-buffer chunks are decremented in bursts rather than individually.

Why it doesn't matter:

  1. Every buffer still gets decremented exactly once per complete
     pass.  The invariant the algorithm actually depends on is
     untouched.

  2. A buffer's survival window is the time between consecutive
     passes.  That's milliseconds to seconds under load.  Whether
     B[0] gets decremented 50us before or 50us after B[63] within
     the same pass is below the resolution of anything usage_count
     is trying to measure.

  3. The bgwriter's feedback loop reads (nextVictimBuffer,
     completePasses, numBufferAllocs) via StrategySyncStart() every
     ~200ms.  nextVictimBuffer still advances at the same *total*
     rate (64 per atomic op, but atomic ops happen 1/64 as often).
     The position it reports can jitter by up to 64 buffers relative
     to the one-at-a-time case, but BgBufferSync()'s smoothed
     estimates operate over thousands of buffers per cycle, so the
     jitter disappears into the averaging.  numBufferAllocs still
     increments once per allocation.  strategy_delta,
     smoothed_alloc, smoothed_density, reusable_buffers_est -- all
     unaffected in any way I can see.

Table form, because it's easier to argue with:

  Property                          | Unpatched      | Batched
  ----------------------------------+----------------+----------------
  Buffers visited per pass          | NBuffers       | NBuffers
  Decrements per buffer per pass    | 1              | 1
  Eviction threshold                | usage_count==0 | usage_count==0
  Max survival (passes)             | 6              | 6
  Decrement ordering within a pass  | interleaved    | chunked
  bgwriter allocation rate signal   | accurate       | accurate
  Cross-socket atomic traffic       | 1 per buffer   | 1 per 64

There is one subtle difference worth naming.  When a backend finds a victim at B[5] of its batch, it returns with MyBatchEnd still sitting at B[63].  The next time that backend needs a victim it resumes at B[6], not at wherever the global hand now points.  So the backend drains its batch over multiple StrategyGetBuffer() calls rather than all at once.  Under heavy load, where batches are consumed in microseconds, this is invisible.  Under light load, the implication is that some buffers can sit with slightly stale usage_count for longer than they would have before.  But "light load" means "the sweep is barely moving and nothing wants to evict anyway" -- so the effect
doesn't show up where it would hurt.

There's also a small positive side-effect: cache locality.  The backend that just touched BufferDescriptor[B[0]] has the adjacent descriptors warm in L1/L2.  Walking B[0..63] locally is cheaper than walking a striped interleaving where each descriptor was last touched by a different core.  I haven't tried to isolate this in perf, but it falls out naturally.


Benchmarks
----------

Jim ran these; I'm still working on reproducing them locally and will post independent numbers in a follow-up.  All bare metal, Linux, huge pages enabled throughout (more on that below), postmaster pinned to node 0 with `numactl --cpunodebind=0` because otherwise stock TPS varied from 31K to 40K depending on which node the postmaster happened to land on at launch -- worth flagging for anyone trying to reproduce.

Workload is pgbench scale 3000 (~45GB) with shared_buffers=32GB, so the working set always spills and the sweep is hot.

  r8i.metal-96xl (384 vCPUs, 2 sockets, 6 NUMA nodes via SNC3):

    pgbench RO:
      Clients   Stock    Patched   Delta
      64        31,457   36,353    +16%
      128       31,678   37,864    +20%
      256       31,510   37,558    +19%
      384       31,431   37,464    +19%
      512       31,329   37,040    +18%

    pgbench RW:
      Clients   Stock    Patched   Delta
      64         7,685    7,713     0%
      128       10,420   10,541    +1%
      256       12,393   12,463    +1%
      384       15,317   15,197    -1%
      512       17,930   17,978     0%

  m6i.metal (128 vCPUs, 2 sockets, Ice Lake):
    RO +19-20%, RW within noise.

  c8i.metal-48xl (192 vCPUs, 1 socket):
    Single-socket -> batch_size=1 -> original code path.  No
    behavioral change.  (I double-checked this one specifically
    because it's the sanity test for the gate.)

  HammerDB TPC-C on m6i.metal (1000 warehouses):
    VUs   Stock     Patched   Delta
    128   358,518   349,787    -2%
    256   332,098   330,272    -1%
    384   365,782   377,519    +3%
    512   370,663   386,526    +4%

No TPC-C regression, which was the thing we were most worried about. An earlier attempt (per-socket partitioned sweep, see below) was -13% on this same workload.

The general shape is: the scaling curve flattens later.  Unpatched, TPS tops out around 128 clients and stays flat up to 512 because backends are spending cycles waiting on the cache line rather than
doing work.  Patched, the curve keeps rising past the point where unpatched plateaus.

Huge pages caveat: all of the above was run with huge pages on, on large-memory instances (the r8i.96xl has 3TB, so Jim never considered running without them).  We have not characterized the non-huge-pages case.  That's on my list; I don't expect it to change the conclusion, but I shouldn't speak for data I haven't collected.


Relationship to Tomas's NUMA series
-----------------------------------

Tomas posted a multi-patch NUMA-awareness series in [1] covering buffer interleaving across nodes, partitioned freelists, partitioned clock sweep, PGPROC interleaving, and related pieces.  I want to be careful here because I don't think we should frame this patch as competing with that work.

One thing I found striking as I re-read the thread: in the benchmarks Tomas posted later in the series, *most of the benefit comes from partitioning the clock sweep*, and the NUMA memory-placement layer on top sometimes runs slower than partitioning alone.  His own conclusion, quoted roughly: the benefit mostly comes from just partitioning the clock sweep, and it's largely independent of the NUMA stuff; the NUMA partitioning is often slower.

That observation is the thing that makes me think batching is worth considering on its own.  It's going after the same bottleneck Tomas's partitioning addresses, but:

  - without splitting global eviction visibility (which is where
    cross-partition stealing gets complicated),
  - without requiring NUMA-aware buffer placement (which has huge
    page alignment, descriptor-partition-mid-page, and resize
    complications that are still being worked out in that thread),
  - without touching PGPROC or bgwriter.

What this patch does *not* do:
  - place buffers on specific NUMA nodes
  - partition the freelist
  - touch PGPROC
  - add new GUCs
  - change bgwriter

What this patch *does* do:
  - target exactly the clock-sweep contention that Tomas's
    partitioning targets, and reduce it by ~64x, in ~30 lines.

If Tomas's series lands in full, this patch becomes redundant for its primary use case (though even within a partitioned sweep, the per-partition atomic still benefits from batching, so it's arguably a useful primitive either way).  If Tomas's series lands incrementally over several cycles -- which the open items in that thread suggest is the realistic path -- this gets us a real chunk of the multi-socket win now.

This patch is also orthogonal to my earlier thread about removing the freelist entirely [2], but given the proximity to that code Jim agreed that I could propose/steward it here on the list for consideration.


Open questions / things I'd like feedback on
--------------------------------------------

- Batch size.  64 is a round number that worked well in testing, but
  Nathan raised the reasonable point that on small shared_buffers
  with high concurrency, a fixed 64 could be unfortunate.  Options:
  scale with shared_buffers (Min(64, NBuffers / N) for some N), scale
  with max_connections, keep it fixed but let operators tune it, or
  make it a function of NUMA node count.  I don't have a strong
  opinion yet; the Min(batch, NBuffers) cap covers the "obviously
  wrong" corner but doesn't speak to the "several hundred backends
  on a few-MB shared_buffers" shape.  Numbers/ideas/proposals welcome.

- NUMA detection.  The gate uses pg_numa_init() /
  pg_numa_get_max_node().  On systems where libnuma isn't available,
  or where get_mempolicy is blocked (some container configurations),
  we fall back to batch size 1.  That's safe but it misses the
  "single socket, many cores, still benefits from fewer atomics"
  case.  Might be worth a way to force-enable, or batching on all
  systems with a smaller batch size when single-socket.  I'd like to
  measure before deciding.

- Eviction pattern on reads.  Nathan also flagged that with batching,
  the buffers a backend ends up pinning in one StrategyGetBuffer()
  call will tend to be contiguous in buffer-id space rather than
  scattered, which is a different allocation pattern than today.
  The usage_count analysis above says this is benign, but if anyone
  has an intuition for a workload where this would be observable
  (e.g., something that cares about the mapping between buffer-id
  and relation locality), I'd like to hear it.

- nextVictimBuffer wraparound.  The current code has a mild overflow
  concern papered over with "highly unlikely and wouldn't be
  particularly harmful".  With batching this is no worse than before,
  but if we're already touching this function, it might be worth
  thinking about whether to tighten it up in the same patch or a
  follow-up.

- Should the non-NUMA value for this be derived from core counts that
  imply L1/L2 cache layouts or simply default to 8 rather than 1 to
  realize some benefit?

- Should there be a postgresql.conf setting for this that takes
  precedence?


I'll run the non-huge-pages variant, reproduce the r8i numbers, poke at the small-shared_buffers corner, and post perf stat output showing the atomic/cache-miss deltas over the next few days.  In the meantime, eyeballs and skepticism welcome -- I would especially welcome comments from Andres, who's been in this code recently, and from Tomas, whose series has the most overlap.

I realize that we're past feature freeze and working on release notes for v19, so the chances of merging this are slim to none.  I think this could be considered a "performance bug fix for NUMA systems" in this release, but that is stretching it a bit.  It is a big ask at this stage to land a change like this.

best.

-greg

[1] https://www.postgresql.org/message-id/099b9433-2855-4f1b-b421-d078a5d82017@vondra.me
[2] https://www.postgresql.org/message-id/f0e3c02e-e217-4f04-8dab-1e7e80a228c0@burd.me

Attachments:

  [application/octet-stream] v1-0001-Reduce-clock-sweep-atomic-contention-by-claiming-.patch (8.2K, ../../79629577-3ad8-4b1c-a469-ebc2cb4c5104@app.fastmail.com/2-v1-0001-Reduce-clock-sweep-atomic-contention-by-claiming-.patch)
  download | inline diff:
From bdcf90fbd89a0aec397a3d57224ae732959733f9 Mon Sep 17 00:00:00 2001
From: Greg Burd <greg@burd.me>
Date: Sat, 25 Apr 2026 15:52:36 -0400
Subject: [PATCH v1] Reduce clock-sweep atomic contention by claiming buffers
 in batches

StrategyGetBuffer() advances nextVictimBuffer via
pg_atomic_fetch_add_u32(..., 1) on every tick.  On multi-socket
systems the cache line holding the counter has to travel over the
interconnect on each operation, pushing a sweep tick from ~20ns (the
same-socket case) into the ~100-200ns range.  With hundreds of
concurrent backends under eviction pressure, that one cache line
becomes the dominant cost in the sweep, visible as elevated
bus-cycles and cache-misses in perf profiles.

Each backend now claims a range of CLOCK_SWEEP_BATCH_SIZE (64)
consecutive buffer IDs with a single fetch-add and iterates through
them privately.  The sweep still advances through the pool in order,
each buffer is still visited exactly once per complete pass, and
usage_count is still decremented exactly once per buffer per pass;
the meaning of usage_count as "how many complete passes a buffer
survives without a re-pin" is preserved.  What changes is the
temporal ordering of decrements within a single pass, which the
algorithm does not depend on.

Wraparound handling is adjusted: with batching, multiple backends
can each see their fetch-add return a value past NBuffers within
the same pass.  Any such backend takes buffer_strategy_lock,
re-reads the counter, and if it is still out of range wraps it with
a single CAS and increments completePasses.  StrategySyncStart()
continues to see a consistent (nextVictimBuffer, completePasses)
pair.

Batching is only useful when the atomic is actually contended
across nodes, so it is applied only when libnuma reports more than
one node (pg_numa_get_max_node() >= 1); otherwise the batch size
stays at 1 and the code path matches master bit-for-bit.  The batch
is also capped at NBuffers so a claim cannot wrap the pool more
than once.

Co-Authored-by: Jim Mlodgenski <mlodj@amazon.com>
Co-Authored-by: Greg Burd <greg@burd.me>
---
 src/backend/storage/buffer/freelist.c | 136 ++++++++++++++++++--------
 1 file changed, 94 insertions(+), 42 deletions(-)

diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index fdb5bad7910..e86ed1f7da0 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -22,6 +22,7 @@
 #include "storage/proc.h"
 #include "storage/shmem.h"
 #include "storage/subsystems.h"
+#include "port/pg_numa.h"
 
 #define INT_ACCESS_ONCE(var)	((int)(*((volatile int *)&(var))))
 
@@ -100,68 +101,101 @@ static BufferDesc *GetBufferFromRing(BufferAccessStrategy strategy,
 static void AddBufferToRing(BufferAccessStrategy strategy,
 							BufferDesc *buf);
 
+/*
+ * Number of buffer IDs to claim from the shared clock hand at once.
+ * Larger values reduce contention on the shared atomic.  With a batch
+ * size of 64, concurrent backends sweep non-overlapping chunks of 64
+ * buffers rather than interleaving one buffer at a time.  The global
+ * sweep order is preserved — each buffer is still visited exactly once
+ * per complete pass.
+ */
+#define CLOCK_SWEEP_BATCH_SIZE 64
+
+/*
+ * Per-backend state for batched clock sweep.
+ */
+static uint32 MyBatchPos = 0;	/* next buffer within batch */
+static uint32 MyBatchEnd = 0;	/* one past last buffer in batch */
+
+/*
+ * Effective batch size for the clock sweep, computed once at startup.
+ * On non-NUMA systems (single socket, no libnuma, or containers blocking
+ * get_mempolicy), this is 1 -- the original one-at-a-time behavior.
+ * On multi-node NUMA systems, this is Min(CLOCK_SWEEP_BATCH_SIZE, NBuffers)
+ * to reduce cross-socket atomic contention on nextVictimBuffer.
+ */
+static uint32 ClockSweepBatchSize = 1;
+
+static inline uint32
+EffectiveBatchSize(void)
+{
+	return ClockSweepBatchSize;
+}
+
 /*
  * ClockSweepTick - Helper routine for StrategyGetBuffer()
  *
- * Move the clock hand one buffer ahead of its current position and return the
- * id of the buffer now under the hand.
+ * Return the next buffer to consider for eviction.  Backends claim batches
+ * of consecutive buffer IDs from the shared clock hand, then iterate through
+ * them locally without further atomic operations.  This preserves the global
+ * sweep order while reducing cross-socket contention on the shared counter.
  */
 static inline uint32
 ClockSweepTick(void)
 {
 	uint32		victim;
 
-	/*
-	 * Atomically move hand ahead one buffer - if there's several processes
-	 * doing this, this can lead to buffers being returned slightly out of
-	 * apparent order.
-	 */
-	victim =
-		pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer, 1);
-
-	if (victim >= NBuffers)
+	if (MyBatchPos >= MyBatchEnd)
 	{
-		uint32		originalVictim = victim;
-
-		/* always wrap what we look up in BufferDescriptors */
-		victim = victim % NBuffers;
-
 		/*
-		 * If we're the one that just caused a wraparound, force
-		 * completePasses to be incremented while holding the spinlock. We
-		 * need the spinlock so StrategySyncStart() can return a consistent
-		 * value consisting of nextVictimBuffer and completePasses.
+		 * Claim a new batch from the shared clock hand.  This is the only
+		 * atomic operation per batch, reducing contention by the batch size.
 		 */
-		if (victim == 0)
+		uint32		start;
+		uint32		batch_size = EffectiveBatchSize();
+
+		start = pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer,
+										batch_size);
+
+		if (start >= (uint32) NBuffers)
 		{
-			uint32		expected;
-			uint32		wrapped;
-			bool		success = false;
+			start = start % NBuffers;
 
-			expected = originalVictim + 1;
+			/*
+			 * If the counter has grown beyond NBuffers, try to wrap it back.
+			 * We must hold the spinlock so StrategySyncStart() can read
+			 * nextVictimBuffer and completePasses consistently.
+			 *
+			 * Multiple backends may enter this section concurrently. After
+			 * acquiring the spinlock, re-read the counter: if another backend
+			 * already wrapped it below NBuffers, we're done.
+			 */
+			SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
 
-			while (!success)
 			{
-				/*
-				 * Acquire the spinlock while increasing completePasses. That
-				 * allows other readers to read nextVictimBuffer and
-				 * completePasses in a consistent manner which is required for
-				 * StrategySyncStart().  In theory delaying the increment
-				 * could lead to an overflow of nextVictimBuffers, but that's
-				 * highly unlikely and wouldn't be particularly harmful.
-				 */
-				SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
-
-				wrapped = expected % NBuffers;
+				uint32		current;
+				uint32		wrapped;
 
-				success = pg_atomic_compare_exchange_u32(&StrategyControl->nextVictimBuffer,
-														 &expected, wrapped);
-				if (success)
-					StrategyControl->completePasses++;
-				SpinLockRelease(&StrategyControl->buffer_strategy_lock);
+				current = pg_atomic_read_u32(&StrategyControl->nextVictimBuffer);
+				if (current >= (uint32) NBuffers)
+				{
+					wrapped = current % NBuffers;
+					if (pg_atomic_compare_exchange_u32(&StrategyControl->nextVictimBuffer,
+													   &current, wrapped))
+						StrategyControl->completePasses++;
+				}
 			}
+
+			SpinLockRelease(&StrategyControl->buffer_strategy_lock);
 		}
+
+		MyBatchPos = start;
+		MyBatchEnd = start + batch_size;
 	}
+
+	victim = MyBatchPos % NBuffers;
+	MyBatchPos++;
+
 	return victim;
 }
 
@@ -408,6 +442,24 @@ StrategyCtlShmemInit(void *arg)
 
 	/* No pending notification */
 	StrategyControl->bgwprocno = -1;
+
+	/*
+	 * Determine the effective clock-sweep batch size.
+	 *
+	 * On multi-node NUMA systems, claiming batches of buffers from the shared
+	 * clock hand reduces cross-socket contention on the atomic counter.  On
+	 * single-socket systems, batching provides no benefit (the atomic is
+	 * already socket-local) and just causes backends to skip buffers, so we
+	 * use batch size 1 for the original behavior.
+	 *
+	 * pg_numa_init() returns -1 when NUMA is unavailable.
+	 * pg_numa_get_max_node() returns 0 for a single NUMA node.
+	 */
+	if (pg_numa_init() != -1 && pg_numa_get_max_node() >= 1)
+		ClockSweepBatchSize = Min(CLOCK_SWEEP_BATCH_SIZE,
+								  (uint32) NBuffers);
+	else
+		ClockSweepBatchSize = 1;
 }
 
 
-- 
2.50.1 (Apple Git-155)



^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention
  2026-04-25 20:08 [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
@ 2026-04-27 13:26 ` Greg Burd <greg@burd.me>
  1 sibling, 0 replies; 21+ messages in thread

From: Greg Burd @ 2026-04-27 13:26 UTC (permalink / raw)
  To: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; +Cc: Andres Freund <andres@anarazel.de>; Tomas Vondra <tomas@vondra.me>; Nathan Bossart <nathandbossart@gmail.com>


On Sat, Apr 25, 2026, at 4:08 PM, Greg Burd wrote:
> Hello hackers,

Hi again, attached is v2:

0001 - unchanged, batches clock-sweep to reduce contention
0002 - changed ComputeClockBatchSize() such that non-NUMA multi-core systems use batches as well and no longer default to batch size 1

Details below...

> A colleague of mine, Jim Mlodgenski, has been poking at NUMA behavior 
> on some of the newer AWS bare-metal instance types (r8i in particular, 
> which exposes 6 NUMA nodes via SNC3 on a 2-socket box), and in the 
> process landed on a very small change to freelist.c that I think is 
> worth showing around.  His patch is attached with some tweaks of my own.
>
> Full disclosure: the exploration that led Jim to this patch idea was 
> done with help from an AI assistant (Kiro); the idea, the benchmarking, 
> and the final shape of the patch are human-driven, but I wanted to be 
> up front about how his investigation started.  Happy to discuss that 
> separately if people want to.
>
> The one-line summary: instead of advancing nextVictimBuffer one buffer 
> at a time via pg_atomic_fetch_add_u32, each backend claims a batch of 
> 64 consecutive buffer IDs from the shared hand and then iterates them 
> privately.  Global sweep order is preserved -- every buffer is still
> visited exactly once per complete pass -- but the atomic contention on 
> that one cache line drops by roughly the batch size.
>
>
> Why this matters
> ----------------
>
> On multi-socket boxes under eviction pressure, every backend that needs 
> a victim buffer ends up CAS'ing the same cache line.  On a single 
> socket, a locked RMW on that cache line stays warm in L1/L2 and 
> completes in ~20ns.  On 2+ sockets, the line bounces over QPI/UPI at 
> ~100-200ns per op, and with hundreds of backends running 
> StrategyGetBuffer() concurrently, the line ping-pongs constantly.  It's 
> a textbook NUMA scalability bottleneck, and once shared_buffers is 
> smaller than the working set and the sweep is running continuously, 
> that single atomic is what you hit in a perf profile (elevated 
> bus-cycles, cache-misses on the cache line holding nextVictimBuffer).
>
> Andres pointed at the same spot in his pgconf.eu 2024 talk, and Tomas 
> called it out in the "Adding basic NUMA awareness" thread [1] -- so 
> this isn't news to anyone who's been looking at this area.  What I 
> think is new is a fix that's just this, without any of the surrounding 
> architectural change.
>
> The framing (credit to Jim): the clock hand is doing two jobs.  It 
> *coordinates* backends so they don't redundantly decrement usage_count 
> on the same buffers and so they eventually visit every buffer in the 
> pool exactly once per pass.  It also *serializes* access to the 
> counter.  Coordination is the part we want.  Serialization is the part 
> that's killing us on bigger NUMA boxes.  Batching keeps the 
> coordination and thins out the serialization.
>
>
> How it works
> ------------
>
> Two per-backend statics, MyBatchPos and MyBatchEnd.  When a backend 
> calls ClockSweepTick() and its local batch is exhausted, it does a 
> single fetch-add of CLOCK_SWEEP_BATCH_SIZE (64) against 
> nextVictimBuffer and now owns that range.  Subsequent ticks just bump 
> the local counter.
>
> Wraparound got a small rewrite.  The original code had the backend that 
> crossed NBuffers drive completePasses++ under the spinlock via a CAS 
> loop.  With batching, multiple backends can each land a fetch-add that 
> returns a value >= NBuffers in the same pass, so the logic now is: 
> whoever sees a start >= NBuffers takes the spinlock, re-reads the 
> counter, and if it's still out of range does a single CAS to wrap it 
> and bumps completePasses.  If somebody else already wrapped, we just 
> release and move on.  StrategySyncStart() still sees a consistent 
> (nextVictimBuffer, completePasses) pair.
>
> The batch size is gated on whether we actually have multiple NUMA 
> nodes.  On a single-socket box the atomic is already socket-local, 
> batching just makes backends skip further ahead than they need to, so 
> we fall back to batch size 1 -- which is bit-for-bit the original 
> behavior.  The guard:
>
>     if (pg_numa_init() != -1 && pg_numa_get_max_node() >= 1)
>         ClockSweepBatchSize = Min(CLOCK_SWEEP_BATCH_SIZE, (uint32) NBuffers);
>     else
>         ClockSweepBatchSize = 1;
>
> Min() against NBuffers covers the small-shared_buffers corner so a 
> batch never wraps the pool multiple times in one claim.

Thinking more about this approach led me to believe that this non-NUMA default is wrong and induces overhead for a very common case.

> Does batching mess up the meaning of usage_count?
> --------------------------------------------------
>
> Short answer: no.  I want to walk through this because it was my first 
> concern too, and I think it's the question that will come up most on 
> review.
>
> The clock sweep's usage_count is an access-frequency approximation 
> measured in units of *complete passes*.  A buffer with usage_count = N 
> survives N passes without a re-pin.  The semantic meaning lives at pass 
> granularity, not at individual-buffer granularity.
>
> What batching changes: intra-pass temporal ordering.  Without batching, 
> with N backends sweeping, decrements are interleaved -- backend A hits 
> B[0], backend B hits B[1], backend C hits B[2].  With batching, backend 
> A hits B[0..63] in a tight local burst, then backend B hits B[64..127], 
> etc.  The 64-buffer chunks are decremented in bursts rather than 
> individually.
>
> Why it doesn't matter:
>
>   1. Every buffer still gets decremented exactly once per complete
>      pass.  The invariant the algorithm actually depends on is
>      untouched.
>
>   2. A buffer's survival window is the time between consecutive
>      passes.  That's milliseconds to seconds under load.  Whether
>      B[0] gets decremented 50us before or 50us after B[63] within
>      the same pass is below the resolution of anything usage_count
>      is trying to measure.
>
>   3. The bgwriter's feedback loop reads (nextVictimBuffer,
>      completePasses, numBufferAllocs) via StrategySyncStart() every
>      ~200ms.  nextVictimBuffer still advances at the same *total*
>      rate (64 per atomic op, but atomic ops happen 1/64 as often).
>      The position it reports can jitter by up to 64 buffers relative
>      to the one-at-a-time case, but BgBufferSync()'s smoothed
>      estimates operate over thousands of buffers per cycle, so the
>      jitter disappears into the averaging.  numBufferAllocs still
>      increments once per allocation.  strategy_delta,
>      smoothed_alloc, smoothed_density, reusable_buffers_est -- all
>      unaffected in any way I can see.
>
> Table form, because it's easier to argue with:
>
>   Property                          | Unpatched      | Batched
>   ----------------------------------+----------------+----------------
>   Buffers visited per pass          | NBuffers       | NBuffers
>   Decrements per buffer per pass    | 1              | 1
>   Eviction threshold                | usage_count==0 | usage_count==0
>   Max survival (passes)             | 6              | 6
>   Decrement ordering within a pass  | interleaved    | chunked
>   bgwriter allocation rate signal   | accurate       | accurate
>   Cross-socket atomic traffic       | 1 per buffer   | 1 per 64
>
> There is one subtle difference worth naming.  When a backend finds a 
> victim at B[5] of its batch, it returns with MyBatchEnd still sitting 
> at B[63].  The next time that backend needs a victim it resumes at 
> B[6], not at wherever the global hand now points.  So the backend 
> drains its batch over multiple StrategyGetBuffer() calls rather than 
> all at once.  Under heavy load, where batches are consumed in 
> microseconds, this is invisible.  Under light load, the implication is 
> that some buffers can sit with slightly stale usage_count for longer 
> than they would have before.  But "light load" means "the sweep is 
> barely moving and nothing wants to evict anyway" -- so the effect
> doesn't show up where it would hurt.
>
> There's also a small positive side-effect: cache locality.  The backend 
> that just touched BufferDescriptor[B[0]] has the adjacent descriptors 
> warm in L1/L2.  Walking B[0..63] locally is cheaper than walking a 
> striped interleaving where each descriptor was last touched by a 
> different core.  I haven't tried to isolate this in perf, but it falls 
> out naturally.
>
>
> Benchmarks
> ----------
>
> Jim ran these; I'm still working on reproducing them locally and will 
> post independent numbers in a follow-up.  All bare metal, Linux, huge 
> pages enabled throughout (more on that below), postmaster pinned to 
> node 0 with `numactl --cpunodebind=0` because otherwise stock TPS 
> varied from 31K to 40K depending on which node the postmaster happened 
> to land on at launch -- worth flagging for anyone trying to reproduce.
>
> Workload is pgbench scale 3000 (~45GB) with shared_buffers=32GB, so the 
> working set always spills and the sweep is hot.
>
>   r8i.metal-96xl (384 vCPUs, 2 sockets, 6 NUMA nodes via SNC3):
>
>     pgbench RO:
>       Clients   Stock    Patched   Delta
>       64        31,457   36,353    +16%
>       128       31,678   37,864    +20%
>       256       31,510   37,558    +19%
>       384       31,431   37,464    +19%
>       512       31,329   37,040    +18%
>
>     pgbench RW:
>       Clients   Stock    Patched   Delta
>       64         7,685    7,713     0%
>       128       10,420   10,541    +1%
>       256       12,393   12,463    +1%
>       384       15,317   15,197    -1%
>       512       17,930   17,978     0%
>
>   m6i.metal (128 vCPUs, 2 sockets, Ice Lake):
>     RO +19-20%, RW within noise.
>
>   c8i.metal-48xl (192 vCPUs, 1 socket):
>     Single-socket -> batch_size=1 -> original code path.  No
>     behavioral change.  (I double-checked this one specifically
>     because it's the sanity test for the gate.)
>
>   HammerDB TPC-C on m6i.metal (1000 warehouses):
>     VUs   Stock     Patched   Delta
>     128   358,518   349,787    -2%
>     256   332,098   330,272    -1%
>     384   365,782   377,519    +3%
>     512   370,663   386,526    +4%
>
> No TPC-C regression, which was the thing we were most worried about. An 
> earlier attempt (per-socket partitioned sweep, see below) was -13% on 
> this same workload.
>
> The general shape is: the scaling curve flattens later.  Unpatched, TPS 
> tops out around 128 clients and stays flat up to 512 because backends 
> are spending cycles waiting on the cache line rather than
> doing work.  Patched, the curve keeps rising past the point where 
> unpatched plateaus.
>
> Huge pages caveat: all of the above was run with huge pages on, on 
> large-memory instances (the r8i.96xl has 3TB, so Jim never considered 
> running without them).  We have not characterized the non-huge-pages 
> case.  That's on my list; I don't expect it to change the conclusion, 
> but I shouldn't speak for data I haven't collected.
>
>
> Relationship to Tomas's NUMA series
> -----------------------------------
>
> Tomas posted a multi-patch NUMA-awareness series in [1] covering buffer 
> interleaving across nodes, partitioned freelists, partitioned clock 
> sweep, PGPROC interleaving, and related pieces.  I want to be careful 
> here because I don't think we should frame this patch as competing with 
> that work.
>
> One thing I found striking as I re-read the thread: in the benchmarks 
> Tomas posted later in the series, *most of the benefit comes from 
> partitioning the clock sweep*, and the NUMA memory-placement layer on 
> top sometimes runs slower than partitioning alone.  His own conclusion, 
> quoted roughly: the benefit mostly comes from just partitioning the 
> clock sweep, and it's largely independent of the NUMA stuff; the NUMA 
> partitioning is often slower.
>
> That observation is the thing that makes me think batching is worth 
> considering on its own.  It's going after the same bottleneck Tomas's 
> partitioning addresses, but:
>
>   - without splitting global eviction visibility (which is where
>     cross-partition stealing gets complicated),
>   - without requiring NUMA-aware buffer placement (which has huge
>     page alignment, descriptor-partition-mid-page, and resize
>     complications that are still being worked out in that thread),
>   - without touching PGPROC or bgwriter.
>
> What this patch does *not* do:
>   - place buffers on specific NUMA nodes
>   - partition the freelist
>   - touch PGPROC
>   - add new GUCs
>   - change bgwriter
>
> What this patch *does* do:
>   - target exactly the clock-sweep contention that Tomas's
>     partitioning targets, and reduce it by ~64x, in ~30 lines.
>
> If Tomas's series lands in full, this patch becomes redundant for its 
> primary use case (though even within a partitioned sweep, the 
> per-partition atomic still benefits from batching, so it's arguably a 
> useful primitive either way).  If Tomas's series lands incrementally 
> over several cycles -- which the open items in that thread suggest is 
> the realistic path -- this gets us a real chunk of the multi-socket win 
> now.
>
> This patch is also orthogonal to my earlier thread about removing the 
> freelist entirely [2], but given the proximity to that code Jim agreed 
> that I could propose/steward it here on the list for consideration.
>
>
> Open questions / things I'd like feedback on
> --------------------------------------------
>
> - Batch size.  64 is a round number that worked well in testing, but
>   Nathan raised the reasonable point that on small shared_buffers
>   with high concurrency, a fixed 64 could be unfortunate.  Options:
>   scale with shared_buffers (Min(64, NBuffers / N) for some N), scale
>   with max_connections, keep it fixed but let operators tune it, or
>   make it a function of NUMA node count.  I don't have a strong
>   opinion yet; the Min(batch, NBuffers) cap covers the "obviously
>   wrong" corner but doesn't speak to the "several hundred backends
>   on a few-MB shared_buffers" shape.  Numbers/ideas/proposals welcome.
>
> - NUMA detection.  The gate uses pg_numa_init() /
>   pg_numa_get_max_node().  On systems where libnuma isn't available,
>   or where get_mempolicy is blocked (some container configurations),
>   we fall back to batch size 1.  That's safe but it misses the
>   "single socket, many cores, still benefits from fewer atomics"
>   case.  Might be worth a way to force-enable, or batching on all
>   systems with a smaller batch size when single-socket.  I'd like to
>   measure before deciding.
>
> - Eviction pattern on reads.  Nathan also flagged that with batching,
>   the buffers a backend ends up pinning in one StrategyGetBuffer()
>   call will tend to be contiguous in buffer-id space rather than
>   scattered, which is a different allocation pattern than today.
>   The usage_count analysis above says this is benign, but if anyone
>   has an intuition for a workload where this would be observable
>   (e.g., something that cares about the mapping between buffer-id
>   and relation locality), I'd like to hear it.
>
> - nextVictimBuffer wraparound.  The current code has a mild overflow
>   concern papered over with "highly unlikely and wouldn't be
>   particularly harmful".  With batching this is no worse than before,
>   but if we're already touching this function, it might be worth
>   thinking about whether to tighten it up in the same patch or a
>   follow-up.
>
> - Should the non-NUMA value for this be derived from core counts that
>   imply L1/L2 cache layouts or simply default to 8 rather than 1 to
>   realize some benefit?

So, I'm answering my own question here.  Yes, it should.  Ideas below.

> - Should there be a postgresql.conf setting for this that takes
>   precedence?
>
>
> I'll run the non-huge-pages variant, reproduce the r8i numbers, poke at 
> the small-shared_buffers corner, and post perf stat output showing the 
> atomic/cache-miss deltas over the next few days.  In the meantime, 
> eyeballs and skepticism welcome -- I would especially welcome comments 
> from Andres, who's been in this code recently, and from Tomas, whose 
> series has the most overlap.
>
> I realize that we're past feature freeze and working on release notes 
> for v19, so the chances of merging this are slim to none.  I think this 
> could be considered a "performance bug fix for NUMA systems" in this 
> release, but that is stretching it a bit.  It is a big ask at this 
> stage to land a change like this.
>
> best.
>
> -greg
>
> [1] 
> https://www.postgresql.org/message-id/099b9433-2855-4f1b-b421-d078a5d82017@vondra.me
> [2] 
> https://www.postgresql.org/message-id/f0e3c02e-e217-4f04-8dab-1e7e80a228c0@burd.me
> Attachments:
> * v1-0001-Reduce-clock-sweep-atomic-contention-by-claiming-.patch

ComputeClockBatchSize() has two phases: select a base batch from hardware topology, then cap it to prevent over-claiming.

Phase 1: Base batch from topology

  int ncpus = pg_get_online_cpus();
  int numa_nodes = (pg_numa_init() != -1) ? pg_numa_get_max_node() + 1 : 1;

  if (numa_nodes > 1)        base_batch = 64;
  else if (ncpus > 16)       base_batch = 32;
  else if (ncpus > 8)        base_batch = 16;
  else if (ncpus > 4)        base_batch = 8;
  else                       base_batch = 1;

The reasoning at each tier:

  - NUMA (multi-socket): Atomic ops cross the interconnect (QPI/UPI/Infinity
    Fabric). Round-trip latency is ~100-300ns vs ~10-40ns intra-socket.
    Batch=64 amortizes that heavily.

  - >16 cores, single socket: Still significant L3 contention, many cores
    competing for the same cache line. Batch=32 cuts atomic ops by 32x.

  - 9-16 cores: Moderate contention. Batch=16.

  - 5-8 cores: Light contention. Batch=8.

  - <=4 cores: Almost no contention. Batch=1 (no batching). The overhead of 
    batching logic isn't worth it, and there's a fairness tradeoff - batching
    means one backend "owns" a range of buffers temporarily, which matters 
    more when there are few buffers per backend.

Phase 2: Cap to prevent over-claiming

  max_batch = (MaxBackends > 0)
      ? pool_nbuffers / (2 * MaxBackends)
      : pool_nbuffers / 200;
  if (max_batch < 1)
      max_batch = 1;

  return Min(base_batch, Min(max_batch, pool_nbuffers));

The cap ensures that if every backend simultaneously claims a batch, the total claimed doesn't exceed half the pool:

  batch_size * MaxBackends <= pool_nbuffers / 2

Why half? If all backends claimed the entire pool simultaneously, they'd each be sweeping overlapping ranges, thus wasting work and defeating the purpose. Keeping total claims under 50% of the pool means at any instant, at most half the buffers are "in flight" being evaluated by backends, and the other half are available for normal operation.

For a small dynamic pool (say 4096 buffers with MaxBackends=200), the cap computes to 4096 / 400 = 10, which overrides any larger base_batch. For the default pool with shared_buffers = 8GB (1M buffers) and MaxBackends=200, the cap is 1000000 / 400 = 2500 which is well above the max base_batch of 64, so the base_batch wins.

The pool_nbuffers floor at the end handles the degenerate case of a pool smaller than the batch size.

The Tradeoff

Larger batches reduce atomic contention but increase sweep unevenness, one backend might sweep through "cold" buffers while another's batch happens to land on "hot" ones. The tiered approach balances this: batch aggressively only when the hardware topology makes contention the dominant cost (NUMA, many-core), and stay conservative on small systems where fairness matters more.


I think this is better because:

1. The original patch only batched on multi-socket NUMA systems. The new algorithm also provides atomic contention benefits on large single-socket systems (>16 cores) where L3 cache contention matters.

2. Conservative on small systems: Systems with ≤4 cores get batch_size=1 (original behavior) since batching overhead outweighs contention benefits and fairness matters more.

3. Prevents pathological over-claiming: The cap mechanism prevents scenarios where many backends claim huge batches relative to a small buffer pool.


Based on the algorithm, here's what different systems would get:

        System         CPUs   NUMA      Total RAM   Shr Buf   Batch Size  Atomic Reduction                                                                          
  ==================  ====  ========  ===========  ========  ==========  ================                                                                           
  r8i.metal-96xl       384     multi      3072GB   2457.6GB          64    64x                                                                                      
  m6i.metal            128     multi       512GB    409.6GB          64    64x                                                                                      
  c8i.metal-48xl       192  1 socket       192GB    153.6GB          32    32x                                                                                      
  Large server          64     multi       256GB    204.8GB          64    64x
  Medium server         32  1 socket        64GB     51.2GB          32    32x                                                                                      
  Small server          16  1 socket        32GB     25.6GB          16    16x                                                                                      
  Developer machine      8  1 socket        16GB     12.8GB           8    8x
  Small VM               4  1 socket         4GB      3.2GB           1    no change
  Overloaded VM          8  1 socket         4GB      3.2GB           8    8x

best.

-greg

Attachments:

  [application/octet-stream] v2-0001-Reduce-clock-sweep-atomic-contention-by-claiming-.patch (8.2K, ../../3d85395b-698e-4f0e-873e-17b3f277f7c7@app.fastmail.com/2-v2-0001-Reduce-clock-sweep-atomic-contention-by-claiming-.patch)
  download | inline diff:
From bdcf90fbd89a0aec397a3d57224ae732959733f9 Mon Sep 17 00:00:00 2001
From: Greg Burd <greg@burd.me>
Date: Sat, 25 Apr 2026 15:52:36 -0400
Subject: [PATCH v2 1/2] Reduce clock-sweep atomic contention by claiming
 buffers in batches

StrategyGetBuffer() advances nextVictimBuffer via
pg_atomic_fetch_add_u32(..., 1) on every tick.  On multi-socket
systems the cache line holding the counter has to travel over the
interconnect on each operation, pushing a sweep tick from ~20ns (the
same-socket case) into the ~100-200ns range.  With hundreds of
concurrent backends under eviction pressure, that one cache line
becomes the dominant cost in the sweep, visible as elevated
bus-cycles and cache-misses in perf profiles.

Each backend now claims a range of CLOCK_SWEEP_BATCH_SIZE (64)
consecutive buffer IDs with a single fetch-add and iterates through
them privately.  The sweep still advances through the pool in order,
each buffer is still visited exactly once per complete pass, and
usage_count is still decremented exactly once per buffer per pass;
the meaning of usage_count as "how many complete passes a buffer
survives without a re-pin" is preserved.  What changes is the
temporal ordering of decrements within a single pass, which the
algorithm does not depend on.

Wraparound handling is adjusted: with batching, multiple backends
can each see their fetch-add return a value past NBuffers within
the same pass.  Any such backend takes buffer_strategy_lock,
re-reads the counter, and if it is still out of range wraps it with
a single CAS and increments completePasses.  StrategySyncStart()
continues to see a consistent (nextVictimBuffer, completePasses)
pair.

Batching is only useful when the atomic is actually contended
across nodes, so it is applied only when libnuma reports more than
one node (pg_numa_get_max_node() >= 1); otherwise the batch size
stays at 1 and the code path matches master bit-for-bit.  The batch
is also capped at NBuffers so a claim cannot wrap the pool more
than once.

Co-Authored-by: Jim Mlodgenski <mlodj@amazon.com>
Co-Authored-by: Greg Burd <greg@burd.me>
---
 src/backend/storage/buffer/freelist.c | 136 ++++++++++++++++++--------
 1 file changed, 94 insertions(+), 42 deletions(-)

diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index fdb5bad7910..e86ed1f7da0 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -22,6 +22,7 @@
 #include "storage/proc.h"
 #include "storage/shmem.h"
 #include "storage/subsystems.h"
+#include "port/pg_numa.h"
 
 #define INT_ACCESS_ONCE(var)	((int)(*((volatile int *)&(var))))
 
@@ -100,68 +101,101 @@ static BufferDesc *GetBufferFromRing(BufferAccessStrategy strategy,
 static void AddBufferToRing(BufferAccessStrategy strategy,
 							BufferDesc *buf);
 
+/*
+ * Number of buffer IDs to claim from the shared clock hand at once.
+ * Larger values reduce contention on the shared atomic.  With a batch
+ * size of 64, concurrent backends sweep non-overlapping chunks of 64
+ * buffers rather than interleaving one buffer at a time.  The global
+ * sweep order is preserved — each buffer is still visited exactly once
+ * per complete pass.
+ */
+#define CLOCK_SWEEP_BATCH_SIZE 64
+
+/*
+ * Per-backend state for batched clock sweep.
+ */
+static uint32 MyBatchPos = 0;	/* next buffer within batch */
+static uint32 MyBatchEnd = 0;	/* one past last buffer in batch */
+
+/*
+ * Effective batch size for the clock sweep, computed once at startup.
+ * On non-NUMA systems (single socket, no libnuma, or containers blocking
+ * get_mempolicy), this is 1 -- the original one-at-a-time behavior.
+ * On multi-node NUMA systems, this is Min(CLOCK_SWEEP_BATCH_SIZE, NBuffers)
+ * to reduce cross-socket atomic contention on nextVictimBuffer.
+ */
+static uint32 ClockSweepBatchSize = 1;
+
+static inline uint32
+EffectiveBatchSize(void)
+{
+	return ClockSweepBatchSize;
+}
+
 /*
  * ClockSweepTick - Helper routine for StrategyGetBuffer()
  *
- * Move the clock hand one buffer ahead of its current position and return the
- * id of the buffer now under the hand.
+ * Return the next buffer to consider for eviction.  Backends claim batches
+ * of consecutive buffer IDs from the shared clock hand, then iterate through
+ * them locally without further atomic operations.  This preserves the global
+ * sweep order while reducing cross-socket contention on the shared counter.
  */
 static inline uint32
 ClockSweepTick(void)
 {
 	uint32		victim;
 
-	/*
-	 * Atomically move hand ahead one buffer - if there's several processes
-	 * doing this, this can lead to buffers being returned slightly out of
-	 * apparent order.
-	 */
-	victim =
-		pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer, 1);
-
-	if (victim >= NBuffers)
+	if (MyBatchPos >= MyBatchEnd)
 	{
-		uint32		originalVictim = victim;
-
-		/* always wrap what we look up in BufferDescriptors */
-		victim = victim % NBuffers;
-
 		/*
-		 * If we're the one that just caused a wraparound, force
-		 * completePasses to be incremented while holding the spinlock. We
-		 * need the spinlock so StrategySyncStart() can return a consistent
-		 * value consisting of nextVictimBuffer and completePasses.
+		 * Claim a new batch from the shared clock hand.  This is the only
+		 * atomic operation per batch, reducing contention by the batch size.
 		 */
-		if (victim == 0)
+		uint32		start;
+		uint32		batch_size = EffectiveBatchSize();
+
+		start = pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer,
+										batch_size);
+
+		if (start >= (uint32) NBuffers)
 		{
-			uint32		expected;
-			uint32		wrapped;
-			bool		success = false;
+			start = start % NBuffers;
 
-			expected = originalVictim + 1;
+			/*
+			 * If the counter has grown beyond NBuffers, try to wrap it back.
+			 * We must hold the spinlock so StrategySyncStart() can read
+			 * nextVictimBuffer and completePasses consistently.
+			 *
+			 * Multiple backends may enter this section concurrently. After
+			 * acquiring the spinlock, re-read the counter: if another backend
+			 * already wrapped it below NBuffers, we're done.
+			 */
+			SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
 
-			while (!success)
 			{
-				/*
-				 * Acquire the spinlock while increasing completePasses. That
-				 * allows other readers to read nextVictimBuffer and
-				 * completePasses in a consistent manner which is required for
-				 * StrategySyncStart().  In theory delaying the increment
-				 * could lead to an overflow of nextVictimBuffers, but that's
-				 * highly unlikely and wouldn't be particularly harmful.
-				 */
-				SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
-
-				wrapped = expected % NBuffers;
+				uint32		current;
+				uint32		wrapped;
 
-				success = pg_atomic_compare_exchange_u32(&StrategyControl->nextVictimBuffer,
-														 &expected, wrapped);
-				if (success)
-					StrategyControl->completePasses++;
-				SpinLockRelease(&StrategyControl->buffer_strategy_lock);
+				current = pg_atomic_read_u32(&StrategyControl->nextVictimBuffer);
+				if (current >= (uint32) NBuffers)
+				{
+					wrapped = current % NBuffers;
+					if (pg_atomic_compare_exchange_u32(&StrategyControl->nextVictimBuffer,
+													   &current, wrapped))
+						StrategyControl->completePasses++;
+				}
 			}
+
+			SpinLockRelease(&StrategyControl->buffer_strategy_lock);
 		}
+
+		MyBatchPos = start;
+		MyBatchEnd = start + batch_size;
 	}
+
+	victim = MyBatchPos % NBuffers;
+	MyBatchPos++;
+
 	return victim;
 }
 
@@ -408,6 +442,24 @@ StrategyCtlShmemInit(void *arg)
 
 	/* No pending notification */
 	StrategyControl->bgwprocno = -1;
+
+	/*
+	 * Determine the effective clock-sweep batch size.
+	 *
+	 * On multi-node NUMA systems, claiming batches of buffers from the shared
+	 * clock hand reduces cross-socket contention on the atomic counter.  On
+	 * single-socket systems, batching provides no benefit (the atomic is
+	 * already socket-local) and just causes backends to skip buffers, so we
+	 * use batch size 1 for the original behavior.
+	 *
+	 * pg_numa_init() returns -1 when NUMA is unavailable.
+	 * pg_numa_get_max_node() returns 0 for a single NUMA node.
+	 */
+	if (pg_numa_init() != -1 && pg_numa_get_max_node() >= 1)
+		ClockSweepBatchSize = Min(CLOCK_SWEEP_BATCH_SIZE,
+								  (uint32) NBuffers);
+	else
+		ClockSweepBatchSize = 1;
 }
 
 
-- 
2.50.1 (Apple Git-155)



  [application/octet-stream] v2-0002-Improve-clock-sweep-batch-sizing-with-CPU-aware-a.patch (5.0K, ../../3d85395b-698e-4f0e-873e-17b3f277f7c7@app.fastmail.com/3-v2-0002-Improve-clock-sweep-batch-sizing-with-CPU-aware-a.patch)
  download | inline diff:
From dce473e1a246d877f0f2313ba7a29a4d0e35dc27 Mon Sep 17 00:00:00 2001
From: Greg Burd <gregburd@amazon.com>
Date: Mon, 27 Apr 2026 08:25:40 -0400
Subject: [PATCH v2 2/2] Improve clock sweep batch sizing with CPU-aware
 algorithm
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Replace simple NUMA-only batch sizing with a tiered approach:

- NUMA systems (multi-socket): batch=64 (high interconnect latency)
- Single socket >16 cores: batch=32 (L3 cache contention)
- Single socket 9-16 cores: batch=16 (moderate contention)
- Single socket 5-8 cores: batch=8 (light contention)
- Single socket ≤4 cores: batch=1 (no batching overhead)

Also adds over-claiming protection: batch_size × MaxBackends ≤ pool_size/2
to ensure total claimed buffers stay under 50% of the pool.

This provides atomic contention benefits on large single-socket systems
while maintaining the original behavior on small systems where fairness
matters more than throughput.

Authored-by: Greg Burd <greg@burd.me>
---
 src/backend/storage/buffer/freelist.c | 91 ++++++++++++++++++++++-----
 1 file changed, 77 insertions(+), 14 deletions(-)

diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index e86ed1f7da0..476d7f420a5 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -24,6 +24,10 @@
 #include "storage/subsystems.h"
 #include "port/pg_numa.h"
 
+#ifdef HAVE_UNISTD_H
+#include <unistd.h>
+#endif
+
 #define INT_ACCESS_ONCE(var)	((int)(*((volatile int *)&(var))))
 
 
@@ -61,6 +65,8 @@ static BufferStrategyControl *StrategyControl = NULL;
 
 static void StrategyCtlShmemRequest(void *arg);
 static void StrategyCtlShmemInit(void *arg);
+static int	pg_get_online_cpus(void);
+static uint32 ComputeClockBatchSize(int pool_nbuffers);
 
 const ShmemCallbacks StrategyCtlShmemCallbacks = {
 	.request_fn = StrategyCtlShmemRequest,
@@ -411,6 +417,69 @@ StrategyNotifyBgWriter(int bgwprocno)
 	SpinLockRelease(&StrategyControl->buffer_strategy_lock);
 }
 
+/*
+ * pg_get_online_cpus -- get the number of online CPU cores
+ */
+static int
+pg_get_online_cpus(void)
+{
+#ifdef _SC_NPROCESSORS_ONLN
+	long		ncpus = sysconf(_SC_NPROCESSORS_ONLN);
+
+	if (ncpus > 0)
+		return (int) ncpus;
+#endif
+	/* Fallback if sysconf is unavailable or fails */
+	return 1;
+}
+
+/*
+ * ComputeClockBatchSize -- compute the effective clock-sweep batch size
+ *
+ * The function has two phases: select a base batch from hardware topology,
+ * then cap it to prevent over-claiming.
+ *
+ * Phase 1: Base batch from topology
+ * - NUMA (multi-socket): batch=64 (high cross-socket latency)
+ * - >16 cores, single socket: batch=32 (L3 contention)
+ * - 9-16 cores: batch=16 (moderate contention)
+ * - 5-8 cores: batch=8 (light contention)
+ * - <=4 cores: batch=1 (no batching overhead)
+ *
+ * Phase 2: Cap to prevent over-claiming
+ * - Ensure batch_size * MaxBackends <= pool_nbuffers / 2
+ * - Keeps total claims under 50% of the pool
+ */
+static uint32
+ComputeClockBatchSize(int pool_nbuffers)
+{
+	int			ncpus = pg_get_online_cpus();
+	int			numa_nodes = (pg_numa_init() != -1) ? pg_numa_get_max_node() + 1 : 1;
+	uint32		base_batch;
+	uint32		max_batch;
+
+	/* Phase 1: Base batch from topology */
+	if (numa_nodes > 1)
+		base_batch = 64;
+	else if (ncpus > 16)
+		base_batch = 32;
+	else if (ncpus > 8)
+		base_batch = 16;
+	else if (ncpus > 4)
+		base_batch = 8;
+	else
+		base_batch = 1;
+
+	/* Phase 2: Cap to prevent over-claiming */
+	max_batch = (MaxBackends > 0)
+		? pool_nbuffers / (2 * MaxBackends)
+		: pool_nbuffers / 200;
+	if (max_batch < 1)
+		max_batch = 1;
+
+	return Min(base_batch, Min(max_batch, (uint32) pool_nbuffers));
+}
+
 
 /*
  * StrategyCtlShmemRequest -- request shared memory for the buffer
@@ -444,22 +513,16 @@ StrategyCtlShmemInit(void *arg)
 	StrategyControl->bgwprocno = -1;
 
 	/*
-	 * Determine the effective clock-sweep batch size.
+	 * Compute the effective clock-sweep batch size based on hardware
+	 * topology.
 	 *
-	 * On multi-node NUMA systems, claiming batches of buffers from the shared
-	 * clock hand reduces cross-socket contention on the atomic counter.  On
-	 * single-socket systems, batching provides no benefit (the atomic is
-	 * already socket-local) and just causes backends to skip buffers, so we
-	 * use batch size 1 for the original behavior.
-	 *
-	 * pg_numa_init() returns -1 when NUMA is unavailable.
-	 * pg_numa_get_max_node() returns 0 for a single NUMA node.
+	 * This uses a tiered approach: larger batches on NUMA systems and
+	 * many-core single-socket systems where atomic contention is high,
+	 * smaller batches or no batching on few-core systems where fairness
+	 * matters more. The batch size is also capped to prevent over-claiming
+	 * when there are many backends relative to the buffer pool size.
 	 */
-	if (pg_numa_init() != -1 && pg_numa_get_max_node() >= 1)
-		ClockSweepBatchSize = Min(CLOCK_SWEEP_BATCH_SIZE,
-								  (uint32) NBuffers);
-	else
-		ClockSweepBatchSize = 1;
+	ClockSweepBatchSize = ComputeClockBatchSize(NBuffers);
 }
 
 
-- 
2.50.1 (Apple Git-155)



^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention
  2026-04-25 20:08 [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
@ 2026-04-27 14:15 ` Andres Freund <andres@anarazel.de>
  2026-04-28 14:30   ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  1 sibling, 1 reply; 21+ messages in thread

From: Andres Freund @ 2026-04-27 14:15 UTC (permalink / raw)
  To: Greg Burd <greg@burd.me>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Tomas Vondra <tomas@vondra.me>; Nathan Bossart <nathandbossart@gmail.com>

Hi,

Thanks for looking into this.

On 2026-04-25 16:08:02 -0400, Greg Burd wrote:
> Does batching mess up the meaning of usage_count?
> --------------------------------------------------
> 
> Short answer: no.  I want to walk through this because it was my first
> concern too, and I think it's the question that will come up most on review.

> 
> The clock sweep's usage_count is an access-frequency approximation measured
> in units of *complete passes*.  A buffer with usage_count = N survives N
> passes without a re-pin.  The semantic meaning lives at pass granularity,
> not at individual-buffer granularity.

> 
> What batching changes: intra-pass temporal ordering.  Without batching, with
> N backends sweeping, decrements are interleaved -- backend A hits B[0],
> backend B hits B[1], backend C hits B[2].  With batching, backend A hits
> B[0..63] in a tight local burst, then backend B hits B[64..127], etc.  The
> 64-buffer chunks are decremented in bursts rather than individually.

> 
> Why it doesn't matter:
> 
>   1. Every buffer still gets decremented exactly once per complete
>      pass.  The invariant the algorithm actually depends on is
>      untouched.
> 
>   2. A buffer's survival window is the time between consecutive
>      passes.  That's milliseconds to seconds under load.  Whether
>      B[0] gets decremented 50us before or 50us after B[63] within
>      the same pass is below the resolution of anything usage_count
>      is trying to measure.

I don't think this is true, unfortunately. Sure, if you have a completely
uniform, IO intensive, workload it is, but that's not all there is.  If you
have a bunch of connections that replace buffers at a low rate and a bunch of
connections that do so at a high rate, the batches "checked out" by the "low
rate" connections won't be processed soon. Thus the buffers in that batch
won't have their usagecount decremented and thus have a stronger "protection"
against replacement.

You can argue that may be OK, because it'd be unlikely that the next sweep
would assign the same buffers to an "low rate" backend again. But that'd be an
argument you'd have to make and validate.


I'm somewhat doubtful that batching that's independent of contention and
independent of the usage rate will work out all that well.  If you instead
went for a partitioned sweep architecture, with balancing between the
different partitions, you don't have that issue. And you have a building block
for more numa awareness etc.


> There is one subtle difference worth naming.  When a backend finds a victim at B[5] of its batch, it returns with MyBatchEnd still sitting at B[63].  The next time that backend needs a victim it resumes at B[6], not at wherever the global hand now points.  So the backend drains its batch over multiple StrategyGetBuffer() calls rather than all at once.  Under heavy load, where batches are consumed in microseconds, this is invisible.  Under light load, the implication is that some buffers can sit with slightly stale usage_count for longer than they would have before.  But "light load" means "the sweep is barely moving and nothing wants to evict anyway" -- so the effect
> doesn't show up where it would hurt.

As mentioned above, this assumes that the replacement rate is uniform between
backends, which I think is not uniformly true outside of benchmarks.


> There's also a small positive side-effect: cache locality.  The backend that
> just touched BufferDescriptor[B[0]] has the adjacent descriptors warm in
> L1/L2.

A BufferDesc is 64bytes. With common cacheline sizes and stuff like adjacent
cacheline prefetching you'll have *maybe* 2 consecutive BufferDescs in L1/L2.
Where it might help more is the TLB.


> Benchmarks
> ----------
> 
> Jim ran these; I'm still working on reproducing them locally and will post
> independent numbers in a follow-up.  All bare metal, Linux, huge pages
> enabled throughout (more on that below), postmaster pinned to node 0 with
> `numactl --cpunodebind=0` because otherwise stock TPS varied from 31K to 40K
> depending on which node the postmaster happened to land on at launch --
> worth flagging for anyone trying to reproduce.

That's an odd one that I think you need to investigate separately.


> Workload is pgbench scale 3000 (~45GB) with shared_buffers=32GB, so the
> working set always spills and the sweep is hot.

Uhm, is this something worth optimizing substantially for?  What you're
measuring here is basically the worst possible way of configuring a database,
with full double buffering and a lot of memory bandwidth dedicated to copying
buffers from one place to another. That's maybe a sane setup if you have a lot
of small databases that you can't configure individually, but that's not the
case when you run a reasonably large workload on a 384vCPU setup.

I think to be really convincing you'd have to do this with actual IO involved
somewhere.



> Relationship to Tomas's NUMA series
> -----------------------------------
> 
> Tomas posted a multi-patch NUMA-awareness series in [1] covering buffer interleaving across nodes, partitioned freelists, partitioned clock sweep, PGPROC interleaving, and related pieces.  I want to be careful here because I don't think we should frame this patch as competing with that work.
> 
> One thing I found striking as I re-read the thread: in the benchmarks Tomas
> posted later in the series, *most of the benefit comes from partitioning the
> clock sweep*, and the NUMA memory-placement layer on top sometimes runs
> slower than partitioning alone.  His own conclusion, quoted roughly: the
> benefit mostly comes from just partitioning the clock sweep, and it's
> largely independent of the NUMA stuff; the NUMA partitioning is often
> slower.

That was partially because he measured on something that didn't really have
significant NUMA effects though...


> That observation is the thing that makes me think batching is worth
> considering on its own.  It's going after the same bottleneck Tomas's
> partitioning addresses, but:
> 
>   - without splitting global eviction visibility (which is where
>     cross-partition stealing gets complicated),

You *are* doing that tho.


>   - without requiring NUMA-aware buffer placement (which has huge
>     page alignment, descriptor-partition-mid-page, and resize
>     complications that are still being worked out in that thread),

You can do the partitioned clock sweep without *any* of that.

Greetings,

Andres Freund





^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention
  2026-04-25 20:08 [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-04-27 14:15 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
@ 2026-04-28 14:30   ` Greg Burd <greg@burd.me>
  2026-07-10 14:00     ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  0 siblings, 1 reply; 21+ messages in thread

From: Greg Burd @ 2026-04-28 14:30 UTC (permalink / raw)
  To: Andres Freund <andres@anarazel.de>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Tomas Vondra <tomas@vondra.me>; Nathan Bossart <nathandbossart@gmail.com>


On Mon, Apr 27, 2026, at 10:15 AM, Andres Freund wrote:
> Hi,
>
> Thanks for looking into this.
>
> On 2026-04-25 16:08:02 -0400, Greg Burd wrote:
>> Does batching mess up the meaning of usage_count?
>> --------------------------------------------------
>> 
>> Short answer: no.  I want to walk through this because it was my first
>> concern too, and I think it's the question that will come up most on review.
>
>> 
>> The clock sweep's usage_count is an access-frequency approximation measured
>> in units of *complete passes*.  A buffer with usage_count = N survives N
>> passes without a re-pin.  The semantic meaning lives at pass granularity,
>> not at individual-buffer granularity.
>
>> 
>> What batching changes: intra-pass temporal ordering.  Without batching, with
>> N backends sweeping, decrements are interleaved -- backend A hits B[0],
>> backend B hits B[1], backend C hits B[2].  With batching, backend A hits
>> B[0..63] in a tight local burst, then backend B hits B[64..127], etc.  The
>> 64-buffer chunks are decremented in bursts rather than individually.
>
>> 
>> Why it doesn't matter:
>> 
>>   1. Every buffer still gets decremented exactly once per complete
>>      pass.  The invariant the algorithm actually depends on is
>>      untouched.
>> 
>>   2. A buffer's survival window is the time between consecutive
>>      passes.  That's milliseconds to seconds under load.  Whether
>>      B[0] gets decremented 50us before or 50us after B[63] within
>>      the same pass is below the resolution of anything usage_count
>>      is trying to measure.
>
> I don't think this is true, unfortunately. Sure, if you have a completely
> uniform, IO intensive, workload it is, but that's not all there is.  If you
> have a bunch of connections that replace buffers at a low rate and a bunch of
> connections that do so at a high rate, the batches "checked out" by the "low
> rate" connections won't be processed soon. Thus the buffers in that batch
> won't have their usagecount decremented and thus have a stronger "protection"
> against replacement.
>
> You can argue that may be OK, because it'd be unlikely that the next sweep
> would assign the same buffers to an "low rate" backend again. But that'd be an
> argument you'd have to make and validate.

You're right, and my "why it doesn't matter" section overstated things. The uniform-workload assumption was sloppy.  Let me try again with the mixed case in mind.

The scenario I think you're describing: a low-rate backend claims B[0..63], finds a victim at B[5], and then doesn't call StrategyGetBuffer() again for a while -- maybe seconds.  During that
time B[6..63] sit with their current usage_count, undecremented, while high-rate backends are sweeping the rest of the pool at full speed. Those 58 buffers get a free pass they wouldn't have gotten in the interleaved case.

I can bound the effect but not dismiss it.  Each slow backend can hold at most 64 undecremented buffers.  With 32GB shared_buffers (~4.2M buffers), 100 slow backends each holding a full batch means ~6,400 buffers delayed -- 0.15% of the pool.  The delay lasts until the backend's next StrategyGetBuffer() call.  So the question is whether 0.15% of buffers with temporarily stale usage_count produces a measurable eviction quality difference.

Two observations that bound it further:

  1. In the current code, a backend only calls ClockSweepTick() when
     it needs a victim.  A low-rate backend barely moves the global
     hand at all.  Without batching, buffers at positions beyond the
     current hand are also "undecremented" -- they just haven't been
     reached yet.  Batching changes *which* specific 64 buffers are
     pending, not the total count of undecremented buffers in the
     pool at any instant.

  2. The buffers in a held batch are contiguous in buffer-ID space.
     Since buffer-ID assignment to relation blocks is effectively
     random (driven by eviction order), those 64 buffers are
     scattered across relations.  There's no systematic bias toward
     protecting hot or cold data -- it's a random sample.

That said, "bounded and random" isn't the same as "zero."  One mitigation that's simple to implement: abandon the remaining batch at transaction boundaries.  Something like resetting MyBatchEnd = MyBatchPos in AtEOXact or equivalent, so a backend that goes idle between transactions doesn't hold a stale batch across an idle period.  That limits the staleness window to the duration of a single transaction, which is when the backend is actively doing work and likely to consume the batch quickly anyway.

I'd like to measure the mixed-workload case directly.  A benchmark with e.g. 50 backends doing heavy sequential scans and 50 doing single-row OLTP, and compare eviction hit rates with and without the patch.  Would that be the kind of validation you'd want to see?

> I'm somewhat doubtful that batching that's independent of contention and
> independent of the usage rate will work out all that well.  If you instead
> went for a partitioned sweep architecture, with balancing between the
> different partitions, you don't have that issue. And you have a building block
> for more numa awareness etc.

I agree that partitioned sweep is architecturally more principled and gives you a foundation for deeper NUMA work.  I'm not arguing that batching is a better long-term architecture.

The pragmatic case for batching is: it's ~30 lines, it addresses the identified bottleneck, and it doesn't foreclose on partitioned sweep later.  If partitioned sweep lands, batching becomes redundant for its primary use case.  If partitioned sweep lands incrementally -- which the open items in that thread suggest is the realistic path -- this gets a chunk of the multi-socket win into users' hands sooner.

One concrete difference: partitioned sweep needs a stealing mechanism for correctness when partitions are unevenly loaded.  Batching avoids that because the "partitions" are ephemeral (one batch cycle) and sequential (global order preserved), so there's no long-lived imbalance to steal from.  Whether that simplicity is worth the tradeoff you identified above is a judgment call, and I take your point that the building-block argument favors partitioning.

I'm also not attached to "batching instead of partitioning."  If you think the right move is to focus effort on partitioned sweep, I'm happy to help with that.  But if there's appetite for a smaller change that ships sooner, this is what I've got.

>> There is one subtle difference worth naming.  When a backend finds a victim at B[5] of its batch, it returns with MyBatchEnd still sitting at B[63].  The next time that backend needs a victim it resumes at B[6], not at wherever the global hand now points.  So the backend drains its batch over multiple StrategyGetBuffer() calls rather than all at once.  Under heavy load, where batches are consumed in microseconds, this is invisible.  Under light load, the implication is that some buffers can sit with slightly stale usage_count for longer than they would have before.  But "light load" means "the sweep is barely moving and nothing wants to evict anyway" -- so the effect
>> doesn't show up where it would hurt.
>
> As mentioned above, this assumes that the replacement rate is uniform between
> backends, which I think is not uniformly true outside of benchmarks.
>
>
>> There's also a small positive side-effect: cache locality.  The backend that
>> just touched BufferDescriptor[B[0]] has the adjacent descriptors warm in
>> L1/L2.
>
> A BufferDesc is 64bytes. With common cacheline sizes and stuff like adjacent
> cacheline prefetching you'll have *maybe* 2 consecutive BufferDescs in L1/L2.
> Where it might help more is the TLB.

You're right, I overstated the L1/L2 argument.  At 64 bytes per descriptor, adjacent cacheline prefetch gets you at most 2 consecutive descriptors, not 64.  TLB is the more plausible benefit -- the batch walks a contiguous virtual address range, which should reduce TLB misses when the descriptor array spans multiple pages.  I haven't tried to isolate this in perf and won't claim it until I have numbers.

>> Benchmarks
>> ----------
>> 
>> Jim ran these; I'm still working on reproducing them locally and will post
>> independent numbers in a follow-up.  All bare metal, Linux, huge pages
>> enabled throughout (more on that below), postmaster pinned to node 0 with
>> `numactl --cpunodebind=0` because otherwise stock TPS varied from 31K to 40K
>> depending on which node the postmaster happened to land on at launch --
>> worth flagging for anyone trying to reproduce.
>
> That's an odd one that I think you need to investigate separately.

Agreed.  I'll investigate and report separately.  My working hypothesis is that it's related to where shared memory gets physically allocated relative to the postmaster's NUMA node, which then affects all child backends.  That's interesting regardless of this patch.

>> Workload is pgbench scale 3000 (~45GB) with shared_buffers=32GB, so the
>> working set always spills and the sweep is hot.
>
> Uhm, is this something worth optimizing substantially for?  What you're
> measuring here is basically the worst possible way of configuring a database,
> with full double buffering and a lot of memory bandwidth dedicated to copying
> buffers from one place to another. That's maybe a sane setup if you have a lot
> of small databases that you can't configure individually, but that's not the
> case when you run a reasonably large workload on a 384vCPU setup.
>
> I think to be really convincing you'd have to do this with actual IO involved
> somewhere.

Fair criticism.  The pgbench setup was designed to isolate the clock sweep bottleneck by keeping everything in the OS page cache, but you're right that it doesn't represent how someone would actually run a database on a 384-vCPU box.  In production you'd either size shared_buffers to hold the working set (no sweep pressure) or have real storage I/O (where I/O latency dilutes sweep contention).

The HammerDB TPC-C numbers (which involve I/O and realistic contention patterns) show flat-to-slightly-positive -- no regression, small win at higher concurrency.  I think that's the more honest picture of what production looks like.  And perhaps "flat to slightly-positive" delta might not be enough juice for the squeeze, especially this late in a cycle.

For the follow-up I'll run:

  - Working set 2-3x shared_buffers on NVMe, so StrategyGetBuffer()
    calls actually hit storage on some fraction of evictions.
  - A mixed OLTP workload (not just pgbench -S) with varied access
    patterns, to address the uniform-workload concern above.
  - perf stat showing bus-cycles, cache-misses, and L3 contention
    deltas, so the mechanism is visible independent of TPS.

I should have led with the TPC-C results and framed the pgbench numbers as "here's where the ceiling is under maximum sweep pressure" rather than presenting them as the headline result.

>> Relationship to Tomas's NUMA series
>> -----------------------------------
>> 
>> Tomas posted a multi-patch NUMA-awareness series in [1] covering buffer interleaving across nodes, partitioned freelists, partitioned clock sweep, PGPROC interleaving, and related pieces.  I want to be careful here because I don't think we should frame this patch as competing with that work.
>> 
>> One thing I found striking as I re-read the thread: in the benchmarks Tomas
>> posted later in the series, *most of the benefit comes from partitioning the
>> clock sweep*, and the NUMA memory-placement layer on top sometimes runs
>> slower than partitioning alone.  His own conclusion, quoted roughly: the
>> benefit mostly comes from just partitioning the clock sweep, and it's
>> largely independent of the NUMA stuff; the NUMA partitioning is often
>> slower.
>
> That was partially because he measured on something that didn't really have
> significant NUMA effects though...

Fair point.  I shouldn't over-generalize from benchmarks run on hardware that wasn't exercising the NUMA dimension.  Retracted.

>> That observation is the thing that makes me think batching is worth
>> considering on its own.  It's going after the same bottleneck Tomas's
>> partitioning addresses, but:
>> 
>>   - without splitting global eviction visibility (which is where
>>     cross-partition stealing gets complicated),
>
> You *are* doing that tho.

You're right.  When a backend holds B[0..63], those buffers are effectively invisible to other backends for eviction consideration until the batch is consumed.  That is a form of split visibility.

The difference from a permanent partition is that the split is short-lived (one batch consumption cycle, microseconds under load) and sequential (the next batch picks up where this one left off in the global order).  There's no long-lived assignment of buffer ranges to backends, so the kind of structural imbalance that drives the need for cross-partition stealing doesn't arise.  But I shouldn't have claimed "without splitting."  The honest framing is: "with much more limited
and transient splitting."

>>   - without requiring NUMA-aware buffer placement (which has huge
>>     page alignment, descriptor-partition-mid-page, and resize
>>     complications that are still being worked out in that thread),
>
> You can do the partitioned clock sweep without *any* of that.

Also correct.  The complications I listed are from Tomas's patches 0001 and 0006 (memory interleaving and NUMA-aware buffer-to-node mapping), not from the clock-sweep partitioning patches 0002-0005.  Partitioned clock sweep alone doesn't require NUMA-aware buffer placement.  I
conflated the two; apologies.

> Greetings,
>
> Andres Freund

To summarize the open items I'm taking away:

  1. Mixed-workload benchmark (high-rate + low-rate backends) to
     measure eviction quality impact of held batches.
  2. I/O-inclusive benchmarks on NVMe with working set > shared_buffers.
  3. Investigate the postmaster NUMA placement variance separately.
  4. Consider batch-abandonment at transaction boundaries as a
     mitigation for the staleness concern.
  5. perf stat data showing the mechanism (bus-cycles, cache-misses).

I'll post results as I have them.  I greatly appreciate your time and thoughtful review.

best.

-greg





^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention
  2026-04-25 20:08 [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-04-27 14:15 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-04-28 14:30   ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
@ 2026-07-10 14:00     ` Greg Burd <greg@burd.me>
  2026-07-10 18:07       ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  0 siblings, 1 reply; 21+ messages in thread

From: Greg Burd @ 2026-07-10 14:00 UTC (permalink / raw)
  To: Andres Freund <andres@anarazel.de>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Tomas Vondra <tomas@vondra.me>; Nathan Bossart <nathandbossart@gmail.com>

Hello again,

Removing the freelist [1] turned out to be a good idea, so why not remove
and simplify more?

Included is a three-patch series (v3, on 2e6578292a9 cf-7000) that
replaces the 0..5 usage_count clock sweep with a cooling-stage clock,
and because this evictor algorithm is scan resistance 0003 removes
the BufferAccessStrategy ring buffers entirely.  The net is slightly
faster (TPS), hotter buffers (fewer misses) and a lot less code to
maintain (no special case for scan resistant work) going forward.

  0001  Batch the clock sweep to reduce nextVictimBuffer atomic contention
  0002  Replace the usage_count clock sweep with a cooling-stage evictor
  0003  Remove BufferAccessStrategy; scan resistance is now intrinsic

Net it is +510/-1642 over 83 files; almost all of the deletion is 0003.

On a 6-NUMA-node r8i.metal-96xl under continuous eviction, this change is
a consistent +4–5% on read-heavy pgbench with equal-or-better hit ratios.
The same test on a 2-node box is flat, which is the tell that the win
is the cross-socket clock-hand contention the batched sweep removes. Under
real storage IO (working set > RAM on local NVMe), the scan-resistance
path is +1.6–3.9% with measurably fewer heap reads.

I'll put the design decisions and the benchmark methodology on the table
first, then talk through the results because if the model is wrong on
principle, or the benchmarks are measuring the wrong thing, I would much
rather hear it now.

Contention arises on high core-count systems from the of cost advancing
nextVictimBuffer in StrategyGetBuffer() via a fetch-add of 1 per tick.
This contention is exacerbated on a multi-socket box.  That one cache
line bounces over the interconnect on every eviction; under pressure
with hundreds of backends it is the dominant cost of the sweep.  This is
why we're investing time on NUMA optimizations.


== What each patch does, and why ==

0001 -- Batch the clock sweep.

  This is the setup/baseline commit and is an incremental improvement
  over the existing batched approach, but it is not the final goal and
  should not be committed without 0002.

  Each backend will claim a run of consecutive buffer IDs with a single
  fetch-add and iterate them privately.  Global sweep order is preserved
  -- each buffer is visited exactly once per pass -- only the intra-pass
  ordering of visits changes, which the algorithm does not depend on.
  The atomic fires ~1/batch as often.

  The batch is one cache line's worth of hand values (PG_CACHE_LINE_SIZE
  / sizeof(uint32)), capped at NBuffers.  It is gated on multi-node NUMA
  (pg_numa_get_max_node() >= 1); on a single socket the batch is 1 and
  the path is byte-identical to today.  This is essentially Jim's patch;
  the only substantive change from his v1/v2 is deriving the batch size
  from the cache-line size rather than a fixed 64/tiered constant.

0002 -- Replace usage_count with a cooling-stage evictor.

  This is the heart of the series and where I most want the design
  attacked.

  A buffer is HOT (recently used) or COOL (an eviction candidate);
  "pinned" is the existing refcount.  There is no per-buffer 0..5
  counter.  A demand-loaded page is admitted COOL (probationary); a
  second access promotes it COOL->HOT.  So a page touched once -- as is
  the case with a sequential scan -- fills and drains the COOL stage and
  is evicted from it without displacing the HOT working set.  This is
  the LeanStore / 2Q-A1 idea, and it is the whole reason 0003 can exist:
  scan resistance stops being a ring-buffer bolt-on and becomes a
  property of the replacement algorithm.

  The 4-bit usage_count field is reinterpreted in place -- bit 0
  HOT/COOL, bit 1 a reference bit, the top 2 bits unused -- so the 64-bit
  buffer-state layout, its refcount / flag / lock offsets.  The only
  change to the StaticAsserts is a new one asserting the field is at
  least 2 bits wide, since we now use two of its bits.  The eviction
  claim (COOL, unpinned -> pinned) stays a CAS so a racing PinBuffer
  always wins; promotion and demotion are single-bit transitions.

  The one non-obvious decision, which I got wrong first and want to flag
  loudly: *Who* demotes HOT->COOL.  My first cut was "prefer-COOL": the
  foreground sweep skips HOT buffers hunting for an already-COOL victim
  and only cools HOT buffers once a full pass finds none.  That
  collapses under an ordinary OLTP workload where the working set is
  larger than shared_buffers (no scan at all).  Every access promotes
  its buffer to HOT, so there are almost no COOL buffers, the "cool a
  full pass" fallback fires on nearly every allocation, and each victim
  search becomes a ~2x full-pool scan of scattered BufferDescs.  I
  measured 3-17x throughput loss vs stock -- a cliff.

  HOT->COOL demotion is done during the background writer's existing LRU
  scan, which already runs ahead of the clock hand.  It demotes just
  enough HOT buffers to keep a supply of COOL victims (bounded by the
  predicted next-cycle allocation, so it does not cool the whole pool),
  under the buffer header lock it already holds.  A single reference bit
  gives a recently-accessed buffer one reprieve before it is cooled,
  which keeps the genuinely-hot set out of the COOL stage under scan
  pressure.  With that, the foreground finds a victim in a single pass
  and the cliff is gone (data below).

  I chose prefer-COOL-plus-bgwriter-precooling because it can protect a
  hot buffer slightly longer (the pre-cooler, tuned to a budget, decides
  when to demote rather than demoting on contact), which should help hit
  ratio on a stable hot set -- IF the pre-cooler keeps up.  When it lags
  (bgwriter off or behind), the foreground force_cool is still there as
  a correctness fallback, but it is the expensive path.

0003 -- Remove BufferAccessStrategy.

  With scan resistance intrinsic, the BAS_BULKREAD/BULKWRITE/VACUUM
  rings are dead weight, so this removes them end to end: the type and
  enum, the ring machinery in freelist.c, the strategy parameter
  threaded through ReadBufferExtended / the ExtendBufferedRel* family /
  read_stream / every scan/vacuum/analyze/index-AM caller, the strategy
  fields on the scan and bulk-insert descriptors, and
  _hash_getbuf_with_strategy.  pg_stat_io's per-strategy IO contexts
  collapse to normal/init (IOOP_REUSE only ever happened while recycling
  a ring buffer, so it is gone), and the vacuum_buffer_usage_limit GUC /
  VACUUM (BUFFER_USAGE_LIMIT ...) option / vacuumdb --buffer-usage-limit
  go with it.

  This is the patch most likely to be contentious for reasons unrelated
  to the sweep: it removes a user-visible GUC and changes pg_stat_io's
  shape.  I have kept it as its own commit precisely so 0001+0002 can be
  judged on the algorithm without swallowing the removal.  If the
  consensus is that the cooling model is fine but the ring machinery
  should stay for other reasons (BUFFER_USAGE_LIMIT as an operator
  control, say), 0003 can simply be dropped and 0002 still stands -- the
  rings just become redundant rather than removed.  I would like to know
  if that is where people land.


== Benchmarks ==

I want to be careful here, because Andres's central criticism of the
original batched-sweep numbers [2] was that they measured the wrong
thing -- an all-in-page-cache config where the only bottleneck left is
the atomic, which is not how anyone runs a 384-vCPU box.  That criticism
is correct and I have tried to design around it, but I have almost
certainly not fully escaped it, so the methodology is laid out below in
enough detail to shoot at.

I refer to the COOL/HOT approach (the changes in this patch set) as
"bcs", I've forgotten what that stands for... "buffer cache solution?"
I really don't remember (ha!).

Hardware / method (both instances bare-metal, Amazon Linux 2023):

- m6i.metal      -- 128 vCPU, 2 sockets, 2 NUMA nodes (distances 10/20), 503GB
- r8i.metal-96xl -- 384 vCPU, 2 sockets, 6 NUMA nodes via SNC3, 3TB

Builds: --buildtype=debugoptimized (-O2), cassert off, --with-libnuma,
both branches from the same tree.  Postmaster pinned with numactl
--cpunodebind=0 --membind=0 (without it stock TPS varied ~30% by launch
node -- worth flagging for anyone reproducing).  pgbench dataset loaded
once per build into its own datadir.

The regime I settled on: a dataset that fits fully in OS page cache but
exceeds shared_buffers, warmed before each cell, caches NOT dropped
between cells.  The point is a sweep that runs continuously with
sub-millisecond read latency and NO storage IO in the critical path --
so what I am measuring is the eviction machinery itself, not the disk
(as best as I can tell).

I sweep the ratio (working-set / shared_buffers) from 0.8 (fits, no
eviction, a control) up to 8x by varying shared_buffers against a fixed
63GB dataset.

3 iterations per cell, medians reported, 256 clients on m6i / 384 on r8i.

Two workloads: uniform pgbench -S (the pure eviction-churn case, and the
worst case for the cooling model -- no hot set to protect), and
"hotscan" (a Zipfian hot set plus a handful of clients running large
range scans -- the scan-resistance case).

I want the regime itself critiqued.  It deliberately removes storage IO
to expose the sweep; the flip side is it is not a production
configuration, and the read-path win (fewer misses under scan
resistance) shows up as read count, not TPS, because a "miss" here is an
OS-cache memcpy, not a device read.

r8i.metal-96xl, uniform pgbench -S, 384 clients, TPS (median of 3):

  ratio  SB(GB)   stock       bcs        delta   stock/bcs hit%
  0.8    ~400     1,623,536   1,692,090   +4.2%   99.2 / 99.9
  1.25   ~320     1,443,343   1,519,290   +5.3%   94.8 / 95.2
  1.5    ~266     1,344,051   1,417,462   +5.5%   91.9 / 92.1
  2      ~200     1,293,871   1,358,550   +5.0%   87.9 / 87.8
  4      ~100     1,194,405   1,240,334   +3.8%   77.1 / 78.5
  8      ~50      1,090,873   1,140,839   +4.6%   69.7 / 70.6

Consistent +4-5% across the eviction range, growing with pressure, with
equal-or-better hit ratio (better at 4x/8x).  bcs also showed lower
cache-miss rate in perf stat (~28-35% vs ~31-37%), which is the batched
sweep's reduced cross-node line bouncing showing through.

r8i, hotscan (Zipfian hot set + range scanners), TPS / hit% / heap reads:

  ratio  stock TPS   bcs TPS    stock hit  bcs hit   stock reads  bcs reads
  1.25   1,833,675   1,871,289   99.43     99.57     7,309,457    5,503,927
  1.5    1,876,846   1,942,877   99.35     99.42     8,335,144    7,565,667
  2      1,868,591   1,853,547   99.12     99.15    11,173,831   10,987,371

The scan-resistance signal: at 1.25-1.5x, bcs holds a higher hit ratio
and does up to 25% fewer heap reads (24.7% at 1.25x, ~9% at 1.5x) -- it
is keeping the hot set resident through the scans where stock lets them
flush it.  Muted in TPS only because everything is in OS cache (a miss
is cheap); on real storage this read reduction is where the win would
land.  At 2x it washes out (enough pressure that both evict heavily).

m6i.metal (2 nodes), uniform, 256 clients -- the smaller box, for contrast:
essentially parity, bcs -2% to +1% across ratios.  The 2-node box barely
exercises the atomic, so 0001's contention win does not appear; that it does
not regress is the result that matters here.

Huge pages on vs off (r8i, uniform, medians): I ran this because the
original thread flagged it as uncharacterized.  bcs won by +3-7% both
ways, no regression without huge pages -- the win is from cutting the
frequency of atomic ops on the counter line, which does not depend on
where the descriptors physically live.  (This is why the batching gate
is NUMA-only and not also huge-pages-gated.)

The "prefer-COOL cliff" I mentioned under 0002, so the failure mode is
on the record: BEFORE moving cooling into the bgwriter, the m6i uniform
run at 256 clients was bcs 274K vs stock 840K at ratio 2 (-67%), and
42.8K vs 762K at 8x (-94%), with cache-miss rate exploding to ~40%.
That is the shape of getting the demotion policy wrong; the r8i +5%
table above is after the fix.

Reproduction: the whole harness (instance launch, OS tuning, per-build
load, the ratio sweep, perf stat capture) is scripted; I will attach it
as a DO-NOT-MERGE commit / put it in the CF entry so the methodology can
be reproduced and picked apart rather than taken on faith.  Raw per-run
CSVs and perf output likewise.


A Real IO (working set > RAM, evictions hitting storage) Benchmark

This is the regime Andres asked for, and the one I flagged earlier as not yet
done cleanly.  The earlier attempt was EBS-latency-bound; this one uses local
NVMe so eviction reads hit real storage at ~microsecond, not ~15ms, latency --
during the run the array sat at 100% utilization and ~145K read IOPS, so the
eviction path is genuinely storage-bound, not cache-served.

m6id.metal -- 128 vCPU, 2 sockets, 2 NUMA nodes, 503GB, 4x1.9TB local NVMe in
RAID0.  Dataset ~700GB (pgbench scale 47000), i.e. LARGER than RAM, so the
working set cannot sit in the OS page cache.  shared_buffers is a small window
over it -- 63GB (11x) and 31GB (22x) -- caches dropped per cell, 256 clients, 3
iterations, medians.  Same builds/method as the in-cache runs otherwise.

hotscan (Zipfian hot set + range scanners), median of 3:

  ratio  SB(GB)   stock TPS   bcs TPS     dTPS    stock/bcs hit   reads d
  11     63       877,357     911,363    +3.9%    96.30 / 96.43   -2.2%
  22     31       873,880     888,010    +1.6%    94.02 / 94.22   -1.5%

This is the result the in-cache runs could only hint at: under real storage IO
the scan-resistance read reduction converts to throughput.  The bcs approach
keeps a higher hit ratio and does 1.5-2.2% fewer heap reads, and here -- unlike
in cache, where a miss is a cheap memcpy -- a read it avoids is an NVMe round
trip, so the read reduction shows up as +1.6-3.9% TPS.

uniform pgbench -S (pure eviction churn, no hot set to protect), median of 3:

  ratio  SB(GB)   stock TPS   bcs TPS     dTPS    stock/bcs hit
  11     63       605,656     616,247    +1.7%    67.0 / 67.3
  22     31       605,517     611,312    +1.0%    63.5 / 63.6

Hit ratio here is 63-67% -- a third of accesses miss and hit NVMe (190M-220M
evictions per run), so this is deep, genuinely storage-bound churn.  bcs is
+1-1.7%, i.e. flat-to-slightly-positive, which is the honest production picture:
with no hot set to protect, scan resistance has nothing to do, and the win is
just the batched sweep's reduced contention showing faintly through the IO wait.
Notably bcs does not regress even when its policy has no advantage to exploit.


== Side note for the curious... ==

Separately, Dhruv Aron has proposed restructuring the shared-buffer lookup table
[2], replacing dynahash with a flat two-array structure. That attacks the
other hot cost on a buffer miss — resolving a page to its buffer — where this
series attacks the eviction that a miss triggers. They touch buf_table.c and a
lock-ordering change in InvalidateBuffer(); this series touches freelist.c and
the per-buffer replacement state, and removes BufferAccessStrategy. The two are
complementary and should compound on the miss path; the only overlap is
InvalidateBuffer()/GetVictimBuffer(), where their extended buffer-header-lock
hold and this series' CAS-claim + bgwriter pre-cooling both take that lock, and
would want reconciling if both land. I have not benchmarked them together (yet).

== What I have not done, honestly ==

  - Hardening the foreground force_cool fallback to be cheap when it
    fires, rather than relying on the bgwriter pre-cooler never lagging.

  - Anything on single-socket beyond "does not regress"; the design is
    not trying to help there.


== The ask ==

  1. 0002's demotion policy: is prefer-COOL + bgwriter pre-cooling the
     right call, or is the other team's cool-in-place the more robust
     default given it has no cliff and no background-process dependency?
     This is the decision everything else hangs on.

  2. Is admitting demand-loaded pages COOL (probationary,
     promote-on-second- touch) an acceptable basis for scan resistance
     in the core buffer manager, i.e. is it OK to make scan resistance
     an algorithm property and retire the strategy rings (0003)?  Or
     should the rings stay and 0002 ride alongside them?

  3. The benchmark methodology: where is the in-OS-cache regime
     misleading, and what would you want measured instead?  I am most
     worried I am flattering the sweep by removing the IO that would
     otherwise hide it.

  4. Reinterpreting the usage_count field as {HOT/COOL, ref} bits and
     collapsing pg_stat_io's contexts -- acceptable, or is there a
     cleaner representation the project would want before this is worth
     pursuing?

I have measured that the 0..5 count is overhead and provides no
meaningful signal at all, that a HOT/COLD approach provides a simpler
more stable and better performing eviction model for the buffer pool.
If you dispute that, let's dig in and compare notes. :)

I would be remiss if I didn't point out the thread [3] by Tomas et. al.,
whose NUMA investigation targets the same bottlenecks, and inspired the
work that led to this set of ideas. 

Thanks for reading this far.  I look forward to the critique.

best.

-greg

[1] Reconsidering the freelist
    https://www.postgresql.org/message-id/f0e3c02e-e217-4f04-8dab-1e7e80a228c0%40burd.me
[2] Re: Restructured Shared Buffer Hash Table
    https://www.postgresql.org/message-id/dbbd1998-19ff-4ac2-b4b1-a39f4ec1b0f5@iki.fi
[3] Adding basic NUMA awareness (Tomas Vondra)
    https://www.postgresql.org/message-id/099b9433-2855-4f1b-b421-d078a5d82017%40vondra.me

Attachments:

  [text/x-patch] v3-0001-Batch-the-clock-sweep-to-reduce-nextVictimBuffer-.patch (8.9K, ../../5bcaa70b-89c7-44b6-b197-638d10997cae@app.fastmail.com/2-v3-0001-Batch-the-clock-sweep-to-reduce-nextVictimBuffer-.patch)
  download | inline diff:
From e73fcc94e915d1b29e7ebed14b659495d78e6cc3 Mon Sep 17 00:00:00 2001
From: Greg Burd <greg@burd.me>
Date: Mon, 6 Jul 2026 17:25:00 -0400
Subject: [PATCH v3 1/3] Batch the clock sweep to reduce nextVictimBuffer
 atomic contention

StrategyGetBuffer() advances the shared clock hand, nextVictimBuffer, with a
pg_atomic_fetch_add_u32(..., 1) on every tick.  On a multi-socket system the
cache line holding that counter has to travel over the interconnect on each
operation, pushing a sweep tick from ~20ns (same-socket, line warm in L1/L2)
into the ~100-200ns range.  Under eviction pressure with hundreds of backends
in StrategyGetBuffer() concurrently, that single cache line becomes the
dominant cost of the sweep, visible as elevated bus-cycles and cache-misses in
a perf profile.

Have each backend claim a run of consecutive buffer IDs from the shared hand
with a single fetch-add and then iterate through them privately.  The sweep
still advances through the pool in order, each buffer is still visited exactly
once per complete pass, and the meaning of the clock state is unchanged; only
the temporal ordering of visits within a pass changes, which the algorithm
does not depend on.  The contended atomic now fires roughly once per batch
rather than once per buffer.

The batch is one cache line's worth of clock-hand values --
PG_CACHE_LINE_SIZE / sizeof(uint32) -- capped at NBuffers so a claim can never
wrap the pool more than once.  Batching only helps when the counter's cache
line actually bounces between sockets, so it is enabled only on multi-node
NUMA hardware (pg_numa_get_max_node() >= 1); on a single socket, or where
libnuma is unavailable, the batch size stays 1 and the code path is
byte-identical to the stock clock sweep.

Wraparound handling is adjusted: with batching, several backends can each see
a fetch-add return a value past NBuffers within the same pass.  Any such
backend takes buffer_strategy_lock, re-reads the counter, and if it is still
out of range wraps it with a single CAS and increments completePasses.
StrategySyncStart() continues to see a consistent (nextVictimBuffer,
completePasses) pair.

This is the batched-clock-sweep idea from Jim Mlodgenski's pgsql-hackers
thread, adapted to derive the batch size from the platform cache-line size.

Co-authored-by: Jim Mlodgenski <mlodj@amazon.com>
---
 src/backend/storage/buffer/freelist.c | 134 +++++++++++++++++---------
 1 file changed, 91 insertions(+), 43 deletions(-)

diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index fdb5bad7910..93fc4dd758b 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -22,6 +22,7 @@
 #include "storage/proc.h"
 #include "storage/shmem.h"
 #include "storage/subsystems.h"
+#include "port/pg_numa.h"
 
 #define INT_ACCESS_ONCE(var)	((int)(*((volatile int *)&(var))))
 
@@ -100,68 +101,92 @@ static BufferDesc *GetBufferFromRing(BufferAccessStrategy strategy,
 static void AddBufferToRing(BufferAccessStrategy strategy,
 							BufferDesc *buf);
 
+/*
+ * Per-backend state for the batched clock sweep.  Each backend claims a run
+ * of consecutive clock-hand values with a single atomic fetch-add and then
+ * iterates through them privately, so the contended nextVictimBuffer cache
+ * line is touched roughly 1/batch as often.  MyBatchPos is the next hand
+ * value to hand out; MyBatchEnd is one past the end of the claimed run.  Both
+ * are absolute (monotonically increasing) hand values; the buffer id is the
+ * value modulo NBuffers.
+ */
+static uint32 MyBatchPos = 0;
+static uint32 MyBatchEnd = 0;
+
+/*
+ * Number of clock-hand values a backend claims per atomic fetch-add,
+ * computed once at startup (see StrategyCtlShmemInit).  When batching is
+ * enabled it is one cache line's worth of hand advance, so concurrent
+ * backends sweep non-overlapping, cache-line-sized runs of the pool; the
+ * global sweep order is preserved (each buffer is still visited exactly once
+ * per pass).  Batching is enabled only on multi-node NUMA hardware; otherwise
+ * this stays 1 and the sweep is byte-identical to the stock clock.
+ */
+static uint32 ClockSweepBatchSize = 1;
+
 /*
  * ClockSweepTick - Helper routine for StrategyGetBuffer()
  *
- * Move the clock hand one buffer ahead of its current position and return the
- * id of the buffer now under the hand.
+ * Return the next buffer to consider for eviction.  Backends claim batches of
+ * consecutive buffer IDs from the shared clock hand, then iterate through
+ * them locally without further atomic operations.  This preserves the global
+ * sweep order while reducing contention on the shared counter.
  */
 static inline uint32
 ClockSweepTick(void)
 {
 	uint32		victim;
 
-	/*
-	 * Atomically move hand ahead one buffer - if there's several processes
-	 * doing this, this can lead to buffers being returned slightly out of
-	 * apparent order.
-	 */
-	victim =
-		pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer, 1);
-
-	if (victim >= NBuffers)
+	if (MyBatchPos >= MyBatchEnd)
 	{
-		uint32		originalVictim = victim;
-
-		/* always wrap what we look up in BufferDescriptors */
-		victim = victim % NBuffers;
-
 		/*
-		 * If we're the one that just caused a wraparound, force
-		 * completePasses to be incremented while holding the spinlock. We
-		 * need the spinlock so StrategySyncStart() can return a consistent
-		 * value consisting of nextVictimBuffer and completePasses.
+		 * Claim a fresh batch from the shared clock hand.  This is the only
+		 * atomic operation per batch, reducing contention by the batch size.
 		 */
-		if (victim == 0)
-		{
-			uint32		expected;
-			uint32		wrapped;
-			bool		success = false;
+		uint32		start;
+		uint32		batch_size = ClockSweepBatchSize;
 
-			expected = originalVictim + 1;
+		start = pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer,
+										batch_size);
 
-			while (!success)
-			{
-				/*
-				 * Acquire the spinlock while increasing completePasses. That
-				 * allows other readers to read nextVictimBuffer and
-				 * completePasses in a consistent manner which is required for
-				 * StrategySyncStart().  In theory delaying the increment
-				 * could lead to an overflow of nextVictimBuffers, but that's
-				 * highly unlikely and wouldn't be particularly harmful.
-				 */
-				SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
+		if (start >= (uint32) NBuffers)
+		{
+			start = start % NBuffers;
 
-				wrapped = expected % NBuffers;
+			/*
+			 * The counter has grown past NBuffers; try to wrap it back.  We
+			 * must hold the spinlock so StrategySyncStart() can read
+			 * nextVictimBuffer and completePasses consistently.
+			 *
+			 * With batching, multiple backends may each land a fetch-add
+			 * that returns a value past NBuffers in the same pass.  After
+			 * acquiring the spinlock we re-read the counter: if another
+			 * backend already wrapped it below NBuffers we are done.
+			 */
+			SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
+			{
+				uint32		current;
+				uint32		wrapped;
 
-				success = pg_atomic_compare_exchange_u32(&StrategyControl->nextVictimBuffer,
-														 &expected, wrapped);
-				if (success)
-					StrategyControl->completePasses++;
-				SpinLockRelease(&StrategyControl->buffer_strategy_lock);
+				current = pg_atomic_read_u32(&StrategyControl->nextVictimBuffer);
+				if (current >= (uint32) NBuffers)
+				{
+					wrapped = current % NBuffers;
+					if (pg_atomic_compare_exchange_u32(&StrategyControl->nextVictimBuffer,
+													   &current, wrapped))
+						StrategyControl->completePasses++;
+				}
 			}
+			SpinLockRelease(&StrategyControl->buffer_strategy_lock);
 		}
+
+		MyBatchPos = start;
+		MyBatchEnd = start + batch_size;
 	}
+
+	victim = MyBatchPos % NBuffers;
+	MyBatchPos++;
+
 	return victim;
 }
 
@@ -408,6 +433,29 @@ StrategyCtlShmemInit(void *arg)
 
 	/* No pending notification */
 	StrategyControl->bgwprocno = -1;
+
+	/*
+	 * Decide whether to batch the clock sweep.
+	 *
+	 * Batching claims a run of consecutive buffer IDs per atomic fetch-add so
+	 * concurrent backends touch the shared nextVictimBuffer cache line ~1/batch
+	 * as often -- a win only when that line actually bounces across sockets,
+	 * i.e. on multi-node NUMA hardware.  On a single socket the atomic is
+	 * already node-local and batching would only make backends skip ahead for
+	 * no benefit, so we fall back to batch size 1 there (byte-identical to the
+	 * stock one-buffer-at-a-time clock sweep).
+	 *
+	 * Batch (> 1) only when libnuma reports more than one node
+	 * (pg_numa_get_max_node() >= 1); pg_numa_init() returns -1 when NUMA is
+	 * unavailable.  (Benchmarking showed the win holds with or without huge
+	 * pages, so huge-page availability is intentionally not part of the gate.)
+	 */
+	if (pg_numa_init() != -1 &&
+		pg_numa_get_max_node() >= 1)
+		ClockSweepBatchSize = Min(PG_CACHE_LINE_SIZE / (uint32) sizeof(uint32),
+								  (uint32) NBuffers);
+	else
+		ClockSweepBatchSize = 1;
 }
 
 
-- 
2.51.2



  [text/x-patch] v3-0002-Replace-the-usage_count-clock-sweep-with-a-coolin.patch (27.5K, ../../5bcaa70b-89c7-44b6-b197-638d10997cae@app.fastmail.com/3-v3-0002-Replace-the-usage_count-clock-sweep-with-a-coolin.patch)
  download | inline diff:
From d8e8f0f16bd0c3610ae1ea5fea80433d670a6505 Mon Sep 17 00:00:00 2001
From: Greg Burd <greg@burd.me>
Date: Mon, 6 Jul 2026 19:22:45 -0400
Subject: [PATCH v3 2/3] Replace the usage_count clock sweep with a
 cooling-stage evictor

Replace the 0..5 usage_count buffer-replacement policy with a cooling-stage
clock (the LeanStore / 2Q-A1 model): a buffer is simply HOT (recently used)
or COOL (an eviction candidate), with "pinned" being the existing refcount.
There is no per-buffer access counter.

  - A demand-loaded page is admitted COOL (probationary), not HOT.  A second
    access via PinBuffer promotes it COOL -> HOT (the rescue).  So a page
    touched once -- a sequential scan -- fills and drains the COOL stage and
    is evicted from it without ever displacing the HOT working set.  Scan
    resistance is intrinsic to the replacement algorithm, which is what lets a
    later commit remove the BufferAccessStrategy ring buffers entirely.

  - The foreground sweep in StrategyGetBuffer() reclaims an already-COOL,
    unpinned buffer, pinning it with a CAS so a racing PinBuffer always wins.
    Promotion (COOL -> HOT) and demotion (HOT -> COOL) are single-bit
    transitions; only the eviction claim is a CAS.

  - The background writer maintains the supply of COOL victims.  As its LRU
    scan runs ahead of the clock hand it demotes HOT buffers to COOL, so the
    foreground finds a victim in a single pass rather than having to cool
    buffers itself.  The demotion is demand-driven -- bounded by the predicted
    allocation for the next cycle -- so it stages just enough COOL buffers
    without cooling the whole pool, and it is done under the buffer header
    lock the scan already holds.  A single second-chance reference bit
    (set by PinBuffer, cleared on the bgwriter's first pass over a HOT buffer)
    spares a recently-accessed buffer one cooling pass, keeping the genuinely
    hot set out of the COOL stage under scan pressure.  BgBufferSync also
    tracks the reusable-buffer density it directly observes on a shorter
    smoothing window, so a burst of probationary/scan COOL pages is followed
    promptly rather than averaged away.

The 4-bit usage_count field is reinterpreted in place: bit 0 is the HOT/COOL
state (BUF_COOLSTATE_ONE), bit 1 the reference bit (BUF_REFBIT).  The 64-bit
buffer-state layout -- refcount, flag and lock offsets and their StaticAsserts
-- is unchanged; only the meaning of the field and the instructions that touch
it change.  A StaticAssert requires the field to be at least 2 bits wide so a
future width change cannot push the reference bit into the flag bits.
BM_MAX_USAGE_COUNT becomes BUF_COOLSTATE_HOT (1), so the pin fast path
saturates at HOT.  Local (temp-table) buffers get the same two-state
treatment; being single-backend they need no background cooler.

Because the reference bit shares the field, the full 4-bit value can be 0..3;
readers that mean "the cooling state" must use BUF_STATE_GET_COOLSTATE(), which
masks to bit 0 and returns only 0 (COOL) or 1 (HOT), never the raw field.
contrib/pg_buffercache is updated accordingly: its usagecount column and the
pg_buffercache_summary average report the cooling state (0 = COOL, 1 = HOT),
and pg_buffercache_usage_counts() buckets on it.  Using the raw 4-bit getter
there would index its BM_MAX_USAGE_COUNT+1 = 2-element arrays with values up
to 3 and overrun the stack; masking to the cooling bit keeps the index in
range.  The reference bit is deliberately not exposed as usagecount.

Depends on the batched clock sweep from the previous commit.
---
 contrib/pg_buffercache/pg_buffercache_pages.c |   6 +-
 src/backend/storage/buffer/bufmgr.c           | 103 ++++++++++----
 src/backend/storage/buffer/freelist.c         | 128 ++++++++++++------
 src/backend/storage/buffer/localbuf.c         |  13 +-
 src/include/storage/buf_internals.h           |  63 +++++++--
 5 files changed, 229 insertions(+), 84 deletions(-)

diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index 510455998aa..b94e6f6fda8 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -161,7 +161,7 @@ pg_buffercache_pages(PG_FUNCTION_ARGS)
 		reldatabase = bufHdr->tag.dbOid;
 		forknum = BufTagGetForkNum(&bufHdr->tag);
 		blocknum = bufHdr->tag.blockNum;
-		usagecount = BUF_STATE_GET_USAGECOUNT(buf_state);
+		usagecount = BUF_STATE_GET_COOLSTATE(buf_state);
 		pinning_backends = BUF_STATE_GET_REFCOUNT(buf_state);
 
 		if (buf_state & BM_DIRTY)
@@ -605,7 +605,7 @@ pg_buffercache_summary(PG_FUNCTION_ARGS)
 		if (buf_state & BM_VALID)
 		{
 			buffers_used++;
-			usagecount_total += BUF_STATE_GET_USAGECOUNT(buf_state);
+			usagecount_total += BUF_STATE_GET_COOLSTATE(buf_state);
 
 			if (buf_state & BM_DIRTY)
 				buffers_dirty++;
@@ -655,7 +655,7 @@ pg_buffercache_usage_counts(PG_FUNCTION_ARGS)
 
 		CHECK_FOR_INTERRUPTS();
 
-		usage_count = BUF_STATE_GET_USAGECOUNT(buf_state);
+		usage_count = BUF_STATE_GET_COOLSTATE(buf_state);
 		usage_counts[usage_count]++;
 
 		if (buf_state & BM_DIRTY)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 9ab282a76d1..1173b50b7c1 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -83,6 +83,7 @@
 /* Bits in SyncOneBuffer's return value */
 #define BUF_WRITTEN				0x01
 #define BUF_REUSABLE			0x02
+#define BUF_COOLED				0x04
 
 #define RELS_BSEARCH_THRESHOLD		20
 
@@ -634,7 +635,7 @@ static void PinBuffer_Locked(BufferDesc *buf);
 static void UnpinBuffer(BufferDesc *buf);
 static void UnpinBufferNoOwner(BufferDesc *buf);
 static void BufferSync(int flags);
-static int	SyncOneBuffer(int buf_id, bool skip_recently_used,
+static int	SyncOneBuffer(int buf_id, bool skip_recently_used, bool cool_if_hot,
 						  WritebackContext *wb_context);
 static void WaitIO(BufferDesc *buf);
 static void AbortBufferIO(Buffer buffer);
@@ -2333,7 +2334,10 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	 * checkpoints, except for their "init" forks, which need to be treated
 	 * just like permanent relations.
 	 */
-	set_bits |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
+	set_bits |= BM_TAG_VALID;
+	/* Admit the newly loaded page COOL (probation); a second access via
+	 * PinBuffer promotes it to HOT.  This is what makes a one-touch scan
+	 * self-evicting -- see the cooling-state notes in buf_internals.h. */
 	if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM)
 		set_bits |= BM_PERMANENT;
 
@@ -3002,7 +3006,9 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 
 			victim_buf_hdr->tag = tag;
 
-			set_bits |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
+			set_bits |= BM_TAG_VALID;
+			/* Admit COOL (probation); see the comment at the other admission
+			 * site and the cooling-state notes in buf_internals.h. */
 			if (bmr.relpersistence == RELPERSISTENCE_PERMANENT || fork == INIT_FORKNUM)
 				set_bits |= BM_PERMANENT;
 
@@ -3332,21 +3338,17 @@ PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy,
 			/* increase refcount */
 			buf_state += BUF_REFCOUNT_ONE;
 
-			if (strategy == NULL)
-			{
-				/* Default case: increase usagecount unless already max. */
-				if (BUF_STATE_GET_USAGECOUNT(buf_state) < BM_MAX_USAGE_COUNT)
-					buf_state += BUF_USAGECOUNT_ONE;
-			}
-			else
-			{
-				/*
-				 * Ring buffers shouldn't evict others from pool.  Thus we
-				 * don't make usagecount more than 1.
-				 */
-				if (BUF_STATE_GET_USAGECOUNT(buf_state) == 0)
-					buf_state += BUF_USAGECOUNT_ONE;
-			}
+			/*
+			 * Accessing a resident buffer promotes it to HOT (the 2Q rescue):
+			 * a page loaded COOL on probation becomes part of the hot working
+			 * set on its second touch.  BM_MAX_USAGE_COUNT is
+			 * BUF_COOLSTATE_HOT (1), so this saturates at HOT and never
+			 * overflows the field.  We also set the second-chance ref bit so
+			 * the bgwriter's next cooling pass spares this recently-used buffer.
+			 */
+			if (BUF_STATE_GET_COOLSTATE(buf_state) < BUF_COOLSTATE_HOT)
+				buf_state += BUF_COOLSTATE_ONE;
+			buf_state |= BUF_REFBIT;
 
 			if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
 											   buf_state))
@@ -3785,7 +3787,7 @@ BufferSync(int flags)
 		 */
 		if (pg_atomic_read_u64(&bufHdr->state) & BM_CHECKPOINT_NEEDED)
 		{
-			if (SyncOneBuffer(buf_id, false, &wb_context) & BUF_WRITTEN)
+			if (SyncOneBuffer(buf_id, false, false, &wb_context) & BUF_WRITTEN)
 			{
 				TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
 				PendingCheckpointerStats.buffers_written++;
@@ -3876,6 +3878,19 @@ BgBufferSync(WritebackContext *wb_context)
 	float		smoothing_samples = 16;
 	float		scan_whole_pool_milliseconds = 120000.0;
 
+	/*
+	 * The cleaner scan directly observes the reusable (COOL, unpinned) buffer
+	 * density over the region it walks, which -- with the cooling-stage
+	 * evictor -- is exactly the sweep's victim predicate.  That observation is
+	 * ground truth for the buffers about to be reused, whereas the strategy
+	 * scan's positional proxy (strategy_delta/recent_alloc) blurs a pool whose
+	 * COOL population is spatially clustered (a scan burst leaves whole regions
+	 * COOL, hot OLTP regions not).  So we let the cleaner's own sample adapt on
+	 * a shorter window than the strategy proxy, tracking a burst of
+	 * probationary/scan COOL pages within a cycle or two instead of lagging it.
+	 */
+	float		cleaner_smoothing_samples = 4;
+
 	/* Used to compute how far we scan ahead */
 	long		strategy_delta;
 	int			bufs_to_lap;
@@ -4073,7 +4088,7 @@ BgBufferSync(WritebackContext *wb_context)
 	/* Execute the LRU scan */
 	while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
 	{
-		int			sync_state = SyncOneBuffer(next_to_clean, true,
+		int			sync_state = SyncOneBuffer(next_to_clean, true, true,
 											   wb_context);
 
 		if (++next_to_clean >= NBuffers)
@@ -4121,7 +4136,7 @@ BgBufferSync(WritebackContext *wb_context)
 	{
 		scans_per_alloc = (float) new_strategy_delta / (float) new_recent_alloc;
 		smoothed_density += (scans_per_alloc - smoothed_density) /
-			smoothing_samples;
+			cleaner_smoothing_samples;
 
 #ifdef BGW_DEBUG
 		elog(DEBUG2, "bgwriter: cleaner density alloc=%u scan=%ld density=%.2f new smoothed=%.2f",
@@ -4140,16 +4155,26 @@ BgBufferSync(WritebackContext *wb_context)
  * If skip_recently_used is true, we don't write currently-pinned buffers, nor
  * buffers marked recently used, as these are not replacement candidates.
  *
+ * If cool_if_hot is true (the bgwriter's LRU scan), an unpinned HOT buffer is
+ * demoted HOT -> COOL as we pass it, pre-staging eviction candidates so the
+ * foreground clock sweep finds a COOL victim in a single pass instead of
+ * having to cool buffers itself (force_cool).  The demotion is done under the
+ * buffer header lock we already hold, so it needs no CAS and cannot race a
+ * concurrent demotion.  A concurrent PinBuffer promotes it back to HOT, which
+ * is the intended 2Q behavior (a re-accessed buffer is rescued).
+ *
  * Returns a bitmask containing the following flag bits:
  *	BUF_WRITTEN: we wrote the buffer.
  *	BUF_REUSABLE: buffer is available for replacement, ie, it has
- *		pin count 0 and usage count 0.
+ *		pin count 0 and is COOL (an eviction candidate).
+ *	BUF_COOLED: we demoted this buffer HOT -> COOL this call.
  *
  * (BUF_WRITTEN could be set in error if FlushBuffer finds the buffer clean
  * after locking it, but we don't care all that much.)
  */
 static int
-SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
+SyncOneBuffer(int buf_id, bool skip_recently_used, bool cool_if_hot,
+			  WritebackContext *wb_context)
 {
 	BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
 	int			result = 0;
@@ -4171,8 +4196,38 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 	 */
 	buf_state = LockBufHdr(bufHdr);
 
+	/*
+	 * Pre-cool with a second chance: if asked, act on an unpinned HOT buffer.
+	 * If its ref bit is set (accessed since our last pass), clear the ref bit
+	 * and leave it HOT -- a recently-used buffer earns one reprieve, keeping
+	 * the hot working set out of the COOL stage under scan pressure.  Only a
+	 * HOT buffer whose ref bit is already clear is demoted HOT -> COOL,
+	 * pre-staging it as an eviction candidate for the foreground sweep.  We
+	 * hold the header lock, so each transition is a plain masked store applied
+	 * atomically by UnlockBufHdrExt; we re-lock to continue the dirty-write
+	 * inspection below.
+	 */
+	if (cool_if_hot &&
+		BUF_STATE_GET_REFCOUNT(buf_state) == 0 &&
+		BUF_STATE_GET_COOLSTATE(buf_state) != BUF_COOLSTATE_COOL)
+	{
+		if (BUF_STATE_GET_REFBIT(buf_state))
+		{
+			/* second chance: consume the ref bit, stay HOT */
+			UnlockBufHdrExt(bufHdr, buf_state, 0, BUF_REFBIT, 0);
+			buf_state = LockBufHdr(bufHdr);
+		}
+		else
+		{
+			/* not re-accessed since last pass: demote to COOL */
+			UnlockBufHdrExt(bufHdr, buf_state, 0, BUF_USAGECOUNT_MASK, 0);
+			buf_state = LockBufHdr(bufHdr);
+			result |= BUF_COOLED;
+		}
+	}
+
 	if (BUF_STATE_GET_REFCOUNT(buf_state) == 0 &&
-		BUF_STATE_GET_USAGECOUNT(buf_state) == 0)
+		BUF_STATE_GET_COOLSTATE(buf_state) == BUF_COOLSTATE_COOL)
 	{
 		result |= BUF_REUSABLE;
 	}
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index 93fc4dd758b..cc77144224c 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -49,6 +49,16 @@ typedef struct
 	uint32		completePasses; /* Complete cycles of the clock-sweep */
 	pg_atomic_uint32 numBufferAllocs;	/* Buffers allocated since last reset */
 
+	/*
+	 * Number of clock-hand values a backend claims per atomic fetch-add.
+	 * Computed once at startup (see StrategyCtlShmemInit).  Kept in shared
+	 * memory rather than a backend-local static so that EXEC_BACKEND children
+	 * (which do not inherit the postmaster's statics) see the same value; a
+	 * backend-local copy would silently reset to 1 there, disabling batching
+	 * on Windows.
+	 */
+	uint32		batchSize;
+
 	/*
 	 * Bgworker process to be notified upon activity or -1 if none. See
 	 * StrategyNotifyBgWriter.
@@ -113,17 +123,6 @@ static void AddBufferToRing(BufferAccessStrategy strategy,
 static uint32 MyBatchPos = 0;
 static uint32 MyBatchEnd = 0;
 
-/*
- * Number of clock-hand values a backend claims per atomic fetch-add,
- * computed once at startup (see StrategyCtlShmemInit).  When batching is
- * enabled it is one cache line's worth of hand advance, so concurrent
- * backends sweep non-overlapping, cache-line-sized runs of the pool; the
- * global sweep order is preserved (each buffer is still visited exactly once
- * per pass).  Batching is enabled only on multi-node NUMA hardware; otherwise
- * this stays 1 and the sweep is byte-identical to the stock clock.
- */
-static uint32 ClockSweepBatchSize = 1;
-
 /*
  * ClockSweepTick - Helper routine for StrategyGetBuffer()
  *
@@ -144,7 +143,7 @@ ClockSweepTick(void)
 		 * atomic operation per batch, reducing contention by the batch size.
 		 */
 		uint32		start;
-		uint32		batch_size = ClockSweepBatchSize;
+		uint32		batch_size = StrategyControl->batchSize;
 
 		start = pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer,
 										batch_size);
@@ -211,6 +210,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 	BufferDesc *buf;
 	int			bgwprocno;
 	int			trycounter;
+	bool		force_cool;
 
 	*from_ring = false;
 
@@ -261,12 +261,32 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 	 */
 	pg_atomic_fetch_add_u32(&StrategyControl->numBufferAllocs, 1);
 
-	/* Use the "clock sweep" algorithm to find a free buffer */
+	/*
+	 * Use the cooling-stage clock sweep to find a victim.
+	 *
+	 * A buffer is HOT (recently used) or COOL (an eviction candidate).  We
+	 * prefer to reclaim an already-COOL buffer and demote a HOT buffer to
+	 * COOL only once a full sweep has found no COOL victim (force_cool) -- so
+	 * an abundant supply of COOL/probationary pages (e.g. a scan) is drained
+	 * before the hot working set is cooled.  Newly loaded pages are admitted
+	 * COOL (see BufferAlloc), so scan resistance falls out of the algorithm.
+	 *
+	 * trycounter bounds the search.  Any tick that does not produce a victim
+	 * and does not make progress -- a pinned buffer, or a HOT buffer skipped
+	 * on a prefer-COOL pass -- decrements it.  Cooling a HOT buffer (under
+	 * force_cool) is progress and resets it.  When a full pass (NBuffers)
+	 * makes no progress we escalate to force_cool so the next pass cools HOT
+	 * buffers into victims; if a force_cool pass ALSO makes no progress every
+	 * buffer is pinned and we fail, matching the stock "no unpinned buffers
+	 * available" contract.
+	 */
 	trycounter = NBuffers;
+	force_cool = false;
 	for (;;)
 	{
 		uint64		old_buf_state;
 		uint64		local_buf_state;
+		bool		no_progress = false;
 
 		buf = GetBufferDescriptor(ClockSweepTick());
 
@@ -279,25 +299,10 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 		{
 			local_buf_state = old_buf_state;
 
-			/*
-			 * If the buffer is pinned or has a nonzero usage_count, we cannot
-			 * use it; decrement the usage_count (unless pinned) and keep
-			 * scanning.
-			 */
-
+			/* If the buffer is pinned we cannot use it; keep scanning. */
 			if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0)
 			{
-				if (--trycounter == 0)
-				{
-					/*
-					 * We've scanned all the buffers without making any state
-					 * changes, so all the buffers are pinned (or were when we
-					 * looked at them). We could hope that someone will free
-					 * one eventually, but it's probably better to fail than
-					 * to risk getting stuck in an infinite loop.
-					 */
-					elog(ERROR, "no unpinned buffers available");
-				}
+				no_progress = true;
 				break;
 			}
 
@@ -308,20 +313,43 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 				continue;
 			}
 
-			if (BUF_STATE_GET_USAGECOUNT(local_buf_state) != 0)
+			if (BUF_STATE_GET_COOLSTATE(local_buf_state) != BUF_COOLSTATE_COOL)
 			{
-				local_buf_state -= BUF_USAGECOUNT_ONE;
+				/*
+				 * HOT buffer.  Prefer a COOL victim: on a normal pass just
+				 * advance the hand (no progress).  Under force_cool demote it
+				 * HOT -> COOL -- the cooling tick, which IS progress.
+				 */
+				if (!force_cool)
+				{
+					no_progress = true;
+					break;			/* advance the hand, look for COOL */
+				}
+
+				local_buf_state &= ~BUF_USAGECOUNT_MASK;	/* HOT -> COOL, clear ref bit */
 
 				if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
 												   local_buf_state))
 				{
+					/*
+					 * Cooling a buffer is progress toward a victim, so reset the
+					 * counter and drop back out of force_cool.  Because we clear
+					 * force_cool after a single demotion, an all-HOT pool can take
+					 * up to ~2 full passes to yield a victim (one to discover no
+					 * COOL buffer exists, one to cool + reclaim).  That is still
+					 * within the stock clock's worst case (up to
+					 * BM_MAX_USAGE_COUNT+1 passes), and in practice the bgwriter
+					 * pre-cooling keeps a COOL victim available so force_cool
+					 * rarely fires at all.
+					 */
 					trycounter = NBuffers;
+					force_cool = false;
 					break;
 				}
 			}
 			else
 			{
-				/* pin the buffer if the CAS succeeds */
+				/* COOL and unpinned: claim it.  Pin if the CAS succeeds. */
 				local_buf_state += BUF_REFCOUNT_ONE;
 
 				if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
@@ -338,6 +366,21 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 				}
 			}
 		}
+
+		/*
+		 * A tick that made no progress toward a victim counts down trycounter.
+		 * A full unproductive pass escalates to force_cool (cool HOT buffers
+		 * into victims); a second unproductive full pass means everything is
+		 * pinned, so fail rather than spin forever.  (A failed CAS above is
+		 * neither progress nor a full miss: we simply retry the same buffer.)
+		 */
+		if (no_progress && --trycounter == 0)
+		{
+			if (force_cool)
+				elog(ERROR, "no unpinned buffers available");
+			force_cool = true;
+			trycounter = NBuffers;
+		}
 	}
 }
 
@@ -452,10 +495,10 @@ StrategyCtlShmemInit(void *arg)
 	 */
 	if (pg_numa_init() != -1 &&
 		pg_numa_get_max_node() >= 1)
-		ClockSweepBatchSize = Min(PG_CACHE_LINE_SIZE / (uint32) sizeof(uint32),
-								  (uint32) NBuffers);
+		StrategyControl->batchSize = Min(PG_CACHE_LINE_SIZE / (uint32) sizeof(uint32),
+										 (uint32) NBuffers);
 	else
-		ClockSweepBatchSize = 1;
+		StrategyControl->batchSize = 1;
 }
 
 
@@ -703,14 +746,13 @@ GetBufferFromRing(BufferAccessStrategy strategy, uint64 *buf_state)
 		/*
 		 * If the buffer is pinned we cannot use it under any circumstances.
 		 *
-		 * If usage_count is 0 or 1 then the buffer is fair game (we expect 1,
-		 * since our own previous usage of the ring element would have left it
-		 * there, but it might've been decremented by clock-sweep since then).
-		 * A higher usage_count indicates someone else has touched the buffer,
-		 * so we shouldn't re-use it.
+		 * With the cooling-state replacement the field holds only COOL or HOT,
+		 * so the stock "usage_count > 1 means another backend touched it"
+		 * heuristic no longer applies: a ring element is reusable whenever it
+		 * is unpinned.  (The whole ring mechanism is removed in a later patch;
+		 * scan resistance is now intrinsic to the sweep.)
 		 */
-		if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0
-			|| BUF_STATE_GET_USAGECOUNT(local_buf_state) > 1)
+		if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0)
 			break;
 
 		/* See equivalent code in PinBuffer() */
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 4870c8e13d0..21c08091c49 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -167,7 +167,7 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
 
 		buf_state = pg_atomic_read_u64(&bufHdr->state);
 		buf_state &= ~(BUF_FLAG_MASK | BUF_USAGECOUNT_MASK);
-		buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
+		buf_state |= BM_TAG_VALID;	/* admit COOL (probation) */
 		pg_atomic_unlocked_write_u64(&bufHdr->state, buf_state);
 
 		*foundPtr = false;
@@ -248,9 +248,10 @@ GetLocalVictimBuffer(void)
 		{
 			uint64		buf_state = pg_atomic_read_u64(&bufHdr->state);
 
-			if (BUF_STATE_GET_USAGECOUNT(buf_state) > 0)
+			if (BUF_STATE_GET_COOLSTATE(buf_state) != BUF_COOLSTATE_COOL)
 			{
-				buf_state -= BUF_USAGECOUNT_ONE;
+				/* HOT: give it a second chance, cool it and keep scanning. */
+				buf_state -= BUF_COOLSTATE_ONE;
 				pg_atomic_unlocked_write_u64(&bufHdr->state, buf_state);
 				trycounter = NLocBuffer;
 			}
@@ -454,7 +455,7 @@ ExtendBufferedRelLocal(BufferManagerRelation bmr,
 
 			victim_buf_hdr->tag = tag;
 
-			buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
+			buf_state |= BM_TAG_VALID;	/* admit COOL (probation) */
 
 			pg_atomic_unlocked_write_u64(&victim_buf_hdr->state, buf_state);
 
@@ -839,9 +840,9 @@ PinLocalBuffer(BufferDesc *buf_hdr, bool adjust_usagecount)
 		NLocalPinnedBuffers++;
 		buf_state += BUF_REFCOUNT_ONE;
 		if (adjust_usagecount &&
-			BUF_STATE_GET_USAGECOUNT(buf_state) < BM_MAX_USAGE_COUNT)
+			BUF_STATE_GET_COOLSTATE(buf_state) < BUF_COOLSTATE_HOT)
 		{
-			buf_state += BUF_USAGECOUNT_ONE;
+			buf_state += BUF_COOLSTATE_ONE;
 		}
 		pg_atomic_unlocked_write_u64(&buf_hdr->state, buf_state);
 
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index 2a1ffcc6c5c..89b11051577 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -67,6 +67,50 @@ StaticAssertDecl(BUF_REFCOUNT_BITS + BUF_USAGECOUNT_BITS + BUF_FLAG_BITS + BUF_L
 #define BUF_USAGECOUNT_ONE \
 	(UINT64CONST(1) << BUF_REFCOUNT_BITS)
 
+/*
+ * Cooling state (LeanStore / 2Q-A1 cooling-stage clock sweep).
+ *
+ * The field historically used for the 0..5 usage_count now holds a single
+ * cooling-state bit: HOT (recently accessed, not an eviction candidate) or
+ * COOL (an eviction candidate).  We reuse BUF_USAGECOUNT_ONE as the unit so
+ * the buffer-state bit geography -- refcount, flag, and lock offsets, and the
+ * 64-bit StaticAsserts -- is unchanged; only the meaning of the field and the
+ * instructions that touch it change.
+ *
+ * A demand-loaded page is admitted COOL (probation); a second access promotes
+ * it to HOT (the rescue).  The sweep prefers COOL victims and demotes HOT to
+ * COOL only when a full pass finds no COOL victim.  So a one-touch scan fills
+ * and drains the COOL stage without displacing the HOT working set -- scan
+ * resistance intrinsic to the replacement algorithm.
+ */
+#define BUF_COOLSTATE_COOL	0
+#define BUF_COOLSTATE_HOT	1
+#define BUF_COOLSTATE_ONE	BUF_USAGECOUNT_ONE
+
+/*
+ * Second-chance reference bit, bit 1 of the (former usagecount) field, one
+ * position above the cooling-state bit.  PinBuffer sets it on every access.
+ * The bgwriter's pre-cooling gives a HOT buffer a second chance: the first
+ * time it passes a HOT buffer whose ref bit is set, it clears the ref bit and
+ * leaves the buffer HOT; only a HOT buffer whose ref bit is already clear (not
+ * re-accessed since the previous bgwriter pass) is demoted to COOL.  This
+ * keeps genuinely-hot pages out of the COOL stage (protecting the working set
+ * from being cooled under scan pressure) while leaving the foreground sweep a
+ * single-pass search over the pre-staged COOL buffers.  A separate bit (not a
+ * count) so it stays a plain masked store under the header lock.
+ */
+#define BUF_REFBIT			(UINT64CONST(2) << BUF_REFCOUNT_BITS)
+#define BUF_STATE_GET_REFBIT(state)	(((state) & BUF_REFBIT) != 0)
+
+/*
+ * The cooling state occupies bit 0 and the reference bit occupies bit 1 of
+ * the (former usagecount) field, so the field must be at least 2 bits wide.
+ * Assert it here so a future change to BUF_USAGECOUNT_BITS cannot silently
+ * push BUF_REFBIT up into the flag bits.
+ */
+StaticAssertDecl(BUF_USAGECOUNT_BITS >= 2,
+				 "cooling state + reference bit need at least 2 bits in the usagecount field");
+
 /* flags related definitions */
 #define BUF_FLAG_SHIFT \
 	(BUF_REFCOUNT_BITS + BUF_USAGECOUNT_BITS)
@@ -92,6 +136,11 @@ StaticAssertDecl(BUF_REFCOUNT_BITS + BUF_USAGECOUNT_BITS + BUF_FLAG_BITS + BUF_L
 #define BUF_STATE_GET_USAGECOUNT(state) \
 	((uint32)(((state) & BUF_USAGECOUNT_MASK) >> BUF_USAGECOUNT_SHIFT))
 
+/* Cooling state (HOT/COOL) from buffer state -- bit 0 of the field only, so
+ * the second-chance ref bit (bit 1) does not perturb the HOT/COOL test. */
+#define BUF_STATE_GET_COOLSTATE(state) \
+	((uint32) (((state) >> BUF_USAGECOUNT_SHIFT) & 1))
+
 /*
  * Flags for buffer descriptors
  *
@@ -134,17 +183,15 @@ StaticAssertDecl(MAX_BACKENDS_BITS <= (BUF_LOCK_BITS - 2),
 
 
 /*
- * The maximum allowed value of usage_count represents a tradeoff between
- * accuracy and speed of the clock-sweep buffer management algorithm.  A
- * large value (comparable to NBuffers) would approximate LRU semantics.
- * But it can take as many as BM_MAX_USAGE_COUNT+1 complete cycles of the
- * clock-sweep hand to find a free buffer, so in practice we don't want the
- * value to be very large.
+ * The cooling state is a single bit (HOT/COOL); the maximum value stored in
+ * the field is therefore BUF_COOLSTATE_HOT.  Retained under the historical
+ * name BM_MAX_USAGE_COUNT so the pin fast path ("promote unless already at
+ * max") reads naturally.
  */
-#define BM_MAX_USAGE_COUNT	5
+#define BM_MAX_USAGE_COUNT	BUF_COOLSTATE_HOT
 
 StaticAssertDecl(BM_MAX_USAGE_COUNT < (UINT64CONST(1) << BUF_USAGECOUNT_BITS),
-				 "BM_MAX_USAGE_COUNT doesn't fit in BUF_USAGECOUNT_BITS bits");
+				 "cooling state doesn't fit in BUF_USAGECOUNT_BITS bits");
 
 /*
  * Buffer tag identifies which disk block the buffer contains.
-- 
2.51.2



  [text/x-patch] v3-0003-Remove-BufferAccessStrategy-scan-resistance-is-no.patch (177.4K, ../../5bcaa70b-89c7-44b6-b197-638d10997cae@app.fastmail.com/4-v3-0003-Remove-BufferAccessStrategy-scan-resistance-is-no.patch)
  download | inline diff:
From b404c46f18946bd118bb1cf77dc0b306f7cb8458 Mon Sep 17 00:00:00 2001
From: Greg Burd <greg@burd.me>
Date: Tue, 7 Jul 2026 08:45:39 -0400
Subject: [PATCH v3 3/3] Remove BufferAccessStrategy; scan resistance is now
 intrinsic

The cooling-stage evictor admits demand-loaded pages COOL and promotes them to
HOT only on a second access, so a one-touch sequential scan fills and drains
the COOL stage without displacing the hot working set.  Scan resistance is
therefore a property of the replacement algorithm itself, and the
BufferAccessStrategy ring buffers that previously provided it are dead weight.
Remove them end to end.

Deleted:
  - the BufferAccessStrategy type and the BufferAccessStrategyType enum
    (BAS_NORMAL/BULKREAD/BULKWRITE/VACUUM);
  - the ring machinery in freelist.c (GetAccessStrategy[WithSize],
    GetAccessStrategyBufferCount, GetAccessStrategyPinLimit,
    FreeAccessStrategy, GetBufferFromRing, AddBufferToRing,
    StrategyRejectBuffer, IOContextForStrategy);
  - the strategy parameter from ReadBufferExtended, ReadBufferWithoutRelcache,
    the ExtendBufferedRel* family, StrategyGetBuffer, read_stream_begin_*,
    and every scan/vacuum/analyze/index-AM caller;
  - the strategy fields on HeapScanDescData, IndexScanDescData,
    BulkInsertStateData, ReadBuffersOperation, and ReadStream;
  - _hash_getbuf_with_strategy (identical to _hash_getbuf without a strategy).

pg_stat_io's per-strategy IO contexts collapse: IOCONTEXT_BULKREAD,
IOCONTEXT_BULKWRITE and IOCONTEXT_VACUUM are removed, leaving IOCONTEXT_INIT
and IOCONTEXT_NORMAL.  IOOP_REUSE only ever occurred while recycling a ring
buffer, so it is no longer tracked; GetVictimBuffer counts IOOP_EVICT only.

The vacuum_buffer_usage_limit GUC and the VACUUM/ANALYZE (BUFFER_USAGE_LIMIT
...) option are removed, along with the VacuumBufferUsageLimit global, the
ring-size plumbing through VacuumParams and parallel vacuum, and vacuumdb's
--buffer-usage-limit client option.  read_stream's per-backend pin budget is
now enforced solely by GetPinLimit()/GetLocalPinLimit(), which already applied
and is unchanged for the (formerly universal) no-strategy case.

Documentation and the stats/amcheck regression tests are updated to drop the
removed contexts and options.
---
 contrib/amcheck/expected/check_heap.out       |  16 +-
 contrib/amcheck/sql/check_heap.sql            |  16 +-
 contrib/amcheck/verify_gin.c                  |  18 +-
 contrib/amcheck/verify_heapam.c               |   3 -
 contrib/amcheck/verify_nbtree.c               |  11 +-
 contrib/bloom/blscan.c                        |   8 -
 contrib/bloom/blutils.c                       |   4 +-
 contrib/bloom/blvacuum.c                      |   2 -
 contrib/pageinspect/rawpage.c                 |   2 +-
 contrib/pg_prewarm/autoprewarm.c              |   1 -
 contrib/pg_prewarm/pg_prewarm.c               |   1 -
 contrib/pg_visibility/pg_visibility.c         |   7 +-
 contrib/pgstattuple/pgstatapprox.c            |   3 -
 contrib/pgstattuple/pgstatindex.c             |   9 +-
 contrib/pgstattuple/pgstattuple.c             |  37 +-
 doc/src/sgml/config.sgml                      |  30 --
 doc/src/sgml/monitoring.sgml                  |  38 +-
 doc/src/sgml/ref/analyze.sgml                 |  21 -
 doc/src/sgml/ref/vacuum.sgml                  |  25 -
 src/backend/access/brin/brin.c                |  11 +-
 src/backend/access/brin/brin_revmap.c         |   2 +-
 src/backend/access/gin/gininsert.c            |   4 +-
 src/backend/access/gin/ginutil.c              |   2 +-
 src/backend/access/gin/ginvacuum.c            |  17 +-
 src/backend/access/gist/gist.c                |   2 +-
 src/backend/access/gist/gistutil.c            |   2 +-
 src/backend/access/gist/gistvacuum.c          |  10 +-
 src/backend/access/hash/hash.c                |  13 +-
 src/backend/access/hash/hashovfl.c            |  55 +--
 src/backend/access/hash/hashpage.c            |  40 +-
 src/backend/access/heap/heapam.c              |  31 +-
 src/backend/access/heap/heapam_handler.c      |   4 +-
 src/backend/access/heap/hio.c                 |  13 +-
 src/backend/access/heap/vacuumlazy.c          |  22 +-
 src/backend/access/heap/visibilitymap.c       |   4 +-
 src/backend/access/nbtree/nbtpage.c           |   2 +-
 src/backend/access/nbtree/nbtree.c            |   4 +-
 src/backend/access/spgist/spgutils.c          |   2 +-
 src/backend/access/spgist/spgvacuum.c         |   4 +-
 src/backend/access/transam/xloginsert.c       |   2 +-
 src/backend/access/transam/xlogutils.c        |   3 +-
 src/backend/catalog/index.c                   |   1 -
 src/backend/commands/analyze.c                |   7 +-
 src/backend/commands/dbcommands.c             |   6 +-
 src/backend/commands/repack.c                 |   2 +-
 src/backend/commands/sequence.c               |   2 +-
 src/backend/commands/vacuum.c                 | 118 +----
 src/backend/commands/vacuumparallel.c         |  21 +-
 src/backend/postmaster/autovacuum.c           |  29 +-
 src/backend/postmaster/datachecksum_state.c   |  33 +-
 src/backend/storage/aio/read_stream.c         |  17 +-
 src/backend/storage/buffer/README             |  44 --
 src/backend/storage/buffer/bufmgr.c           | 185 +++-----
 src/backend/storage/buffer/freelist.c         | 434 +-----------------
 src/backend/storage/freespace/freespace.c     |   4 +-
 src/backend/storage/smgr/md.c                 |  11 +-
 src/backend/utils/activity/pgstat_io.c        |  45 +-
 src/backend/utils/init/globals.c              |   1 -
 src/backend/utils/misc/guc_parameters.dat     |  10 -
 src/backend/utils/misc/postgresql.conf.sample |   3 -
 src/bin/scripts/vacuumdb.c                    |  13 -
 src/bin/scripts/vacuuming.c                   |  21 -
 src/bin/scripts/vacuuming.h                   |   1 -
 src/include/access/genam.h                    |   1 -
 src/include/access/hash.h                     |   9 +-
 src/include/access/heapam.h                   |   4 +-
 src/include/access/hio.h                      |   1 -
 src/include/access/tableam.h                  |   8 +-
 src/include/commands/vacuum.h                 |   8 +-
 src/include/miscadmin.h                       |   9 -
 src/include/pgstat.h                          |   5 +-
 src/include/storage/buf.h                     |   7 -
 src/include/storage/buf_internals.h           |   6 +-
 src/include/storage/bufmgr.h                  |  32 +-
 src/include/storage/read_stream.h             |   5 +-
 src/include/utils/guc_hooks.h                 |   2 -
 src/test/modules/test_aio/test_aio.c          |   7 +-
 src/test/regress/expected/stats.out           | 120 +----
 src/test/regress/expected/vacuum.out          |  19 -
 src/test/regress/sql/stats.sql                |  42 --
 src/test/regress/sql/vacuum.sql               |  15 -
 src/tools/pgindent/typedefs.list              |   2 -
 82 files changed, 252 insertions(+), 1559 deletions(-)

diff --git a/contrib/amcheck/expected/check_heap.out b/contrib/amcheck/expected/check_heap.out
index 979e5e84e72..569b0202f1c 100644
--- a/contrib/amcheck/expected/check_heap.out
+++ b/contrib/amcheck/expected/check_heap.out
@@ -67,11 +67,11 @@ INSERT INTO heaptest (a, b)
 	(SELECT gs, repeat('x', gs)
 		FROM generate_series(1,50) gs);
 -- pg_stat_io test:
--- verify_heapam always uses a BAS_BULKREAD BufferAccessStrategy, whereas a
--- sequential scan does so only if the table is large enough when compared to
--- shared buffers (see initscan()). CREATE DATABASE ... also unconditionally
--- uses a BAS_BULKREAD strategy, but we have chosen to use a tablespace and
--- verify_heapam to provide coverage instead of adding another expensive
+-- verify_heapam reads the heap through the buffer manager; with the
+-- cooling-stage clock sweep there are no per-strategy IO contexts, so the
+-- reads are counted in the 'normal' context.  CREATE DATABASE ... likewise
+-- reads through the normal context, but we have chosen to use a tablespace
+-- and verify_heapam to provide coverage instead of adding another expensive
 -- operation to the main regression test suite.
 --
 -- Create an alternative tablespace and move the heaptest table to it, causing
@@ -83,7 +83,7 @@ INSERT INTO heaptest (a, b)
 SET allow_in_place_tablespaces = true;
 CREATE TABLESPACE regress_test_stats_tblspc LOCATION '';
 SELECT sum(reads) AS stats_bulkreads_before
-  FROM pg_stat_io WHERE context = 'bulkread' \gset
+  FROM pg_stat_io WHERE context = 'normal' AND object = 'relation' \gset
 BEGIN;
 ALTER TABLE heaptest SET TABLESPACE regress_test_stats_tblspc;
 -- Check that valid options are not rejected nor corruption reported
@@ -111,7 +111,7 @@ SELECT * FROM verify_heapam(relation := 'heaptest', startblock := 0, endblock :=
 COMMIT;
 -- verify_heapam should have read in the page written out by
 --   ALTER TABLE ... SET TABLESPACE ...
--- causing an additional bulkread, which should be reflected in pg_stat_io.
+-- causing additional reads, which should be reflected in pg_stat_io.
 SELECT pg_stat_force_next_flush();
  pg_stat_force_next_flush 
 --------------------------
@@ -119,7 +119,7 @@ SELECT pg_stat_force_next_flush();
 (1 row)
 
 SELECT sum(reads) AS stats_bulkreads_after
-  FROM pg_stat_io WHERE context = 'bulkread' \gset
+  FROM pg_stat_io WHERE context = 'normal' AND object = 'relation' \gset
 SELECT :stats_bulkreads_after > :stats_bulkreads_before;
  ?column? 
 ----------
diff --git a/contrib/amcheck/sql/check_heap.sql b/contrib/amcheck/sql/check_heap.sql
index 1745bae634e..61f710dbeda 100644
--- a/contrib/amcheck/sql/check_heap.sql
+++ b/contrib/amcheck/sql/check_heap.sql
@@ -27,11 +27,11 @@ INSERT INTO heaptest (a, b)
 		FROM generate_series(1,50) gs);
 
 -- pg_stat_io test:
--- verify_heapam always uses a BAS_BULKREAD BufferAccessStrategy, whereas a
--- sequential scan does so only if the table is large enough when compared to
--- shared buffers (see initscan()). CREATE DATABASE ... also unconditionally
--- uses a BAS_BULKREAD strategy, but we have chosen to use a tablespace and
--- verify_heapam to provide coverage instead of adding another expensive
+-- verify_heapam reads the heap through the buffer manager; with the
+-- cooling-stage clock sweep there are no per-strategy IO contexts, so the
+-- reads are counted in the 'normal' context.  CREATE DATABASE ... likewise
+-- reads through the normal context, but we have chosen to use a tablespace
+-- and verify_heapam to provide coverage instead of adding another expensive
 -- operation to the main regression test suite.
 --
 -- Create an alternative tablespace and move the heaptest table to it, causing
@@ -43,7 +43,7 @@ INSERT INTO heaptest (a, b)
 SET allow_in_place_tablespaces = true;
 CREATE TABLESPACE regress_test_stats_tblspc LOCATION '';
 SELECT sum(reads) AS stats_bulkreads_before
-  FROM pg_stat_io WHERE context = 'bulkread' \gset
+  FROM pg_stat_io WHERE context = 'normal' AND object = 'relation' \gset
 BEGIN;
 ALTER TABLE heaptest SET TABLESPACE regress_test_stats_tblspc;
 -- Check that valid options are not rejected nor corruption reported
@@ -56,10 +56,10 @@ COMMIT;
 
 -- verify_heapam should have read in the page written out by
 --   ALTER TABLE ... SET TABLESPACE ...
--- causing an additional bulkread, which should be reflected in pg_stat_io.
+-- causing additional reads, which should be reflected in pg_stat_io.
 SELECT pg_stat_force_next_flush();
 SELECT sum(reads) AS stats_bulkreads_after
-  FROM pg_stat_io WHERE context = 'bulkread' \gset
+  FROM pg_stat_io WHERE context = 'normal' AND object = 'relation' \gset
 SELECT :stats_bulkreads_after > :stats_bulkreads_before;
 
 CREATE ROLE regress_heaptest_role;
diff --git a/contrib/amcheck/verify_gin.c b/contrib/amcheck/verify_gin.c
index fa06689ed5b..ef2e64475c6 100644
--- a/contrib/amcheck/verify_gin.c
+++ b/contrib/amcheck/verify_gin.c
@@ -63,8 +63,7 @@ static void gin_check_parent_keys_consistency(Relation rel,
 static void check_index_page(Relation rel, Buffer buffer, BlockNumber blockNo);
 static IndexTuple gin_refind_parent(Relation rel,
 									BlockNumber parentblkno,
-									BlockNumber childblkno,
-									BufferAccessStrategy strategy);
+									BlockNumber childblkno);
 static ItemId PageGetItemIdCareful(Relation rel, BlockNumber block, Page page,
 								   OffsetNumber offset);
 
@@ -133,7 +132,6 @@ ginReadTupleWithoutState(IndexTuple itup, int *nitems)
 static void
 gin_check_posting_tree_parent_keys_consistency(Relation rel, BlockNumber posting_tree_root)
 {
-	BufferAccessStrategy strategy = GetAccessStrategy(BAS_BULKREAD);
 	GinPostingTreeScanItem *stack;
 	MemoryContext mctx;
 	MemoryContext oldcontext;
@@ -171,8 +169,7 @@ gin_check_posting_tree_parent_keys_consistency(Relation rel, BlockNumber posting
 
 		CHECK_FOR_INTERRUPTS();
 
-		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno,
-									RBM_NORMAL, strategy);
+		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno, RBM_NORMAL);
 		LockBuffer(buffer, GIN_SHARE);
 		page = BufferGetPage(buffer);
 
@@ -391,7 +388,6 @@ gin_check_parent_keys_consistency(Relation rel,
 								  void *callback_state,
 								  bool readonly)
 {
-	BufferAccessStrategy strategy = GetAccessStrategy(BAS_BULKREAD);
 	GinScanItem *stack;
 	MemoryContext mctx;
 	MemoryContext oldcontext;
@@ -430,8 +426,7 @@ gin_check_parent_keys_consistency(Relation rel,
 
 		CHECK_FOR_INTERRUPTS();
 
-		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno,
-									RBM_NORMAL, strategy);
+		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno, RBM_NORMAL);
 		LockBuffer(buffer, GIN_SHARE);
 		page = BufferGetPage(buffer);
 		maxoff = PageGetMaxOffsetNumber(page);
@@ -567,7 +562,7 @@ gin_check_parent_keys_consistency(Relation rel,
 					 */
 					pfree(stack->parenttup);
 					stack->parenttup = gin_refind_parent(rel, stack->parentblk,
-														 stack->blkno, strategy);
+														 stack->blkno);
 
 					/* We found it - make a final check before failing */
 					if (!stack->parenttup)
@@ -719,7 +714,7 @@ check_index_page(Relation rel, Buffer buffer, BlockNumber blockNo)
  */
 static IndexTuple
 gin_refind_parent(Relation rel, BlockNumber parentblkno,
-				  BlockNumber childblkno, BufferAccessStrategy strategy)
+				  BlockNumber childblkno)
 {
 	Buffer		parentbuf;
 	Page		parentpage;
@@ -727,8 +722,7 @@ gin_refind_parent(Relation rel, BlockNumber parentblkno,
 				parent_maxoff;
 	IndexTuple	result = NULL;
 
-	parentbuf = ReadBufferExtended(rel, MAIN_FORKNUM, parentblkno, RBM_NORMAL,
-								   strategy);
+	parentbuf = ReadBufferExtended(rel, MAIN_FORKNUM, parentblkno, RBM_NORMAL);
 
 	LockBuffer(parentbuf, GIN_SHARE);
 	parentpage = BufferGetPage(parentbuf);
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 20ff58aa782..e4336eb5fc2 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -126,7 +126,6 @@ typedef struct HeapCheckContext
 	 * recent block in the buffer yielded by the read stream API.
 	 */
 	BlockNumber blkno;
-	BufferAccessStrategy bstrategy;
 	Buffer		buffer;
 	Page		page;
 
@@ -374,7 +373,6 @@ verify_heapam(PG_FUNCTION_ARGS)
 		PG_RETURN_NULL();
 	}
 
-	ctx.bstrategy = GetAccessStrategy(BAS_BULKREAD);
 	ctx.buffer = InvalidBuffer;
 	ctx.page = NULL;
 
@@ -472,7 +470,6 @@ verify_heapam(PG_FUNCTION_ARGS)
 	}
 
 	stream = read_stream_begin_relation(stream_flags,
-										ctx.bstrategy,
 										ctx.rel,
 										MAIN_FORKNUM,
 										stream_cb,
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 3ef2d66f826..18cd8928fdd 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -88,8 +88,6 @@ typedef struct BtreeCheckState
 	bool		checkunique;
 	/* Per-page context */
 	MemoryContext targetcontext;
-	/* Buffer access strategy */
-	BufferAccessStrategy checkstrategy;
 
 	/*
 	 * Info for uniqueness checking. Fill this field and the one below once
@@ -490,7 +488,6 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	state->targetcontext = AllocSetContextCreate(CurrentMemoryContext,
 												 "amcheck context",
 												 ALLOCSET_DEFAULT_SIZES);
-	state->checkstrategy = GetAccessStrategy(BAS_BULKREAD);
 
 	/* Get true root block from meta-page */
 	metapage = palloc_btree_page(state, BTREE_METAPAGE);
@@ -1115,7 +1112,7 @@ bt_recheck_sibling_links(BtreeCheckState *state,
 
 		/* Couple locks in the usual order for nbtree:  Left to right */
 		lbuf = ReadBufferExtended(state->rel, MAIN_FORKNUM, leftcurrent,
-								  RBM_NORMAL, state->checkstrategy);
+								  RBM_NORMAL);
 		LockBuffer(lbuf, BT_READ);
 		_bt_checkpage(state->rel, lbuf);
 		page = BufferGetPage(lbuf);
@@ -1138,8 +1135,7 @@ bt_recheck_sibling_links(BtreeCheckState *state,
 		if (newtargetblock != leftcurrent)
 		{
 			newtargetbuf = ReadBufferExtended(state->rel, MAIN_FORKNUM,
-											  newtargetblock, RBM_NORMAL,
-											  state->checkstrategy);
+											  newtargetblock, RBM_NORMAL);
 			LockBuffer(newtargetbuf, BT_READ);
 			_bt_checkpage(state->rel, newtargetbuf);
 			page = BufferGetPage(newtargetbuf);
@@ -3300,8 +3296,7 @@ palloc_btree_page(BtreeCheckState *state, BlockNumber blocknum)
 	 * We copy the page into local storage to avoid holding pin on the buffer
 	 * longer than we must.
 	 */
-	buffer = ReadBufferExtended(state->rel, MAIN_FORKNUM, blocknum, RBM_NORMAL,
-								state->checkstrategy);
+	buffer = ReadBufferExtended(state->rel, MAIN_FORKNUM, blocknum, RBM_NORMAL);
 	LockBuffer(buffer, BT_READ);
 
 	/*
diff --git a/contrib/bloom/blscan.c b/contrib/bloom/blscan.c
index 1a0e42021ec..ed62d5c04e6 100644
--- a/contrib/bloom/blscan.c
+++ b/contrib/bloom/blscan.c
@@ -80,7 +80,6 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BlockNumber blkno,
 				npages;
 	int			i;
-	BufferAccessStrategy bas;
 	BloomScanOpaque so = (BloomScanOpaque) scan->opaque;
 	BlockRangeReadStreamPrivate p;
 	ReadStream *stream;
@@ -113,11 +112,6 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		}
 	}
 
-	/*
-	 * We're going to read the whole index. This is why we use appropriate
-	 * buffer access strategy.
-	 */
-	bas = GetAccessStrategy(BAS_BULKREAD);
 	npages = RelationGetNumberOfBlocks(scan->indexRelation);
 	pgstat_count_index_scan(scan->indexRelation);
 	if (scan->instrument)
@@ -133,7 +127,6 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										bas,
 										scan->indexRelation,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -184,7 +177,6 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	Assert(read_stream_next_buffer(stream, NULL) == InvalidBuffer);
 	read_stream_end(stream);
-	FreeAccessStrategy(bas);
 
 	return ntids;
 }
diff --git a/contrib/bloom/blutils.c b/contrib/bloom/blutils.c
index 5111cdc6dd6..6c1ec15c59c 100644
--- a/contrib/bloom/blutils.c
+++ b/contrib/bloom/blutils.c
@@ -392,7 +392,7 @@ BloomNewBuffer(Relation index)
 	}
 
 	/* Must extend the file */
-	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM,
 							   EB_LOCK_FIRST);
 
 	return buffer;
@@ -460,7 +460,7 @@ BloomInitMetapage(Relation index, ForkNumber forknum)
 	 * block number 0 (BLOOM_METAPAGE_BLKNO).  No need to hold the extension
 	 * lock because there cannot be concurrent inserters yet.
 	 */
-	metaBuffer = ReadBufferExtended(index, forknum, P_NEW, RBM_NORMAL, NULL);
+	metaBuffer = ReadBufferExtended(index, forknum, P_NEW, RBM_NORMAL);
 	LockBuffer(metaBuffer, BUFFER_LOCK_EXCLUSIVE);
 	Assert(BufferGetBlockNumber(metaBuffer) == BLOOM_METAPAGE_BLKNO);
 
diff --git a/contrib/bloom/blvacuum.c b/contrib/bloom/blvacuum.c
index 6beb1c20ebb..08d7705e365 100644
--- a/contrib/bloom/blvacuum.c
+++ b/contrib/bloom/blvacuum.c
@@ -66,7 +66,6 @@ blbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										index,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -219,7 +218,6 @@ blvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										index,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/contrib/pageinspect/rawpage.c b/contrib/pageinspect/rawpage.c
index d136593edb2..bbe8edf9161 100644
--- a/contrib/pageinspect/rawpage.c
+++ b/contrib/pageinspect/rawpage.c
@@ -188,7 +188,7 @@ get_raw_page_internal(text *relname, ForkNumber forknum, BlockNumber blkno)
 
 	/* Take a verbatim copy of the page */
 
-	buf = ReadBufferExtended(rel, forknum, blkno, RBM_NORMAL, NULL);
+	buf = ReadBufferExtended(rel, forknum, blkno, RBM_NORMAL);
 	LockBuffer(buf, BUFFER_LOCK_SHARE);
 
 	memcpy(raw_page_data, BufferGetPage(buf), BLCKSZ);
diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index deb4c2671b5..33ad88ea8b1 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -630,7 +630,6 @@ autoprewarm_database_main(Datum main_arg)
 			stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 												READ_STREAM_DEFAULT |
 												READ_STREAM_USE_BATCHING,
-												NULL,
 												rel,
 												p.forknum,
 												apw_read_stream_next_block,
diff --git a/contrib/pg_prewarm/pg_prewarm.c b/contrib/pg_prewarm/pg_prewarm.c
index c2716086693..716a6754a7e 100644
--- a/contrib/pg_prewarm/pg_prewarm.c
+++ b/contrib/pg_prewarm/pg_prewarm.c
@@ -251,7 +251,6 @@ pg_prewarm(PG_FUNCTION_ARGS)
 		stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 											READ_STREAM_FULL |
 											READ_STREAM_USE_BATCHING,
-											NULL,
 											rel,
 											forkNumber,
 											block_range_read_stream_cb,
diff --git a/contrib/pg_visibility/pg_visibility.c b/contrib/pg_visibility/pg_visibility.c
index dfab0b64cf5..ccb829a09eb 100644
--- a/contrib/pg_visibility/pg_visibility.c
+++ b/contrib/pg_visibility/pg_visibility.c
@@ -488,7 +488,6 @@ collect_visibility_data(Oid relid, bool include_pd)
 	vbits	   *info;
 	BlockNumber blkno;
 	Buffer		vmbuffer = InvalidBuffer;
-	BufferAccessStrategy bstrategy = GetAccessStrategy(BAS_BULKREAD);
 	BlockRangeReadStreamPrivate p;
 	ReadStream *stream = NULL;
 
@@ -514,7 +513,6 @@ collect_visibility_data(Oid relid, bool include_pd)
 		 */
 		stream = read_stream_begin_relation(READ_STREAM_FULL |
 											READ_STREAM_USE_BATCHING,
-											bstrategy,
 											rel,
 											MAIN_FORKNUM,
 											block_range_read_stream_cb,
@@ -538,8 +536,7 @@ collect_visibility_data(Oid relid, bool include_pd)
 
 		/*
 		 * Page-level data requires reading every block, so only get it if the
-		 * caller needs it.  Use a buffer access strategy, too, to prevent
-		 * cache-trashing.
+		 * caller needs it.
 		 */
 		if (include_pd)
 		{
@@ -700,7 +697,6 @@ collect_corrupt_items(Oid relid, bool all_visible, bool all_frozen)
 	Relation	rel;
 	corrupt_items *items;
 	Buffer		vmbuffer = InvalidBuffer;
-	BufferAccessStrategy bstrategy = GetAccessStrategy(BAS_BULKREAD);
 	TransactionId OldestXmin = InvalidTransactionId;
 	struct collect_corrupt_items_read_stream_private p;
 	ReadStream *stream;
@@ -734,7 +730,6 @@ collect_corrupt_items(Oid relid, bool all_visible, bool all_frozen)
 	p.all_frozen = all_frozen;
 	p.all_visible = all_visible;
 	stream = read_stream_begin_relation(READ_STREAM_FULL,
-										bstrategy,
 										rel,
 										MAIN_FORKNUM,
 										collect_corrupt_items_read_stream_next_block,
diff --git a/contrib/pgstattuple/pgstatapprox.c b/contrib/pgstattuple/pgstatapprox.c
index 21e0b50fb4b..8e17d48991e 100644
--- a/contrib/pgstattuple/pgstatapprox.c
+++ b/contrib/pgstattuple/pgstatapprox.c
@@ -116,13 +116,11 @@ static void
 statapprox_heap(Relation rel, output_type *stat)
 {
 	BlockNumber nblocks;
-	BufferAccessStrategy bstrategy;
 	TransactionId OldestXmin;
 	StatApproxReadStreamPrivate p;
 	ReadStream *stream;
 
 	OldestXmin = GetOldestNonRemovableTransactionId(rel);
-	bstrategy = GetAccessStrategy(BAS_BULKREAD);
 
 	nblocks = RelationGetNumberOfBlocks(rel);
 
@@ -141,7 +139,6 @@ statapprox_heap(Relation rel, output_type *stat)
 	 * caution.
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_FULL,
-										bstrategy,
 										rel,
 										MAIN_FORKNUM,
 										statapprox_heap_read_stream_next,
diff --git a/contrib/pgstattuple/pgstatindex.c b/contrib/pgstattuple/pgstatindex.c
index 8951ad0aac4..a0922b386a0 100644
--- a/contrib/pgstattuple/pgstatindex.c
+++ b/contrib/pgstattuple/pgstatindex.c
@@ -217,7 +217,6 @@ pgstatindex_impl(Relation rel, FunctionCallInfo fcinfo)
 	BlockNumber nblocks;
 	BlockNumber blkno;
 	BTIndexStat indexStat;
-	BufferAccessStrategy bstrategy = GetAccessStrategy(BAS_BULKREAD);
 	BlockRangeReadStreamPrivate p;
 	ReadStream *stream;
 	BlockNumber startblk;
@@ -254,7 +253,7 @@ pgstatindex_impl(Relation rel, FunctionCallInfo fcinfo)
 	 * Read metapage
 	 */
 	{
-		Buffer		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, 0, RBM_NORMAL, bstrategy);
+		Buffer		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, 0, RBM_NORMAL);
 		Page		page = BufferGetPage(buffer);
 		BTMetaPageData *metad = BTPageGetMeta(page);
 
@@ -291,7 +290,6 @@ pgstatindex_impl(Relation rel, FunctionCallInfo fcinfo)
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										bstrategy,
 										rel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -612,7 +610,6 @@ pgstathashindex(PG_FUNCTION_ARGS)
 	BlockNumber blkno;
 	Relation	rel;
 	HashIndexStat stats;
-	BufferAccessStrategy bstrategy;
 	HeapTuple	tuple;
 	TupleDesc	tupleDesc;
 	Datum		values[8];
@@ -665,9 +662,6 @@ pgstathashindex(PG_FUNCTION_ARGS)
 	/* Get the current relation length */
 	nblocks = RelationGetNumberOfBlocks(rel);
 
-	/* prepare access strategy for this index */
-	bstrategy = GetAccessStrategy(BAS_BULKREAD);
-
 	/* Scan all blocks except the metapage (0th page) using streaming reads */
 	startblk = HASH_METAPAGE + 1;
 
@@ -680,7 +674,6 @@ pgstathashindex(PG_FUNCTION_ARGS)
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										bstrategy,
 										rel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/contrib/pgstattuple/pgstattuple.c b/contrib/pgstattuple/pgstattuple.c
index 6a7f8cb4a7c..90f7938e953 100644
--- a/contrib/pgstattuple/pgstattuple.c
+++ b/contrib/pgstattuple/pgstattuple.c
@@ -64,22 +64,18 @@ typedef struct pgstattuple_type
 	uint64		free_space;		/* free/reusable space in bytes */
 } pgstattuple_type;
 
-typedef void (*pgstat_page) (pgstattuple_type *, Relation, BlockNumber,
-							 BufferAccessStrategy);
+typedef void (*pgstat_page) (pgstattuple_type *, Relation, BlockNumber);
 
 static Datum build_pgstattuple_type(pgstattuple_type *stat,
 									FunctionCallInfo fcinfo);
 static Datum pgstat_relation(Relation rel, FunctionCallInfo fcinfo);
 static Datum pgstat_heap(Relation rel, FunctionCallInfo fcinfo);
 static void pgstat_btree_page(pgstattuple_type *stat,
-							  Relation rel, BlockNumber blkno,
-							  BufferAccessStrategy bstrategy);
+							  Relation rel, BlockNumber blkno);
 static void pgstat_hash_page(pgstattuple_type *stat,
-							 Relation rel, BlockNumber blkno,
-							 BufferAccessStrategy bstrategy);
+							 Relation rel, BlockNumber blkno);
 static void pgstat_gist_page(pgstattuple_type *stat,
-							 Relation rel, BlockNumber blkno,
-							 BufferAccessStrategy bstrategy);
+							 Relation rel, BlockNumber blkno);
 static Datum pgstat_index(Relation rel, BlockNumber start,
 						  pgstat_page pagefn, FunctionCallInfo fcinfo);
 static void pgstat_index_page(pgstattuple_type *stat, Page page,
@@ -376,7 +372,7 @@ pgstat_heap(Relation rel, FunctionCallInfo fcinfo)
 			CHECK_FOR_INTERRUPTS();
 
 			buffer = ReadBufferExtended(rel, MAIN_FORKNUM, block,
-										RBM_NORMAL, hscan->rs_strategy);
+										RBM_NORMAL);
 			LockBuffer(buffer, BUFFER_LOCK_SHARE);
 			stat.free_space += PageGetExactFreeSpace(BufferGetPage(buffer));
 			UnlockReleaseBuffer(buffer);
@@ -389,7 +385,7 @@ pgstat_heap(Relation rel, FunctionCallInfo fcinfo)
 		CHECK_FOR_INTERRUPTS();
 
 		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, block,
-									RBM_NORMAL, hscan->rs_strategy);
+									RBM_NORMAL);
 		LockBuffer(buffer, BUFFER_LOCK_SHARE);
 		stat.free_space += PageGetExactFreeSpace(BufferGetPage(buffer));
 		UnlockReleaseBuffer(buffer);
@@ -408,13 +404,12 @@ pgstat_heap(Relation rel, FunctionCallInfo fcinfo)
  * pgstat_btree_page -- check tuples in a btree page
  */
 static void
-pgstat_btree_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
-				  BufferAccessStrategy bstrategy)
+pgstat_btree_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno)
 {
 	Buffer		buf;
 	Page		page;
 
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, bstrategy);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 	LockBuffer(buf, BT_READ);
 	page = BufferGetPage(buf);
 
@@ -452,13 +447,12 @@ pgstat_btree_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
  * pgstat_hash_page -- check tuples in a hash page
  */
 static void
-pgstat_hash_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
-				 BufferAccessStrategy bstrategy)
+pgstat_hash_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno)
 {
 	Buffer		buf;
 	Page		page;
 
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, bstrategy);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 	LockBuffer(buf, HASH_READ);
 	page = BufferGetPage(buf);
 
@@ -500,13 +494,12 @@ pgstat_hash_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
  * pgstat_gist_page -- check tuples in a gist page
  */
 static void
-pgstat_gist_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
-				 BufferAccessStrategy bstrategy)
+pgstat_gist_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno)
 {
 	Buffer		buf;
 	Page		page;
 
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, bstrategy);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 	LockBuffer(buf, GIST_SHARE);
 	page = BufferGetPage(buf);
 	if (PageIsNew(page))
@@ -539,12 +532,8 @@ pgstat_index(Relation rel, BlockNumber start, pgstat_page pagefn,
 {
 	BlockNumber nblocks;
 	BlockNumber blkno;
-	BufferAccessStrategy bstrategy;
 	pgstattuple_type stat = {0};
 
-	/* prepare access strategy for this index */
-	bstrategy = GetAccessStrategy(BAS_BULKREAD);
-
 	blkno = start;
 	for (;;)
 	{
@@ -565,7 +554,7 @@ pgstat_index(Relation rel, BlockNumber start, pgstat_page pagefn,
 		{
 			CHECK_FOR_INTERRUPTS();
 
-			pagefn(&stat, rel, blkno, bstrategy);
+			pagefn(&stat, rel, blkno);
 		}
 	}
 
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index c67130c620e..535226c05db 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2107,36 +2107,6 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
-     <varlistentry id="guc-vacuum-buffer-usage-limit" xreflabel="vacuum_buffer_usage_limit">
-      <term>
-       <varname>vacuum_buffer_usage_limit</varname> (<type>integer</type>)
-       <indexterm>
-        <primary><varname>vacuum_buffer_usage_limit</varname> configuration parameter</primary>
-       </indexterm>
-      </term>
-      <listitem>
-       <para>
-        Specifies the size of the
-        <glossterm linkend="glossary-buffer-access-strategy">Buffer Access Strategy</glossterm>
-        used by the <command>VACUUM</command> and <command>ANALYZE</command>
-        commands.  A setting of <literal>0</literal> will allow the operation
-        to use any number of <varname>shared_buffers</varname>.  Otherwise
-        valid sizes range from <literal>128 kB</literal> to
-        <literal>16 GB</literal>.  If the specified size would exceed 1/8 the
-        size of <varname>shared_buffers</varname>, the size is silently capped
-        to that value.  The default value is <literal>2MB</literal>.  If
-        this value is specified without units, it is taken as kilobytes.  This
-        parameter can be set at any time.  It can be overridden for
-        <xref linkend="sql-vacuum"/> and <xref linkend="sql-analyze"/>
-        when passing the <option>BUFFER_USAGE_LIMIT</option> option.  Higher
-        settings can allow <command>VACUUM</command> and
-        <command>ANALYZE</command> to run more quickly, but having too large a
-        setting may cause too many other useful pages to be evicted from
-        shared buffers.
-       </para>
-      </listitem>
-     </varlistentry>
-
      <varlistentry id="guc-logical-decoding-work-mem" xreflabel="logical_decoding_work_mem">
       <term><varname>logical_decoding_work_mem</varname> (<type>integer</type>)
       <indexterm>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 858788b227c..d83142fd039 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -2993,28 +2993,6 @@ description | Waiting for a newly initialized WAL file to reach durable storage
           <literal>init</literal>.
          </para>
         </listitem>
-        <listitem>
-         <para>
-          <literal>vacuum</literal>: I/O operations performed outside of shared
-          buffers while vacuuming and analyzing permanent relations. Temporary
-          table vacuums use the same local buffer pool as other temporary table
-          I/O operations and are tracked in <varname>context</varname>
-          <literal>normal</literal>.
-         </para>
-        </listitem>
-        <listitem>
-         <para>
-          <literal>bulkread</literal>: Certain large read I/O operations
-          done outside of shared buffers, for example, a sequential scan of a
-          large table.
-         </para>
-        </listitem>
-        <listitem>
-         <para>
-          <literal>bulkwrite</literal>: Certain large write I/O operations
-          done outside of shared buffers, such as <command>COPY</command>.
-         </para>
-        </listitem>
        </itemizedlist>
       </entry>
      </row>
@@ -3180,13 +3158,8 @@ description | Waiting for a newly initialized WAL file to reach durable storage
         buffer in order to make it available for another use.
        </para>
        <para>
-        In <varname>context</varname> <literal>normal</literal>, this counts
-        the number of times a block was evicted from a buffer and replaced with
-        another block. In <varname>context</varname>s
-        <literal>bulkwrite</literal>, <literal>bulkread</literal>, and
-        <literal>vacuum</literal>, this counts the number of times a block was
-        evicted from shared buffers in order to add the shared buffer to a
-        separate, size-limited ring buffer for use in a bulk I/O operation.
+        This counts the number of times a block was evicted from a buffer and
+        replaced with another block.
         </para>
       </entry>
      </row>
@@ -3197,10 +3170,9 @@ description | Waiting for a newly initialized WAL file to reach durable storage
         <structfield>reuses</structfield> <type>bigint</type>
        </para>
        <para>
-        The number of times an existing buffer in a size-limited ring buffer
-        outside of shared buffers was reused as part of an I/O operation in the
-        <literal>bulkread</literal>, <literal>bulkwrite</literal>, or
-        <literal>vacuum</literal> <varname>context</varname>s.
+        Always zero.  This column previously counted reuses of buffers in a
+        size-limited ring buffer (buffer access strategy); ring buffers have
+        been removed, so no reuses are tracked.
        </para>
       </entry>
      </row>
diff --git a/doc/src/sgml/ref/analyze.sgml b/doc/src/sgml/ref/analyze.sgml
index ec81f00fecf..7f5159111d8 100644
--- a/doc/src/sgml/ref/analyze.sgml
+++ b/doc/src/sgml/ref/analyze.sgml
@@ -27,7 +27,6 @@ ANALYZE [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] [ <r
 
     VERBOSE [ <replaceable class="parameter">boolean</replaceable> ]
     SKIP_LOCKED [ <replaceable class="parameter">boolean</replaceable> ]
-    BUFFER_USAGE_LIMIT <replaceable class="parameter">size</replaceable>
 
 <phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase>
 
@@ -88,26 +87,6 @@ ANALYZE [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] [ <r
     </listitem>
    </varlistentry>
 
-   <varlistentry>
-    <term><literal>BUFFER_USAGE_LIMIT</literal></term>
-    <listitem>
-     <para>
-      Specifies the
-      <glossterm linkend="glossary-buffer-access-strategy">Buffer Access Strategy</glossterm>
-      ring buffer size for <command>ANALYZE</command>.  This size is used to
-      calculate the number of shared buffers which will be reused as part of
-      this strategy.  <literal>0</literal> disables use of a
-      <literal>Buffer Access Strategy</literal>.   When this option is not
-      specified, <command>ANALYZE</command> uses the value from
-      <xref linkend="guc-vacuum-buffer-usage-limit"/>.  Higher settings can
-      allow <command>ANALYZE</command> to run more quickly, but having too
-      large a setting may cause too many other useful pages to be evicted from
-      shared buffers.  The minimum value is <literal>128 kB</literal> and the
-      maximum value is <literal>16 GB</literal>.
-     </para>
-    </listitem>
-   </varlistentry>
-
    <varlistentry>
     <term><replaceable class="parameter">boolean</replaceable></term>
     <listitem>
diff --git a/doc/src/sgml/ref/vacuum.sgml b/doc/src/sgml/ref/vacuum.sgml
index 38ee973ea05..d44a49f8efe 100644
--- a/doc/src/sgml/ref/vacuum.sgml
+++ b/doc/src/sgml/ref/vacuum.sgml
@@ -37,7 +37,6 @@ VACUUM [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] [ <re
     PARALLEL <replaceable class="parameter">integer</replaceable>
     SKIP_DATABASE_STATS [ <replaceable class="parameter">boolean</replaceable> ]
     ONLY_DATABASE_STATS [ <replaceable class="parameter">boolean</replaceable> ]
-    BUFFER_USAGE_LIMIT <replaceable class="parameter">size</replaceable>
     FULL [ <replaceable class="parameter">boolean</replaceable> ]
 
 <phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase>
@@ -309,30 +308,6 @@ VACUUM [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] [ <re
     </listitem>
    </varlistentry>
 
-   <varlistentry>
-    <term><literal>BUFFER_USAGE_LIMIT</literal></term>
-    <listitem>
-     <para>
-      Specifies the
-      <glossterm linkend="glossary-buffer-access-strategy">Buffer Access Strategy</glossterm>
-      ring buffer size for <command>VACUUM</command>.  This size is used to
-      calculate the number of shared buffers which will be reused as part of
-      this strategy.  <literal>0</literal> disables use of a
-      <literal>Buffer Access Strategy</literal>.  If <option>ANALYZE</option>
-      is also specified, the <option>BUFFER_USAGE_LIMIT</option> value is used
-      for both the vacuum and analyze stages.  This option can't be used with
-      the <option>FULL</option> option except if <option>ANALYZE</option> is
-      also specified.  When this option is not specified,
-      <command>VACUUM</command> uses the value from
-      <xref linkend="guc-vacuum-buffer-usage-limit"/>.  Higher settings can
-      allow <command>VACUUM</command> to run more quickly, but having too
-      large a setting may cause too many other useful pages to be evicted from
-      shared buffers.  The minimum value is <literal>128 kB</literal> and the
-      maximum value is <literal>16 GB</literal>.
-     </para>
-    </listitem>
-   </varlistentry>
-
    <varlistentry>
     <term><literal>FULL</literal></term>
     <listitem>
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index bdb30752e09..5d6539f5483 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -224,7 +224,7 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void form_and_spill_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
-static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
+static void brin_vacuum_scan(Relation idxrel);
 static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
 								BrinMemTuple *dtup, const Datum *values, const bool *nulls);
 static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
@@ -1129,7 +1129,7 @@ brinbuild(Relation heap, Relation index, IndexInfo *indexInfo)
 	 * whole relation will be rolled back.
 	 */
 
-	meta = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM, NULL,
+	meta = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM,
 							 EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 	Assert(BufferGetBlockNumber(meta) == BRIN_METAPAGE_BLKNO);
 
@@ -1281,7 +1281,7 @@ brinbuildempty(Relation index)
 	Buffer		metabuf;
 
 	/* An empty BRIN index has a metapage only. */
-	metabuf = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+	metabuf = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM,
 								EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 
 	/* Initialize and xlog metabuffer. */
@@ -1336,7 +1336,7 @@ brinvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
 	heapRel = table_open(IndexGetRelation(RelationGetRelid(info->index), false),
 						 AccessShareLock);
 
-	brin_vacuum_scan(info->index, info->strategy);
+	brin_vacuum_scan(info->index);
 
 	brinsummarize(info->index, heapRel, BRIN_ALL_BLOCKRANGES, false,
 				  &stats->num_index_tuples, &stats->num_index_tuples);
@@ -2171,7 +2171,7 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
  * and such.
  */
 static void
-brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
+brin_vacuum_scan(Relation idxrel)
 {
 	BlockRangeReadStreamPrivate p;
 	ReadStream *stream;
@@ -2187,7 +2187,6 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										strategy,
 										idxrel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/src/backend/access/brin/brin_revmap.c b/src/backend/access/brin/brin_revmap.c
index 233355cb2d5..01c4104ac88 100644
--- a/src/backend/access/brin/brin_revmap.c
+++ b/src/backend/access/brin/brin_revmap.c
@@ -559,7 +559,7 @@ revmap_physical_extend(BrinRevmap *revmap)
 	}
 	else
 	{
-		buf = ExtendBufferedRel(BMR_REL(irel), MAIN_FORKNUM, NULL,
+		buf = ExtendBufferedRel(BMR_REL(irel), MAIN_FORKNUM,
 								EB_LOCK_FIRST);
 		if (BufferGetBlockNumber(buf) != mapBlk)
 		{
diff --git a/src/backend/access/gin/gininsert.c b/src/backend/access/gin/gininsert.c
index cb9ed3b563c..32ad65cc95f 100644
--- a/src/backend/access/gin/gininsert.c
+++ b/src/backend/access/gin/gininsert.c
@@ -815,9 +815,9 @@ ginbuildempty(Relation index)
 				MetaBuffer;
 
 	/* An empty GIN index has two pages. */
-	MetaBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+	MetaBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM,
 								   EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
-	RootBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+	RootBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM,
 								   EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 
 	/* Initialize and xlog metabuffer and root buffer. */
diff --git a/src/backend/access/gin/ginutil.c b/src/backend/access/gin/ginutil.c
index e7cba81d477..2d7394aedd5 100644
--- a/src/backend/access/gin/ginutil.c
+++ b/src/backend/access/gin/ginutil.c
@@ -336,7 +336,7 @@ GinNewBuffer(Relation index)
 	}
 
 	/* Must extend the file */
-	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM,
 							   EB_LOCK_FIRST);
 
 	return buffer;
diff --git a/src/backend/access/gin/ginvacuum.c b/src/backend/access/gin/ginvacuum.c
index 840543eb664..86538de39d6 100644
--- a/src/backend/access/gin/ginvacuum.c
+++ b/src/backend/access/gin/ginvacuum.c
@@ -32,7 +32,6 @@ struct GinVacuumState
 	IndexBulkDeleteCallback callback;
 	void	   *callback_state;
 	GinState	ginstate;
-	BufferAccessStrategy strategy;
 	MemoryContext tmpCxt;
 };
 
@@ -289,7 +288,7 @@ ginScanPostingTreeToDelete(GinVacuumState *gvs, DataPageDeleteStack *myStackItem
 			childBuffer = ReadBufferExtended(gvs->index,
 											 MAIN_FORKNUM,
 											 PostingItemGetBlockNumber(pitem),
-											 RBM_NORMAL, gvs->strategy);
+											 RBM_NORMAL);
 			LockBuffer(childBuffer, GIN_EXCLUSIVE);
 
 			/* Allocate a child stack entry on first use; reuse thereafter */
@@ -389,7 +388,7 @@ ginVacuumPostingTreeLeaves(GinVacuumState *gvs, BlockNumber blkno)
 		PostingItem *pitem;
 
 		buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, gvs->strategy);
+									RBM_NORMAL);
 		LockBuffer(buffer, GIN_SHARE);
 		page = BufferGetPage(buffer);
 
@@ -430,7 +429,7 @@ ginVacuumPostingTreeLeaves(GinVacuumState *gvs, BlockNumber blkno)
 			break;
 
 		buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, gvs->strategy);
+									RBM_NORMAL);
 		LockBuffer(buffer, GIN_EXCLUSIVE);
 		page = BufferGetPage(buffer);
 	}
@@ -454,7 +453,7 @@ ginVacuumPostingTree(GinVacuumState *gvs, BlockNumber rootBlkno)
 		bool		deleted PG_USED_FOR_ASSERTS_ONLY;
 
 		buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, rootBlkno,
-									RBM_NORMAL, gvs->strategy);
+									RBM_NORMAL);
 
 		/*
 		 * Lock posting tree root for cleanup to ensure there are no
@@ -615,7 +614,6 @@ ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	gvs.index = index;
 	gvs.callback = callback;
 	gvs.callback_state = callback_state;
-	gvs.strategy = info->strategy;
 	initGinState(&gvs.ginstate, index);
 
 	/* first time through? */
@@ -636,7 +634,7 @@ ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	gvs.result = stats;
 
 	buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
-								RBM_NORMAL, info->strategy);
+								RBM_NORMAL);
 
 	/* find leaf page */
 	for (;;)
@@ -669,7 +667,7 @@ ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 
 		UnlockReleaseBuffer(buffer);
 		buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, info->strategy);
+									RBM_NORMAL);
 	}
 
 	/* right now we found leftmost page in entry's BTree */
@@ -712,7 +710,7 @@ ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 			break;
 
 		buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, info->strategy);
+									RBM_NORMAL);
 		LockBuffer(buffer, GIN_EXCLUSIVE);
 	}
 
@@ -794,7 +792,6 @@ ginvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										index,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/src/backend/access/gist/gist.c b/src/backend/access/gist/gist.c
index 8565e225be7..ae494742a4b 100644
--- a/src/backend/access/gist/gist.c
+++ b/src/backend/access/gist/gist.c
@@ -142,7 +142,7 @@ gistbuildempty(Relation index)
 	Buffer		buffer;
 
 	/* Initialize the root page */
-	buffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM,
 							   EB_SKIP_EXTENSION_LOCK | EB_LOCK_FIRST);
 
 	/* Initialize and xlog buffer */
diff --git a/src/backend/access/gist/gistutil.c b/src/backend/access/gist/gistutil.c
index 0f58f61879f..170174f62fd 100644
--- a/src/backend/access/gist/gistutil.c
+++ b/src/backend/access/gist/gistutil.c
@@ -877,7 +877,7 @@ gistNewBuffer(Relation r, Relation heaprel)
 	}
 
 	/* Must extend the file */
-	buffer = ExtendBufferedRel(BMR_REL(r), MAIN_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(r), MAIN_FORKNUM,
 							   EB_LOCK_FIRST);
 
 	return buffer;
diff --git a/src/backend/access/gist/gistvacuum.c b/src/backend/access/gist/gistvacuum.c
index 686a0418054..c366ed35ea8 100644
--- a/src/backend/access/gist/gistvacuum.c
+++ b/src/backend/access/gist/gistvacuum.c
@@ -218,7 +218,6 @@ gistvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										rel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -491,8 +490,7 @@ restart:
 		/* check for vacuum delay while not holding any buffer lock */
 		vacuum_delay_point(false);
 
-		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL,
-									info->strategy);
+		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 		goto restart;
 	}
 }
@@ -524,8 +522,7 @@ gistvacuum_delete_empty_pages(IndexVacuumInfo *info, GistVacState *vstate)
 		int			ntodelete;
 		int			deleted;
 
-		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, (BlockNumber) blkno,
-									RBM_NORMAL, info->strategy);
+		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, (BlockNumber) blkno, RBM_NORMAL);
 
 		LockBuffer(buffer, GIST_SHARE);
 		page = BufferGetPage(buffer);
@@ -590,8 +587,7 @@ gistvacuum_delete_empty_pages(IndexVacuumInfo *info, GistVacState *vstate)
 			if (PageGetMaxOffsetNumber(page) == FirstOffsetNumber)
 				break;
 
-			leafbuf = ReadBufferExtended(rel, MAIN_FORKNUM, leafs_to_delete[i],
-										 RBM_NORMAL, info->strategy);
+			leafbuf = ReadBufferExtended(rel, MAIN_FORKNUM, leafs_to_delete[i], RBM_NORMAL);
 			LockBuffer(leafbuf, GIST_EXCLUSIVE);
 			gistcheckpage(rel, leafbuf);
 
diff --git a/src/backend/access/hash/hash.c b/src/backend/access/hash/hash.c
index 8d8cd30dc38..1bbb7224bfc 100644
--- a/src/backend/access/hash/hash.c
+++ b/src/backend/access/hash/hash.c
@@ -542,7 +542,6 @@ hashbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										rel,
 										MAIN_FORKNUM,
 										hash_bulkdelete_read_stream_cb,
@@ -616,7 +615,7 @@ bucket_loop:
 
 		bucket_buf = buf;
 
-		hashbucketcleanup(rel, cur_bucket, bucket_buf, blkno, info->strategy,
+		hashbucketcleanup(rel, cur_bucket, bucket_buf, blkno,
 						  cachedmetap->hashm_maxbucket,
 						  cachedmetap->hashm_highmask,
 						  cachedmetap->hashm_lowmask, &tuples_removed,
@@ -765,7 +764,7 @@ hashvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
  */
 void
 hashbucketcleanup(Relation rel, Bucket cur_bucket, Buffer bucket_buf,
-				  BlockNumber bucket_blkno, BufferAccessStrategy bstrategy,
+				  BlockNumber bucket_blkno,
 				  uint32 maxbucket, uint32 highmask, uint32 lowmask,
 				  double *tuples_removed, double *num_index_tuples,
 				  bool split_cleanup,
@@ -935,9 +934,8 @@ hashbucketcleanup(Relation rel, Bucket cur_bucket, Buffer bucket_buf,
 		if (!BlockNumberIsValid(blkno))
 			break;
 
-		next_buf = _hash_getbuf_with_strategy(rel, blkno, HASH_WRITE,
-											  LH_OVERFLOW_PAGE,
-											  bstrategy);
+		next_buf = _hash_getbuf(rel, blkno, HASH_WRITE,
+								LH_OVERFLOW_PAGE);
 
 		/*
 		 * release the lock on previous page after acquiring the lock on next
@@ -1004,8 +1002,7 @@ hashbucketcleanup(Relation rel, Bucket cur_bucket, Buffer bucket_buf,
 	 * ordering of tuples for a scan that has started before it.
 	 */
 	if (bucket_dirty && IsBufferCleanupOK(bucket_buf))
-		_hash_squeezebucket(rel, cur_bucket, bucket_blkno, bucket_buf,
-							bstrategy);
+		_hash_squeezebucket(rel, cur_bucket, bucket_blkno, bucket_buf);
 	else
 		LockBuffer(bucket_buf, BUFFER_LOCK_UNLOCK);
 }
diff --git a/src/backend/access/hash/hashovfl.c b/src/backend/access/hash/hashovfl.c
index dbc57ef958c..5b35fed33f9 100644
--- a/src/backend/access/hash/hashovfl.c
+++ b/src/backend/access/hash/hashovfl.c
@@ -491,8 +491,7 @@ _hash_firstfreebit(uint32 map)
 BlockNumber
 _hash_freeovflpage(Relation rel, Buffer bucketbuf, Buffer ovflbuf,
 				   Buffer wbuf, IndexTuple *itups, OffsetNumber *itup_offsets,
-				   Size *tups_size, uint16 nitups,
-				   BufferAccessStrategy bstrategy)
+				   Size *tups_size, uint16 nitups)
 {
 	HashMetaPage metap;
 	Buffer		metabuf;
@@ -539,20 +538,16 @@ _hash_freeovflpage(Relation rel, Buffer bucketbuf, Buffer ovflbuf,
 		if (prevblkno == writeblkno)
 			prevbuf = wbuf;
 		else
-			prevbuf = _hash_getbuf_with_strategy(rel,
-												 prevblkno,
-												 HASH_WRITE,
-												 LH_BUCKET_PAGE | LH_OVERFLOW_PAGE,
-												 bstrategy);
+			prevbuf = _hash_getbuf(rel,
+								   prevblkno,
+								   HASH_WRITE,
+								   LH_BUCKET_PAGE | LH_OVERFLOW_PAGE);
 	}
 	if (BlockNumberIsValid(nextblkno))
-		nextbuf = _hash_getbuf_with_strategy(rel,
-											 nextblkno,
-											 HASH_WRITE,
-											 LH_OVERFLOW_PAGE,
-											 bstrategy);
-
-	/* Note: bstrategy is intentionally not used for metapage and bitmap */
+		nextbuf = _hash_getbuf(rel,
+							   nextblkno,
+							   HASH_WRITE,
+							   LH_OVERFLOW_PAGE);
 
 	/* Read the metapage so we can determine which bitmap page to use */
 	metabuf = _hash_getbuf(rel, HASH_METAPAGE, HASH_READ, LH_META_PAGE);
@@ -843,8 +838,7 @@ void
 _hash_squeezebucket(Relation rel,
 					Bucket bucket,
 					BlockNumber bucket_blkno,
-					Buffer bucket_buf,
-					BufferAccessStrategy bstrategy)
+					Buffer bucket_buf)
 {
 	BlockNumber wblkno;
 	BlockNumber rblkno;
@@ -886,11 +880,10 @@ _hash_squeezebucket(Relation rel,
 		rblkno = ropaque->hasho_nextblkno;
 		if (rbuf != InvalidBuffer)
 			_hash_relbuf(rel, rbuf);
-		rbuf = _hash_getbuf_with_strategy(rel,
-										  rblkno,
-										  HASH_WRITE,
-										  LH_OVERFLOW_PAGE,
-										  bstrategy);
+		rbuf = _hash_getbuf(rel,
+							rblkno,
+							HASH_WRITE,
+							LH_OVERFLOW_PAGE);
 		rpage = BufferGetPage(rbuf);
 		ropaque = HashPageGetOpaque(rpage);
 		Assert(ropaque->hasho_bucket == bucket);
@@ -952,11 +945,10 @@ readpage:
 
 				/* don't need to move to next page if we reached the read page */
 				if (wblkno != rblkno)
-					next_wbuf = _hash_getbuf_with_strategy(rel,
-														   wblkno,
-														   HASH_WRITE,
-														   LH_OVERFLOW_PAGE,
-														   bstrategy);
+					next_wbuf = _hash_getbuf(rel,
+											 wblkno,
+											 HASH_WRITE,
+											 LH_OVERFLOW_PAGE);
 
 				if (nitups > 0)
 				{
@@ -1098,7 +1090,7 @@ readpage:
 
 		/* free this overflow page (releases rbuf) */
 		_hash_freeovflpage(rel, bucket_buf, rbuf, wbuf, itups, itup_offsets,
-						   tups_size, nitups, bstrategy);
+						   tups_size, nitups);
 
 		/* be tidy */
 		for (i = 0; i < nitups; i++)
@@ -1115,11 +1107,10 @@ readpage:
 			return;
 		}
 
-		rbuf = _hash_getbuf_with_strategy(rel,
-										  rblkno,
-										  HASH_WRITE,
-										  LH_OVERFLOW_PAGE,
-										  bstrategy);
+		rbuf = _hash_getbuf(rel,
+							rblkno,
+							HASH_WRITE,
+							LH_OVERFLOW_PAGE);
 		rpage = BufferGetPage(rbuf);
 		ropaque = HashPageGetOpaque(rpage);
 		Assert(ropaque->hasho_bucket == bucket);
diff --git a/src/backend/access/hash/hashpage.c b/src/backend/access/hash/hashpage.c
index 8099b0d021f..bdf1a255f1f 100644
--- a/src/backend/access/hash/hashpage.c
+++ b/src/backend/access/hash/hashpage.c
@@ -139,8 +139,7 @@ _hash_getinitbuf(Relation rel, BlockNumber blkno)
 	if (blkno == P_NEW)
 		elog(ERROR, "hash AM does not use P_NEW");
 
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_ZERO_AND_LOCK,
-							 NULL);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_ZERO_AND_LOCK);
 
 	/* ref count and lock type are correct */
 
@@ -209,7 +208,7 @@ _hash_getnewbuf(Relation rel, BlockNumber blkno, ForkNumber forkNum)
 	/* smgr insists we explicitly extend the relation */
 	if (blkno == nblocks)
 	{
-		buf = ExtendBufferedRel(BMR_REL(rel), forkNum, NULL,
+		buf = ExtendBufferedRel(BMR_REL(rel), forkNum,
 								EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 		if (BufferGetBlockNumber(buf) != blkno)
 			elog(ERROR, "unexpected hash relation size: %u, should be %u",
@@ -217,8 +216,7 @@ _hash_getnewbuf(Relation rel, BlockNumber blkno, ForkNumber forkNum)
 	}
 	else
 	{
-		buf = ReadBufferExtended(rel, forkNum, blkno, RBM_ZERO_AND_LOCK,
-								 NULL);
+		buf = ReadBufferExtended(rel, forkNum, blkno, RBM_ZERO_AND_LOCK);
 	}
 
 	/* ref count and lock type are correct */
@@ -229,34 +227,6 @@ _hash_getnewbuf(Relation rel, BlockNumber blkno, ForkNumber forkNum)
 	return buf;
 }
 
-/*
- *	_hash_getbuf_with_strategy() -- Get a buffer with nondefault strategy.
- *
- *		This is identical to _hash_getbuf() but also allows a buffer access
- *		strategy to be specified.  We use this for VACUUM operations.
- */
-Buffer
-_hash_getbuf_with_strategy(Relation rel, BlockNumber blkno,
-						   int access, int flags,
-						   BufferAccessStrategy bstrategy)
-{
-	Buffer		buf;
-
-	if (blkno == P_NEW)
-		elog(ERROR, "hash AM does not use P_NEW");
-
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, bstrategy);
-
-	if (access != HASH_NOLOCK)
-		LockBuffer(buf, access);
-
-	/* ref count and lock type are correct */
-
-	_hash_checkpage(rel, buf, flags);
-
-	return buf;
-}
-
 /*
  *	_hash_relbuf() -- release a locked buffer.
  *
@@ -758,7 +728,7 @@ restart_expand:
 		/* Release the metapage lock. */
 		LockBuffer(metabuf, BUFFER_LOCK_UNLOCK);
 
-		hashbucketcleanup(rel, old_bucket, buf_oblkno, start_oblkno, NULL,
+		hashbucketcleanup(rel, old_bucket, buf_oblkno, start_oblkno,
 						  maxbucket, highmask, lowmask, NULL, NULL, true,
 						  NULL, NULL);
 
@@ -1333,7 +1303,7 @@ _hash_splitbucket(Relation rel,
 	{
 		LockBuffer(bucket_nbuf, BUFFER_LOCK_UNLOCK);
 		hashbucketcleanup(rel, obucket, bucket_obuf,
-						  BufferGetBlockNumber(bucket_obuf), NULL,
+						  BufferGetBlockNumber(bucket_obuf),
 						  maxbucket, highmask, lowmask, NULL, NULL, true,
 						  NULL, NULL);
 	}
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index abfd8e8970a..4331ab53437 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -359,7 +359,6 @@ static void
 initscan(HeapScanDesc scan, ScanKey key, bool keep_startblock)
 {
 	ParallelBlockTableScanDesc bpscan = NULL;
-	bool		allow_strat;
 	bool		allow_sync;
 
 	/*
@@ -396,24 +395,10 @@ initscan(HeapScanDesc scan, ScanKey key, bool keep_startblock)
 	if (!RelationUsesLocalBuffers(scan->rs_base.rs_rd) &&
 		scan->rs_nblocks > NBuffers / 4)
 	{
-		allow_strat = (scan->rs_base.rs_flags & SO_ALLOW_STRAT) != 0;
 		allow_sync = (scan->rs_base.rs_flags & SO_ALLOW_SYNC) != 0;
 	}
 	else
-		allow_strat = allow_sync = false;
-
-	if (allow_strat)
-	{
-		/* During a rescan, keep the previous strategy object. */
-		if (scan->rs_strategy == NULL)
-			scan->rs_strategy = GetAccessStrategy(BAS_BULKREAD);
-	}
-	else
-	{
-		if (scan->rs_strategy != NULL)
-			FreeAccessStrategy(scan->rs_strategy);
-		scan->rs_strategy = NULL;
-	}
+		allow_sync = false;
 
 	if (scan->rs_base.rs_parallel != NULL)
 	{
@@ -1202,7 +1187,6 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 	scan->rs_base.rs_flags = flags;
 	scan->rs_base.rs_parallel = parallel_scan;
 	scan->rs_base.rs_instrument = NULL;
-	scan->rs_strategy = NULL;	/* set in initscan */
 	scan->rs_cbuf = InvalidBuffer;
 
 	/*
@@ -1273,8 +1257,7 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 
 	/*
 	 * Set up a read stream for sequential scans and TID range scans. This
-	 * should be done after initscan() because initscan() allocates the
-	 * BufferAccessStrategy object passed to the read stream API.
+	 * should be done after initscan().
 	 */
 	if (scan->rs_base.rs_flags & SO_TYPE_SEQSCAN ||
 		scan->rs_base.rs_flags & SO_TYPE_TIDRANGESCAN)
@@ -1295,7 +1278,6 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 		 */
 		scan->rs_read_stream = read_stream_begin_relation(READ_STREAM_SEQUENTIAL |
 														  READ_STREAM_USE_BATCHING,
-														  scan->rs_strategy,
 														  scan->rs_base.rs_rd,
 														  MAIN_FORKNUM,
 														  cb,
@@ -1306,7 +1288,6 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 	{
 		scan->rs_read_stream = read_stream_begin_relation(READ_STREAM_DEFAULT |
 														  READ_STREAM_USE_BATCHING,
-														  scan->rs_strategy,
 														  scan->rs_base.rs_rd,
 														  MAIN_FORKNUM,
 														  bitmapheap_stream_read_next,
@@ -1402,9 +1383,6 @@ heap_endscan(TableScanDesc sscan)
 	if (BufferIsValid(scan->rs_vmbuffer))
 		ReleaseBuffer(scan->rs_vmbuffer);
 
-	/*
-	 * Must free the read stream before freeing the BufferAccessStrategy.
-	 */
 	if (scan->rs_read_stream)
 		read_stream_end(scan->rs_read_stream);
 
@@ -1416,9 +1394,6 @@ heap_endscan(TableScanDesc sscan)
 	if (scan->rs_base.rs_key)
 		pfree(scan->rs_base.rs_key);
 
-	if (scan->rs_strategy != NULL)
-		FreeAccessStrategy(scan->rs_strategy);
-
 	if (scan->rs_parallelworkerdata != NULL)
 		pfree(scan->rs_parallelworkerdata);
 
@@ -1939,7 +1914,6 @@ GetBulkInsertState(void)
 	BulkInsertState bistate;
 
 	bistate = (BulkInsertState) palloc_object(BulkInsertStateData);
-	bistate->strategy = GetAccessStrategy(BAS_BULKWRITE);
 	bistate->current_buf = InvalidBuffer;
 	bistate->next_free = InvalidBlockNumber;
 	bistate->last_free = InvalidBlockNumber;
@@ -1955,7 +1929,6 @@ FreeBulkInsertState(BulkInsertState bistate)
 {
 	if (bistate->current_buf != InvalidBuffer)
 		ReleaseBuffer(bistate->current_buf);
-	FreeAccessStrategy(bistate->strategy);
 	pfree(bistate);
 }
 
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index bf87430cf01..d14658d7218 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -2213,9 +2213,9 @@ heapam_scan_sample_next_block(TableScanDesc scan, SampleScanState *scanstate)
 	 */
 	CHECK_FOR_INTERRUPTS();
 
-	/* Read page using selected strategy */
+	/* Read page */
 	hscan->rs_cbuf = ReadBufferExtended(hscan->rs_base.rs_rd, MAIN_FORKNUM,
-										blockno, RBM_NORMAL, hscan->rs_strategy);
+										blockno, RBM_NORMAL);
 
 	/* in pagemode, prune the page and determine visible tuple offsets */
 	if (hscan->rs_base.rs_flags & SO_ALLOW_PAGEMODE)
diff --git a/src/backend/access/heap/hio.c b/src/backend/access/heap/hio.c
index e96e0f77d92..9bf7abc247f 100644
--- a/src/backend/access/heap/hio.c
+++ b/src/backend/access/heap/hio.c
@@ -80,7 +80,8 @@ RelationPutHeapTuple(Relation relation,
 }
 
 /*
- * Read in a buffer in mode, using bulk-insert strategy if bistate isn't NULL.
+ * Read in a buffer in mode, using the bistate's pinned target page if
+ * available.
  */
 static Buffer
 ReadBufferBI(Relation relation, BlockNumber targetBlock,
@@ -91,7 +92,7 @@ ReadBufferBI(Relation relation, BlockNumber targetBlock,
 	/* If not bulk-insert, exactly like ReadBuffer */
 	if (!bistate)
 		return ReadBufferExtended(relation, MAIN_FORKNUM, targetBlock,
-								  mode, NULL);
+								  mode);
 
 	/* If we have the desired block already pinned, re-pin and return it */
 	if (bistate->current_buf != InvalidBuffer)
@@ -113,9 +114,9 @@ ReadBufferBI(Relation relation, BlockNumber targetBlock,
 		bistate->current_buf = InvalidBuffer;
 	}
 
-	/* Perform a read using the buffer strategy */
+	/* Perform the read */
 	buffer = ReadBufferExtended(relation, MAIN_FORKNUM, targetBlock,
-								mode, bistate->strategy);
+								mode);
 
 	/* Save the selected block as target for future inserts */
 	IncrBufferRefCount(buffer);
@@ -337,7 +338,6 @@ RelationAddBlocks(Relation relation, BulkInsertState bistate,
 	 * way larger.
 	 */
 	first_block = ExtendBufferedRelBy(BMR_REL(relation), MAIN_FORKNUM,
-									  bistate ? bistate->strategy : NULL,
 									  EB_LOCK_FIRST,
 									  extend_by_pages,
 									  victim_buffers,
@@ -484,8 +484,7 @@ RelationAddBlocks(Relation relation, BulkInsertState bistate,
  *
  *	The caller can also provide a BulkInsertState object to optimize many
  *	insertions into the same relation.  This keeps a pin on the current
- *	insertion target page (to save pin/unpin cycles) and also passes a
- *	BULKWRITE buffer selection strategy object to the buffer manager.
+ *	insertion target page (to save pin/unpin cycles).
  *	Passing NULL for bistate selects the default behavior.
  *
  *	We don't fill existing pages further than the fillfactor, except for large
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 39395aed0d5..8bc8e470af3 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -257,7 +257,6 @@ typedef struct LVRelState
 	int			nindexes;
 
 	/* Buffer access strategy and parallel vacuum state */
-	BufferAccessStrategy bstrategy;
 	ParallelVacuumState *pvs;
 
 	/* Aggressive VACUUM? (must set relfrozenxid >= FreezeLimit) */
@@ -621,8 +620,7 @@ heap_vacuum_eager_scan_setup(LVRelState *vacrel, const VacuumParams *params)
  *		and locked the relation.
  */
 void
-heap_vacuum_rel(Relation rel, const VacuumParams *params,
-				BufferAccessStrategy bstrategy)
+heap_vacuum_rel(Relation rel, const VacuumParams *params)
 {
 	LVRelState *vacrel;
 	bool		verbose,
@@ -699,7 +697,6 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
 	vacrel->rel = rel;
 	vac_open_indexes(vacrel->rel, RowExclusiveLock, &vacrel->nindexes,
 					 &vacrel->indrels);
-	vacrel->bstrategy = bstrategy;
 	if (instrument && vacrel->nindexes > 0)
 	{
 		/* Copy index names used by instrumentation (not error reporting) */
@@ -1311,7 +1308,6 @@ lazy_scan_heap(LVRelState *vacrel)
 	 * explicit work in heap_vac_scan_next_block.
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE,
-										vacrel->bstrategy,
 										vacrel->rel,
 										MAIN_FORKNUM,
 										heap_vac_scan_next_block,
@@ -2670,7 +2666,6 @@ lazy_vacuum_heap_rel(LVRelState *vacrel)
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_USE_BATCHING,
-										vacrel->bstrategy,
 										vacrel->rel,
 										MAIN_FORKNUM,
 										vacuum_reap_lp_read_stream_next,
@@ -2904,13 +2899,6 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel)
 
 		VacuumFailsafeActive = true;
 
-		/*
-		 * Abandon use of a buffer access strategy to allow use of all of
-		 * shared buffers.  We assume the caller who allocated the memory for
-		 * the BufferAccessStrategy will free it.
-		 */
-		vacrel->bstrategy = NULL;
-
 		/* Disable index vacuuming, index cleanup, and heap rel truncation */
 		vacrel->do_index_vacuuming = false;
 		vacrel->do_index_cleanup = false;
@@ -3023,7 +3011,6 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	ivinfo.estimated_count = true;
 	ivinfo.message_level = DEBUG2;
 	ivinfo.num_heap_tuples = reltuples;
-	ivinfo.strategy = vacrel->bstrategy;
 
 	/*
 	 * Update error traceback information.
@@ -3074,7 +3061,6 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	ivinfo.message_level = DEBUG2;
 
 	ivinfo.num_heap_tuples = reltuples;
-	ivinfo.strategy = vacrel->bstrategy;
 
 	/*
 	 * Update error traceback information.
@@ -3354,8 +3340,7 @@ count_nondeletable_pages(LVRelState *vacrel, bool *lock_waiter_detected)
 			prefetchedUntil = prefetchStart;
 		}
 
-		buf = ReadBufferExtended(vacrel->rel, MAIN_FORKNUM, blkno, RBM_NORMAL,
-								 vacrel->bstrategy);
+		buf = ReadBufferExtended(vacrel->rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 
 		/* In this phase we only need shared access to the buffer */
 		LockBuffer(buf, BUFFER_LOCK_SHARE);
@@ -3446,8 +3431,7 @@ dead_items_alloc(LVRelState *vacrel, int nworkers)
 			vacrel->pvs = parallel_vacuum_init(vacrel->rel, vacrel->indrels,
 											   vacrel->nindexes, nworkers,
 											   vac_work_mem,
-											   vacrel->verbose ? INFO : DEBUG2,
-											   vacrel->bstrategy);
+											   vacrel->verbose ? INFO : DEBUG2);
 
 		/*
 		 * If parallel mode started, dead_items and dead_items_info spaces are
diff --git a/src/backend/access/heap/visibilitymap.c b/src/backend/access/heap/visibilitymap.c
index 4fd470702aa..b2612b1f710 100644
--- a/src/backend/access/heap/visibilitymap.c
+++ b/src/backend/access/heap/visibilitymap.c
@@ -575,7 +575,7 @@ vm_readbuf(Relation rel, BlockNumber blkno, bool extend)
 	}
 	else
 		buf = ReadBufferExtended(rel, VISIBILITYMAP_FORKNUM, blkno,
-								 RBM_ZERO_ON_ERROR, NULL);
+								 RBM_ZERO_ON_ERROR);
 
 	/*
 	 * Initializing the page when needed is trickier than it looks, because of
@@ -611,7 +611,7 @@ vm_extend(Relation rel, BlockNumber vm_nblocks)
 {
 	Buffer		buf;
 
-	buf = ExtendBufferedRelTo(BMR_REL(rel), VISIBILITYMAP_FORKNUM, NULL,
+	buf = ExtendBufferedRelTo(BMR_REL(rel), VISIBILITYMAP_FORKNUM,
 							  EB_CREATE_FORK_IF_NEEDED |
 							  EB_CLEAR_SIZE_CACHE,
 							  vm_nblocks,
diff --git a/src/backend/access/nbtree/nbtpage.c b/src/backend/access/nbtree/nbtpage.c
index 406088dcb57..23f31f07f17 100644
--- a/src/backend/access/nbtree/nbtpage.c
+++ b/src/backend/access/nbtree/nbtpage.c
@@ -960,7 +960,7 @@ _bt_allocbuf(Relation rel, Relation heaprel)
 	 * otherwise would make, as we can't use _bt_lockbuf() without introducing
 	 * a race.
 	 */
-	buf = ExtendBufferedRel(BMR_REL(rel), MAIN_FORKNUM, NULL, EB_LOCK_FIRST);
+	buf = ExtendBufferedRel(BMR_REL(rel), MAIN_FORKNUM, EB_LOCK_FIRST);
 	if (!RelationUsesLocalBuffers(rel))
 		VALGRIND_MAKE_MEM_DEFINED(BufferGetPage(buf), BLCKSZ);
 
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 3df2c752ead..379a2d37def 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -1324,7 +1324,6 @@ btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										rel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -1730,8 +1729,7 @@ backtrack:
 		 * recycle all-zero pages, not fail.  Also, we want to use a
 		 * nondefault buffer access strategy.
 		 */
-		buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL,
-								 info->strategy);
+		buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 		goto backtrack;
 	}
 
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index f2ee333f60d..785e385aab6 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -432,7 +432,7 @@ SpGistNewBuffer(Relation index)
 		ReleaseBuffer(buffer);
 	}
 
-	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM,
 							   EB_LOCK_FIRST);
 
 	return buffer;
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index c461f8dc02d..9f644edaf82 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -704,8 +704,7 @@ spgprocesspending(spgBulkDeleteState *bds)
 
 		/* examine the referenced page */
 		blkno = ItemPointerGetBlockNumber(&pitem->tid);
-		buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, bds->info->strategy);
+		buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno, RBM_NORMAL);
 		LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE);
 		page = BufferGetPage(buffer);
 
@@ -830,7 +829,6 @@ spgvacuumscan(spgBulkDeleteState *bds)
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										bds->info->strategy,
 										index,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/src/backend/access/transam/xloginsert.c b/src/backend/access/transam/xloginsert.c
index f2e10b82b7d..0c16b7fef0f 100644
--- a/src/backend/access/transam/xloginsert.c
+++ b/src/backend/access/transam/xloginsert.c
@@ -1348,7 +1348,7 @@ log_newpage_range(Relation rel, ForkNumber forknum,
 		while (nbufs < XLR_MAX_BLOCK_ID && blkno < endblk)
 		{
 			Buffer		buf = ReadBufferExtended(rel, forknum, blkno,
-												 RBM_NORMAL, NULL);
+												 RBM_NORMAL);
 
 			LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
 
diff --git a/src/backend/access/transam/xlogutils.c b/src/backend/access/transam/xlogutils.c
index d8c179c5dcc..e5be49f8b5d 100644
--- a/src/backend/access/transam/xlogutils.c
+++ b/src/backend/access/transam/xlogutils.c
@@ -519,7 +519,7 @@ XLogReadBufferExtended(RelFileLocator rlocator, ForkNumber forknum,
 	{
 		/* page exists in file */
 		buffer = ReadBufferWithoutRelcache(rlocator, forknum, blkno,
-										   mode, NULL, true);
+										   mode, true);
 	}
 	else
 	{
@@ -536,7 +536,6 @@ XLogReadBufferExtended(RelFileLocator rlocator, ForkNumber forknum,
 		Assert(InRecovery);
 		buffer = ExtendBufferedRelTo(BMR_SMGR(smgr, RELPERSISTENCE_PERMANENT),
 									 forknum,
-									 NULL,
 									 EB_PERFORMING_RECOVERY |
 									 EB_SKIP_EXTENSION_LOCK,
 									 blkno + 1,
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 81bba4beac7..b0a7b8a33cd 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -3431,7 +3431,6 @@ validate_index(Oid heapId, Oid indexId, Snapshot snapshot)
 	ivinfo.estimated_count = true;
 	ivinfo.message_level = DEBUG2;
 	ivinfo.num_heap_tuples = heapRelation->rd_rel->reltuples;
-	ivinfo.strategy = NULL;
 
 	/*
 	 * Encode TIDs as int8 values for the sort, rather than directly sorting
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index f66e80b757c..9b117a53124 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -72,7 +72,6 @@ int			default_statistics_target = 100;
 
 /* A few variables that don't seem worth passing around as parameters */
 static MemoryContext anl_context = NULL;
-static BufferAccessStrategy vac_strategy;
 
 
 static void do_analyze_rel(Relation onerel,
@@ -108,8 +107,7 @@ static Datum ind_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
  */
 void
 analyze_rel(Oid relid, RangeVar *relation,
-			const VacuumParams *params, List *va_cols, bool in_outer_xact,
-			BufferAccessStrategy bstrategy)
+			const VacuumParams *params, List *va_cols, bool in_outer_xact)
 {
 	Relation	onerel;
 	int			elevel;
@@ -124,7 +122,6 @@ analyze_rel(Oid relid, RangeVar *relation,
 		elevel = DEBUG2;
 
 	/* Set up static variables */
-	vac_strategy = bstrategy;
 
 	/*
 	 * Check for user-requested abort.
@@ -730,7 +727,6 @@ do_analyze_rel(Relation onerel, const VacuumParams *params,
 			ivinfo.estimated_count = true;
 			ivinfo.message_level = elevel;
 			ivinfo.num_heap_tuples = onerel->rd_rel->reltuples;
-			ivinfo.strategy = vac_strategy;
 
 			stats = index_vacuum_cleanup(&ivinfo, NULL);
 
@@ -1302,7 +1298,6 @@ acquire_sample_rows(Relation onerel, int elevel,
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_USE_BATCHING,
-										vac_strategy,
 										scan->rs_rd,
 										MAIN_FORKNUM,
 										block_sampling_read_stream_next,
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index f0819d15ab7..6f65d48b6bd 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -262,7 +262,6 @@ ScanSourceDatabasePgClass(Oid tbid, Oid dbid, char *srcpath)
 	LockRelId	relid;
 	Snapshot	snapshot;
 	SMgrRelation smgr;
-	BufferAccessStrategy bstrategy;
 
 	/* Get pg_class relfilenumber. */
 	relfilenumber = RelationMapOidToFilenumberForDatabase(srcpath,
@@ -282,9 +281,6 @@ ScanSourceDatabasePgClass(Oid tbid, Oid dbid, char *srcpath)
 	nblocks = smgrnblocks(smgr, MAIN_FORKNUM);
 	smgrclose(smgr);
 
-	/* Use a buffer access strategy since this is a bulk read operation. */
-	bstrategy = GetAccessStrategy(BAS_BULKREAD);
-
 	/*
 	 * As explained in the function header comments, we need a snapshot that
 	 * will see all committed transactions as committed, and our transaction
@@ -299,7 +295,7 @@ ScanSourceDatabasePgClass(Oid tbid, Oid dbid, char *srcpath)
 		CHECK_FOR_INTERRUPTS();
 
 		buf = ReadBufferWithoutRelcache(rlocator, MAIN_FORKNUM, blkno,
-										RBM_NORMAL, bstrategy, true);
+										RBM_NORMAL, true);
 
 		LockBuffer(buf, BUFFER_LOCK_SHARE);
 		page = BufferGetPage(buf);
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index faa07d1a118..b7d2fb4d228 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -2456,7 +2456,7 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 			if (params->options & CLUOPT_VERBOSE)
 				vac_params.options |= VACOPT_VERBOSE;
 			analyze_rel(tableOid, NULL, &vac_params,
-						stmt->relation->va_cols, true, NULL);
+						stmt->relation->va_cols, true);
 			PopActiveSnapshot();
 			CommandCounterIncrement();
 		}
diff --git a/src/backend/commands/sequence.c b/src/backend/commands/sequence.c
index 551667650ba..ac1ab948211 100644
--- a/src/backend/commands/sequence.c
+++ b/src/backend/commands/sequence.c
@@ -358,7 +358,7 @@ fill_seq_fork_with_data(Relation rel, HeapTuple tuple, ForkNumber forkNum)
 
 	/* Initialize first page of relation with special magic number */
 
-	buf = ExtendBufferedRel(BMR_REL(rel), forkNum, NULL,
+	buf = ExtendBufferedRel(BMR_REL(rel), forkNum,
 							EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 	Assert(BufferGetBlockNumber(buf) == 0);
 
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 38539a6fd3d..66acd27bd48 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -127,32 +127,11 @@ static void vac_truncate_clog(TransactionId frozenXID,
 							  TransactionId lastSaneFrozenXid,
 							  MultiXactId lastSaneMinMulti);
 static bool vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
-					   BufferAccessStrategy bstrategy, bool isTopLevel);
+					   bool isTopLevel);
 static double compute_parallel_delay(void);
 static VacOptValue get_vacoptval_from_boolean(DefElem *def);
 static bool vac_tid_reaped(ItemPointer itemptr, void *state);
 
-/*
- * GUC check function to ensure GUC value specified is within the allowable
- * range.
- */
-bool
-check_vacuum_buffer_usage_limit(int *newval, void **extra,
-								GucSource source)
-{
-	/* Value upper and lower hard limits are inclusive */
-	if (*newval == 0 || (*newval >= MIN_BAS_VAC_RING_SIZE_KB &&
-						 *newval <= MAX_BAS_VAC_RING_SIZE_KB))
-		return true;
-
-	/* Value does not fall within any allowable range */
-	GUC_check_errdetail("\"%s\" must be 0 or between %d kB and %d kB.",
-						"vacuum_buffer_usage_limit",
-						MIN_BAS_VAC_RING_SIZE_KB, MAX_BAS_VAC_RING_SIZE_KB);
-
-	return false;
-}
-
 /*
  * Primary entry point for manual VACUUM and ANALYZE commands
  *
@@ -163,7 +142,6 @@ void
 ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 {
 	VacuumParams params;
-	BufferAccessStrategy bstrategy = NULL;
 	bool		verbose = false;
 	bool		skip_locked = false;
 	bool		analyze = false;
@@ -172,7 +150,6 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 	bool		disable_page_skipping = false;
 	bool		process_main = true;
 	bool		process_toast = true;
-	int			ring_size;
 	bool		skip_database_stats = false;
 	bool		only_database_stats = false;
 	MemoryContext vac_context;
@@ -188,12 +165,6 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 	/* Will be set later if we recurse to a TOAST table. */
 	params.toast_parent = InvalidOid;
 
-	/*
-	 * Set this to an invalid value so it is clear whether or not a
-	 * BUFFER_USAGE_LIMIT was specified when making the access strategy.
-	 */
-	ring_size = -1;
-
 	/* Parse options list */
 	foreach(lc, vacstmt->options)
 	{
@@ -204,32 +175,6 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 			verbose = defGetBoolean(opt);
 		else if (strcmp(opt->defname, "skip_locked") == 0)
 			skip_locked = defGetBoolean(opt);
-		else if (strcmp(opt->defname, "buffer_usage_limit") == 0)
-		{
-			const char *hintmsg;
-			int			result;
-			char	   *vac_buffer_size;
-
-			vac_buffer_size = defGetString(opt);
-
-			/*
-			 * Check that the specified value is valid and the size falls
-			 * within the hard upper and lower limits if it is not 0.
-			 */
-			if (!parse_int(vac_buffer_size, &result, GUC_UNIT_KB, &hintmsg) ||
-				(result != 0 &&
-				 (result < MIN_BAS_VAC_RING_SIZE_KB || result > MAX_BAS_VAC_RING_SIZE_KB)))
-			{
-				ereport(ERROR,
-						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-						 errmsg("%s option must be 0 or between %d kB and %d kB",
-								"BUFFER_USAGE_LIMIT",
-								MIN_BAS_VAC_RING_SIZE_KB, MAX_BAS_VAC_RING_SIZE_KB),
-						 hintmsg ? errhint_internal("%s", _(hintmsg)) : 0));
-			}
-
-			ring_size = result;
-		}
 		else if (!vacstmt->is_vacuumcmd)
 			ereport(ERROR,
 					(errcode(ERRCODE_SYNTAX_ERROR),
@@ -325,17 +270,6 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("VACUUM FULL cannot be performed in parallel")));
 
-	/*
-	 * BUFFER_USAGE_LIMIT does nothing for VACUUM (FULL) so just raise an
-	 * ERROR for that case.  VACUUM (FULL, ANALYZE) does make use of it, so
-	 * we'll permit that.
-	 */
-	if (ring_size != -1 && (params.options & VACOPT_FULL) &&
-		!(params.options & VACOPT_ANALYZE))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("BUFFER_USAGE_LIMIT cannot be specified for VACUUM FULL")));
-
 	/*
 	 * Make sure VACOPT_ANALYZE is specified if any column lists are present.
 	 */
@@ -431,38 +365,8 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 										"Vacuum",
 										ALLOCSET_DEFAULT_SIZES);
 
-	/*
-	 * Make a buffer strategy object in the cross-transaction memory context.
-	 * We needn't bother making this for VACUUM (FULL) or VACUUM
-	 * (ONLY_DATABASE_STATS) as they'll not make use of it.  VACUUM (FULL,
-	 * ANALYZE) is possible, so we'd better ensure that we make a strategy
-	 * when we see ANALYZE.
-	 */
-	if ((params.options & (VACOPT_ONLY_DATABASE_STATS |
-						   VACOPT_FULL)) == 0 ||
-		(params.options & VACOPT_ANALYZE) != 0)
-	{
-
-		MemoryContext old_context = MemoryContextSwitchTo(vac_context);
-
-		Assert(ring_size >= -1);
-
-		/*
-		 * If BUFFER_USAGE_LIMIT was specified by the VACUUM or ANALYZE
-		 * command, it overrides the value of VacuumBufferUsageLimit.  Either
-		 * value may be 0, in which case GetAccessStrategyWithSize() will
-		 * return NULL, effectively allowing full use of shared buffers.
-		 */
-		if (ring_size == -1)
-			ring_size = VacuumBufferUsageLimit;
-
-		bstrategy = GetAccessStrategyWithSize(BAS_VACUUM, ring_size);
-
-		MemoryContextSwitchTo(old_context);
-	}
-
 	/* Now go through the common routine */
-	vacuum(vacstmt->rels, &params, bstrategy, vac_context, isTopLevel);
+	vacuum(vacstmt->rels, &params, vac_context, isTopLevel);
 
 	/* Finally, clean up the vacuum memory context */
 	MemoryContextDelete(vac_context);
@@ -479,19 +383,13 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
  * params contains a set of parameters that can be used to customize the
  * behavior.
  *
- * bstrategy may be passed in as NULL when the caller does not want to
- * restrict the number of shared_buffers that VACUUM / ANALYZE can use,
- * otherwise, the caller must build a BufferAccessStrategy with the number of
- * shared_buffers that VACUUM / ANALYZE should try to limit themselves to
- * using.
- *
  * isTopLevel should be passed down from ProcessUtility.
  *
  * It is the caller's responsibility that all parameters are allocated in a
  * memory context that will not disappear at transaction commit.
  */
 void
-vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrategy,
+vacuum(List *relations, const VacuumParams *params,
 	   MemoryContext vac_context, bool isTopLevel)
 {
 	static bool in_vacuum = false;
@@ -630,7 +528,7 @@ vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrate
 
 			if (params->options & VACOPT_VACUUM)
 			{
-				if (!vacuum_rel(vrel->oid, vrel->relation, *params, bstrategy,
+				if (!vacuum_rel(vrel->oid, vrel->relation, *params,
 								isTopLevel))
 					continue;
 			}
@@ -649,7 +547,7 @@ vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrate
 				}
 
 				analyze_rel(vrel->oid, vrel->relation, params,
-							vrel->va_cols, in_outer_xact, bstrategy);
+							vrel->va_cols, in_outer_xact);
 
 				if (use_own_xacts)
 				{
@@ -2010,7 +1908,7 @@ vac_truncate_clog(TransactionId frozenXID,
  */
 static bool
 vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
-		   BufferAccessStrategy bstrategy, bool isTopLevel)
+		   bool isTopLevel)
 {
 	LOCKMODE	lmode;
 	Relation	rel;
@@ -2307,7 +2205,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 			rel = NULL;
 		}
 		else
-			table_relation_vacuum(rel, &params, bstrategy);
+			table_relation_vacuum(rel, &params);
 	}
 
 	/* Roll back any GUC changes executed by index functions */
@@ -2344,7 +2242,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 		toast_vacuum_params.options |= VACOPT_PROCESS_MAIN;
 		toast_vacuum_params.toast_parent = relid;
 
-		vacuum_rel(toast_relid, NULL, toast_vacuum_params, bstrategy,
+		vacuum_rel(toast_relid, NULL, toast_vacuum_params,
 				   isTopLevel);
 	}
 
diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c
index 41cefcfde54..8b4b835db9b 100644
--- a/src/backend/commands/vacuumparallel.c
+++ b/src/backend/commands/vacuumparallel.c
@@ -125,12 +125,6 @@ typedef struct PVShared
 	 */
 	int			maintenance_work_mem_worker;
 
-	/*
-	 * The number of buffers each worker's Buffer Access Strategy ring should
-	 * contain.
-	 */
-	int			ring_nbuffers;
-
 	/*
 	 * Shared vacuum cost balance.  During parallel vacuum,
 	 * VacuumSharedCostBalance points to this value and it accumulates the
@@ -257,9 +251,6 @@ struct ParallelVacuumState
 	int			nindexes_parallel_cleanup;
 	int			nindexes_parallel_condcleanup;
 
-	/* Buffer access strategy used by leader process */
-	BufferAccessStrategy bstrategy;
-
 	/*
 	 * Error reporting state.  The error callback is set only for workers
 	 * processes during parallel index vacuum.
@@ -304,7 +295,7 @@ static void parallel_vacuum_dsm_detach(dsm_segment *seg, Datum arg);
 ParallelVacuumState *
 parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes,
 					 int nrequested_workers, int vac_work_mem,
-					 int elevel, BufferAccessStrategy bstrategy)
+					 int elevel)
 {
 	ParallelVacuumState *pvs;
 	ParallelContext *pcxt;
@@ -345,7 +336,6 @@ parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes,
 	pvs->indrels = indrels;
 	pvs->nindexes = nindexes;
 	pvs->will_parallel_vacuum = will_parallel_vacuum;
-	pvs->bstrategy = bstrategy;
 	pvs->heaprel = rel;
 
 	EnterParallelMode();
@@ -447,9 +437,6 @@ parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes,
 	shared->dead_items_handle = TidStoreGetHandle(dead_items);
 	shared->dead_items_dsa_handle = dsa_get_handle(TidStoreGetDSA(dead_items));
 
-	/* Use the same buffer size for all workers */
-	shared->ring_nbuffers = GetAccessStrategyBufferCount(bstrategy);
-
 	pg_atomic_init_u32(&(shared->cost_balance), 0);
 	pg_atomic_init_u32(&(shared->active_nworkers), 0);
 	pg_atomic_init_u32(&(shared->idx), 0);
@@ -1091,7 +1078,6 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
 	ivinfo.message_level = DEBUG2;
 	ivinfo.estimated_count = pvs->shared->estimated_count;
 	ivinfo.num_heap_tuples = pvs->shared->reltuples;
-	ivinfo.strategy = pvs->bstrategy;
 
 	/* Update error traceback information */
 	pvs->indname = pstrdup(RelationGetRelationName(indrel));
@@ -1294,10 +1280,6 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
 	pvs.indname = NULL;
 	pvs.status = PARALLEL_INDVAC_STATUS_INITIAL;
 
-	/* Each parallel VACUUM worker gets its own access strategy. */
-	pvs.bstrategy = GetAccessStrategyWithSize(BAS_VACUUM,
-											  shared->ring_nbuffers * (BLCKSZ / 1024));
-
 	/* Setup error traceback support for ereport() */
 	errcallback.callback = parallel_vacuum_error_callback;
 	errcallback.arg = &pvs;
@@ -1328,7 +1310,6 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
 
 	vac_close_indexes(nindexes, indrels, RowExclusiveLock);
 	table_close(rel, ShareUpdateExclusiveLock);
-	FreeAccessStrategy(pvs.bstrategy);
 
 	if (shared->is_autovacuum)
 		pv_shared_cost_params = NULL;
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 45abf48768a..e021e798805 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -386,8 +386,7 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
 									  bool *dovacuum, bool *doanalyze, bool *wraparound,
 									  AutoVacuumScores *scores);
 
-static void autovacuum_do_vac_analyze(autovac_table *tab,
-									  BufferAccessStrategy bstrategy);
+static void autovacuum_do_vac_analyze(autovac_table *tab);
 static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
 										 TupleDesc pg_class_desc);
 static void perform_work_item(AutoVacuumWorkItem *workitem);
@@ -1936,7 +1935,6 @@ do_autovacuum(void)
 	HASHCTL		ctl;
 	HTAB	   *table_toast_map;
 	ListCell   *volatile cell;
-	BufferAccessStrategy bstrategy;
 	ScanKeyData key;
 	TupleDesc	pg_class_desc;
 	int			effective_multixact_freeze_max_age;
@@ -2322,23 +2320,6 @@ do_autovacuum(void)
 		autovacuum_analyze_score_weight != 0.0)
 		list_sort(tables_to_process, TableToProcessComparator);
 
-	/*
-	 * Optionally, create a buffer access strategy object for VACUUM to use.
-	 * We use the same BufferAccessStrategy object for all tables VACUUMed by
-	 * this worker to prevent autovacuum from blowing out shared buffers.
-	 *
-	 * VacuumBufferUsageLimit being set to 0 results in
-	 * GetAccessStrategyWithSize returning NULL, effectively meaning we can
-	 * use up to all of shared buffers.
-	 *
-	 * If we later enter failsafe mode on any of the tables being vacuumed, we
-	 * will cease use of the BufferAccessStrategy only for that table.
-	 *
-	 * XXX should we consider adding code to adjust the size of this if
-	 * VacuumBufferUsageLimit changes?
-	 */
-	bstrategy = GetAccessStrategyWithSize(BAS_VACUUM, VacuumBufferUsageLimit);
-
 	/*
 	 * create a memory context to act as fake PortalContext, so that the
 	 * contexts created in the vacuum code are cleaned up for each table.
@@ -2516,7 +2497,7 @@ do_autovacuum(void)
 			MemoryContextSwitchTo(PortalContext);
 
 			/* have at it */
-			autovacuum_do_vac_analyze(tab, bstrategy);
+			autovacuum_do_vac_analyze(tab);
 
 			/*
 			 * Clear a possible query-cancel signal, to avoid a late reaction
@@ -2636,8 +2617,6 @@ deleted:
 #ifdef USE_VALGRIND
 	hash_destroy(table_toast_map);
 	FreeTupleDesc(pg_class_desc);
-	if (bstrategy)
-		pfree(bstrategy);
 #endif
 
 	/* Run the rest in xact context, mainly to avoid Valgrind leak warnings */
@@ -3348,7 +3327,7 @@ relation_needs_vacanalyze(Oid relid,
  * disappear at transaction commit.
  */
 static void
-autovacuum_do_vac_analyze(autovac_table *tab, BufferAccessStrategy bstrategy)
+autovacuum_do_vac_analyze(autovac_table *tab)
 {
 	RangeVar   *rangevar;
 	VacuumRelation *rel;
@@ -3371,7 +3350,7 @@ autovacuum_do_vac_analyze(autovac_table *tab, BufferAccessStrategy bstrategy)
 	rel_list = list_make1(rel);
 	MemoryContextSwitchTo(old_context);
 
-	vacuum(rel_list, &tab->at_params, bstrategy, vac_context, true);
+	vacuum(rel_list, &tab->at_params, vac_context, true);
 
 	MemoryContextDelete(vac_context);
 }
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 68557c16cb9..52390fd5423 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -390,7 +390,7 @@ static List *BuildRelationList(bool temp_relations, bool include_shared);
 static void FreeDatabaseList(List *dblist);
 static DataChecksumsWorkerResult ProcessDatabase(DataChecksumsWorkerDatabase *db);
 static bool ProcessAllDatabases(void);
-static bool ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy);
+static bool ProcessSingleRelationFork(Relation reln, ForkNumber forkNum);
 static void launcher_cancel_handler(SIGNAL_ARGS);
 static void WaitForAllTransactionsToFinish(void);
 
@@ -686,7 +686,7 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
  * error is raised in the lower levels.
  */
 static bool
-ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy)
+ProcessSingleRelationFork(Relation reln, ForkNumber forkNum)
 {
 	BlockNumber numblocks = RelationGetNumberOfBlocksInFork(reln, forkNum);
 	char		activity[NAMEDATALEN * 2 + 128];
@@ -709,7 +709,7 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg
 	 */
 	for (BlockNumber blknum = 0; blknum < numblocks; blknum++)
 	{
-		Buffer		buf = ReadBufferExtended(reln, forkNum, blknum, RBM_NORMAL, strategy);
+		Buffer		buf = ReadBufferExtended(reln, forkNum, blknum, RBM_NORMAL);
 
 		/* Need to get an exclusive lock to mark the buffer as dirty */
 		LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
@@ -772,7 +772,7 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg
  * error is raised in the lower levels.
  */
 static bool
-ProcessSingleRelationByOid(Oid relationId, BufferAccessStrategy strategy)
+ProcessSingleRelationByOid(Oid relationId)
 {
 	Relation	rel;
 	bool		aborted = false;
@@ -799,7 +799,7 @@ ProcessSingleRelationByOid(Oid relationId, BufferAccessStrategy strategy)
 	{
 		if (smgrexists(rel->rd_smgr, fnum))
 		{
-			if (!ProcessSingleRelationFork(rel, fnum, strategy))
+			if (!ProcessSingleRelationFork(rel, fnum))
 			{
 				aborted = true;
 				break;
@@ -1546,7 +1546,6 @@ DataChecksumsWorkerMain(Datum arg)
 	Oid			dboid;
 	List	   *RelationList = NIL;
 	List	   *InitialTempTableList = NIL;
-	BufferAccessStrategy strategy;
 	bool		aborted = false;
 	int64		rels_done;
 	bool		process_shared;
@@ -1613,11 +1612,6 @@ DataChecksumsWorkerMain(Datum arg)
 	VacuumUpdateCosts();
 	VacuumCostBalance = 0;
 
-	/*
-	 * Create and set the vacuum strategy as our buffer strategy.
-	 */
-	strategy = GetAccessStrategy(BAS_VACUUM);
-
 	RelationList = BuildRelationList(false, process_shared);
 
 	/* Update the total number of relations to be processed in this DB. */
@@ -1639,9 +1633,7 @@ DataChecksumsWorkerMain(Datum arg)
 	rels_done = 0;
 	foreach_oid(reloid, RelationList)
 	{
-		bool		costs_updated = false;
-
-		if (!ProcessSingleRelationByOid(reloid, strategy))
+		if (!ProcessSingleRelationByOid(reloid))
 		{
 			aborted = true;
 			break;
@@ -1657,8 +1649,7 @@ DataChecksumsWorkerMain(Datum arg)
 
 		/*
 		 * Check if the cost settings changed during runtime and if so, update
-		 * to reflect the new values and signal that the access strategy needs
-		 * to be refreshed.
+		 * to reflect the new values.
 		 */
 		LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
 		if (DataChecksumState->worker_invocation != worker_invocation)
@@ -1669,7 +1660,6 @@ DataChecksumsWorkerMain(Datum arg)
 		if ((DataChecksumState->launch_cost_delay != DataChecksumState->cost_delay)
 			|| (DataChecksumState->launch_cost_limit != DataChecksumState->cost_limit))
 		{
-			costs_updated = true;
 			VacuumCostDelay = DataChecksumState->launch_cost_delay;
 			VacuumCostLimit = DataChecksumState->launch_cost_limit;
 			VacuumUpdateCosts();
@@ -1677,19 +1667,10 @@ DataChecksumsWorkerMain(Datum arg)
 			DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
 			DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
 		}
-		else
-			costs_updated = false;
 		LWLockRelease(DataChecksumsWorkerLock);
-
-		if (costs_updated)
-		{
-			FreeAccessStrategy(strategy);
-			strategy = GetAccessStrategy(BAS_VACUUM);
-		}
 	}
 
 	list_free(RelationList);
-	FreeAccessStrategy(strategy);
 
 	if (aborted || abort_requested)
 	{
diff --git a/src/backend/storage/aio/read_stream.c b/src/backend/storage/aio/read_stream.c
index a318539e56c..08d344f063d 100644
--- a/src/backend/storage/aio/read_stream.c
+++ b/src/backend/storage/aio/read_stream.c
@@ -757,7 +757,6 @@ read_stream_look_ahead(ReadStream *stream)
  */
 static ReadStream *
 read_stream_begin_impl(int flags,
-					   BufferAccessStrategy strategy,
 					   Relation rel,
 					   SMgrRelation smgr,
 					   char persistence,
@@ -771,7 +770,6 @@ read_stream_begin_impl(int flags,
 	int16		queue_size;
 	int16		queue_overflow;
 	int			max_ios;
-	int			strategy_pin_limit;
 	uint32		max_pinned_buffers;
 	uint32		max_possible_buffer_limit;
 	Oid			tablespace_id;
@@ -835,10 +833,6 @@ read_stream_begin_impl(int flags,
 	max_pinned_buffers = Min(max_pinned_buffers,
 							 PG_INT16_MAX - queue_overflow - 1);
 
-	/* Give the strategy a chance to limit the number of buffers we pin. */
-	strategy_pin_limit = GetAccessStrategyPinLimit(strategy);
-	max_pinned_buffers = Min(strategy_pin_limit, max_pinned_buffers);
-
 	/*
 	 * Also limit our queue to the maximum number of pins we could ever be
 	 * allowed to acquire according to the buffer manager.  We may not really
@@ -962,7 +956,6 @@ read_stream_begin_impl(int flags,
 		stream->ios[i].op.smgr = smgr;
 		stream->ios[i].op.persistence = persistence;
 		stream->ios[i].op.forknum = forknum;
-		stream->ios[i].op.strategy = strategy;
 	}
 
 	return stream;
@@ -974,7 +967,6 @@ read_stream_begin_impl(int flags,
  */
 ReadStream *
 read_stream_begin_relation(int flags,
-						   BufferAccessStrategy strategy,
 						   Relation rel,
 						   ForkNumber forknum,
 						   ReadStreamBlockNumberCB callback,
@@ -982,7 +974,6 @@ read_stream_begin_relation(int flags,
 						   size_t per_buffer_data_size)
 {
 	return read_stream_begin_impl(flags,
-								  strategy,
 								  rel,
 								  RelationGetSmgr(rel),
 								  rel->rd_rel->relpersistence,
@@ -998,7 +989,6 @@ read_stream_begin_relation(int flags,
  */
 ReadStream *
 read_stream_begin_smgr_relation(int flags,
-								BufferAccessStrategy strategy,
 								SMgrRelation smgr,
 								char smgr_persistence,
 								ForkNumber forknum,
@@ -1007,7 +997,6 @@ read_stream_begin_smgr_relation(int flags,
 								size_t per_buffer_data_size)
 {
 	return read_stream_begin_impl(flags,
-								  strategy,
 								  NULL,
 								  smgr,
 								  smgr_persistence,
@@ -1370,13 +1359,11 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data)
  * Transitional support for code that would like to perform or skip reads
  * itself, without using the stream.  Returns, and consumes, the next block
  * number that would be read by the stream's look-ahead algorithm, or
- * InvalidBlockNumber if the end of the stream is reached.  Also reports the
- * strategy that would be used to read it.
+ * InvalidBlockNumber if the end of the stream is reached.
  */
 BlockNumber
-read_stream_next_block(ReadStream *stream, BufferAccessStrategy *strategy)
+read_stream_next_block(ReadStream *stream)
 {
-	*strategy = stream->ios[0].op.strategy;
 	return read_stream_get_block(stream, NULL);
 }
 
diff --git a/src/backend/storage/buffer/README b/src/backend/storage/buffer/README
index b332e002ba1..58df6bd011a 100644
--- a/src/backend/storage/buffer/README
+++ b/src/backend/storage/buffer/README
@@ -203,50 +203,6 @@ have to give up and try another buffer.  This however is not a concern
 of the basic select-a-victim-buffer algorithm.)
 
 
-Buffer Ring Replacement Strategy
----------------------------------
-
-When running a query that needs to access a large number of pages just once,
-such as VACUUM or a large sequential scan, a different strategy is used.
-A page that has been touched only by such a scan is unlikely to be needed
-again soon, so instead of running the normal clock-sweep algorithm and
-blowing out the entire buffer cache, a small ring of buffers is allocated
-using the normal clock-sweep algorithm and those buffers are reused for the
-whole scan.  This also implies that much of the write traffic caused by such
-a statement will be done by the backend itself and not pushed off onto other
-processes.
-
-For sequential scans, a 256KB ring is used. That's small enough to fit in L2
-cache, which makes transferring pages from OS cache to shared buffer cache
-efficient.  Even less would often be enough, but the ring must be big enough
-to accommodate all pages in the scan that are pinned concurrently.  256KB
-should also be enough to leave a small cache trail for other backends to
-join in a synchronized seq scan.  If a ring buffer is dirtied and its LSN
-updated, we would normally have to write and flush WAL before we could
-re-use the buffer; in this case we instead discard the buffer from the ring
-and (later) choose a replacement using the normal clock-sweep algorithm.
-Hence this strategy works best for scans that are read-only (or at worst
-update hint bits).  In a scan that modifies every page in the scan, like a
-bulk UPDATE or DELETE, the buffers in the ring will always be dirtied and
-the ring strategy effectively degrades to the normal strategy.
-
-VACUUM uses a ring like sequential scans, however, the size of this ring is
-controlled by the vacuum_buffer_usage_limit GUC.  Dirty pages are not removed
-from the ring.  Instead, the WAL is flushed if needed to allow reuse of the
-buffers.  Before introducing the buffer ring strategy in 8.3, VACUUM's buffers
-were sent to the freelist, which was effectively a buffer ring of 1 buffer,
-resulting in excessive WAL flushing.
-
-Bulk writes work similarly to VACUUM.  Currently this applies only to
-COPY IN and CREATE TABLE AS SELECT.  (Might it be interesting to make
-seqscan UPDATE and DELETE use the bulkwrite strategy?)  For bulk writes
-we use a ring size of 16MB (but not more than 1/8th of shared_buffers).
-Smaller sizes have been shown to result in the COPY blocking too often
-for WAL flushes.  While it's okay for a background vacuum to be slowed by
-doing its own WAL flushing, we'd prefer that COPY not be subject to that,
-so we let it use up a bit more of the buffer arena.
-
-
 Background Writer's Processing
 ------------------------------
 
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 1173b50b7c1..30f2443e577 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -612,10 +612,9 @@ ForgetPrivateRefCountEntry(PrivateRefCountEntry *ref)
 static Buffer ReadBuffer_common(Relation rel,
 								SMgrRelation smgr, char smgr_persistence,
 								ForkNumber forkNum, BlockNumber blockNum,
-								ReadBufferMode mode, BufferAccessStrategy strategy);
+								ReadBufferMode mode);
 static BlockNumber ExtendBufferedRelCommon(BufferManagerRelation bmr,
 										   ForkNumber fork,
-										   BufferAccessStrategy strategy,
 										   uint32 flags,
 										   uint32 extend_by,
 										   BlockNumber extend_upto,
@@ -623,13 +622,12 @@ static BlockNumber ExtendBufferedRelCommon(BufferManagerRelation bmr,
 										   uint32 *extended_by);
 static BlockNumber ExtendBufferedRelShared(BufferManagerRelation bmr,
 										   ForkNumber fork,
-										   BufferAccessStrategy strategy,
 										   uint32 flags,
 										   uint32 extend_by,
 										   BlockNumber extend_upto,
 										   Buffer *buffers,
 										   uint32 *extended_by);
-static bool PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy,
+static bool PinBuffer(BufferDesc *buf,
 					  bool skip_if_not_valid);
 static void PinBuffer_Locked(BufferDesc *buf);
 static void UnpinBuffer(BufferDesc *buf);
@@ -645,7 +643,6 @@ static inline BufferDesc *BufferAlloc(SMgrRelation smgr,
 									  char relpersistence,
 									  ForkNumber forkNum,
 									  BlockNumber blockNum,
-									  BufferAccessStrategy strategy,
 									  bool *foundPtr, IOContext io_context);
 static bool AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress);
 static void CheckReadBuffersOperation(ReadBuffersOperation *operation, bool is_complete);
@@ -654,7 +651,7 @@ static pg_attribute_always_inline void TrackBufferHit(IOObject io_object,
 													  IOContext io_context,
 													  Relation rel, char persistence, SMgrRelation smgr,
 													  ForkNumber forknum, BlockNumber blocknum);
-static Buffer GetVictimBuffer(BufferAccessStrategy strategy, IOContext io_context);
+static Buffer GetVictimBuffer(IOContext io_context);
 static void FlushUnlockedBuffer(BufferDesc *buf, SMgrRelation reln,
 								IOObject io_object, IOContext io_context);
 static void FlushBuffer(BufferDesc *buf, SMgrRelation reln,
@@ -858,7 +855,7 @@ ReadRecentBuffer(RelFileLocator rlocator, ForkNumber forkNum, BlockNumber blockN
 		 * pin.
 		 */
 		if (BufferTagsEqual(&tag, &bufHdr->tag) &&
-			PinBuffer(bufHdr, NULL, true))
+			PinBuffer(bufHdr, true))
 		{
 			if (BufferTagsEqual(&tag, &bufHdr->tag))
 			{
@@ -874,12 +871,12 @@ ReadRecentBuffer(RelFileLocator rlocator, ForkNumber forkNum, BlockNumber blockN
 
 /*
  * ReadBuffer -- a shorthand for ReadBufferExtended, for reading from main
- *		fork with RBM_NORMAL mode and default strategy.
+ *		fork with RBM_NORMAL mode.
  */
 Buffer
 ReadBuffer(Relation reln, BlockNumber blockNum)
 {
-	return ReadBufferExtended(reln, MAIN_FORKNUM, blockNum, RBM_NORMAL, NULL);
+	return ReadBufferExtended(reln, MAIN_FORKNUM, blockNum, RBM_NORMAL);
 }
 
 /*
@@ -919,13 +916,10 @@ ReadBuffer(Relation reln, BlockNumber blockNum)
  * a cleanup-strength lock on the page.
  *
  * RBM_NORMAL_NO_LOG mode is treated the same as RBM_NORMAL here.
- *
- * If strategy is not NULL, a nondefault buffer access strategy is used.
- * See buffer/README for details.
  */
 inline Buffer
 ReadBufferExtended(Relation reln, ForkNumber forkNum, BlockNumber blockNum,
-				   ReadBufferMode mode, BufferAccessStrategy strategy)
+				   ReadBufferMode mode)
 {
 	Buffer		buf;
 
@@ -935,7 +929,7 @@ ReadBufferExtended(Relation reln, ForkNumber forkNum, BlockNumber blockNum,
 	 * ReadBuffer_common().
 	 */
 	buf = ReadBuffer_common(reln, RelationGetSmgr(reln), 0,
-							forkNum, blockNum, mode, strategy);
+							forkNum, blockNum, mode);
 
 	return buf;
 }
@@ -954,14 +948,14 @@ ReadBufferExtended(Relation reln, ForkNumber forkNum, BlockNumber blockNum,
 Buffer
 ReadBufferWithoutRelcache(RelFileLocator rlocator, ForkNumber forkNum,
 						  BlockNumber blockNum, ReadBufferMode mode,
-						  BufferAccessStrategy strategy, bool permanent)
+						  bool permanent)
 {
 	SMgrRelation smgr = smgropen(rlocator, INVALID_PROC_NUMBER);
 
 	return ReadBuffer_common(NULL, smgr,
 							 permanent ? RELPERSISTENCE_PERMANENT : RELPERSISTENCE_UNLOGGED,
 							 forkNum, blockNum,
-							 mode, strategy);
+							 mode);
 }
 
 /*
@@ -970,13 +964,12 @@ ReadBufferWithoutRelcache(RelFileLocator rlocator, ForkNumber forkNum,
 Buffer
 ExtendBufferedRel(BufferManagerRelation bmr,
 				  ForkNumber forkNum,
-				  BufferAccessStrategy strategy,
 				  uint32 flags)
 {
 	Buffer		buf;
 	uint32		extend_by = 1;
 
-	ExtendBufferedRelBy(bmr, forkNum, strategy, flags, extend_by,
+	ExtendBufferedRelBy(bmr, forkNum, flags, extend_by,
 						&buf, &extend_by);
 
 	return buf;
@@ -1002,7 +995,6 @@ ExtendBufferedRel(BufferManagerRelation bmr,
 BlockNumber
 ExtendBufferedRelBy(BufferManagerRelation bmr,
 					ForkNumber fork,
-					BufferAccessStrategy strategy,
 					uint32 flags,
 					uint32 extend_by,
 					Buffer *buffers,
@@ -1015,7 +1007,7 @@ ExtendBufferedRelBy(BufferManagerRelation bmr,
 	if (bmr.relpersistence == '\0')
 		bmr.relpersistence = bmr.rel->rd_rel->relpersistence;
 
-	return ExtendBufferedRelCommon(bmr, fork, strategy, flags,
+	return ExtendBufferedRelCommon(bmr, fork, flags,
 								   extend_by, InvalidBlockNumber,
 								   buffers, extended_by);
 }
@@ -1031,7 +1023,6 @@ ExtendBufferedRelBy(BufferManagerRelation bmr,
 Buffer
 ExtendBufferedRelTo(BufferManagerRelation bmr,
 					ForkNumber fork,
-					BufferAccessStrategy strategy,
 					uint32 flags,
 					BlockNumber extend_to,
 					ReadBufferMode mode)
@@ -1097,7 +1088,7 @@ ExtendBufferedRelTo(BufferManagerRelation bmr,
 		if ((uint64) current_size + num_pages > extend_to)
 			num_pages = extend_to - current_size;
 
-		first_block = ExtendBufferedRelCommon(bmr, fork, strategy, flags,
+		first_block = ExtendBufferedRelCommon(bmr, fork, flags,
 											  num_pages, extend_to,
 											  buffers, &extended_by);
 
@@ -1123,7 +1114,7 @@ ExtendBufferedRelTo(BufferManagerRelation bmr,
 	{
 		Assert(extended_by == 0);
 		buffer = ReadBuffer_common(bmr.rel, BMR_GET_SMGR(bmr), bmr.relpersistence,
-								   fork, extend_to - 1, mode, strategy);
+								   fork, extend_to - 1, mode);
 	}
 
 	return buffer;
@@ -1226,7 +1217,6 @@ PinBufferForBlock(Relation rel,
 				  char persistence,
 				  ForkNumber forkNum,
 				  BlockNumber blockNum,
-				  BufferAccessStrategy strategy,
 				  IOObject io_object,
 				  IOContext io_context,
 				  bool *foundPtr)
@@ -1250,7 +1240,7 @@ PinBufferForBlock(Relation rel,
 		bufHdr = LocalBufferAlloc(smgr, forkNum, blockNum, foundPtr);
 	else
 		bufHdr = BufferAlloc(smgr, persistence, forkNum, blockNum,
-							 strategy, foundPtr, io_context);
+							 foundPtr, io_context);
 
 	if (*foundPtr)
 		TrackBufferHit(io_object, io_context, rel, persistence, smgr, forkNum, blockNum);
@@ -1276,8 +1266,7 @@ PinBufferForBlock(Relation rel,
 static pg_attribute_always_inline Buffer
 ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
 				  ForkNumber forkNum,
-				  BlockNumber blockNum, ReadBufferMode mode,
-				  BufferAccessStrategy strategy)
+				  BlockNumber blockNum, ReadBufferMode mode)
 {
 	ReadBuffersOperation operation;
 	Buffer		buffer;
@@ -1313,7 +1302,7 @@ ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
 		if (mode == RBM_ZERO_AND_LOCK || mode == RBM_ZERO_AND_CLEANUP_LOCK)
 			flags |= EB_LOCK_FIRST;
 
-		return ExtendBufferedRel(BMR_REL(rel), forkNum, strategy, flags);
+		return ExtendBufferedRel(BMR_REL(rel), forkNum, flags);
 	}
 
 	if (rel)
@@ -1335,12 +1324,12 @@ ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
 		}
 		else
 		{
-			io_context = IOContextForStrategy(strategy);
+			io_context = IOCONTEXT_NORMAL;
 			io_object = IOOBJECT_RELATION;
 		}
 
 		buffer = PinBufferForBlock(rel, smgr, persistence,
-								   forkNum, blockNum, strategy,
+								   forkNum, blockNum,
 								   io_object, io_context, &found);
 		ZeroAndLockBuffer(buffer, mode, found);
 		return buffer;
@@ -1358,7 +1347,6 @@ ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
 	operation.rel = rel;
 	operation.persistence = persistence;
 	operation.forknum = forkNum;
-	operation.strategy = strategy;
 	if (StartReadBuffer(&operation,
 						&buffer,
 						blockNum,
@@ -1399,7 +1387,7 @@ StartReadBuffersImpl(ReadBuffersOperation *operation,
 	}
 	else
 	{
-		io_context = IOContextForStrategy(operation->strategy);
+		io_context = IOCONTEXT_NORMAL;
 		io_object = IOOBJECT_RELATION;
 	}
 
@@ -1450,7 +1438,6 @@ StartReadBuffersImpl(ReadBuffersOperation *operation,
 										   operation->persistence,
 										   operation->forknum,
 										   blockNum + i,
-										   operation->strategy,
 										   io_object, io_context,
 										   &found);
 		}
@@ -1771,7 +1758,7 @@ WaitReadBuffers(ReadBuffersOperation *operation)
 	}
 	else
 	{
-		io_context = IOContextForStrategy(operation->strategy);
+		io_context = IOCONTEXT_NORMAL;
 		io_object = IOOBJECT_RELATION;
 	}
 
@@ -1961,7 +1948,7 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress)
 	}
 	else
 	{
-		io_context = IOContextForStrategy(operation->strategy);
+		io_context = IOCONTEXT_NORMAL;
 		io_object = IOOBJECT_RELATION;
 	}
 
@@ -2180,24 +2167,18 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress)
  *		buffer.  If no buffer exists already, selects a replacement victim and
  *		evicts the old page, but does NOT read in new page.
  *
- * "strategy" can be a buffer replacement strategy object, or NULL for
- * the default strategy.  The selected buffer's usage_count is advanced when
- * using the default strategy, but otherwise possibly not (see PinBuffer).
- *
  * The returned buffer is pinned and is already marked as holding the
  * desired page.  If it already did have the desired page, *foundPtr is
  * set true.  Otherwise, *foundPtr is set false.
  *
- * io_context is passed as an output parameter to avoid calling
- * IOContextForStrategy() when there is a shared buffers hit and no IO
- * statistics need be captured.
+ * io_context is passed as an output parameter to avoid capturing IO
+ * statistics when there is a shared buffers hit and no IO occurs.
  *
  * No locks are held either at entry or exit.
  */
 static pg_attribute_always_inline BufferDesc *
 BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 			BlockNumber blockNum,
-			BufferAccessStrategy strategy,
 			bool *foundPtr, IOContext io_context)
 {
 	BufferTag	newTag;			/* identity of requested block */
@@ -2235,7 +2216,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 		 */
 		buf = GetBufferDescriptor(existing_buf_id);
 
-		valid = PinBuffer(buf, strategy, false);
+		valid = PinBuffer(buf, false);
 
 		/* Can release the mapping lock as soon as we've pinned it */
 		LWLockRelease(newPartitionLock);
@@ -2266,7 +2247,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	 * don't hold any conflicting locks. If so we'll have to undo our work
 	 * later.
 	 */
-	victim_buffer = GetVictimBuffer(strategy, io_context);
+	victim_buffer = GetVictimBuffer(io_context);
 	victim_buf_hdr = GetBufferDescriptor(victim_buffer - 1);
 
 	/*
@@ -2297,7 +2278,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 
 		existing_buf_hdr = GetBufferDescriptor(existing_buf_id);
 
-		valid = PinBuffer(existing_buf_hdr, strategy, false);
+		valid = PinBuffer(existing_buf_hdr, false);
 
 		/* Can release the mapping lock as soon as we've pinned it */
 		LWLockRelease(newPartitionLock);
@@ -2335,9 +2316,12 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	 * just like permanent relations.
 	 */
 	set_bits |= BM_TAG_VALID;
-	/* Admit the newly loaded page COOL (probation); a second access via
+
+	/*
+	 * Admit the newly loaded page COOL (probation); a second access via
 	 * PinBuffer promotes it to HOT.  This is what makes a one-touch scan
-	 * self-evicting -- see the cooling-state notes in buf_internals.h. */
+	 * self-evicting -- see the cooling-state notes in buf_internals.h.
+	 */
 	if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM)
 		set_bits |= BM_PERMANENT;
 
@@ -2549,12 +2533,11 @@ InvalidateVictimBuffer(BufferDesc *buf_hdr)
 }
 
 static Buffer
-GetVictimBuffer(BufferAccessStrategy strategy, IOContext io_context)
+GetVictimBuffer(IOContext io_context)
 {
 	BufferDesc *buf_hdr;
 	Buffer		buf;
 	uint64		buf_state;
-	bool		from_ring;
 
 	/*
 	 * Ensure, before we pin a victim buffer, that there's a free refcount
@@ -2570,7 +2553,7 @@ again:
 	 * Select a victim buffer.  The buffer is returned pinned and owned by
 	 * this backend.
 	 */
-	buf_hdr = StrategyGetBuffer(strategy, &buf_state, &from_ring);
+	buf_hdr = StrategyGetBuffer(&buf_state);
 	buf = BufferDescriptorGetBuffer(buf_hdr);
 
 	/*
@@ -2614,26 +2597,6 @@ again:
 			goto again;
 		}
 
-		/*
-		 * If using a nondefault strategy, and this victim came from the
-		 * strategy ring, let the strategy decide whether to reject it when
-		 * reusing it would require a WAL flush.  This only applies to
-		 * permanent buffers; unlogged buffers can have fake LSNs, so
-		 * XLogNeedsFlush() is not meaningful for them.
-		 *
-		 * We need to hold the content lock in at least share-exclusive mode
-		 * to safely inspect the page LSN, so this couldn't have been done
-		 * inside StrategyGetBuffer().
-		 */
-		if (strategy && from_ring &&
-			buf_state & BM_PERMANENT &&
-			XLogNeedsFlush(BufferGetLSN(buf_hdr)) &&
-			StrategyRejectBuffer(strategy, buf_hdr, from_ring))
-		{
-			UnlockReleaseBuffer(buf);
-			goto again;
-		}
-
 		/* OK, do the I/O */
 		FlushBuffer(buf_hdr, NULL, IOOBJECT_RELATION, io_context);
 		LockBuffer(buf, BUFFER_LOCK_UNLOCK);
@@ -2646,23 +2609,15 @@ again:
 	if (buf_state & BM_VALID)
 	{
 		/*
-		 * When a BufferAccessStrategy is in use, blocks evicted from shared
-		 * buffers are counted as IOOP_EVICT in the corresponding context
-		 * (e.g. IOCONTEXT_BULKWRITE). Shared buffers are evicted by a
-		 * strategy in two cases: 1) while initially claiming buffers for the
-		 * strategy ring 2) to replace an existing strategy ring buffer
-		 * because it is pinned or in use and cannot be reused.
+		 * Blocks evicted from shared buffers are counted as IOOP_EVICT.
 		 *
-		 * Blocks evicted from buffers already in the strategy ring are
-		 * counted as IOOP_REUSE in the corresponding strategy context.
-		 *
-		 * At this point, we can accurately count evictions and reuses,
-		 * because we have successfully claimed the valid buffer. Previously,
-		 * we may have been forced to release the buffer due to concurrent
-		 * pinners or erroring out.
+		 * At this point, we can accurately count evictions, because we have
+		 * successfully claimed the valid buffer. Previously, we may have been
+		 * forced to release the buffer due to concurrent pinners or erroring
+		 * out.
 		 */
 		pgstat_count_io_op(IOOBJECT_RELATION, io_context,
-						   from_ring ? IOOP_REUSE : IOOP_EVICT, 1, 0);
+						   IOOP_EVICT, 1, 0);
 	}
 
 	/*
@@ -2754,7 +2709,6 @@ LimitAdditionalPins(uint32 *additional_pins)
 static BlockNumber
 ExtendBufferedRelCommon(BufferManagerRelation bmr,
 						ForkNumber fork,
-						BufferAccessStrategy strategy,
 						uint32 flags,
 						uint32 extend_by,
 						BlockNumber extend_upto,
@@ -2789,7 +2743,7 @@ ExtendBufferedRelCommon(BufferManagerRelation bmr,
 											 buffers, &extend_by);
 	}
 	else
-		first_block = ExtendBufferedRelShared(bmr, fork, strategy, flags,
+		first_block = ExtendBufferedRelShared(bmr, fork, flags,
 											  extend_by, extend_upto,
 											  buffers, &extend_by);
 	*extended_by = extend_by;
@@ -2812,7 +2766,6 @@ ExtendBufferedRelCommon(BufferManagerRelation bmr,
 static BlockNumber
 ExtendBufferedRelShared(BufferManagerRelation bmr,
 						ForkNumber fork,
-						BufferAccessStrategy strategy,
 						uint32 flags,
 						uint32 extend_by,
 						BlockNumber extend_upto,
@@ -2820,7 +2773,7 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 						uint32 *extended_by)
 {
 	BlockNumber first_block;
-	IOContext	io_context = IOContextForStrategy(strategy);
+	IOContext	io_context = IOCONTEXT_NORMAL;
 	instr_time	io_start;
 
 	LimitAdditionalPins(&extend_by);
@@ -2839,7 +2792,7 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 	{
 		Block		buf_block;
 
-		buffers[i] = GetVictimBuffer(strategy, io_context);
+		buffers[i] = GetVictimBuffer(io_context);
 		buf_block = BufHdrGetBlock(GetBufferDescriptor(buffers[i] - 1));
 
 		/* new buffers are zero-filled */
@@ -2957,7 +2910,7 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 			 * Pin the existing buffer before releasing the partition lock,
 			 * preventing it from being evicted.
 			 */
-			valid = PinBuffer(existing_hdr, strategy, false);
+			valid = PinBuffer(existing_hdr, false);
 
 			LWLockRelease(partition_lock);
 			UnpinBuffer(victim_buf_hdr);
@@ -3007,8 +2960,11 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 			victim_buf_hdr->tag = tag;
 
 			set_bits |= BM_TAG_VALID;
-			/* Admit COOL (probation); see the comment at the other admission
-			 * site and the cooling-state notes in buf_internals.h. */
+
+			/*
+			 * Admit COOL (probation); see the comment at the other admission
+			 * site and the cooling-state notes in buf_internals.h.
+			 */
 			if (bmr.relpersistence == RELPERSISTENCE_PERMANENT || fork == INIT_FORKNUM)
 				set_bits |= BM_PERMANENT;
 
@@ -3275,13 +3231,8 @@ ReleaseAndReadBuffer(Buffer buffer,
 /*
  * PinBuffer -- make buffer unavailable for replacement.
  *
- * For the default access strategy, the buffer's usage_count is incremented
- * when we first pin it; for other strategies we just make sure the usage_count
- * isn't zero.  (The idea of the latter is that we don't want synchronized
- * heap scans to inflate the count, but we need it to not be zero to discourage
- * other backends from stealing buffers from our ring.  As long as we cycle
- * through the ring faster than the global clock-sweep cycles, buffers in
- * our ring won't be chosen as victims for replacement by other backends.)
+ * The buffer's cooling state is promoted to HOT (the 2Q rescue) when we pin
+ * it; see the cooling-state notes in buf_internals.h.
  *
  * This should be applied only to shared buffers, never local ones.
  *
@@ -3298,7 +3249,7 @@ ReleaseAndReadBuffer(Buffer buffer,
  * (recently) invalid and has not been pinned.
  */
 static bool
-PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy,
+PinBuffer(BufferDesc *buf,
 		  bool skip_if_not_valid)
 {
 	Buffer		b = BufferDescriptorGetBuffer(buf);
@@ -3396,7 +3347,7 @@ PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy,
  * ResourceOwnerEnlarge(CurrentResourceOwner);
  *
  * Currently, no callers of this function want to modify the buffer's
- * usage_count at all, so there's no need for a strategy parameter.
+ * cooling state at all.
  * Also we don't bother with a BM_VALID test (the caller could check that for
  * itself).
  *
@@ -4659,22 +4610,8 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object,
 			  false);
 
 	/*
-	 * When a strategy is in use, only flushes of dirty buffers already in the
-	 * strategy ring are counted as strategy writes (IOCONTEXT
-	 * [BULKREAD|BULKWRITE|VACUUM] IOOP_WRITE) for the purpose of IO
-	 * statistics tracking.
-	 *
-	 * If a shared buffer initially added to the ring must be flushed before
-	 * being used, this is counted as an IOCONTEXT_NORMAL IOOP_WRITE.
-	 *
-	 * If a shared buffer which was added to the ring later because the
-	 * current strategy buffer is pinned or in use or because all strategy
-	 * buffers were dirty and rejected (for BAS_BULKREAD operations only)
-	 * requires flushing, this is counted as an IOCONTEXT_NORMAL IOOP_WRITE
-	 * (from_ring will be false).
-	 *
-	 * When a strategy is not in use, the write can only be a "regular" write
-	 * of a dirty shared buffer (IOCONTEXT_NORMAL IOOP_WRITE).
+	 * All writes of a dirty shared buffer are counted as an IOCONTEXT_NORMAL
+	 * IOOP_WRITE for the purpose of IO statistics tracking.
 	 */
 	pgstat_count_io_op_time(io_object, io_context,
 							IOOP_WRITE, io_start, 1, BLCKSZ);
@@ -5435,8 +5372,6 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 	BlockNumber nblocks;
 	BlockNumber blkno;
 	PGIOAlignedBlock buf;
-	BufferAccessStrategy bstrategy_src;
-	BufferAccessStrategy bstrategy_dst;
 	BlockRangeReadStreamPrivate p;
 	ReadStream *src_stream;
 	SMgrRelation src_smgr;
@@ -5464,10 +5399,6 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 	smgrextend(smgropen(dstlocator, INVALID_PROC_NUMBER), forkNum, nblocks - 1,
 			   buf.data, true);
 
-	/* This is a bulk operation, so use buffer access strategies. */
-	bstrategy_src = GetAccessStrategy(BAS_BULKREAD);
-	bstrategy_dst = GetAccessStrategy(BAS_BULKWRITE);
-
 	/* Initialize streaming read */
 	p.current_blocknum = 0;
 	p.last_exclusive = nblocks;
@@ -5479,7 +5410,6 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 	 */
 	src_stream = read_stream_begin_smgr_relation(READ_STREAM_FULL |
 												 READ_STREAM_USE_BATCHING,
-												 bstrategy_src,
 												 src_smgr,
 												 permanent ? RELPERSISTENCE_PERMANENT : RELPERSISTENCE_UNLOGGED,
 												 forkNum,
@@ -5499,7 +5429,7 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 
 		dstBuf = ReadBufferWithoutRelcache(dstlocator, forkNum,
 										   BufferGetBlockNumber(srcBuf),
-										   RBM_ZERO_AND_LOCK, bstrategy_dst,
+										   RBM_ZERO_AND_LOCK,
 										   permanent);
 		dstPage = BufferGetPage(dstBuf);
 
@@ -5520,9 +5450,6 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 	}
 	Assert(read_stream_next_buffer(src_stream, NULL) == InvalidBuffer);
 	read_stream_end(src_stream);
-
-	FreeAccessStrategy(bstrategy_src);
-	FreeAccessStrategy(bstrategy_dst);
 }
 
 /* ---------------------------------------------------------------------
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index cc77144224c..63b2aa9b607 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -77,40 +77,6 @@ const ShmemCallbacks StrategyCtlShmemCallbacks = {
 	.init_fn = StrategyCtlShmemInit,
 };
 
-/*
- * Private (non-shared) state for managing a ring of shared buffers to re-use.
- * This is currently the only kind of BufferAccessStrategy object, but someday
- * we might have more kinds.
- */
-typedef struct BufferAccessStrategyData
-{
-	/* Overall strategy type */
-	BufferAccessStrategyType btype;
-	/* Number of elements in buffers[] array */
-	int			nbuffers;
-
-	/*
-	 * Index of the "current" slot in the ring, ie, the one most recently
-	 * returned by GetBufferFromRing.
-	 */
-	int			current;
-
-	/*
-	 * Array of buffer numbers.  InvalidBuffer (that is, zero) indicates we
-	 * have not yet selected a buffer for this ring slot.  For allocation
-	 * simplicity this is palloc'd together with the fixed fields of the
-	 * struct.
-	 */
-	Buffer		buffers[FLEXIBLE_ARRAY_MEMBER];
-}			BufferAccessStrategyData;
-
-
-/* Prototypes for internal functions */
-static BufferDesc *GetBufferFromRing(BufferAccessStrategy strategy,
-									 uint64 *buf_state);
-static void AddBufferToRing(BufferAccessStrategy strategy,
-							BufferDesc *buf);
-
 /*
  * Per-backend state for the batched clock sweep.  Each backend claims a run
  * of consecutive clock-hand values with a single atomic fetch-add and then
@@ -157,8 +123,8 @@ ClockSweepTick(void)
 			 * must hold the spinlock so StrategySyncStart() can read
 			 * nextVictimBuffer and completePasses consistently.
 			 *
-			 * With batching, multiple backends may each land a fetch-add
-			 * that returns a value past NBuffers in the same pass.  After
+			 * With batching, multiple backends may each land a fetch-add that
+			 * returns a value past NBuffers in the same pass.  After
 			 * acquiring the spinlock we re-read the counter: if another
 			 * backend already wrapped it below NBuffers we are done.
 			 */
@@ -196,8 +162,6 @@ ClockSweepTick(void)
  *	GetVictimBuffer(). The only hard requirement GetVictimBuffer() has is that
  *	the selected buffer must not currently be pinned by anyone.
  *
- *	strategy is a BufferAccessStrategy object, or NULL for default strategy.
- *
  *	It is the callers responsibility to ensure the buffer ownership can be
  *	tracked via TrackNewBufferPin().
  *
@@ -205,29 +169,13 @@ ClockSweepTick(void)
  *	before returning.
  */
 BufferDesc *
-StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_ring)
+StrategyGetBuffer(uint64 *buf_state)
 {
 	BufferDesc *buf;
 	int			bgwprocno;
 	int			trycounter;
 	bool		force_cool;
 
-	*from_ring = false;
-
-	/*
-	 * If given a strategy object, see whether it can select a buffer. We
-	 * assume strategy objects don't need buffer_strategy_lock.
-	 */
-	if (strategy != NULL)
-	{
-		buf = GetBufferFromRing(strategy, buf_state);
-		if (buf != NULL)
-		{
-			*from_ring = true;
-			return buf;
-		}
-	}
-
 	/*
 	 * If asked, we need to waken the bgwriter. Since we don't want to rely on
 	 * a spinlock for this we force a read from shared memory once, and then
@@ -256,8 +204,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 
 	/*
 	 * We count buffer allocation requests so that the bgwriter can estimate
-	 * the rate of buffer consumption.  Note that buffers recycled by a
-	 * strategy object are intentionally not counted here.
+	 * the rate of buffer consumption.
 	 */
 	pg_atomic_fetch_add_u32(&StrategyControl->numBufferAllocs, 1);
 
@@ -323,7 +270,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 				if (!force_cool)
 				{
 					no_progress = true;
-					break;			/* advance the hand, look for COOL */
+					break;		/* advance the hand, look for COOL */
 				}
 
 				local_buf_state &= ~BUF_USAGECOUNT_MASK;	/* HOT -> COOL, clear ref bit */
@@ -356,8 +303,6 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 												   local_buf_state))
 				{
 					/* Found a usable buffer */
-					if (strategy != NULL)
-						AddBufferToRing(strategy, buf);
 					*buf_state = local_buf_state;
 
 					TrackNewBufferPin(BufferDescriptorGetBuffer(buf));
@@ -368,11 +313,12 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 		}
 
 		/*
-		 * A tick that made no progress toward a victim counts down trycounter.
-		 * A full unproductive pass escalates to force_cool (cool HOT buffers
-		 * into victims); a second unproductive full pass means everything is
-		 * pinned, so fail rather than spin forever.  (A failed CAS above is
-		 * neither progress nor a full miss: we simply retry the same buffer.)
+		 * A tick that made no progress toward a victim counts down
+		 * trycounter. A full unproductive pass escalates to force_cool (cool
+		 * HOT buffers into victims); a second unproductive full pass means
+		 * everything is pinned, so fail rather than spin forever.  (A failed
+		 * CAS above is neither progress nor a full miss: we simply retry the
+		 * same buffer.)
 		 */
 		if (no_progress && --trycounter == 0)
 		{
@@ -500,361 +446,3 @@ StrategyCtlShmemInit(void *arg)
 	else
 		StrategyControl->batchSize = 1;
 }
-
-
-/* ----------------------------------------------------------------
- *				Backend-private buffer ring management
- * ----------------------------------------------------------------
- */
-
-
-/*
- * GetAccessStrategy -- create a BufferAccessStrategy object
- *
- * The object is allocated in the current memory context.
- */
-BufferAccessStrategy
-GetAccessStrategy(BufferAccessStrategyType btype)
-{
-	int			ring_size_kb;
-
-	/*
-	 * Select ring size to use.  See buffer/README for rationales.
-	 *
-	 * Note: if you change the ring size for BAS_BULKREAD, see also
-	 * SYNC_SCAN_REPORT_INTERVAL in access/heap/syncscan.c.
-	 */
-	switch (btype)
-	{
-		case BAS_NORMAL:
-			/* if someone asks for NORMAL, just give 'em a "default" object */
-			return NULL;
-
-		case BAS_BULKREAD:
-			{
-				int			ring_max_kb;
-
-				/*
-				 * The ring always needs to be large enough to allow some
-				 * separation in time between providing a buffer to the user
-				 * of the strategy and that buffer being reused. Otherwise the
-				 * user's pin will prevent reuse of the buffer, even without
-				 * concurrent activity.
-				 *
-				 * We also need to ensure the ring always is large enough for
-				 * SYNC_SCAN_REPORT_INTERVAL, as noted above.
-				 *
-				 * Thus we start out a minimal size and increase the size
-				 * further if appropriate.
-				 */
-				ring_size_kb = 256;
-
-				/*
-				 * There's no point in a larger ring if we won't be allowed to
-				 * pin sufficiently many buffers.  But we never limit to less
-				 * than the minimal size above.
-				 */
-				ring_max_kb = GetPinLimit() * (BLCKSZ / 1024);
-				ring_max_kb = Max(ring_size_kb, ring_max_kb);
-
-				/*
-				 * We would like the ring to additionally have space for the
-				 * configured degree of IO concurrency. While being read in,
-				 * buffers can obviously not yet be reused.
-				 *
-				 * Each IO can be up to io_combine_limit blocks large, and we
-				 * want to start up to effective_io_concurrency IOs.
-				 *
-				 * Note that effective_io_concurrency may be 0, which disables
-				 * AIO.
-				 */
-				ring_size_kb += (BLCKSZ / 1024) *
-					io_combine_limit * effective_io_concurrency;
-
-				if (ring_size_kb > ring_max_kb)
-					ring_size_kb = ring_max_kb;
-				break;
-			}
-		case BAS_BULKWRITE:
-			ring_size_kb = 16 * 1024;
-			break;
-		case BAS_VACUUM:
-			ring_size_kb = 2048;
-			break;
-
-		default:
-			elog(ERROR, "unrecognized buffer access strategy: %d",
-				 (int) btype);
-			return NULL;		/* keep compiler quiet */
-	}
-
-	return GetAccessStrategyWithSize(btype, ring_size_kb);
-}
-
-/*
- * GetAccessStrategyWithSize -- create a BufferAccessStrategy object with a
- *		number of buffers equivalent to the passed in size.
- *
- * If the given ring size is 0, no BufferAccessStrategy will be created and
- * the function will return NULL.  ring_size_kb must not be negative.
- */
-BufferAccessStrategy
-GetAccessStrategyWithSize(BufferAccessStrategyType btype, int ring_size_kb)
-{
-	int			ring_buffers;
-	BufferAccessStrategy strategy;
-
-	Assert(ring_size_kb >= 0);
-
-	/* Figure out how many buffers ring_size_kb is */
-	ring_buffers = ring_size_kb / (BLCKSZ / 1024);
-
-	/* 0 means unlimited, so no BufferAccessStrategy required */
-	if (ring_buffers == 0)
-		return NULL;
-
-	/* Cap to 1/8th of shared_buffers */
-	ring_buffers = Min(NBuffers / 8, ring_buffers);
-
-	/* NBuffers should never be less than 16, so this shouldn't happen */
-	Assert(ring_buffers > 0);
-
-	/* Allocate the object and initialize all elements to zeroes */
-	strategy = (BufferAccessStrategy)
-		palloc0(offsetof(BufferAccessStrategyData, buffers) +
-				ring_buffers * sizeof(Buffer));
-
-	/* Set fields that don't start out zero */
-	strategy->btype = btype;
-	strategy->nbuffers = ring_buffers;
-
-	return strategy;
-}
-
-/*
- * GetAccessStrategyBufferCount -- an accessor for the number of buffers in
- *		the ring
- *
- * Returns 0 on NULL input to match behavior of GetAccessStrategyWithSize()
- * returning NULL with 0 size.
- */
-int
-GetAccessStrategyBufferCount(BufferAccessStrategy strategy)
-{
-	if (strategy == NULL)
-		return 0;
-
-	return strategy->nbuffers;
-}
-
-/*
- * GetAccessStrategyPinLimit -- get cap of number of buffers that should be pinned
- *
- * When pinning extra buffers to look ahead, users of a ring-based strategy are
- * in danger of pinning too much of the ring at once while performing look-ahead.
- * For some strategies, that means "escaping" from the ring, and in others it
- * means forcing dirty data to disk very frequently with associated WAL
- * flushing.  Since external code has no insight into any of that, allow
- * individual strategy types to expose a clamp that should be applied when
- * deciding on a maximum number of buffers to pin at once.
- *
- * Callers should combine this number with other relevant limits and take the
- * minimum.
- */
-int
-GetAccessStrategyPinLimit(BufferAccessStrategy strategy)
-{
-	if (strategy == NULL)
-		return NBuffers;
-
-	switch (strategy->btype)
-	{
-		case BAS_BULKREAD:
-
-			/*
-			 * Since BAS_BULKREAD uses StrategyRejectBuffer(), dirty buffers
-			 * shouldn't be a problem and the caller is free to pin up to the
-			 * entire ring at once.
-			 */
-			return strategy->nbuffers;
-
-		default:
-
-			/*
-			 * Tell caller not to pin more than half the buffers in the ring.
-			 * This is a trade-off between look ahead distance and deferring
-			 * writeback and associated WAL traffic.
-			 */
-			return strategy->nbuffers / 2;
-	}
-}
-
-/*
- * FreeAccessStrategy -- release a BufferAccessStrategy object
- *
- * A simple pfree would do at the moment, but we would prefer that callers
- * don't assume that much about the representation of BufferAccessStrategy.
- */
-void
-FreeAccessStrategy(BufferAccessStrategy strategy)
-{
-	/* don't crash if called on a "default" strategy */
-	if (strategy != NULL)
-		pfree(strategy);
-}
-
-/*
- * GetBufferFromRing -- returns a buffer from the ring, or NULL if the
- *		ring is empty / not usable.
- *
- * The buffer is pinned and marked as owned, using TrackNewBufferPin(), before
- * returning.
- */
-static BufferDesc *
-GetBufferFromRing(BufferAccessStrategy strategy, uint64 *buf_state)
-{
-	BufferDesc *buf;
-	Buffer		bufnum;
-	uint64		old_buf_state;
-	uint64		local_buf_state;	/* to avoid repeated (de-)referencing */
-
-
-	/* Advance to next ring slot */
-	if (++strategy->current >= strategy->nbuffers)
-		strategy->current = 0;
-
-	/*
-	 * If the slot hasn't been filled yet, tell the caller to allocate a new
-	 * buffer with the normal allocation strategy.  He will then fill this
-	 * slot by calling AddBufferToRing with the new buffer.
-	 */
-	bufnum = strategy->buffers[strategy->current];
-	if (bufnum == InvalidBuffer)
-		return NULL;
-
-	buf = GetBufferDescriptor(bufnum - 1);
-
-	/*
-	 * Check whether the buffer can be used and pin it if so. Do this using a
-	 * CAS loop, to avoid having to lock the buffer header.
-	 */
-	old_buf_state = pg_atomic_read_u64(&buf->state);
-	for (;;)
-	{
-		local_buf_state = old_buf_state;
-
-		/*
-		 * If the buffer is pinned we cannot use it under any circumstances.
-		 *
-		 * With the cooling-state replacement the field holds only COOL or HOT,
-		 * so the stock "usage_count > 1 means another backend touched it"
-		 * heuristic no longer applies: a ring element is reusable whenever it
-		 * is unpinned.  (The whole ring mechanism is removed in a later patch;
-		 * scan resistance is now intrinsic to the sweep.)
-		 */
-		if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0)
-			break;
-
-		/* See equivalent code in PinBuffer() */
-		if (unlikely(local_buf_state & BM_LOCKED))
-		{
-			old_buf_state = WaitBufHdrUnlocked(buf);
-			continue;
-		}
-
-		/* pin the buffer if the CAS succeeds */
-		local_buf_state += BUF_REFCOUNT_ONE;
-
-		if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
-										   local_buf_state))
-		{
-			*buf_state = local_buf_state;
-
-			TrackNewBufferPin(BufferDescriptorGetBuffer(buf));
-			return buf;
-		}
-	}
-
-	/*
-	 * Tell caller to allocate a new buffer with the normal allocation
-	 * strategy.  He'll then replace this ring element via AddBufferToRing.
-	 */
-	return NULL;
-}
-
-/*
- * AddBufferToRing -- add a buffer to the buffer ring
- *
- * Caller must hold the buffer header spinlock on the buffer.  Since this
- * is called with the spinlock held, it had better be quite cheap.
- */
-static void
-AddBufferToRing(BufferAccessStrategy strategy, BufferDesc *buf)
-{
-	strategy->buffers[strategy->current] = BufferDescriptorGetBuffer(buf);
-}
-
-/*
- * Utility function returning the IOContext of a given BufferAccessStrategy's
- * strategy ring.
- */
-IOContext
-IOContextForStrategy(BufferAccessStrategy strategy)
-{
-	if (!strategy)
-		return IOCONTEXT_NORMAL;
-
-	switch (strategy->btype)
-	{
-		case BAS_NORMAL:
-
-			/*
-			 * Currently, GetAccessStrategy() returns NULL for
-			 * BufferAccessStrategyType BAS_NORMAL, so this case is
-			 * unreachable.
-			 */
-			pg_unreachable();
-			return IOCONTEXT_NORMAL;
-		case BAS_BULKREAD:
-			return IOCONTEXT_BULKREAD;
-		case BAS_BULKWRITE:
-			return IOCONTEXT_BULKWRITE;
-		case BAS_VACUUM:
-			return IOCONTEXT_VACUUM;
-	}
-
-	elog(ERROR, "unrecognized BufferAccessStrategyType: %d", strategy->btype);
-	pg_unreachable();
-}
-
-/*
- * StrategyRejectBuffer -- consider rejecting a dirty buffer
- *
- * When a nondefault strategy is used, the buffer manager calls this function
- * when it turns out that the buffer selected by StrategyGetBuffer needs to
- * be written out and doing so would require flushing WAL too.  This gives us
- * a chance to choose a different victim.
- *
- * Returns true if buffer manager should ask for a new victim, and false
- * if this buffer should be written and re-used.
- */
-bool
-StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf, bool from_ring)
-{
-	/* We only do this in bulkread mode */
-	if (strategy->btype != BAS_BULKREAD)
-		return false;
-
-	/* Don't muck with behavior of normal buffer-replacement strategy */
-	if (!from_ring ||
-		strategy->buffers[strategy->current] != BufferDescriptorGetBuffer(buf))
-		return false;
-
-	/*
-	 * Remove the dirty buffer from the ring; necessary to prevent infinite
-	 * loop if all ring members are dirty.
-	 */
-	strategy->buffers[strategy->current] = InvalidBuffer;
-
-	return true;
-}
diff --git a/src/backend/storage/freespace/freespace.c b/src/backend/storage/freespace/freespace.c
index 006edab9d77..29cb2d70f66 100644
--- a/src/backend/storage/freespace/freespace.c
+++ b/src/backend/storage/freespace/freespace.c
@@ -603,7 +603,7 @@ fsm_readbuf(Relation rel, FSMAddress addr, bool extend)
 			return InvalidBuffer;
 	}
 	else
-		buf = ReadBufferExtended(rel, FSM_FORKNUM, blkno, RBM_ZERO_ON_ERROR, NULL);
+		buf = ReadBufferExtended(rel, FSM_FORKNUM, blkno, RBM_ZERO_ON_ERROR);
 
 	/*
 	 * Initializing the page when needed is trickier than it looks, because of
@@ -638,7 +638,7 @@ fsm_readbuf(Relation rel, FSMAddress addr, bool extend)
 static Buffer
 fsm_extend(Relation rel, BlockNumber fsm_nblocks)
 {
-	return ExtendBufferedRelTo(BMR_REL(rel), FSM_FORKNUM, NULL,
+	return ExtendBufferedRelTo(BMR_REL(rel), FSM_FORKNUM,
 							   EB_CREATE_FORK_IF_NEEDED |
 							   EB_CLEAR_SIZE_CACHE,
 							   fsm_nblocks,
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 718c1cfc0f9..3c3e35ab8a8 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -1540,15 +1540,8 @@ register_dirty_segment(SMgrRelation reln, ForkNumber forknum, MdfdVec *seg)
 							FilePathName(seg->mdfd_vfd))));
 
 		/*
-		 * We have no way of knowing if the current IOContext is
-		 * IOCONTEXT_NORMAL or IOCONTEXT_[BULKREAD, BULKWRITE, VACUUM] at this
-		 * point, so count the fsync as being in the IOCONTEXT_NORMAL
-		 * IOContext. This is probably okay, because the number of backend
-		 * fsyncs doesn't say anything about the efficacy of the
-		 * BufferAccessStrategy. And counting both fsyncs done in
-		 * IOCONTEXT_NORMAL and IOCONTEXT_[BULKREAD, BULKWRITE, VACUUM] under
-		 * IOCONTEXT_NORMAL is likely clearer when investigating the number of
-		 * backend fsyncs.
+		 * Count the fsync in the IOCONTEXT_NORMAL IOContext, the only
+		 * IOContext relations are read/written under.
 		 */
 		pgstat_count_io_op_time(IOOBJECT_RELATION, IOCONTEXT_NORMAL,
 								IOOP_FSYNC, io_start, 1, 0);
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 38bae7b15d2..d66a23749d5 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -241,16 +241,10 @@ pgstat_get_io_context_name(IOContext io_context)
 {
 	switch (io_context)
 	{
-		case IOCONTEXT_BULKREAD:
-			return "bulkread";
-		case IOCONTEXT_BULKWRITE:
-			return "bulkwrite";
 		case IOCONTEXT_INIT:
 			return "init";
 		case IOCONTEXT_NORMAL:
 			return "normal";
-		case IOCONTEXT_VACUUM:
-			return "vacuum";
 	}
 
 	elog(ERROR, "unrecognized IOContext value: %d", io_context);
@@ -451,18 +445,6 @@ pgstat_tracks_io_object(BackendType bktype, IOObject io_object,
 	 * IOContexts, and, while it may not be inherently incorrect for them to
 	 * do so, excluding those rows from the view makes the view easier to use.
 	 */
-	if ((bktype == B_CHECKPOINTER || bktype == B_BG_WRITER) &&
-		(io_context == IOCONTEXT_BULKREAD ||
-		 io_context == IOCONTEXT_BULKWRITE ||
-		 io_context == IOCONTEXT_VACUUM))
-		return false;
-
-	if (bktype == B_AUTOVAC_LAUNCHER && io_context == IOCONTEXT_VACUUM)
-		return false;
-
-	if ((bktype == B_AUTOVAC_WORKER || bktype == B_AUTOVAC_LAUNCHER) &&
-		io_context == IOCONTEXT_BULKWRITE)
-		return false;
 
 	return true;
 }
@@ -479,8 +461,6 @@ bool
 pgstat_tracks_io_op(BackendType bktype, IOObject io_object,
 					IOContext io_context, IOOp io_op)
 {
-	bool		strategy_io_context;
-
 	/* if (io_context, io_object) will never collect stats, we're done */
 	if (!pgstat_tracks_io_object(bktype, io_object, io_context))
 		return false;
@@ -521,17 +501,13 @@ pgstat_tracks_io_op(BackendType bktype, IOObject io_object,
 	/*
 	 * Some IOOps are not valid in certain IOContexts and some IOOps are only
 	 * valid in certain contexts.
+	 *
+	 * IOOP_REUSE was only relevant when a BufferAccessStrategy was in use.
+	 * Buffer access strategies (ring buffers) have been removed -- scan
+	 * resistance is now intrinsic to the cooling-stage clock sweep -- so
+	 * IOOP_REUSE never occurs.
 	 */
-	if (io_context == IOCONTEXT_BULKREAD && io_op == IOOP_EXTEND)
-		return false;
-
-	strategy_io_context = io_context == IOCONTEXT_BULKREAD ||
-		io_context == IOCONTEXT_BULKWRITE || io_context == IOCONTEXT_VACUUM;
-
-	/*
-	 * IOOP_REUSE is only relevant when a BufferAccessStrategy is in use.
-	 */
-	if (!strategy_io_context && io_op == IOOP_REUSE)
+	if (io_op == IOOP_REUSE)
 		return false;
 
 	/*
@@ -545,14 +521,5 @@ pgstat_tracks_io_op(BackendType bktype, IOObject io_object,
 		!(io_op == IOOP_WRITE || io_op == IOOP_READ || io_op == IOOP_FSYNC))
 		return false;
 
-	/*
-	 * IOOP_FSYNC IOOps done by a backend using a BufferAccessStrategy are
-	 * counted in the IOCONTEXT_NORMAL IOContext. See comment in
-	 * register_dirty_segment() for more details.
-	 */
-	if (strategy_io_context && io_op == IOOP_FSYNC)
-		return false;
-
-
 	return true;
 }
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index bbd28d14d99..555c04272f5 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -149,7 +149,6 @@ int			autovacuum_max_parallel_workers = 0;
 int			MaxBackends = 0;
 
 /* GUC parameters for vacuum */
-int			VacuumBufferUsageLimit = 2048;
 
 int			VacuumCostPageHit = 1;
 int			VacuumCostPageMiss = 2;
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index dd799a5e70f..bcde0c1ee3e 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -3326,16 +3326,6 @@
   boot_val => 'DEFAULT_UPDATE_PROCESS_TITLE',
 },
 
-{ name => 'vacuum_buffer_usage_limit', type => 'int', context => 'PGC_USERSET', group => 'RESOURCES_MEM',
-  short_desc => 'Sets the buffer pool size for VACUUM, ANALYZE, and autovacuum.',
-  flags => 'GUC_UNIT_KB',
-  variable => 'VacuumBufferUsageLimit',
-  boot_val => '2048',
-  min => '0',
-  max => 'MAX_BAS_VAC_RING_SIZE_KB',
-  check_hook => 'check_vacuum_buffer_usage_limit',
-},
-
 { name => 'vacuum_cost_delay', type => 'real', context => 'PGC_USERSET', group => 'VACUUM_COST_DELAY',
   short_desc => 'Vacuum cost delay in milliseconds.',
   flags => 'GUC_UNIT_MS',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 47e1221f3c3..ba393a1fe28 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -164,9 +164,6 @@
                                         #   mmap
                                         # (change requires restart)
 #min_dynamic_shared_memory = 0MB        # (change requires restart)
-#vacuum_buffer_usage_limit = 2MB        # size of vacuum and analyze buffer access strategy ring;
-                                        # 0 to disable vacuum buffer access strategy;
-                                        # range 128kB to 16GB
 
 # SLRU buffers (change requires restart)
 #commit_timestamp_buffers = 0           # memory for pg_commit_ts (0 = auto)
diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index f8158ca6a78..f78e57ff2c3 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -57,7 +57,6 @@ main(int argc, char *argv[])
 		{"no-truncate", no_argument, NULL, 10},
 		{"no-process-toast", no_argument, NULL, 11},
 		{"no-process-main", no_argument, NULL, 12},
-		{"buffer-usage-limit", required_argument, NULL, 13},
 		{"missing-stats-only", no_argument, NULL, 14},
 		{"dry-run", no_argument, NULL, 15},
 		{NULL, 0, NULL, 0}
@@ -202,9 +201,6 @@ main(int argc, char *argv[])
 			case 12:
 				vacopts.process_main = false;
 				break;
-			case 13:
-				vacopts.buffer_usage_limit = escape_quotes(optarg);
-				break;
 			case 14:
 				vacopts.missing_stats_only = true;
 				break;
@@ -290,14 +286,6 @@ main(int argc, char *argv[])
 		pg_fatal("cannot use the \"%s\" option with the \"%s\" option",
 				 "no-index-cleanup", "force-index-cleanup");
 
-	/*
-	 * buffer-usage-limit is not allowed with VACUUM FULL unless ANALYZE is
-	 * included too.
-	 */
-	if (vacopts.buffer_usage_limit && vacopts.full && !vacopts.and_analyze)
-		pg_fatal("cannot use the \"%s\" option with the \"%s\" option",
-				 "buffer-usage-limit", "full");
-
 	/*
 	 * Prohibit --missing-stats-only without --analyze-only or
 	 * --analyze-in-stages.
@@ -352,7 +340,6 @@ help(const char *progname)
 	printf(_("  %s [OPTION]... [DBNAME]\n"), progname);
 	printf(_("\nOptions:\n"));
 	printf(_("  -a, --all                       vacuum all databases\n"));
-	printf(_("      --buffer-usage-limit=SIZE   size of ring buffer used for vacuum\n"));
 	printf(_("  -d, --dbname=DBNAME             database to vacuum\n"));
 	printf(_("      --disable-page-skipping     disable all page-skipping behavior\n"));
 	printf(_("      --dry-run                   show the commands that would be sent to the server\n"));
diff --git a/src/bin/scripts/vacuuming.c b/src/bin/scripts/vacuuming.c
index 855a5754c98..078a3bbb516 100644
--- a/src/bin/scripts/vacuuming.c
+++ b/src/bin/scripts/vacuuming.c
@@ -264,13 +264,6 @@ vacuum_one_database(ConnParams *cparams,
 				 "--parallel", "13");
 	}
 
-	if (vacopts->buffer_usage_limit && PQserverVersion(conn) < 160000)
-	{
-		PQfinish(conn);
-		pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s",
-				 "--buffer-usage-limit", "16");
-	}
-
 	if (vacopts->missing_stats_only && PQserverVersion(conn) < 150000)
 	{
 		PQfinish(conn);
@@ -865,13 +858,6 @@ prepare_vacuum_command(PGconn *conn, PQExpBuffer sql,
 				appendPQExpBuffer(sql, "%sVERBOSE", sep);
 				sep = comma;
 			}
-			if (vacopts->buffer_usage_limit)
-			{
-				Assert(serverVersion >= 160000);
-				appendPQExpBuffer(sql, "%sBUFFER_USAGE_LIMIT '%s'", sep,
-								  vacopts->buffer_usage_limit);
-				sep = comma;
-			}
 			if (sep != paren)
 				appendPQExpBufferChar(sql, ')');
 		}
@@ -974,13 +960,6 @@ prepare_vacuum_command(PGconn *conn, PQExpBuffer sql,
 								  vacopts->parallel_workers);
 				sep = comma;
 			}
-			if (vacopts->buffer_usage_limit)
-			{
-				Assert(serverVersion >= 160000);
-				appendPQExpBuffer(sql, "%sBUFFER_USAGE_LIMIT '%s'", sep,
-								  vacopts->buffer_usage_limit);
-				sep = comma;
-			}
 			if (sep != paren)
 				appendPQExpBufferChar(sql, ')');
 		}
diff --git a/src/bin/scripts/vacuuming.h b/src/bin/scripts/vacuuming.h
index 5a491db2526..83a2469a6aa 100644
--- a/src/bin/scripts/vacuuming.h
+++ b/src/bin/scripts/vacuuming.h
@@ -49,7 +49,6 @@ typedef struct vacuumingOptions
 	bool		process_main;
 	bool		process_toast;
 	bool		skip_database_stats;
-	char	   *buffer_usage_limit;
 	bool		missing_stats_only;
 	bool		echo;
 	bool		quiet;
diff --git a/src/include/access/genam.h b/src/include/access/genam.h
index 68bfe405db3..bc87f6f7cc0 100644
--- a/src/include/access/genam.h
+++ b/src/include/access/genam.h
@@ -58,7 +58,6 @@ typedef struct IndexVacuumInfo
 	bool		estimated_count;	/* num_heap_tuples is an estimate */
 	int			message_level;	/* ereport level for progress messages */
 	double		num_heap_tuples;	/* tuples remaining in heap */
-	BufferAccessStrategy strategy;	/* access strategy for reads */
 } IndexVacuumInfo;
 
 /*
diff --git a/src/include/access/hash.h b/src/include/access/hash.h
index a8702f0e5ea..7c9268fe953 100644
--- a/src/include/access/hash.h
+++ b/src/include/access/hash.h
@@ -405,12 +405,11 @@ extern void _hash_pgaddmultitup(Relation rel, Buffer buf, IndexTuple *itups,
 extern Buffer _hash_addovflpage(Relation rel, Buffer metabuf, Buffer buf, bool retain_pin);
 extern BlockNumber _hash_freeovflpage(Relation rel, Buffer bucketbuf, Buffer ovflbuf,
 									  Buffer wbuf, IndexTuple *itups, OffsetNumber *itup_offsets,
-									  Size *tups_size, uint16 nitups, BufferAccessStrategy bstrategy);
+									  Size *tups_size, uint16 nitups);
 extern void _hash_initbitmapbuffer(Buffer buf, uint16 bmsize, bool initpage);
 extern void _hash_squeezebucket(Relation rel,
 								Bucket bucket, BlockNumber bucket_blkno,
-								Buffer bucket_buf,
-								BufferAccessStrategy bstrategy);
+								Buffer bucket_buf);
 extern uint32 _hash_ovflblkno_to_bitno(HashMetaPage metap, BlockNumber ovflblkno);
 
 /* hashpage.c */
@@ -428,9 +427,6 @@ extern void _hash_initbuf(Buffer buf, uint32 max_bucket, uint32 num_bucket,
 						  uint32 flag, bool initpage);
 extern Buffer _hash_getnewbuf(Relation rel, BlockNumber blkno,
 							  ForkNumber forkNum);
-extern Buffer _hash_getbuf_with_strategy(Relation rel, BlockNumber blkno,
-										 int access, int flags,
-										 BufferAccessStrategy bstrategy);
 extern void _hash_relbuf(Relation rel, Buffer buf);
 extern void _hash_dropbuf(Relation rel, Buffer buf);
 extern void _hash_dropscanbuf(Relation rel, HashScanOpaque so);
@@ -481,7 +477,6 @@ extern void _hash_kill_items(IndexScanDesc scan);
 /* hash.c */
 extern void hashbucketcleanup(Relation rel, Bucket cur_bucket,
 							  Buffer bucket_buf, BlockNumber bucket_blkno,
-							  BufferAccessStrategy bstrategy,
 							  uint32 maxbucket, uint32 highmask, uint32 lowmask,
 							  double *tuples_removed, double *num_index_tuples,
 							  bool split_cleanup,
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 5176478c295..0967b659794 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -72,8 +72,6 @@ typedef struct HeapScanDescData
 	Buffer		rs_cbuf;		/* current buffer in scan, if any */
 	/* NB: if rs_cbuf is not InvalidBuffer, we hold a pin on that buffer */
 
-	BufferAccessStrategy rs_strategy;	/* access strategy for reads */
-
 	HeapTupleData rs_ctup;		/* current tuple in scan, if any */
 
 	/* For scans that stream reads */
@@ -466,7 +464,7 @@ extern void log_heap_prune_and_freeze(Relation relation, Buffer buffer,
 
 /* in heap/vacuumlazy.c */
 extern void heap_vacuum_rel(Relation rel,
-							const VacuumParams *params, BufferAccessStrategy bstrategy);
+							const VacuumParams *params);
 #ifdef USE_ASSERT_CHECKING
 extern bool heap_page_is_all_visible(Relation rel, Buffer buf,
 									 GlobalVisState *vistest,
diff --git a/src/include/access/hio.h b/src/include/access/hio.h
index 60cfc375fd5..63bfcc884ee 100644
--- a/src/include/access/hio.h
+++ b/src/include/access/hio.h
@@ -28,7 +28,6 @@
  */
 typedef struct BulkInsertStateData
 {
-	BufferAccessStrategy strategy;	/* our BULKWRITE strategy object */
 	Buffer		current_buf;	/* current insertion target page */
 
 	/*
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index f2c36696bca..83ba0f3ca73 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -689,8 +689,7 @@ typedef struct TableAmRoutine
 	 * integrate with autovacuum's scheduling.
 	 */
 	void		(*relation_vacuum) (Relation rel,
-									const VacuumParams *params,
-									BufferAccessStrategy bstrategy);
+									const VacuumParams *params);
 
 	/*
 	 * Prepare to analyze block `blockno` of `scan`. The scan has been started
@@ -1774,10 +1773,9 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
  * routine, even if (for ANALYZE) it is part of the same VACUUM command.
  */
 static inline void
-table_relation_vacuum(Relation rel, const VacuumParams *params,
-					  BufferAccessStrategy bstrategy)
+table_relation_vacuum(Relation rel, const VacuumParams *params)
 {
-	rel->rd_tableam->relation_vacuum(rel, params, bstrategy);
+	rel->rd_tableam->relation_vacuum(rel, params);
 }
 
 /*
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 956d9cea36d..14a9b530bcf 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -363,7 +363,7 @@ extern PGDLLIMPORT int64 parallel_vacuum_worker_delay_ns;
 /* in commands/vacuum.c */
 extern void ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel);
 extern void vacuum(List *relations, const VacuumParams *params,
-				   BufferAccessStrategy bstrategy, MemoryContext vac_context,
+				   MemoryContext vac_context,
 				   bool isTopLevel);
 extern void vac_open_indexes(Relation relation, LOCKMODE lockmode,
 							 int *nindexes, Relation **Irel);
@@ -407,8 +407,7 @@ extern void VacuumUpdateCosts(void);
 /* in commands/vacuumparallel.c */
 extern ParallelVacuumState *parallel_vacuum_init(Relation rel, Relation *indrels,
 												 int nindexes, int nrequested_workers,
-												 int vac_work_mem, int elevel,
-												 BufferAccessStrategy bstrategy);
+												 int vac_work_mem, int elevel);
 extern void parallel_vacuum_end(ParallelVacuumState *pvs, IndexBulkDeleteResult **istats);
 extern TidStore *parallel_vacuum_get_dead_items(ParallelVacuumState *pvs,
 												VacDeadItemsInfo **dead_items_info_p);
@@ -428,8 +427,7 @@ extern void parallel_vacuum_main(dsm_segment *seg, shm_toc *toc);
 
 /* in commands/analyze.c */
 extern void analyze_rel(Oid relid, RangeVar *relation,
-						const VacuumParams *params, List *va_cols, bool in_outer_xact,
-						BufferAccessStrategy bstrategy);
+						const VacuumParams *params, List *va_cols, bool in_outer_xact);
 extern bool attribute_is_analyzable(Relation onerel, int attnum, Form_pg_attribute attr,
 									int *p_attstattarget);
 extern bool std_typanalyze(VacAttrStats *stats);
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 7170a4bff98..c5d582b152b 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -273,15 +273,6 @@ extern PGDLLIMPORT double hash_mem_multiplier;
 extern PGDLLIMPORT int maintenance_work_mem;
 extern PGDLLIMPORT int max_parallel_maintenance_workers;
 
-/*
- * Upper and lower hard limits for the buffer access strategy ring size
- * specified by the VacuumBufferUsageLimit GUC and BUFFER_USAGE_LIMIT option
- * to VACUUM and ANALYZE.
- */
-#define MIN_BAS_VAC_RING_SIZE_KB 128
-#define MAX_BAS_VAC_RING_SIZE_KB (16 * 1024 * 1024)
-
-extern PGDLLIMPORT int VacuumBufferUsageLimit;
 extern PGDLLIMPORT int VacuumCostPageHit;
 extern PGDLLIMPORT int VacuumCostPageMiss;
 extern PGDLLIMPORT int VacuumCostPageDirty;
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..68b4e5093a2 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -287,14 +287,11 @@ typedef enum IOObject
 
 typedef enum IOContext
 {
-	IOCONTEXT_BULKREAD,
-	IOCONTEXT_BULKWRITE,
 	IOCONTEXT_INIT,
 	IOCONTEXT_NORMAL,
-	IOCONTEXT_VACUUM,
 } IOContext;
 
-#define IOCONTEXT_NUM_TYPES (IOCONTEXT_VACUUM + 1)
+#define IOCONTEXT_NUM_TYPES (IOCONTEXT_NORMAL + 1)
 
 /*
  * Enumeration of IO operations.
diff --git a/src/include/storage/buf.h b/src/include/storage/buf.h
index b21445522b1..fa942a64f05 100644
--- a/src/include/storage/buf.h
+++ b/src/include/storage/buf.h
@@ -36,11 +36,4 @@ typedef int Buffer;
  */
 #define BufferIsLocal(buffer)	((buffer) < 0)
 
-/*
- * Buffer access strategy objects.
- *
- * BufferAccessStrategyData is private to freelist.c
- */
-typedef struct BufferAccessStrategyData *BufferAccessStrategy;
-
 #endif							/* BUF_H */
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index 89b11051577..de5139323c0 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -627,11 +627,7 @@ extern void TerminateBufferIO(BufferDesc *buf, bool clear_dirty, uint64 set_flag
 
 
 /* freelist.c */
-extern IOContext IOContextForStrategy(BufferAccessStrategy strategy);
-extern BufferDesc *StrategyGetBuffer(BufferAccessStrategy strategy,
-									 uint64 *buf_state, bool *from_ring);
-extern bool StrategyRejectBuffer(BufferAccessStrategy strategy,
-								 BufferDesc *buf, bool from_ring);
+extern BufferDesc *StrategyGetBuffer(uint64 *buf_state);
 
 extern int	StrategySyncStart(uint32 *complete_passes, uint32 *num_buf_alloc);
 extern void StrategyNotifyBgWriter(int bgwprocno);
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 6837b35fc6d..36023792f36 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -25,21 +25,6 @@
 
 typedef void *Block;
 
-/*
- * Possible arguments for GetAccessStrategy().
- *
- * If adding a new BufferAccessStrategyType, also add a new IOContext so
- * IO statistics using this strategy are tracked.
- */
-typedef enum BufferAccessStrategyType
-{
-	BAS_NORMAL,					/* Normal random access */
-	BAS_BULKREAD,				/* Large read-only scan (hint bit updates are
-								 * ok) */
-	BAS_BULKWRITE,				/* Large multi-block write (e.g. COPY IN) */
-	BAS_VACUUM,					/* VACUUM */
-} BufferAccessStrategyType;
-
 /* Possible modes for ReadBufferExtended() */
 typedef enum
 {
@@ -135,7 +120,6 @@ struct ReadBuffersOperation
 	SMgrRelation smgr;
 	char		persistence;
 	ForkNumber	forknum;
-	BufferAccessStrategy strategy;
 
 	/*
 	 * The following private members are private state for communication
@@ -235,11 +219,10 @@ extern bool ReadRecentBuffer(RelFileLocator rlocator, ForkNumber forkNum,
 							 BlockNumber blockNum, Buffer recent_buffer);
 extern Buffer ReadBuffer(Relation reln, BlockNumber blockNum);
 extern Buffer ReadBufferExtended(Relation reln, ForkNumber forkNum,
-								 BlockNumber blockNum, ReadBufferMode mode,
-								 BufferAccessStrategy strategy);
+								 BlockNumber blockNum, ReadBufferMode mode);
 extern Buffer ReadBufferWithoutRelcache(RelFileLocator rlocator,
 										ForkNumber forkNum, BlockNumber blockNum,
-										ReadBufferMode mode, BufferAccessStrategy strategy,
+										ReadBufferMode mode,
 										bool permanent);
 
 extern bool StartReadBuffer(ReadBuffersOperation *operation,
@@ -266,18 +249,15 @@ extern Buffer ReleaseAndReadBuffer(Buffer buffer, Relation relation,
 
 extern Buffer ExtendBufferedRel(BufferManagerRelation bmr,
 								ForkNumber forkNum,
-								BufferAccessStrategy strategy,
 								uint32 flags);
 extern BlockNumber ExtendBufferedRelBy(BufferManagerRelation bmr,
 									   ForkNumber fork,
-									   BufferAccessStrategy strategy,
 									   uint32 flags,
 									   uint32 extend_by,
 									   Buffer *buffers,
 									   uint32 *extended_by);
 extern Buffer ExtendBufferedRelTo(BufferManagerRelation bmr,
 								  ForkNumber fork,
-								  BufferAccessStrategy strategy,
 								  uint32 flags,
 								  BlockNumber extend_to,
 								  ReadBufferMode mode);
@@ -376,14 +356,6 @@ extern void AtProcExit_LocalBuffers(void);
 
 /* in freelist.c */
 
-extern BufferAccessStrategy GetAccessStrategy(BufferAccessStrategyType btype);
-extern BufferAccessStrategy GetAccessStrategyWithSize(BufferAccessStrategyType btype,
-													  int ring_size_kb);
-extern int	GetAccessStrategyBufferCount(BufferAccessStrategy strategy);
-extern int	GetAccessStrategyPinLimit(BufferAccessStrategy strategy);
-
-extern void FreeAccessStrategy(BufferAccessStrategy strategy);
-
 
 /* inline functions */
 
diff --git a/src/include/storage/read_stream.h b/src/include/storage/read_stream.h
index 48995c6d534..9e5fbbed9e8 100644
--- a/src/include/storage/read_stream.h
+++ b/src/include/storage/read_stream.h
@@ -83,17 +83,14 @@ extern BlockNumber block_range_read_stream_cb(ReadStream *stream,
 											  void *callback_private_data,
 											  void *per_buffer_data);
 extern ReadStream *read_stream_begin_relation(int flags,
-											  BufferAccessStrategy strategy,
 											  Relation rel,
 											  ForkNumber forknum,
 											  ReadStreamBlockNumberCB callback,
 											  void *callback_private_data,
 											  size_t per_buffer_data_size);
 extern Buffer read_stream_next_buffer(ReadStream *stream, void **per_buffer_data);
-extern BlockNumber read_stream_next_block(ReadStream *stream,
-										  BufferAccessStrategy *strategy);
+extern BlockNumber read_stream_next_block(ReadStream *stream);
 extern ReadStream *read_stream_begin_smgr_relation(int flags,
-												   BufferAccessStrategy strategy,
 												   SMgrRelation smgr,
 												   char smgr_persistence,
 												   ForkNumber forknum,
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 307f4fbaefe..3f28558d47c 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -31,8 +31,6 @@ extern void assign_application_name(const char *newval, void *extra);
 extern const char *show_archive_command(void);
 extern bool check_autovacuum_work_mem(int *newval, void **extra,
 									  GucSource source);
-extern bool check_vacuum_buffer_usage_limit(int *newval, void **extra,
-											GucSource source);
 extern bool check_backtrace_functions(char **newval, void **extra,
 									  GucSource source);
 extern void assign_backtrace_functions(const char *newval, void *extra);
diff --git a/src/test/modules/test_aio/test_aio.c b/src/test/modules/test_aio/test_aio.c
index 35efba1a5e3..f60153f801f 100644
--- a/src/test/modules/test_aio/test_aio.c
+++ b/src/test/modules/test_aio/test_aio.c
@@ -194,7 +194,6 @@ grow_rel(PG_FUNCTION_ARGS)
 
 		ExtendBufferedRelBy(BMR_REL(rel),
 							MAIN_FORKNUM,
-							NULL,
 							0,
 							extend_by_pages,
 							victim_buffers,
@@ -231,7 +230,7 @@ modify_rel_block(PG_FUNCTION_ARGS)
 	rel = relation_open(relid, AccessExclusiveLock);
 
 	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno,
-							 RBM_ZERO_ON_ERROR, NULL);
+							 RBM_ZERO_ON_ERROR);
 
 	LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
 
@@ -331,7 +330,7 @@ create_toy_buffer(Relation rel, BlockNumber blkno)
 	uint64		unset_bits = 0;
 
 	/* place buffer in shared buffers without erroring out */
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_ZERO_AND_LOCK, NULL);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_ZERO_AND_LOCK);
 	LockBuffer(buf, BUFFER_LOCK_UNLOCK);
 
 	if (RelationUsesLocalBuffers(rel))
@@ -737,7 +736,6 @@ read_buffers(PG_FUNCTION_ARGS)
 		operation->rel = rel;
 		operation->smgr = smgr;
 		operation->persistence = rel->rd_rel->relpersistence;
-		operation->strategy = NULL;
 		operation->forknum = MAIN_FORKNUM;
 
 		io_reqds[nios] = StartReadBuffers(operation,
@@ -879,7 +877,6 @@ read_stream_for_blocks(PG_FUNCTION_ARGS)
 	rel = relation_open(relid, AccessShareLock);
 
 	stream = read_stream_begin_relation(READ_STREAM_FULL,
-										NULL,
 										rel,
 										MAIN_FORKNUM,
 										read_stream_for_blocks_cb,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index 03cbc1cdef5..e4b9de0b4a8 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -16,22 +16,16 @@ SHOW track_counts;  -- must be on
 SELECT backend_type, object, context FROM pg_stat_io
   ORDER BY backend_type COLLATE "C", object COLLATE "C", context COLLATE "C";
 backend_type|object|context
-autovacuum launcher|relation|bulkread
 autovacuum launcher|relation|init
 autovacuum launcher|relation|normal
 autovacuum launcher|wal|init
 autovacuum launcher|wal|normal
-autovacuum worker|relation|bulkread
 autovacuum worker|relation|init
 autovacuum worker|relation|normal
-autovacuum worker|relation|vacuum
 autovacuum worker|wal|init
 autovacuum worker|wal|normal
-background worker|relation|bulkread
-background worker|relation|bulkwrite
 background worker|relation|init
 background worker|relation|normal
-background worker|relation|vacuum
 background worker|temp relation|normal
 background worker|wal|init
 background worker|wal|normal
@@ -43,67 +37,43 @@ checkpointer|relation|init
 checkpointer|relation|normal
 checkpointer|wal|init
 checkpointer|wal|normal
-client backend|relation|bulkread
-client backend|relation|bulkwrite
 client backend|relation|init
 client backend|relation|normal
-client backend|relation|vacuum
 client backend|temp relation|normal
 client backend|wal|init
 client backend|wal|normal
-datachecksums launcher|relation|bulkread
-datachecksums launcher|relation|bulkwrite
 datachecksums launcher|relation|init
 datachecksums launcher|relation|normal
-datachecksums launcher|relation|vacuum
 datachecksums launcher|temp relation|normal
 datachecksums launcher|wal|init
 datachecksums launcher|wal|normal
-datachecksums worker|relation|bulkread
-datachecksums worker|relation|bulkwrite
 datachecksums worker|relation|init
 datachecksums worker|relation|normal
-datachecksums worker|relation|vacuum
 datachecksums worker|temp relation|normal
 datachecksums worker|wal|init
 datachecksums worker|wal|normal
-io worker|relation|bulkread
-io worker|relation|bulkwrite
 io worker|relation|init
 io worker|relation|normal
-io worker|relation|vacuum
 io worker|temp relation|normal
 io worker|wal|init
 io worker|wal|normal
-slotsync worker|relation|bulkread
-slotsync worker|relation|bulkwrite
 slotsync worker|relation|init
 slotsync worker|relation|normal
-slotsync worker|relation|vacuum
 slotsync worker|temp relation|normal
 slotsync worker|wal|init
 slotsync worker|wal|normal
-standalone backend|relation|bulkread
-standalone backend|relation|bulkwrite
 standalone backend|relation|init
 standalone backend|relation|normal
-standalone backend|relation|vacuum
 standalone backend|wal|init
 standalone backend|wal|normal
-startup|relation|bulkread
-startup|relation|bulkwrite
 startup|relation|init
 startup|relation|normal
-startup|relation|vacuum
 startup|wal|init
 startup|wal|normal
 walreceiver|wal|init
 walreceiver|wal|normal
-walsender|relation|bulkread
-walsender|relation|bulkwrite
 walsender|relation|init
 walsender|relation|normal
-walsender|relation|vacuum
 walsender|temp relation|normal
 walsender|wal|init
 walsender|wal|normal
@@ -111,7 +81,7 @@ walsummarizer|wal|init
 walsummarizer|wal|normal
 walwriter|wal|init
 walwriter|wal|normal
-(95 rows)
+(65 rows)
 \a
 -- List of registered statistics kinds.
 SELECT id, name, fixed_amount,
@@ -1757,70 +1727,6 @@ SELECT :io_sum_local_new_tblspc_writes > :io_sum_local_after_writes;
 (1 row)
 
 RESET temp_buffers;
--- Test that reuse of strategy buffers and reads of blocks into these reused
--- buffers while VACUUMing are tracked in pg_stat_io. If there is sufficient
--- demand for shared buffers from concurrent queries, some buffers may be
--- pinned by other backends before they can be reused. In such cases, the
--- backend will evict a buffer from outside the ring and add it to the
--- ring. This is considered an eviction and not a reuse.
--- Set wal_skip_threshold smaller than the expected size of
--- test_io_vac_strategy so that, even if wal_level is minimal, VACUUM FULL will
--- fsync the newly rewritten test_io_vac_strategy instead of writing it to WAL.
--- Writing it to WAL will result in the newly written relation pages being in
--- shared buffers -- preventing us from testing BAS_VACUUM BufferAccessStrategy
--- reads.
-SET wal_skip_threshold = '1 kB';
-SELECT sum(reuses) AS reuses, sum(reads) AS reads, sum(evictions) AS evictions
-  FROM pg_stat_io WHERE context = 'vacuum' \gset io_sum_vac_strategy_before_
-CREATE TABLE test_io_vac_strategy(a int, b int) WITH (autovacuum_enabled = 'false');
-INSERT INTO test_io_vac_strategy SELECT i, i from generate_series(1, 4500)i;
--- Ensure that the next VACUUM will need to perform IO by rewriting the table
--- first with VACUUM (FULL).
-VACUUM (FULL) test_io_vac_strategy;
--- Use the minimum BUFFER_USAGE_LIMIT to cause reuses or evictions with the
--- smallest table possible.
-VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) test_io_vac_strategy;
-SELECT pg_stat_force_next_flush();
- pg_stat_force_next_flush 
---------------------------
- 
-(1 row)
-
-SELECT sum(reuses) AS reuses, sum(reads) AS reads, sum(evictions) AS evictions
-  FROM pg_stat_io WHERE context = 'vacuum' \gset io_sum_vac_strategy_after_
-SELECT :io_sum_vac_strategy_after_reads > :io_sum_vac_strategy_before_reads;
- ?column? 
-----------
- t
-(1 row)
-
-SELECT (:io_sum_vac_strategy_after_reuses + :io_sum_vac_strategy_after_evictions) >
-  (:io_sum_vac_strategy_before_reuses + :io_sum_vac_strategy_before_evictions);
- ?column? 
-----------
- t
-(1 row)
-
-RESET wal_skip_threshold;
--- Test that extends done by a CTAS, which uses a BAS_BULKWRITE
--- BufferAccessStrategy, are tracked in pg_stat_io.
-SELECT sum(extends) AS io_sum_bulkwrite_strategy_extends_before
-  FROM pg_stat_io WHERE context = 'bulkwrite' \gset
-CREATE TABLE test_io_bulkwrite_strategy AS SELECT i FROM generate_series(1,100)i;
-SELECT pg_stat_force_next_flush();
- pg_stat_force_next_flush 
---------------------------
- 
-(1 row)
-
-SELECT sum(extends) AS io_sum_bulkwrite_strategy_extends_after
-  FROM pg_stat_io WHERE context = 'bulkwrite' \gset
-SELECT :io_sum_bulkwrite_strategy_extends_after > :io_sum_bulkwrite_strategy_extends_before;
- ?column? 
-----------
- t
-(1 row)
-
 -- Test IO stats reset
 SELECT pg_stat_have_stats('io', 0, 0);
  pg_stat_have_stats 
@@ -1841,20 +1747,16 @@ SELECT pg_stat_reset_shared('io');
 SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS io_stats_post_reset
   FROM pg_stat_io \gset
 SELECT :io_stats_post_reset < :io_stats_pre_reset;
- ?column? 
-----------
- t
-(1 row)
-
+ERROR:  syntax error at or near ":"
+LINE 1: SELECT :io_stats_post_reset < :io_stats_pre_reset;
+               ^
 SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_post_reset
   FROM pg_stat_get_backend_io(pg_backend_pid()) \gset
 -- pg_stat_reset_shared() did not reset backend IO stats
 SELECT :my_io_stats_pre_reset <= :my_io_stats_post_reset;
- ?column? 
-----------
- t
-(1 row)
-
+ERROR:  syntax error at or near ":"
+LINE 1: SELECT :my_io_stats_pre_reset <= :my_io_stats_post_reset;
+               ^
 -- but pg_stat_reset_backend_stats() does
 SELECT pg_stat_reset_backend_stats(pg_backend_pid());
  pg_stat_reset_backend_stats 
@@ -1865,11 +1767,9 @@ SELECT pg_stat_reset_backend_stats(pg_backend_pid());
 SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_post_backend_reset
   FROM pg_stat_get_backend_io(pg_backend_pid()) \gset
 SELECT :my_io_stats_pre_reset > :my_io_stats_post_backend_reset;
- ?column? 
-----------
- t
-(1 row)
-
+ERROR:  syntax error at or near ":"
+LINE 1: SELECT :my_io_stats_pre_reset > :my_io_stats_post_backend_re...
+               ^
 -- Check invalid input for pg_stat_get_backend_io()
 SELECT pg_stat_get_backend_io(NULL);
  pg_stat_get_backend_io 
diff --git a/src/test/regress/expected/vacuum.out b/src/test/regress/expected/vacuum.out
index d4696bc3325..85d06907cec 100644
--- a/src/test/regress/expected/vacuum.out
+++ b/src/test/regress/expected/vacuum.out
@@ -526,25 +526,6 @@ SELECT t.relfilenode = :toast_filenode AS is_same_toast_filenode
  f
 (1 row)
 
--- BUFFER_USAGE_LIMIT option
-VACUUM (BUFFER_USAGE_LIMIT '512 kB') vac_option_tab;
-ANALYZE (BUFFER_USAGE_LIMIT '512 kB') vac_option_tab;
--- try disabling the buffer usage limit
-VACUUM (BUFFER_USAGE_LIMIT 0) vac_option_tab;
-ANALYZE (BUFFER_USAGE_LIMIT 0) vac_option_tab;
--- value exceeds max size error
-VACUUM (BUFFER_USAGE_LIMIT 16777220) vac_option_tab;
-ERROR:  BUFFER_USAGE_LIMIT option must be 0 or between 128 kB and 16777216 kB
--- value is less than min size error
-VACUUM (BUFFER_USAGE_LIMIT 120) vac_option_tab;
-ERROR:  BUFFER_USAGE_LIMIT option must be 0 or between 128 kB and 16777216 kB
--- integer overflow error
-VACUUM (BUFFER_USAGE_LIMIT 10000000000) vac_option_tab;
-ERROR:  BUFFER_USAGE_LIMIT option must be 0 or between 128 kB and 16777216 kB
-HINT:  Value exceeds integer range.
--- incompatible with VACUUM FULL error
-VACUUM (BUFFER_USAGE_LIMIT '512 kB', FULL) vac_option_tab;
-ERROR:  BUFFER_USAGE_LIMIT cannot be specified for VACUUM FULL
 -- SKIP_DATABASE_STATS option
 VACUUM (SKIP_DATABASE_STATS) vactst;
 -- ONLY_DATABASE_STATS option
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index 4c265d1245c..7a049d88a77 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -814,48 +814,6 @@ SELECT sum(writes) AS io_sum_local_new_tblspc_writes
 SELECT :io_sum_local_new_tblspc_writes > :io_sum_local_after_writes;
 RESET temp_buffers;
 
--- Test that reuse of strategy buffers and reads of blocks into these reused
--- buffers while VACUUMing are tracked in pg_stat_io. If there is sufficient
--- demand for shared buffers from concurrent queries, some buffers may be
--- pinned by other backends before they can be reused. In such cases, the
--- backend will evict a buffer from outside the ring and add it to the
--- ring. This is considered an eviction and not a reuse.
-
--- Set wal_skip_threshold smaller than the expected size of
--- test_io_vac_strategy so that, even if wal_level is minimal, VACUUM FULL will
--- fsync the newly rewritten test_io_vac_strategy instead of writing it to WAL.
--- Writing it to WAL will result in the newly written relation pages being in
--- shared buffers -- preventing us from testing BAS_VACUUM BufferAccessStrategy
--- reads.
-SET wal_skip_threshold = '1 kB';
-SELECT sum(reuses) AS reuses, sum(reads) AS reads, sum(evictions) AS evictions
-  FROM pg_stat_io WHERE context = 'vacuum' \gset io_sum_vac_strategy_before_
-CREATE TABLE test_io_vac_strategy(a int, b int) WITH (autovacuum_enabled = 'false');
-INSERT INTO test_io_vac_strategy SELECT i, i from generate_series(1, 4500)i;
--- Ensure that the next VACUUM will need to perform IO by rewriting the table
--- first with VACUUM (FULL).
-VACUUM (FULL) test_io_vac_strategy;
--- Use the minimum BUFFER_USAGE_LIMIT to cause reuses or evictions with the
--- smallest table possible.
-VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) test_io_vac_strategy;
-SELECT pg_stat_force_next_flush();
-SELECT sum(reuses) AS reuses, sum(reads) AS reads, sum(evictions) AS evictions
-  FROM pg_stat_io WHERE context = 'vacuum' \gset io_sum_vac_strategy_after_
-SELECT :io_sum_vac_strategy_after_reads > :io_sum_vac_strategy_before_reads;
-SELECT (:io_sum_vac_strategy_after_reuses + :io_sum_vac_strategy_after_evictions) >
-  (:io_sum_vac_strategy_before_reuses + :io_sum_vac_strategy_before_evictions);
-RESET wal_skip_threshold;
-
--- Test that extends done by a CTAS, which uses a BAS_BULKWRITE
--- BufferAccessStrategy, are tracked in pg_stat_io.
-SELECT sum(extends) AS io_sum_bulkwrite_strategy_extends_before
-  FROM pg_stat_io WHERE context = 'bulkwrite' \gset
-CREATE TABLE test_io_bulkwrite_strategy AS SELECT i FROM generate_series(1,100)i;
-SELECT pg_stat_force_next_flush();
-SELECT sum(extends) AS io_sum_bulkwrite_strategy_extends_after
-  FROM pg_stat_io WHERE context = 'bulkwrite' \gset
-SELECT :io_sum_bulkwrite_strategy_extends_after > :io_sum_bulkwrite_strategy_extends_before;
-
 -- Test IO stats reset
 SELECT pg_stat_have_stats('io', 0, 0);
 SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS io_stats_pre_reset
diff --git a/src/test/regress/sql/vacuum.sql b/src/test/regress/sql/vacuum.sql
index 247b8e23b23..506c4389765 100644
--- a/src/test/regress/sql/vacuum.sql
+++ b/src/test/regress/sql/vacuum.sql
@@ -390,21 +390,6 @@ SELECT t.relfilenode = :toast_filenode AS is_same_toast_filenode
   FROM pg_class c, pg_class t
   WHERE c.reltoastrelid = t.oid AND c.relname = 'vac_option_tab';
 
--- BUFFER_USAGE_LIMIT option
-VACUUM (BUFFER_USAGE_LIMIT '512 kB') vac_option_tab;
-ANALYZE (BUFFER_USAGE_LIMIT '512 kB') vac_option_tab;
--- try disabling the buffer usage limit
-VACUUM (BUFFER_USAGE_LIMIT 0) vac_option_tab;
-ANALYZE (BUFFER_USAGE_LIMIT 0) vac_option_tab;
--- value exceeds max size error
-VACUUM (BUFFER_USAGE_LIMIT 16777220) vac_option_tab;
--- value is less than min size error
-VACUUM (BUFFER_USAGE_LIMIT 120) vac_option_tab;
--- integer overflow error
-VACUUM (BUFFER_USAGE_LIMIT 10000000000) vac_option_tab;
--- incompatible with VACUUM FULL error
-VACUUM (BUFFER_USAGE_LIMIT '512 kB', FULL) vac_option_tab;
-
 -- SKIP_DATABASE_STATS option
 VACUUM (SKIP_DATABASE_STATS) vactst;
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index f6e87d04b0e..a08f9c3ce29 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -355,8 +355,6 @@ BtreeLevel
 Bucket
 BufFile
 Buffer
-BufferAccessStrategy
-BufferAccessStrategyType
 BufferCacheOsPagesContext
 BufferCacheOsPagesRec
 BufferDesc
-- 
2.51.2



^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention
  2026-04-25 20:08 [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-04-27 14:15 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-04-28 14:30   ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 14:00     ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
@ 2026-07-10 18:07       ` Greg Burd <greg@burd.me>
  2026-07-11 15:54         ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  0 siblings, 1 reply; 21+ messages in thread

From: Greg Burd @ 2026-07-10 18:07 UTC (permalink / raw)
  To: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; +Cc: Tomas Vondra <tomas@vondra.me>; Nathan Bossart <nathandbossart@gmail.com>; Andres Freund <andres@anarazel.de>


On Fri, Jul 10, 2026, at 10:00 AM, Greg Burd wrote:
> Hello again,
>
> Removing the freelist [1] turned out to be a good idea, so why not remove
> and simplify more?
>
> Included is a three-patch series (v3, on 2e6578292a9 cf-7000) that
> replaces the 0..5 usage_count clock sweep with a cooling-stage clock,
> and because this evictor algorithm is scan resistance 0003 removes
> the BufferAccessStrategy ring buffers entirely.  The net is slightly
> faster (TPS), hotter buffers (fewer misses) and a lot less code to
> maintain (no special case for scan resistant work) going forward.
>
>   0001  Batch the clock sweep to reduce nextVictimBuffer atomic contention
>   0002  Replace the usage_count clock sweep with a cooling-stage evictor
>   0003  Remove BufferAccessStrategy; scan resistance is now intrinsic
>
> Net it is +510/-1642 over 83 files; almost all of the deletion is 0003.
>
> On a 6-NUMA-node r8i.metal-96xl under continuous eviction, this change is
> a consistent +4–5% on read-heavy pgbench with equal-or-better hit ratios.
> The same test on a 2-node box is flat, which is the tell that the win
> is the cross-socket clock-hand contention the batched sweep removes. Under
> real storage IO (working set > RAM on local NVMe), the scan-resistance
> path is +1.6–3.9% with measurably fewer heap reads.
>
> I'll put the design decisions and the benchmark methodology on the table
> first, then talk through the results because if the model is wrong on
> principle, or the benchmarks are measuring the wrong thing, I would much
> rather hear it now.
>
> Contention arises on high core-count systems from the of cost advancing
> nextVictimBuffer in StrategyGetBuffer() via a fetch-add of 1 per tick.
> This contention is exacerbated on a multi-socket box.  That one cache
> line bounces over the interconnect on every eviction; under pressure
> with hundreds of backends it is the dominant cost of the sweep.  This is
> why we're investing time on NUMA optimizations.
>
>
> == What each patch does, and why ==
>
> 0001 -- Batch the clock sweep.
>
>   This is the setup/baseline commit and is an incremental improvement
>   over the existing batched approach, but it is not the final goal and
>   should not be committed without 0002.
>
>   Each backend will claim a run of consecutive buffer IDs with a single
>   fetch-add and iterate them privately.  Global sweep order is preserved
>   -- each buffer is visited exactly once per pass -- only the intra-pass
>   ordering of visits changes, which the algorithm does not depend on.
>   The atomic fires ~1/batch as often.
>
>   The batch is one cache line's worth of hand values (PG_CACHE_LINE_SIZE
>   / sizeof(uint32)), capped at NBuffers.  It is gated on multi-node NUMA
>   (pg_numa_get_max_node() >= 1); on a single socket the batch is 1 and
>   the path is byte-identical to today.  This is essentially Jim's patch;
>   the only substantive change from his v1/v2 is deriving the batch size
>   from the cache-line size rather than a fixed 64/tiered constant.
>
> 0002 -- Replace usage_count with a cooling-stage evictor.
>
>   This is the heart of the series and where I most want the design
>   attacked.
>
>   A buffer is HOT (recently used) or COOL (an eviction candidate);
>   "pinned" is the existing refcount.  There is no per-buffer 0..5
>   counter.  A demand-loaded page is admitted COOL (probationary); a
>   second access promotes it COOL->HOT.  So a page touched once -- as is
>   the case with a sequential scan -- fills and drains the COOL stage and
>   is evicted from it without displacing the HOT working set.  This is
>   the LeanStore / 2Q-A1 idea, and it is the whole reason 0003 can exist:
>   scan resistance stops being a ring-buffer bolt-on and becomes a
>   property of the replacement algorithm.
>
>   The 4-bit usage_count field is reinterpreted in place -- bit 0
>   HOT/COOL, bit 1 a reference bit, the top 2 bits unused -- so the 64-bit
>   buffer-state layout, its refcount / flag / lock offsets.  The only
>   change to the StaticAsserts is a new one asserting the field is at
>   least 2 bits wide, since we now use two of its bits.  The eviction
>   claim (COOL, unpinned -> pinned) stays a CAS so a racing PinBuffer
>   always wins; promotion and demotion are single-bit transitions.
>
>   The one non-obvious decision, which I got wrong first and want to flag
>   loudly: *Who* demotes HOT->COOL.  My first cut was "prefer-COOL": the
>   foreground sweep skips HOT buffers hunting for an already-COOL victim
>   and only cools HOT buffers once a full pass finds none.  That
>   collapses under an ordinary OLTP workload where the working set is
>   larger than shared_buffers (no scan at all).  Every access promotes
>   its buffer to HOT, so there are almost no COOL buffers, the "cool a
>   full pass" fallback fires on nearly every allocation, and each victim
>   search becomes a ~2x full-pool scan of scattered BufferDescs.  I
>   measured 3-17x throughput loss vs stock -- a cliff.
>
>   HOT->COOL demotion is done during the background writer's existing LRU
>   scan, which already runs ahead of the clock hand.  It demotes just
>   enough HOT buffers to keep a supply of COOL victims (bounded by the
>   predicted next-cycle allocation, so it does not cool the whole pool),
>   under the buffer header lock it already holds.  A single reference bit
>   gives a recently-accessed buffer one reprieve before it is cooled,
>   which keeps the genuinely-hot set out of the COOL stage under scan
>   pressure.  With that, the foreground finds a victim in a single pass
>   and the cliff is gone (data below).
>
>   I chose prefer-COOL-plus-bgwriter-precooling because it can protect a
>   hot buffer slightly longer (the pre-cooler, tuned to a budget, decides
>   when to demote rather than demoting on contact), which should help hit
>   ratio on a stable hot set -- IF the pre-cooler keeps up.  When it lags
>   (bgwriter off or behind), the foreground force_cool is still there as
>   a correctness fallback, but it is the expensive path.
>
> 0003 -- Remove BufferAccessStrategy.
>
>   With scan resistance intrinsic, the BAS_BULKREAD/BULKWRITE/VACUUM
>   rings are dead weight, so this removes them end to end: the type and
>   enum, the ring machinery in freelist.c, the strategy parameter
>   threaded through ReadBufferExtended / the ExtendBufferedRel* family /
>   read_stream / every scan/vacuum/analyze/index-AM caller, the strategy
>   fields on the scan and bulk-insert descriptors, and
>   _hash_getbuf_with_strategy.  pg_stat_io's per-strategy IO contexts
>   collapse to normal/init (IOOP_REUSE only ever happened while recycling
>   a ring buffer, so it is gone), and the vacuum_buffer_usage_limit GUC /
>   VACUUM (BUFFER_USAGE_LIMIT ...) option / vacuumdb --buffer-usage-limit
>   go with it.
>
>   This is the patch most likely to be contentious for reasons unrelated
>   to the sweep: it removes a user-visible GUC and changes pg_stat_io's
>   shape.  I have kept it as its own commit precisely so 0001+0002 can be
>   judged on the algorithm without swallowing the removal.  If the
>   consensus is that the cooling model is fine but the ring machinery
>   should stay for other reasons (BUFFER_USAGE_LIMIT as an operator
>   control, say), 0003 can simply be dropped and 0002 still stands -- the
>   rings just become redundant rather than removed.  I would like to know
>   if that is where people land.
>
>
> == Benchmarks ==
>
> I want to be careful here, because Andres's central criticism of the
> original batched-sweep numbers [2] was that they measured the wrong
> thing -- an all-in-page-cache config where the only bottleneck left is
> the atomic, which is not how anyone runs a 384-vCPU box.  That criticism
> is correct and I have tried to design around it, but I have almost
> certainly not fully escaped it, so the methodology is laid out below in
> enough detail to shoot at.
>
> I refer to the COOL/HOT approach (the changes in this patch set) as
> "bcs", I've forgotten what that stands for... "buffer cache solution?"
> I really don't remember (ha!).
>
> Hardware / method (both instances bare-metal, Amazon Linux 2023):
>
> - m6i.metal      -- 128 vCPU, 2 sockets, 2 NUMA nodes (distances 10/20), 503GB
> - r8i.metal-96xl -- 384 vCPU, 2 sockets, 6 NUMA nodes via SNC3, 3TB
>
> Builds: --buildtype=debugoptimized (-O2), cassert off, --with-libnuma,
> both branches from the same tree.  Postmaster pinned with numactl
> --cpunodebind=0 --membind=0 (without it stock TPS varied ~30% by launch
> node -- worth flagging for anyone reproducing).  pgbench dataset loaded
> once per build into its own datadir.
>
> The regime I settled on: a dataset that fits fully in OS page cache but
> exceeds shared_buffers, warmed before each cell, caches NOT dropped
> between cells.  The point is a sweep that runs continuously with
> sub-millisecond read latency and NO storage IO in the critical path --
> so what I am measuring is the eviction machinery itself, not the disk
> (as best as I can tell).
>
> I sweep the ratio (working-set / shared_buffers) from 0.8 (fits, no
> eviction, a control) up to 8x by varying shared_buffers against a fixed
> 63GB dataset.
>
> 3 iterations per cell, medians reported, 256 clients on m6i / 384 on r8i.
>
> Two workloads: uniform pgbench -S (the pure eviction-churn case, and the
> worst case for the cooling model -- no hot set to protect), and
> "hotscan" (a Zipfian hot set plus a handful of clients running large
> range scans -- the scan-resistance case).
>
> I want the regime itself critiqued.  It deliberately removes storage IO
> to expose the sweep; the flip side is it is not a production
> configuration, and the read-path win (fewer misses under scan
> resistance) shows up as read count, not TPS, because a "miss" here is an
> OS-cache memcpy, not a device read.
>
> r8i.metal-96xl, uniform pgbench -S, 384 clients, TPS (median of 3):
>
>   ratio  SB(GB)   stock       bcs        delta   stock/bcs hit%
>   0.8    ~400     1,623,536   1,692,090   +4.2%   99.2 / 99.9
>   1.25   ~320     1,443,343   1,519,290   +5.3%   94.8 / 95.2
>   1.5    ~266     1,344,051   1,417,462   +5.5%   91.9 / 92.1
>   2      ~200     1,293,871   1,358,550   +5.0%   87.9 / 87.8
>   4      ~100     1,194,405   1,240,334   +3.8%   77.1 / 78.5
>   8      ~50      1,090,873   1,140,839   +4.6%   69.7 / 70.6
>
> Consistent +4-5% across the eviction range, growing with pressure, with
> equal-or-better hit ratio (better at 4x/8x).  bcs also showed lower
> cache-miss rate in perf stat (~28-35% vs ~31-37%), which is the batched
> sweep's reduced cross-node line bouncing showing through.
>
> r8i, hotscan (Zipfian hot set + range scanners), TPS / hit% / heap reads:
>
>   ratio  stock TPS   bcs TPS    stock hit  bcs hit   stock reads  bcs reads
>   1.25   1,833,675   1,871,289   99.43     99.57     7,309,457    5,503,927
>   1.5    1,876,846   1,942,877   99.35     99.42     8,335,144    7,565,667
>   2      1,868,591   1,853,547   99.12     99.15    11,173,831   10,987,371
>
> The scan-resistance signal: at 1.25-1.5x, bcs holds a higher hit ratio
> and does up to 25% fewer heap reads (24.7% at 1.25x, ~9% at 1.5x) -- it
> is keeping the hot set resident through the scans where stock lets them
> flush it.  Muted in TPS only because everything is in OS cache (a miss
> is cheap); on real storage this read reduction is where the win would
> land.  At 2x it washes out (enough pressure that both evict heavily).
>
> m6i.metal (2 nodes), uniform, 256 clients -- the smaller box, for contrast:
> essentially parity, bcs -2% to +1% across ratios.  The 2-node box barely
> exercises the atomic, so 0001's contention win does not appear; that it does
> not regress is the result that matters here.
>
> Huge pages on vs off (r8i, uniform, medians): I ran this because the
> original thread flagged it as uncharacterized.  bcs won by +3-7% both
> ways, no regression without huge pages -- the win is from cutting the
> frequency of atomic ops on the counter line, which does not depend on
> where the descriptors physically live.  (This is why the batching gate
> is NUMA-only and not also huge-pages-gated.)
>
> The "prefer-COOL cliff" I mentioned under 0002, so the failure mode is
> on the record: BEFORE moving cooling into the bgwriter, the m6i uniform
> run at 256 clients was bcs 274K vs stock 840K at ratio 2 (-67%), and
> 42.8K vs 762K at 8x (-94%), with cache-miss rate exploding to ~40%.
> That is the shape of getting the demotion policy wrong; the r8i +5%
> table above is after the fix.
>
> Reproduction: the whole harness (instance launch, OS tuning, per-build
> load, the ratio sweep, perf stat capture) is scripted; I will attach it
> as a DO-NOT-MERGE commit / put it in the CF entry so the methodology can
> be reproduced and picked apart rather than taken on faith.  Raw per-run
> CSVs and perf output likewise.
>
>
> A Real IO (working set > RAM, evictions hitting storage) Benchmark
>
> This is the regime Andres asked for, and the one I flagged earlier as not yet
> done cleanly.  The earlier attempt was EBS-latency-bound; this one uses local
> NVMe so eviction reads hit real storage at ~microsecond, not ~15ms, latency --
> during the run the array sat at 100% utilization and ~145K read IOPS, so the
> eviction path is genuinely storage-bound, not cache-served.
>
> m6id.metal -- 128 vCPU, 2 sockets, 2 NUMA nodes, 503GB, 4x1.9TB local NVMe in
> RAID0.  Dataset ~700GB (pgbench scale 47000), i.e. LARGER than RAM, so the
> working set cannot sit in the OS page cache.  shared_buffers is a small window
> over it -- 63GB (11x) and 31GB (22x) -- caches dropped per cell, 256 clients, 3
> iterations, medians.  Same builds/method as the in-cache runs otherwise.
>
> hotscan (Zipfian hot set + range scanners), median of 3:
>
>   ratio  SB(GB)   stock TPS   bcs TPS     dTPS    stock/bcs hit   reads d
>   11     63       877,357     911,363    +3.9%    96.30 / 96.43   -2.2%
>   22     31       873,880     888,010    +1.6%    94.02 / 94.22   -1.5%
>
> This is the result the in-cache runs could only hint at: under real storage IO
> the scan-resistance read reduction converts to throughput.  The bcs approach
> keeps a higher hit ratio and does 1.5-2.2% fewer heap reads, and here -- unlike
> in cache, where a miss is a cheap memcpy -- a read it avoids is an NVMe round
> trip, so the read reduction shows up as +1.6-3.9% TPS.
>
> uniform pgbench -S (pure eviction churn, no hot set to protect), median of 3:
>
>   ratio  SB(GB)   stock TPS   bcs TPS     dTPS    stock/bcs hit
>   11     63       605,656     616,247    +1.7%    67.0 / 67.3
>   22     31       605,517     611,312    +1.0%    63.5 / 63.6
>
> Hit ratio here is 63-67% -- a third of accesses miss and hit NVMe (190M-220M
> evictions per run), so this is deep, genuinely storage-bound churn.  bcs is
> +1-1.7%, i.e. flat-to-slightly-positive, which is the honest production picture:
> with no hot set to protect, scan resistance has nothing to do, and the win is
> just the batched sweep's reduced contention showing faintly through the IO wait.
> Notably bcs does not regress even when its policy has no advantage to exploit.
>
>
> == Side note for the curious... ==
>
> Separately, Dhruv Aron has proposed restructuring the shared-buffer lookup table
> [2], replacing dynahash with a flat two-array structure. That attacks the
> other hot cost on a buffer miss — resolving a page to its buffer — where this
> series attacks the eviction that a miss triggers. They touch buf_table.c and a
> lock-ordering change in InvalidateBuffer(); this series touches freelist.c and
> the per-buffer replacement state, and removes BufferAccessStrategy. The two are
> complementary and should compound on the miss path; the only overlap is
> InvalidateBuffer()/GetVictimBuffer(), where their extended buffer-header-lock
> hold and this series' CAS-claim + bgwriter pre-cooling both take that lock, and
> would want reconciling if both land. I have not benchmarked them together (yet).
>
> == What I have not done, honestly ==
>
>   - Hardening the foreground force_cool fallback to be cheap when it
>     fires, rather than relying on the bgwriter pre-cooler never lagging.
>
>   - Anything on single-socket beyond "does not regress"; the design is
>     not trying to help there.
>
>
> == The ask ==
>
>   1. 0002's demotion policy: is prefer-COOL + bgwriter pre-cooling the
>      right call, or is the other team's cool-in-place the more robust
>      default given it has no cliff and no background-process dependency?
>      This is the decision everything else hangs on.
>
>   2. Is admitting demand-loaded pages COOL (probationary,
>      promote-on-second- touch) an acceptable basis for scan resistance
>      in the core buffer manager, i.e. is it OK to make scan resistance
>      an algorithm property and retire the strategy rings (0003)?  Or
>      should the rings stay and 0002 ride alongside them?
>
>   3. The benchmark methodology: where is the in-OS-cache regime
>      misleading, and what would you want measured instead?  I am most
>      worried I am flattering the sweep by removing the IO that would
>      otherwise hide it.
>
>   4. Reinterpreting the usage_count field as {HOT/COOL, ref} bits and
>      collapsing pg_stat_io's contexts -- acceptable, or is there a
>      cleaner representation the project would want before this is worth
>      pursuing?
>
> I have measured that the 0..5 count is overhead and provides no
> meaningful signal at all, that a HOT/COLD approach provides a simpler
> more stable and better performing eviction model for the buffer pool.
> If you dispute that, let's dig in and compare notes. :)
>
> I would be remiss if I didn't point out the thread [3] by Tomas et. al.,
> whose NUMA investigation targets the same bottlenecks, and inspired the
> work that led to this set of ideas. 
>
> Thanks for reading this far.  I look forward to the critique.
>
> best.
>
> -greg
>
> [1] Reconsidering the freelist
>     
> https://www.postgresql.org/message-id/f0e3c02e-e217-4f04-8dab-1e7e80a228c0%40burd.me
> [2] Re: Restructured Shared Buffer Hash Table
>     
> https://www.postgresql.org/message-id/dbbd1998-19ff-4ac2-b4b1-a39f4ec1b0f5@iki.fi
> [3] Adding basic NUMA awareness (Tomas Vondra)
>     
> https://www.postgresql.org/message-id/099b9433-2855-4f1b-b421-d078a5d82017%40vondra.me
> Attachments:
> * v3-0001-Batch-the-clock-sweep-to-reduce-nextVictimBuffer-.patch
> * v3-0002-Replace-the-usage_count-clock-sweep-with-a-coolin.patch
> * v3-0003-Remove-BufferAccessStrategy-scan-resistance-is-no.patch

Rebased v4 onto c71d43025d7.

-greg

Attachments:

  [text/x-patch] v4-0001-Batch-the-clock-sweep-to-reduce-nextVictimBuffer-.patch (8.9K, ../../76c33898-3baa-4f7b-8b72-57dd390fcffe@app.fastmail.com/2-v4-0001-Batch-the-clock-sweep-to-reduce-nextVictimBuffer-.patch)
  download | inline diff:
From 7289cb2565fb331c000c46d42746ec5eb7b1ba99 Mon Sep 17 00:00:00 2001
From: Greg Burd <greg@burd.me>
Date: Mon, 6 Jul 2026 17:25:00 -0400
Subject: [PATCH v4 1/3] Batch the clock sweep to reduce nextVictimBuffer
 atomic contention

StrategyGetBuffer() advances the shared clock hand, nextVictimBuffer, with a
pg_atomic_fetch_add_u32(..., 1) on every tick.  On a multi-socket system the
cache line holding that counter has to travel over the interconnect on each
operation, pushing a sweep tick from ~20ns (same-socket, line warm in L1/L2)
into the ~100-200ns range.  Under eviction pressure with hundreds of backends
in StrategyGetBuffer() concurrently, that single cache line becomes the
dominant cost of the sweep, visible as elevated bus-cycles and cache-misses in
a perf profile.

Have each backend claim a run of consecutive buffer IDs from the shared hand
with a single fetch-add and then iterate through them privately.  The sweep
still advances through the pool in order, each buffer is still visited exactly
once per complete pass, and the meaning of the clock state is unchanged; only
the temporal ordering of visits within a pass changes, which the algorithm
does not depend on.  The contended atomic now fires roughly once per batch
rather than once per buffer.

The batch is one cache line's worth of clock-hand values --
PG_CACHE_LINE_SIZE / sizeof(uint32) -- capped at NBuffers so a claim can never
wrap the pool more than once.  Batching only helps when the counter's cache
line actually bounces between sockets, so it is enabled only on multi-node
NUMA hardware (pg_numa_get_max_node() >= 1); on a single socket, or where
libnuma is unavailable, the batch size stays 1 and the code path is
byte-identical to the stock clock sweep.

Wraparound handling is adjusted: with batching, several backends can each see
a fetch-add return a value past NBuffers within the same pass.  Any such
backend takes buffer_strategy_lock, re-reads the counter, and if it is still
out of range wraps it with a single CAS and increments completePasses.
StrategySyncStart() continues to see a consistent (nextVictimBuffer,
completePasses) pair.

This is the batched-clock-sweep idea from Jim Mlodgenski's pgsql-hackers
thread, adapted to derive the batch size from the platform cache-line size.

Co-authored-by: Jim Mlodgenski <mlodj@amazon.com>
---
 src/backend/storage/buffer/freelist.c | 134 +++++++++++++++++---------
 1 file changed, 91 insertions(+), 43 deletions(-)

diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index fdb5bad7910..93fc4dd758b 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -22,6 +22,7 @@
 #include "storage/proc.h"
 #include "storage/shmem.h"
 #include "storage/subsystems.h"
+#include "port/pg_numa.h"
 
 #define INT_ACCESS_ONCE(var)	((int)(*((volatile int *)&(var))))
 
@@ -100,68 +101,92 @@ static BufferDesc *GetBufferFromRing(BufferAccessStrategy strategy,
 static void AddBufferToRing(BufferAccessStrategy strategy,
 							BufferDesc *buf);
 
+/*
+ * Per-backend state for the batched clock sweep.  Each backend claims a run
+ * of consecutive clock-hand values with a single atomic fetch-add and then
+ * iterates through them privately, so the contended nextVictimBuffer cache
+ * line is touched roughly 1/batch as often.  MyBatchPos is the next hand
+ * value to hand out; MyBatchEnd is one past the end of the claimed run.  Both
+ * are absolute (monotonically increasing) hand values; the buffer id is the
+ * value modulo NBuffers.
+ */
+static uint32 MyBatchPos = 0;
+static uint32 MyBatchEnd = 0;
+
+/*
+ * Number of clock-hand values a backend claims per atomic fetch-add,
+ * computed once at startup (see StrategyCtlShmemInit).  When batching is
+ * enabled it is one cache line's worth of hand advance, so concurrent
+ * backends sweep non-overlapping, cache-line-sized runs of the pool; the
+ * global sweep order is preserved (each buffer is still visited exactly once
+ * per pass).  Batching is enabled only on multi-node NUMA hardware; otherwise
+ * this stays 1 and the sweep is byte-identical to the stock clock.
+ */
+static uint32 ClockSweepBatchSize = 1;
+
 /*
  * ClockSweepTick - Helper routine for StrategyGetBuffer()
  *
- * Move the clock hand one buffer ahead of its current position and return the
- * id of the buffer now under the hand.
+ * Return the next buffer to consider for eviction.  Backends claim batches of
+ * consecutive buffer IDs from the shared clock hand, then iterate through
+ * them locally without further atomic operations.  This preserves the global
+ * sweep order while reducing contention on the shared counter.
  */
 static inline uint32
 ClockSweepTick(void)
 {
 	uint32		victim;
 
-	/*
-	 * Atomically move hand ahead one buffer - if there's several processes
-	 * doing this, this can lead to buffers being returned slightly out of
-	 * apparent order.
-	 */
-	victim =
-		pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer, 1);
-
-	if (victim >= NBuffers)
+	if (MyBatchPos >= MyBatchEnd)
 	{
-		uint32		originalVictim = victim;
-
-		/* always wrap what we look up in BufferDescriptors */
-		victim = victim % NBuffers;
-
 		/*
-		 * If we're the one that just caused a wraparound, force
-		 * completePasses to be incremented while holding the spinlock. We
-		 * need the spinlock so StrategySyncStart() can return a consistent
-		 * value consisting of nextVictimBuffer and completePasses.
+		 * Claim a fresh batch from the shared clock hand.  This is the only
+		 * atomic operation per batch, reducing contention by the batch size.
 		 */
-		if (victim == 0)
-		{
-			uint32		expected;
-			uint32		wrapped;
-			bool		success = false;
+		uint32		start;
+		uint32		batch_size = ClockSweepBatchSize;
 
-			expected = originalVictim + 1;
+		start = pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer,
+										batch_size);
 
-			while (!success)
-			{
-				/*
-				 * Acquire the spinlock while increasing completePasses. That
-				 * allows other readers to read nextVictimBuffer and
-				 * completePasses in a consistent manner which is required for
-				 * StrategySyncStart().  In theory delaying the increment
-				 * could lead to an overflow of nextVictimBuffers, but that's
-				 * highly unlikely and wouldn't be particularly harmful.
-				 */
-				SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
+		if (start >= (uint32) NBuffers)
+		{
+			start = start % NBuffers;
 
-				wrapped = expected % NBuffers;
+			/*
+			 * The counter has grown past NBuffers; try to wrap it back.  We
+			 * must hold the spinlock so StrategySyncStart() can read
+			 * nextVictimBuffer and completePasses consistently.
+			 *
+			 * With batching, multiple backends may each land a fetch-add
+			 * that returns a value past NBuffers in the same pass.  After
+			 * acquiring the spinlock we re-read the counter: if another
+			 * backend already wrapped it below NBuffers we are done.
+			 */
+			SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
+			{
+				uint32		current;
+				uint32		wrapped;
 
-				success = pg_atomic_compare_exchange_u32(&StrategyControl->nextVictimBuffer,
-														 &expected, wrapped);
-				if (success)
-					StrategyControl->completePasses++;
-				SpinLockRelease(&StrategyControl->buffer_strategy_lock);
+				current = pg_atomic_read_u32(&StrategyControl->nextVictimBuffer);
+				if (current >= (uint32) NBuffers)
+				{
+					wrapped = current % NBuffers;
+					if (pg_atomic_compare_exchange_u32(&StrategyControl->nextVictimBuffer,
+													   &current, wrapped))
+						StrategyControl->completePasses++;
+				}
 			}
+			SpinLockRelease(&StrategyControl->buffer_strategy_lock);
 		}
+
+		MyBatchPos = start;
+		MyBatchEnd = start + batch_size;
 	}
+
+	victim = MyBatchPos % NBuffers;
+	MyBatchPos++;
+
 	return victim;
 }
 
@@ -408,6 +433,29 @@ StrategyCtlShmemInit(void *arg)
 
 	/* No pending notification */
 	StrategyControl->bgwprocno = -1;
+
+	/*
+	 * Decide whether to batch the clock sweep.
+	 *
+	 * Batching claims a run of consecutive buffer IDs per atomic fetch-add so
+	 * concurrent backends touch the shared nextVictimBuffer cache line ~1/batch
+	 * as often -- a win only when that line actually bounces across sockets,
+	 * i.e. on multi-node NUMA hardware.  On a single socket the atomic is
+	 * already node-local and batching would only make backends skip ahead for
+	 * no benefit, so we fall back to batch size 1 there (byte-identical to the
+	 * stock one-buffer-at-a-time clock sweep).
+	 *
+	 * Batch (> 1) only when libnuma reports more than one node
+	 * (pg_numa_get_max_node() >= 1); pg_numa_init() returns -1 when NUMA is
+	 * unavailable.  (Benchmarking showed the win holds with or without huge
+	 * pages, so huge-page availability is intentionally not part of the gate.)
+	 */
+	if (pg_numa_init() != -1 &&
+		pg_numa_get_max_node() >= 1)
+		ClockSweepBatchSize = Min(PG_CACHE_LINE_SIZE / (uint32) sizeof(uint32),
+								  (uint32) NBuffers);
+	else
+		ClockSweepBatchSize = 1;
 }
 
 
-- 
2.51.2



  [text/x-patch] v4-0002-Replace-the-usage_count-clock-sweep-with-a-coolin.patch (27.5K, ../../76c33898-3baa-4f7b-8b72-57dd390fcffe@app.fastmail.com/3-v4-0002-Replace-the-usage_count-clock-sweep-with-a-coolin.patch)
  download | inline diff:
From aec577d727ae87e79226746b7c94ea4ac7ced71a Mon Sep 17 00:00:00 2001
From: Greg Burd <greg@burd.me>
Date: Mon, 6 Jul 2026 19:22:45 -0400
Subject: [PATCH v4 2/3] Replace the usage_count clock sweep with a
 cooling-stage evictor

Replace the 0..5 usage_count buffer-replacement policy with a cooling-stage
clock (the LeanStore / 2Q-A1 model): a buffer is simply HOT (recently used)
or COOL (an eviction candidate), with "pinned" being the existing refcount.
There is no per-buffer access counter.

  - A demand-loaded page is admitted COOL (probationary), not HOT.  A second
    access via PinBuffer promotes it COOL -> HOT (the rescue).  So a page
    touched once -- a sequential scan -- fills and drains the COOL stage and
    is evicted from it without ever displacing the HOT working set.  Scan
    resistance is intrinsic to the replacement algorithm, which is what lets a
    later commit remove the BufferAccessStrategy ring buffers entirely.

  - The foreground sweep in StrategyGetBuffer() reclaims an already-COOL,
    unpinned buffer, pinning it with a CAS so a racing PinBuffer always wins.
    Promotion (COOL -> HOT) and demotion (HOT -> COOL) are single-bit
    transitions; only the eviction claim is a CAS.

  - The background writer maintains the supply of COOL victims.  As its LRU
    scan runs ahead of the clock hand it demotes HOT buffers to COOL, so the
    foreground finds a victim in a single pass rather than having to cool
    buffers itself.  The demotion is demand-driven -- bounded by the predicted
    allocation for the next cycle -- so it stages just enough COOL buffers
    without cooling the whole pool, and it is done under the buffer header
    lock the scan already holds.  A single second-chance reference bit
    (set by PinBuffer, cleared on the bgwriter's first pass over a HOT buffer)
    spares a recently-accessed buffer one cooling pass, keeping the genuinely
    hot set out of the COOL stage under scan pressure.  BgBufferSync also
    tracks the reusable-buffer density it directly observes on a shorter
    smoothing window, so a burst of probationary/scan COOL pages is followed
    promptly rather than averaged away.

The 4-bit usage_count field is reinterpreted in place: bit 0 is the HOT/COOL
state (BUF_COOLSTATE_ONE), bit 1 the reference bit (BUF_REFBIT).  The 64-bit
buffer-state layout -- refcount, flag and lock offsets and their StaticAsserts
-- is unchanged; only the meaning of the field and the instructions that touch
it change.  A StaticAssert requires the field to be at least 2 bits wide so a
future width change cannot push the reference bit into the flag bits.
BM_MAX_USAGE_COUNT becomes BUF_COOLSTATE_HOT (1), so the pin fast path
saturates at HOT.  Local (temp-table) buffers get the same two-state
treatment; being single-backend they need no background cooler.

Because the reference bit shares the field, the full 4-bit value can be 0..3;
readers that mean "the cooling state" must use BUF_STATE_GET_COOLSTATE(), which
masks to bit 0 and returns only 0 (COOL) or 1 (HOT), never the raw field.
contrib/pg_buffercache is updated accordingly: its usagecount column and the
pg_buffercache_summary average report the cooling state (0 = COOL, 1 = HOT),
and pg_buffercache_usage_counts() buckets on it.  Using the raw 4-bit getter
there would index its BM_MAX_USAGE_COUNT+1 = 2-element arrays with values up
to 3 and overrun the stack; masking to the cooling bit keeps the index in
range.  The reference bit is deliberately not exposed as usagecount.

Depends on the batched clock sweep from the previous commit.
---
 contrib/pg_buffercache/pg_buffercache_pages.c |   6 +-
 src/backend/storage/buffer/bufmgr.c           | 103 ++++++++++----
 src/backend/storage/buffer/freelist.c         | 128 ++++++++++++------
 src/backend/storage/buffer/localbuf.c         |  13 +-
 src/include/storage/buf_internals.h           |  63 +++++++--
 5 files changed, 229 insertions(+), 84 deletions(-)

diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index 510455998aa..b94e6f6fda8 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -161,7 +161,7 @@ pg_buffercache_pages(PG_FUNCTION_ARGS)
 		reldatabase = bufHdr->tag.dbOid;
 		forknum = BufTagGetForkNum(&bufHdr->tag);
 		blocknum = bufHdr->tag.blockNum;
-		usagecount = BUF_STATE_GET_USAGECOUNT(buf_state);
+		usagecount = BUF_STATE_GET_COOLSTATE(buf_state);
 		pinning_backends = BUF_STATE_GET_REFCOUNT(buf_state);
 
 		if (buf_state & BM_DIRTY)
@@ -605,7 +605,7 @@ pg_buffercache_summary(PG_FUNCTION_ARGS)
 		if (buf_state & BM_VALID)
 		{
 			buffers_used++;
-			usagecount_total += BUF_STATE_GET_USAGECOUNT(buf_state);
+			usagecount_total += BUF_STATE_GET_COOLSTATE(buf_state);
 
 			if (buf_state & BM_DIRTY)
 				buffers_dirty++;
@@ -655,7 +655,7 @@ pg_buffercache_usage_counts(PG_FUNCTION_ARGS)
 
 		CHECK_FOR_INTERRUPTS();
 
-		usage_count = BUF_STATE_GET_USAGECOUNT(buf_state);
+		usage_count = BUF_STATE_GET_COOLSTATE(buf_state);
 		usage_counts[usage_count]++;
 
 		if (buf_state & BM_DIRTY)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 9ab282a76d1..1173b50b7c1 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -83,6 +83,7 @@
 /* Bits in SyncOneBuffer's return value */
 #define BUF_WRITTEN				0x01
 #define BUF_REUSABLE			0x02
+#define BUF_COOLED				0x04
 
 #define RELS_BSEARCH_THRESHOLD		20
 
@@ -634,7 +635,7 @@ static void PinBuffer_Locked(BufferDesc *buf);
 static void UnpinBuffer(BufferDesc *buf);
 static void UnpinBufferNoOwner(BufferDesc *buf);
 static void BufferSync(int flags);
-static int	SyncOneBuffer(int buf_id, bool skip_recently_used,
+static int	SyncOneBuffer(int buf_id, bool skip_recently_used, bool cool_if_hot,
 						  WritebackContext *wb_context);
 static void WaitIO(BufferDesc *buf);
 static void AbortBufferIO(Buffer buffer);
@@ -2333,7 +2334,10 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	 * checkpoints, except for their "init" forks, which need to be treated
 	 * just like permanent relations.
 	 */
-	set_bits |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
+	set_bits |= BM_TAG_VALID;
+	/* Admit the newly loaded page COOL (probation); a second access via
+	 * PinBuffer promotes it to HOT.  This is what makes a one-touch scan
+	 * self-evicting -- see the cooling-state notes in buf_internals.h. */
 	if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM)
 		set_bits |= BM_PERMANENT;
 
@@ -3002,7 +3006,9 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 
 			victim_buf_hdr->tag = tag;
 
-			set_bits |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
+			set_bits |= BM_TAG_VALID;
+			/* Admit COOL (probation); see the comment at the other admission
+			 * site and the cooling-state notes in buf_internals.h. */
 			if (bmr.relpersistence == RELPERSISTENCE_PERMANENT || fork == INIT_FORKNUM)
 				set_bits |= BM_PERMANENT;
 
@@ -3332,21 +3338,17 @@ PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy,
 			/* increase refcount */
 			buf_state += BUF_REFCOUNT_ONE;
 
-			if (strategy == NULL)
-			{
-				/* Default case: increase usagecount unless already max. */
-				if (BUF_STATE_GET_USAGECOUNT(buf_state) < BM_MAX_USAGE_COUNT)
-					buf_state += BUF_USAGECOUNT_ONE;
-			}
-			else
-			{
-				/*
-				 * Ring buffers shouldn't evict others from pool.  Thus we
-				 * don't make usagecount more than 1.
-				 */
-				if (BUF_STATE_GET_USAGECOUNT(buf_state) == 0)
-					buf_state += BUF_USAGECOUNT_ONE;
-			}
+			/*
+			 * Accessing a resident buffer promotes it to HOT (the 2Q rescue):
+			 * a page loaded COOL on probation becomes part of the hot working
+			 * set on its second touch.  BM_MAX_USAGE_COUNT is
+			 * BUF_COOLSTATE_HOT (1), so this saturates at HOT and never
+			 * overflows the field.  We also set the second-chance ref bit so
+			 * the bgwriter's next cooling pass spares this recently-used buffer.
+			 */
+			if (BUF_STATE_GET_COOLSTATE(buf_state) < BUF_COOLSTATE_HOT)
+				buf_state += BUF_COOLSTATE_ONE;
+			buf_state |= BUF_REFBIT;
 
 			if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
 											   buf_state))
@@ -3785,7 +3787,7 @@ BufferSync(int flags)
 		 */
 		if (pg_atomic_read_u64(&bufHdr->state) & BM_CHECKPOINT_NEEDED)
 		{
-			if (SyncOneBuffer(buf_id, false, &wb_context) & BUF_WRITTEN)
+			if (SyncOneBuffer(buf_id, false, false, &wb_context) & BUF_WRITTEN)
 			{
 				TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
 				PendingCheckpointerStats.buffers_written++;
@@ -3876,6 +3878,19 @@ BgBufferSync(WritebackContext *wb_context)
 	float		smoothing_samples = 16;
 	float		scan_whole_pool_milliseconds = 120000.0;
 
+	/*
+	 * The cleaner scan directly observes the reusable (COOL, unpinned) buffer
+	 * density over the region it walks, which -- with the cooling-stage
+	 * evictor -- is exactly the sweep's victim predicate.  That observation is
+	 * ground truth for the buffers about to be reused, whereas the strategy
+	 * scan's positional proxy (strategy_delta/recent_alloc) blurs a pool whose
+	 * COOL population is spatially clustered (a scan burst leaves whole regions
+	 * COOL, hot OLTP regions not).  So we let the cleaner's own sample adapt on
+	 * a shorter window than the strategy proxy, tracking a burst of
+	 * probationary/scan COOL pages within a cycle or two instead of lagging it.
+	 */
+	float		cleaner_smoothing_samples = 4;
+
 	/* Used to compute how far we scan ahead */
 	long		strategy_delta;
 	int			bufs_to_lap;
@@ -4073,7 +4088,7 @@ BgBufferSync(WritebackContext *wb_context)
 	/* Execute the LRU scan */
 	while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
 	{
-		int			sync_state = SyncOneBuffer(next_to_clean, true,
+		int			sync_state = SyncOneBuffer(next_to_clean, true, true,
 											   wb_context);
 
 		if (++next_to_clean >= NBuffers)
@@ -4121,7 +4136,7 @@ BgBufferSync(WritebackContext *wb_context)
 	{
 		scans_per_alloc = (float) new_strategy_delta / (float) new_recent_alloc;
 		smoothed_density += (scans_per_alloc - smoothed_density) /
-			smoothing_samples;
+			cleaner_smoothing_samples;
 
 #ifdef BGW_DEBUG
 		elog(DEBUG2, "bgwriter: cleaner density alloc=%u scan=%ld density=%.2f new smoothed=%.2f",
@@ -4140,16 +4155,26 @@ BgBufferSync(WritebackContext *wb_context)
  * If skip_recently_used is true, we don't write currently-pinned buffers, nor
  * buffers marked recently used, as these are not replacement candidates.
  *
+ * If cool_if_hot is true (the bgwriter's LRU scan), an unpinned HOT buffer is
+ * demoted HOT -> COOL as we pass it, pre-staging eviction candidates so the
+ * foreground clock sweep finds a COOL victim in a single pass instead of
+ * having to cool buffers itself (force_cool).  The demotion is done under the
+ * buffer header lock we already hold, so it needs no CAS and cannot race a
+ * concurrent demotion.  A concurrent PinBuffer promotes it back to HOT, which
+ * is the intended 2Q behavior (a re-accessed buffer is rescued).
+ *
  * Returns a bitmask containing the following flag bits:
  *	BUF_WRITTEN: we wrote the buffer.
  *	BUF_REUSABLE: buffer is available for replacement, ie, it has
- *		pin count 0 and usage count 0.
+ *		pin count 0 and is COOL (an eviction candidate).
+ *	BUF_COOLED: we demoted this buffer HOT -> COOL this call.
  *
  * (BUF_WRITTEN could be set in error if FlushBuffer finds the buffer clean
  * after locking it, but we don't care all that much.)
  */
 static int
-SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
+SyncOneBuffer(int buf_id, bool skip_recently_used, bool cool_if_hot,
+			  WritebackContext *wb_context)
 {
 	BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
 	int			result = 0;
@@ -4171,8 +4196,38 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 	 */
 	buf_state = LockBufHdr(bufHdr);
 
+	/*
+	 * Pre-cool with a second chance: if asked, act on an unpinned HOT buffer.
+	 * If its ref bit is set (accessed since our last pass), clear the ref bit
+	 * and leave it HOT -- a recently-used buffer earns one reprieve, keeping
+	 * the hot working set out of the COOL stage under scan pressure.  Only a
+	 * HOT buffer whose ref bit is already clear is demoted HOT -> COOL,
+	 * pre-staging it as an eviction candidate for the foreground sweep.  We
+	 * hold the header lock, so each transition is a plain masked store applied
+	 * atomically by UnlockBufHdrExt; we re-lock to continue the dirty-write
+	 * inspection below.
+	 */
+	if (cool_if_hot &&
+		BUF_STATE_GET_REFCOUNT(buf_state) == 0 &&
+		BUF_STATE_GET_COOLSTATE(buf_state) != BUF_COOLSTATE_COOL)
+	{
+		if (BUF_STATE_GET_REFBIT(buf_state))
+		{
+			/* second chance: consume the ref bit, stay HOT */
+			UnlockBufHdrExt(bufHdr, buf_state, 0, BUF_REFBIT, 0);
+			buf_state = LockBufHdr(bufHdr);
+		}
+		else
+		{
+			/* not re-accessed since last pass: demote to COOL */
+			UnlockBufHdrExt(bufHdr, buf_state, 0, BUF_USAGECOUNT_MASK, 0);
+			buf_state = LockBufHdr(bufHdr);
+			result |= BUF_COOLED;
+		}
+	}
+
 	if (BUF_STATE_GET_REFCOUNT(buf_state) == 0 &&
-		BUF_STATE_GET_USAGECOUNT(buf_state) == 0)
+		BUF_STATE_GET_COOLSTATE(buf_state) == BUF_COOLSTATE_COOL)
 	{
 		result |= BUF_REUSABLE;
 	}
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index 93fc4dd758b..cc77144224c 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -49,6 +49,16 @@ typedef struct
 	uint32		completePasses; /* Complete cycles of the clock-sweep */
 	pg_atomic_uint32 numBufferAllocs;	/* Buffers allocated since last reset */
 
+	/*
+	 * Number of clock-hand values a backend claims per atomic fetch-add.
+	 * Computed once at startup (see StrategyCtlShmemInit).  Kept in shared
+	 * memory rather than a backend-local static so that EXEC_BACKEND children
+	 * (which do not inherit the postmaster's statics) see the same value; a
+	 * backend-local copy would silently reset to 1 there, disabling batching
+	 * on Windows.
+	 */
+	uint32		batchSize;
+
 	/*
 	 * Bgworker process to be notified upon activity or -1 if none. See
 	 * StrategyNotifyBgWriter.
@@ -113,17 +123,6 @@ static void AddBufferToRing(BufferAccessStrategy strategy,
 static uint32 MyBatchPos = 0;
 static uint32 MyBatchEnd = 0;
 
-/*
- * Number of clock-hand values a backend claims per atomic fetch-add,
- * computed once at startup (see StrategyCtlShmemInit).  When batching is
- * enabled it is one cache line's worth of hand advance, so concurrent
- * backends sweep non-overlapping, cache-line-sized runs of the pool; the
- * global sweep order is preserved (each buffer is still visited exactly once
- * per pass).  Batching is enabled only on multi-node NUMA hardware; otherwise
- * this stays 1 and the sweep is byte-identical to the stock clock.
- */
-static uint32 ClockSweepBatchSize = 1;
-
 /*
  * ClockSweepTick - Helper routine for StrategyGetBuffer()
  *
@@ -144,7 +143,7 @@ ClockSweepTick(void)
 		 * atomic operation per batch, reducing contention by the batch size.
 		 */
 		uint32		start;
-		uint32		batch_size = ClockSweepBatchSize;
+		uint32		batch_size = StrategyControl->batchSize;
 
 		start = pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer,
 										batch_size);
@@ -211,6 +210,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 	BufferDesc *buf;
 	int			bgwprocno;
 	int			trycounter;
+	bool		force_cool;
 
 	*from_ring = false;
 
@@ -261,12 +261,32 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 	 */
 	pg_atomic_fetch_add_u32(&StrategyControl->numBufferAllocs, 1);
 
-	/* Use the "clock sweep" algorithm to find a free buffer */
+	/*
+	 * Use the cooling-stage clock sweep to find a victim.
+	 *
+	 * A buffer is HOT (recently used) or COOL (an eviction candidate).  We
+	 * prefer to reclaim an already-COOL buffer and demote a HOT buffer to
+	 * COOL only once a full sweep has found no COOL victim (force_cool) -- so
+	 * an abundant supply of COOL/probationary pages (e.g. a scan) is drained
+	 * before the hot working set is cooled.  Newly loaded pages are admitted
+	 * COOL (see BufferAlloc), so scan resistance falls out of the algorithm.
+	 *
+	 * trycounter bounds the search.  Any tick that does not produce a victim
+	 * and does not make progress -- a pinned buffer, or a HOT buffer skipped
+	 * on a prefer-COOL pass -- decrements it.  Cooling a HOT buffer (under
+	 * force_cool) is progress and resets it.  When a full pass (NBuffers)
+	 * makes no progress we escalate to force_cool so the next pass cools HOT
+	 * buffers into victims; if a force_cool pass ALSO makes no progress every
+	 * buffer is pinned and we fail, matching the stock "no unpinned buffers
+	 * available" contract.
+	 */
 	trycounter = NBuffers;
+	force_cool = false;
 	for (;;)
 	{
 		uint64		old_buf_state;
 		uint64		local_buf_state;
+		bool		no_progress = false;
 
 		buf = GetBufferDescriptor(ClockSweepTick());
 
@@ -279,25 +299,10 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 		{
 			local_buf_state = old_buf_state;
 
-			/*
-			 * If the buffer is pinned or has a nonzero usage_count, we cannot
-			 * use it; decrement the usage_count (unless pinned) and keep
-			 * scanning.
-			 */
-
+			/* If the buffer is pinned we cannot use it; keep scanning. */
 			if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0)
 			{
-				if (--trycounter == 0)
-				{
-					/*
-					 * We've scanned all the buffers without making any state
-					 * changes, so all the buffers are pinned (or were when we
-					 * looked at them). We could hope that someone will free
-					 * one eventually, but it's probably better to fail than
-					 * to risk getting stuck in an infinite loop.
-					 */
-					elog(ERROR, "no unpinned buffers available");
-				}
+				no_progress = true;
 				break;
 			}
 
@@ -308,20 +313,43 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 				continue;
 			}
 
-			if (BUF_STATE_GET_USAGECOUNT(local_buf_state) != 0)
+			if (BUF_STATE_GET_COOLSTATE(local_buf_state) != BUF_COOLSTATE_COOL)
 			{
-				local_buf_state -= BUF_USAGECOUNT_ONE;
+				/*
+				 * HOT buffer.  Prefer a COOL victim: on a normal pass just
+				 * advance the hand (no progress).  Under force_cool demote it
+				 * HOT -> COOL -- the cooling tick, which IS progress.
+				 */
+				if (!force_cool)
+				{
+					no_progress = true;
+					break;			/* advance the hand, look for COOL */
+				}
+
+				local_buf_state &= ~BUF_USAGECOUNT_MASK;	/* HOT -> COOL, clear ref bit */
 
 				if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
 												   local_buf_state))
 				{
+					/*
+					 * Cooling a buffer is progress toward a victim, so reset the
+					 * counter and drop back out of force_cool.  Because we clear
+					 * force_cool after a single demotion, an all-HOT pool can take
+					 * up to ~2 full passes to yield a victim (one to discover no
+					 * COOL buffer exists, one to cool + reclaim).  That is still
+					 * within the stock clock's worst case (up to
+					 * BM_MAX_USAGE_COUNT+1 passes), and in practice the bgwriter
+					 * pre-cooling keeps a COOL victim available so force_cool
+					 * rarely fires at all.
+					 */
 					trycounter = NBuffers;
+					force_cool = false;
 					break;
 				}
 			}
 			else
 			{
-				/* pin the buffer if the CAS succeeds */
+				/* COOL and unpinned: claim it.  Pin if the CAS succeeds. */
 				local_buf_state += BUF_REFCOUNT_ONE;
 
 				if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
@@ -338,6 +366,21 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 				}
 			}
 		}
+
+		/*
+		 * A tick that made no progress toward a victim counts down trycounter.
+		 * A full unproductive pass escalates to force_cool (cool HOT buffers
+		 * into victims); a second unproductive full pass means everything is
+		 * pinned, so fail rather than spin forever.  (A failed CAS above is
+		 * neither progress nor a full miss: we simply retry the same buffer.)
+		 */
+		if (no_progress && --trycounter == 0)
+		{
+			if (force_cool)
+				elog(ERROR, "no unpinned buffers available");
+			force_cool = true;
+			trycounter = NBuffers;
+		}
 	}
 }
 
@@ -452,10 +495,10 @@ StrategyCtlShmemInit(void *arg)
 	 */
 	if (pg_numa_init() != -1 &&
 		pg_numa_get_max_node() >= 1)
-		ClockSweepBatchSize = Min(PG_CACHE_LINE_SIZE / (uint32) sizeof(uint32),
-								  (uint32) NBuffers);
+		StrategyControl->batchSize = Min(PG_CACHE_LINE_SIZE / (uint32) sizeof(uint32),
+										 (uint32) NBuffers);
 	else
-		ClockSweepBatchSize = 1;
+		StrategyControl->batchSize = 1;
 }
 
 
@@ -703,14 +746,13 @@ GetBufferFromRing(BufferAccessStrategy strategy, uint64 *buf_state)
 		/*
 		 * If the buffer is pinned we cannot use it under any circumstances.
 		 *
-		 * If usage_count is 0 or 1 then the buffer is fair game (we expect 1,
-		 * since our own previous usage of the ring element would have left it
-		 * there, but it might've been decremented by clock-sweep since then).
-		 * A higher usage_count indicates someone else has touched the buffer,
-		 * so we shouldn't re-use it.
+		 * With the cooling-state replacement the field holds only COOL or HOT,
+		 * so the stock "usage_count > 1 means another backend touched it"
+		 * heuristic no longer applies: a ring element is reusable whenever it
+		 * is unpinned.  (The whole ring mechanism is removed in a later patch;
+		 * scan resistance is now intrinsic to the sweep.)
 		 */
-		if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0
-			|| BUF_STATE_GET_USAGECOUNT(local_buf_state) > 1)
+		if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0)
 			break;
 
 		/* See equivalent code in PinBuffer() */
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 4870c8e13d0..21c08091c49 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -167,7 +167,7 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
 
 		buf_state = pg_atomic_read_u64(&bufHdr->state);
 		buf_state &= ~(BUF_FLAG_MASK | BUF_USAGECOUNT_MASK);
-		buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
+		buf_state |= BM_TAG_VALID;	/* admit COOL (probation) */
 		pg_atomic_unlocked_write_u64(&bufHdr->state, buf_state);
 
 		*foundPtr = false;
@@ -248,9 +248,10 @@ GetLocalVictimBuffer(void)
 		{
 			uint64		buf_state = pg_atomic_read_u64(&bufHdr->state);
 
-			if (BUF_STATE_GET_USAGECOUNT(buf_state) > 0)
+			if (BUF_STATE_GET_COOLSTATE(buf_state) != BUF_COOLSTATE_COOL)
 			{
-				buf_state -= BUF_USAGECOUNT_ONE;
+				/* HOT: give it a second chance, cool it and keep scanning. */
+				buf_state -= BUF_COOLSTATE_ONE;
 				pg_atomic_unlocked_write_u64(&bufHdr->state, buf_state);
 				trycounter = NLocBuffer;
 			}
@@ -454,7 +455,7 @@ ExtendBufferedRelLocal(BufferManagerRelation bmr,
 
 			victim_buf_hdr->tag = tag;
 
-			buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
+			buf_state |= BM_TAG_VALID;	/* admit COOL (probation) */
 
 			pg_atomic_unlocked_write_u64(&victim_buf_hdr->state, buf_state);
 
@@ -839,9 +840,9 @@ PinLocalBuffer(BufferDesc *buf_hdr, bool adjust_usagecount)
 		NLocalPinnedBuffers++;
 		buf_state += BUF_REFCOUNT_ONE;
 		if (adjust_usagecount &&
-			BUF_STATE_GET_USAGECOUNT(buf_state) < BM_MAX_USAGE_COUNT)
+			BUF_STATE_GET_COOLSTATE(buf_state) < BUF_COOLSTATE_HOT)
 		{
-			buf_state += BUF_USAGECOUNT_ONE;
+			buf_state += BUF_COOLSTATE_ONE;
 		}
 		pg_atomic_unlocked_write_u64(&buf_hdr->state, buf_state);
 
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index 2a1ffcc6c5c..89b11051577 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -67,6 +67,50 @@ StaticAssertDecl(BUF_REFCOUNT_BITS + BUF_USAGECOUNT_BITS + BUF_FLAG_BITS + BUF_L
 #define BUF_USAGECOUNT_ONE \
 	(UINT64CONST(1) << BUF_REFCOUNT_BITS)
 
+/*
+ * Cooling state (LeanStore / 2Q-A1 cooling-stage clock sweep).
+ *
+ * The field historically used for the 0..5 usage_count now holds a single
+ * cooling-state bit: HOT (recently accessed, not an eviction candidate) or
+ * COOL (an eviction candidate).  We reuse BUF_USAGECOUNT_ONE as the unit so
+ * the buffer-state bit geography -- refcount, flag, and lock offsets, and the
+ * 64-bit StaticAsserts -- is unchanged; only the meaning of the field and the
+ * instructions that touch it change.
+ *
+ * A demand-loaded page is admitted COOL (probation); a second access promotes
+ * it to HOT (the rescue).  The sweep prefers COOL victims and demotes HOT to
+ * COOL only when a full pass finds no COOL victim.  So a one-touch scan fills
+ * and drains the COOL stage without displacing the HOT working set -- scan
+ * resistance intrinsic to the replacement algorithm.
+ */
+#define BUF_COOLSTATE_COOL	0
+#define BUF_COOLSTATE_HOT	1
+#define BUF_COOLSTATE_ONE	BUF_USAGECOUNT_ONE
+
+/*
+ * Second-chance reference bit, bit 1 of the (former usagecount) field, one
+ * position above the cooling-state bit.  PinBuffer sets it on every access.
+ * The bgwriter's pre-cooling gives a HOT buffer a second chance: the first
+ * time it passes a HOT buffer whose ref bit is set, it clears the ref bit and
+ * leaves the buffer HOT; only a HOT buffer whose ref bit is already clear (not
+ * re-accessed since the previous bgwriter pass) is demoted to COOL.  This
+ * keeps genuinely-hot pages out of the COOL stage (protecting the working set
+ * from being cooled under scan pressure) while leaving the foreground sweep a
+ * single-pass search over the pre-staged COOL buffers.  A separate bit (not a
+ * count) so it stays a plain masked store under the header lock.
+ */
+#define BUF_REFBIT			(UINT64CONST(2) << BUF_REFCOUNT_BITS)
+#define BUF_STATE_GET_REFBIT(state)	(((state) & BUF_REFBIT) != 0)
+
+/*
+ * The cooling state occupies bit 0 and the reference bit occupies bit 1 of
+ * the (former usagecount) field, so the field must be at least 2 bits wide.
+ * Assert it here so a future change to BUF_USAGECOUNT_BITS cannot silently
+ * push BUF_REFBIT up into the flag bits.
+ */
+StaticAssertDecl(BUF_USAGECOUNT_BITS >= 2,
+				 "cooling state + reference bit need at least 2 bits in the usagecount field");
+
 /* flags related definitions */
 #define BUF_FLAG_SHIFT \
 	(BUF_REFCOUNT_BITS + BUF_USAGECOUNT_BITS)
@@ -92,6 +136,11 @@ StaticAssertDecl(BUF_REFCOUNT_BITS + BUF_USAGECOUNT_BITS + BUF_FLAG_BITS + BUF_L
 #define BUF_STATE_GET_USAGECOUNT(state) \
 	((uint32)(((state) & BUF_USAGECOUNT_MASK) >> BUF_USAGECOUNT_SHIFT))
 
+/* Cooling state (HOT/COOL) from buffer state -- bit 0 of the field only, so
+ * the second-chance ref bit (bit 1) does not perturb the HOT/COOL test. */
+#define BUF_STATE_GET_COOLSTATE(state) \
+	((uint32) (((state) >> BUF_USAGECOUNT_SHIFT) & 1))
+
 /*
  * Flags for buffer descriptors
  *
@@ -134,17 +183,15 @@ StaticAssertDecl(MAX_BACKENDS_BITS <= (BUF_LOCK_BITS - 2),
 
 
 /*
- * The maximum allowed value of usage_count represents a tradeoff between
- * accuracy and speed of the clock-sweep buffer management algorithm.  A
- * large value (comparable to NBuffers) would approximate LRU semantics.
- * But it can take as many as BM_MAX_USAGE_COUNT+1 complete cycles of the
- * clock-sweep hand to find a free buffer, so in practice we don't want the
- * value to be very large.
+ * The cooling state is a single bit (HOT/COOL); the maximum value stored in
+ * the field is therefore BUF_COOLSTATE_HOT.  Retained under the historical
+ * name BM_MAX_USAGE_COUNT so the pin fast path ("promote unless already at
+ * max") reads naturally.
  */
-#define BM_MAX_USAGE_COUNT	5
+#define BM_MAX_USAGE_COUNT	BUF_COOLSTATE_HOT
 
 StaticAssertDecl(BM_MAX_USAGE_COUNT < (UINT64CONST(1) << BUF_USAGECOUNT_BITS),
-				 "BM_MAX_USAGE_COUNT doesn't fit in BUF_USAGECOUNT_BITS bits");
+				 "cooling state doesn't fit in BUF_USAGECOUNT_BITS bits");
 
 /*
  * Buffer tag identifies which disk block the buffer contains.
-- 
2.51.2



  [text/x-patch] v4-0003-Remove-BufferAccessStrategy-scan-resistance-is-no.patch (177.4K, ../../76c33898-3baa-4f7b-8b72-57dd390fcffe@app.fastmail.com/4-v4-0003-Remove-BufferAccessStrategy-scan-resistance-is-no.patch)
  download | inline diff:
From 961b81d8b84b945f96a51f915ac28512890c0c48 Mon Sep 17 00:00:00 2001
From: Greg Burd <greg@burd.me>
Date: Tue, 7 Jul 2026 08:45:39 -0400
Subject: [PATCH v4 3/3] Remove BufferAccessStrategy; scan resistance is now
 intrinsic

The cooling-stage evictor admits demand-loaded pages COOL and promotes them to
HOT only on a second access, so a one-touch sequential scan fills and drains
the COOL stage without displacing the hot working set.  Scan resistance is
therefore a property of the replacement algorithm itself, and the
BufferAccessStrategy ring buffers that previously provided it are dead weight.
Remove them end to end.

Deleted:
  - the BufferAccessStrategy type and the BufferAccessStrategyType enum
    (BAS_NORMAL/BULKREAD/BULKWRITE/VACUUM);
  - the ring machinery in freelist.c (GetAccessStrategy[WithSize],
    GetAccessStrategyBufferCount, GetAccessStrategyPinLimit,
    FreeAccessStrategy, GetBufferFromRing, AddBufferToRing,
    StrategyRejectBuffer, IOContextForStrategy);
  - the strategy parameter from ReadBufferExtended, ReadBufferWithoutRelcache,
    the ExtendBufferedRel* family, StrategyGetBuffer, read_stream_begin_*,
    and every scan/vacuum/analyze/index-AM caller;
  - the strategy fields on HeapScanDescData, IndexScanDescData,
    BulkInsertStateData, ReadBuffersOperation, and ReadStream;
  - _hash_getbuf_with_strategy (identical to _hash_getbuf without a strategy).

pg_stat_io's per-strategy IO contexts collapse: IOCONTEXT_BULKREAD,
IOCONTEXT_BULKWRITE and IOCONTEXT_VACUUM are removed, leaving IOCONTEXT_INIT
and IOCONTEXT_NORMAL.  IOOP_REUSE only ever occurred while recycling a ring
buffer, so it is no longer tracked; GetVictimBuffer counts IOOP_EVICT only.

The vacuum_buffer_usage_limit GUC and the VACUUM/ANALYZE (BUFFER_USAGE_LIMIT
...) option are removed, along with the VacuumBufferUsageLimit global, the
ring-size plumbing through VacuumParams and parallel vacuum, and vacuumdb's
--buffer-usage-limit client option.  read_stream's per-backend pin budget is
now enforced solely by GetPinLimit()/GetLocalPinLimit(), which already applied
and is unchanged for the (formerly universal) no-strategy case.

Documentation and the stats/amcheck regression tests are updated to drop the
removed contexts and options.
---
 contrib/amcheck/expected/check_heap.out       |  16 +-
 contrib/amcheck/sql/check_heap.sql            |  16 +-
 contrib/amcheck/verify_gin.c                  |  18 +-
 contrib/amcheck/verify_heapam.c               |   3 -
 contrib/amcheck/verify_nbtree.c               |  11 +-
 contrib/bloom/blscan.c                        |   8 -
 contrib/bloom/blutils.c                       |   4 +-
 contrib/bloom/blvacuum.c                      |   2 -
 contrib/pageinspect/rawpage.c                 |   2 +-
 contrib/pg_prewarm/autoprewarm.c              |   1 -
 contrib/pg_prewarm/pg_prewarm.c               |   1 -
 contrib/pg_visibility/pg_visibility.c         |   7 +-
 contrib/pgstattuple/pgstatapprox.c            |   3 -
 contrib/pgstattuple/pgstatindex.c             |   9 +-
 contrib/pgstattuple/pgstattuple.c             |  37 +-
 doc/src/sgml/config.sgml                      |  30 --
 doc/src/sgml/monitoring.sgml                  |  38 +-
 doc/src/sgml/ref/analyze.sgml                 |  21 -
 doc/src/sgml/ref/vacuum.sgml                  |  25 -
 src/backend/access/brin/brin.c                |  11 +-
 src/backend/access/brin/brin_revmap.c         |   2 +-
 src/backend/access/gin/gininsert.c            |   4 +-
 src/backend/access/gin/ginutil.c              |   2 +-
 src/backend/access/gin/ginvacuum.c            |  17 +-
 src/backend/access/gist/gist.c                |   2 +-
 src/backend/access/gist/gistutil.c            |   2 +-
 src/backend/access/gist/gistvacuum.c          |  10 +-
 src/backend/access/hash/hash.c                |  13 +-
 src/backend/access/hash/hashovfl.c            |  55 +--
 src/backend/access/hash/hashpage.c            |  40 +-
 src/backend/access/heap/heapam.c              |  31 +-
 src/backend/access/heap/heapam_handler.c      |   4 +-
 src/backend/access/heap/hio.c                 |  13 +-
 src/backend/access/heap/vacuumlazy.c          |  22 +-
 src/backend/access/heap/visibilitymap.c       |   4 +-
 src/backend/access/nbtree/nbtpage.c           |   2 +-
 src/backend/access/nbtree/nbtree.c            |   4 +-
 src/backend/access/spgist/spgutils.c          |   2 +-
 src/backend/access/spgist/spgvacuum.c         |   4 +-
 src/backend/access/transam/xloginsert.c       |   2 +-
 src/backend/access/transam/xlogutils.c        |   3 +-
 src/backend/catalog/index.c                   |   1 -
 src/backend/commands/analyze.c                |   7 +-
 src/backend/commands/dbcommands.c             |   6 +-
 src/backend/commands/repack.c                 |   2 +-
 src/backend/commands/sequence.c               |   2 +-
 src/backend/commands/vacuum.c                 | 118 +----
 src/backend/commands/vacuumparallel.c         |  21 +-
 src/backend/postmaster/autovacuum.c           |  29 +-
 src/backend/postmaster/datachecksum_state.c   |  33 +-
 src/backend/storage/aio/read_stream.c         |  17 +-
 src/backend/storage/buffer/README             |  44 --
 src/backend/storage/buffer/bufmgr.c           | 185 +++-----
 src/backend/storage/buffer/freelist.c         | 434 +-----------------
 src/backend/storage/freespace/freespace.c     |   4 +-
 src/backend/storage/smgr/md.c                 |  11 +-
 src/backend/utils/activity/pgstat_io.c        |  45 +-
 src/backend/utils/init/globals.c              |   1 -
 src/backend/utils/misc/guc_parameters.dat     |  10 -
 src/backend/utils/misc/postgresql.conf.sample |   3 -
 src/bin/scripts/vacuumdb.c                    |  13 -
 src/bin/scripts/vacuuming.c                   |  21 -
 src/bin/scripts/vacuuming.h                   |   1 -
 src/include/access/genam.h                    |   1 -
 src/include/access/hash.h                     |   9 +-
 src/include/access/heapam.h                   |   4 +-
 src/include/access/hio.h                      |   1 -
 src/include/access/tableam.h                  |   8 +-
 src/include/commands/vacuum.h                 |   8 +-
 src/include/miscadmin.h                       |   9 -
 src/include/pgstat.h                          |   5 +-
 src/include/storage/buf.h                     |   7 -
 src/include/storage/buf_internals.h           |   6 +-
 src/include/storage/bufmgr.h                  |  32 +-
 src/include/storage/read_stream.h             |   5 +-
 src/include/utils/guc_hooks.h                 |   2 -
 src/test/modules/test_aio/test_aio.c          |   7 +-
 src/test/regress/expected/stats.out           | 120 +----
 src/test/regress/expected/vacuum.out          |  19 -
 src/test/regress/sql/stats.sql                |  42 --
 src/test/regress/sql/vacuum.sql               |  15 -
 src/tools/pgindent/typedefs.list              |   2 -
 82 files changed, 252 insertions(+), 1559 deletions(-)

diff --git a/contrib/amcheck/expected/check_heap.out b/contrib/amcheck/expected/check_heap.out
index 979e5e84e72..569b0202f1c 100644
--- a/contrib/amcheck/expected/check_heap.out
+++ b/contrib/amcheck/expected/check_heap.out
@@ -67,11 +67,11 @@ INSERT INTO heaptest (a, b)
 	(SELECT gs, repeat('x', gs)
 		FROM generate_series(1,50) gs);
 -- pg_stat_io test:
--- verify_heapam always uses a BAS_BULKREAD BufferAccessStrategy, whereas a
--- sequential scan does so only if the table is large enough when compared to
--- shared buffers (see initscan()). CREATE DATABASE ... also unconditionally
--- uses a BAS_BULKREAD strategy, but we have chosen to use a tablespace and
--- verify_heapam to provide coverage instead of adding another expensive
+-- verify_heapam reads the heap through the buffer manager; with the
+-- cooling-stage clock sweep there are no per-strategy IO contexts, so the
+-- reads are counted in the 'normal' context.  CREATE DATABASE ... likewise
+-- reads through the normal context, but we have chosen to use a tablespace
+-- and verify_heapam to provide coverage instead of adding another expensive
 -- operation to the main regression test suite.
 --
 -- Create an alternative tablespace and move the heaptest table to it, causing
@@ -83,7 +83,7 @@ INSERT INTO heaptest (a, b)
 SET allow_in_place_tablespaces = true;
 CREATE TABLESPACE regress_test_stats_tblspc LOCATION '';
 SELECT sum(reads) AS stats_bulkreads_before
-  FROM pg_stat_io WHERE context = 'bulkread' \gset
+  FROM pg_stat_io WHERE context = 'normal' AND object = 'relation' \gset
 BEGIN;
 ALTER TABLE heaptest SET TABLESPACE regress_test_stats_tblspc;
 -- Check that valid options are not rejected nor corruption reported
@@ -111,7 +111,7 @@ SELECT * FROM verify_heapam(relation := 'heaptest', startblock := 0, endblock :=
 COMMIT;
 -- verify_heapam should have read in the page written out by
 --   ALTER TABLE ... SET TABLESPACE ...
--- causing an additional bulkread, which should be reflected in pg_stat_io.
+-- causing additional reads, which should be reflected in pg_stat_io.
 SELECT pg_stat_force_next_flush();
  pg_stat_force_next_flush 
 --------------------------
@@ -119,7 +119,7 @@ SELECT pg_stat_force_next_flush();
 (1 row)
 
 SELECT sum(reads) AS stats_bulkreads_after
-  FROM pg_stat_io WHERE context = 'bulkread' \gset
+  FROM pg_stat_io WHERE context = 'normal' AND object = 'relation' \gset
 SELECT :stats_bulkreads_after > :stats_bulkreads_before;
  ?column? 
 ----------
diff --git a/contrib/amcheck/sql/check_heap.sql b/contrib/amcheck/sql/check_heap.sql
index 1745bae634e..61f710dbeda 100644
--- a/contrib/amcheck/sql/check_heap.sql
+++ b/contrib/amcheck/sql/check_heap.sql
@@ -27,11 +27,11 @@ INSERT INTO heaptest (a, b)
 		FROM generate_series(1,50) gs);
 
 -- pg_stat_io test:
--- verify_heapam always uses a BAS_BULKREAD BufferAccessStrategy, whereas a
--- sequential scan does so only if the table is large enough when compared to
--- shared buffers (see initscan()). CREATE DATABASE ... also unconditionally
--- uses a BAS_BULKREAD strategy, but we have chosen to use a tablespace and
--- verify_heapam to provide coverage instead of adding another expensive
+-- verify_heapam reads the heap through the buffer manager; with the
+-- cooling-stage clock sweep there are no per-strategy IO contexts, so the
+-- reads are counted in the 'normal' context.  CREATE DATABASE ... likewise
+-- reads through the normal context, but we have chosen to use a tablespace
+-- and verify_heapam to provide coverage instead of adding another expensive
 -- operation to the main regression test suite.
 --
 -- Create an alternative tablespace and move the heaptest table to it, causing
@@ -43,7 +43,7 @@ INSERT INTO heaptest (a, b)
 SET allow_in_place_tablespaces = true;
 CREATE TABLESPACE regress_test_stats_tblspc LOCATION '';
 SELECT sum(reads) AS stats_bulkreads_before
-  FROM pg_stat_io WHERE context = 'bulkread' \gset
+  FROM pg_stat_io WHERE context = 'normal' AND object = 'relation' \gset
 BEGIN;
 ALTER TABLE heaptest SET TABLESPACE regress_test_stats_tblspc;
 -- Check that valid options are not rejected nor corruption reported
@@ -56,10 +56,10 @@ COMMIT;
 
 -- verify_heapam should have read in the page written out by
 --   ALTER TABLE ... SET TABLESPACE ...
--- causing an additional bulkread, which should be reflected in pg_stat_io.
+-- causing additional reads, which should be reflected in pg_stat_io.
 SELECT pg_stat_force_next_flush();
 SELECT sum(reads) AS stats_bulkreads_after
-  FROM pg_stat_io WHERE context = 'bulkread' \gset
+  FROM pg_stat_io WHERE context = 'normal' AND object = 'relation' \gset
 SELECT :stats_bulkreads_after > :stats_bulkreads_before;
 
 CREATE ROLE regress_heaptest_role;
diff --git a/contrib/amcheck/verify_gin.c b/contrib/amcheck/verify_gin.c
index fa06689ed5b..ef2e64475c6 100644
--- a/contrib/amcheck/verify_gin.c
+++ b/contrib/amcheck/verify_gin.c
@@ -63,8 +63,7 @@ static void gin_check_parent_keys_consistency(Relation rel,
 static void check_index_page(Relation rel, Buffer buffer, BlockNumber blockNo);
 static IndexTuple gin_refind_parent(Relation rel,
 									BlockNumber parentblkno,
-									BlockNumber childblkno,
-									BufferAccessStrategy strategy);
+									BlockNumber childblkno);
 static ItemId PageGetItemIdCareful(Relation rel, BlockNumber block, Page page,
 								   OffsetNumber offset);
 
@@ -133,7 +132,6 @@ ginReadTupleWithoutState(IndexTuple itup, int *nitems)
 static void
 gin_check_posting_tree_parent_keys_consistency(Relation rel, BlockNumber posting_tree_root)
 {
-	BufferAccessStrategy strategy = GetAccessStrategy(BAS_BULKREAD);
 	GinPostingTreeScanItem *stack;
 	MemoryContext mctx;
 	MemoryContext oldcontext;
@@ -171,8 +169,7 @@ gin_check_posting_tree_parent_keys_consistency(Relation rel, BlockNumber posting
 
 		CHECK_FOR_INTERRUPTS();
 
-		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno,
-									RBM_NORMAL, strategy);
+		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno, RBM_NORMAL);
 		LockBuffer(buffer, GIN_SHARE);
 		page = BufferGetPage(buffer);
 
@@ -391,7 +388,6 @@ gin_check_parent_keys_consistency(Relation rel,
 								  void *callback_state,
 								  bool readonly)
 {
-	BufferAccessStrategy strategy = GetAccessStrategy(BAS_BULKREAD);
 	GinScanItem *stack;
 	MemoryContext mctx;
 	MemoryContext oldcontext;
@@ -430,8 +426,7 @@ gin_check_parent_keys_consistency(Relation rel,
 
 		CHECK_FOR_INTERRUPTS();
 
-		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno,
-									RBM_NORMAL, strategy);
+		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno, RBM_NORMAL);
 		LockBuffer(buffer, GIN_SHARE);
 		page = BufferGetPage(buffer);
 		maxoff = PageGetMaxOffsetNumber(page);
@@ -567,7 +562,7 @@ gin_check_parent_keys_consistency(Relation rel,
 					 */
 					pfree(stack->parenttup);
 					stack->parenttup = gin_refind_parent(rel, stack->parentblk,
-														 stack->blkno, strategy);
+														 stack->blkno);
 
 					/* We found it - make a final check before failing */
 					if (!stack->parenttup)
@@ -719,7 +714,7 @@ check_index_page(Relation rel, Buffer buffer, BlockNumber blockNo)
  */
 static IndexTuple
 gin_refind_parent(Relation rel, BlockNumber parentblkno,
-				  BlockNumber childblkno, BufferAccessStrategy strategy)
+				  BlockNumber childblkno)
 {
 	Buffer		parentbuf;
 	Page		parentpage;
@@ -727,8 +722,7 @@ gin_refind_parent(Relation rel, BlockNumber parentblkno,
 				parent_maxoff;
 	IndexTuple	result = NULL;
 
-	parentbuf = ReadBufferExtended(rel, MAIN_FORKNUM, parentblkno, RBM_NORMAL,
-								   strategy);
+	parentbuf = ReadBufferExtended(rel, MAIN_FORKNUM, parentblkno, RBM_NORMAL);
 
 	LockBuffer(parentbuf, GIN_SHARE);
 	parentpage = BufferGetPage(parentbuf);
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 20ff58aa782..e4336eb5fc2 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -126,7 +126,6 @@ typedef struct HeapCheckContext
 	 * recent block in the buffer yielded by the read stream API.
 	 */
 	BlockNumber blkno;
-	BufferAccessStrategy bstrategy;
 	Buffer		buffer;
 	Page		page;
 
@@ -374,7 +373,6 @@ verify_heapam(PG_FUNCTION_ARGS)
 		PG_RETURN_NULL();
 	}
 
-	ctx.bstrategy = GetAccessStrategy(BAS_BULKREAD);
 	ctx.buffer = InvalidBuffer;
 	ctx.page = NULL;
 
@@ -472,7 +470,6 @@ verify_heapam(PG_FUNCTION_ARGS)
 	}
 
 	stream = read_stream_begin_relation(stream_flags,
-										ctx.bstrategy,
 										ctx.rel,
 										MAIN_FORKNUM,
 										stream_cb,
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 3ef2d66f826..18cd8928fdd 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -88,8 +88,6 @@ typedef struct BtreeCheckState
 	bool		checkunique;
 	/* Per-page context */
 	MemoryContext targetcontext;
-	/* Buffer access strategy */
-	BufferAccessStrategy checkstrategy;
 
 	/*
 	 * Info for uniqueness checking. Fill this field and the one below once
@@ -490,7 +488,6 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	state->targetcontext = AllocSetContextCreate(CurrentMemoryContext,
 												 "amcheck context",
 												 ALLOCSET_DEFAULT_SIZES);
-	state->checkstrategy = GetAccessStrategy(BAS_BULKREAD);
 
 	/* Get true root block from meta-page */
 	metapage = palloc_btree_page(state, BTREE_METAPAGE);
@@ -1115,7 +1112,7 @@ bt_recheck_sibling_links(BtreeCheckState *state,
 
 		/* Couple locks in the usual order for nbtree:  Left to right */
 		lbuf = ReadBufferExtended(state->rel, MAIN_FORKNUM, leftcurrent,
-								  RBM_NORMAL, state->checkstrategy);
+								  RBM_NORMAL);
 		LockBuffer(lbuf, BT_READ);
 		_bt_checkpage(state->rel, lbuf);
 		page = BufferGetPage(lbuf);
@@ -1138,8 +1135,7 @@ bt_recheck_sibling_links(BtreeCheckState *state,
 		if (newtargetblock != leftcurrent)
 		{
 			newtargetbuf = ReadBufferExtended(state->rel, MAIN_FORKNUM,
-											  newtargetblock, RBM_NORMAL,
-											  state->checkstrategy);
+											  newtargetblock, RBM_NORMAL);
 			LockBuffer(newtargetbuf, BT_READ);
 			_bt_checkpage(state->rel, newtargetbuf);
 			page = BufferGetPage(newtargetbuf);
@@ -3300,8 +3296,7 @@ palloc_btree_page(BtreeCheckState *state, BlockNumber blocknum)
 	 * We copy the page into local storage to avoid holding pin on the buffer
 	 * longer than we must.
 	 */
-	buffer = ReadBufferExtended(state->rel, MAIN_FORKNUM, blocknum, RBM_NORMAL,
-								state->checkstrategy);
+	buffer = ReadBufferExtended(state->rel, MAIN_FORKNUM, blocknum, RBM_NORMAL);
 	LockBuffer(buffer, BT_READ);
 
 	/*
diff --git a/contrib/bloom/blscan.c b/contrib/bloom/blscan.c
index 1a0e42021ec..ed62d5c04e6 100644
--- a/contrib/bloom/blscan.c
+++ b/contrib/bloom/blscan.c
@@ -80,7 +80,6 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BlockNumber blkno,
 				npages;
 	int			i;
-	BufferAccessStrategy bas;
 	BloomScanOpaque so = (BloomScanOpaque) scan->opaque;
 	BlockRangeReadStreamPrivate p;
 	ReadStream *stream;
@@ -113,11 +112,6 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		}
 	}
 
-	/*
-	 * We're going to read the whole index. This is why we use appropriate
-	 * buffer access strategy.
-	 */
-	bas = GetAccessStrategy(BAS_BULKREAD);
 	npages = RelationGetNumberOfBlocks(scan->indexRelation);
 	pgstat_count_index_scan(scan->indexRelation);
 	if (scan->instrument)
@@ -133,7 +127,6 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										bas,
 										scan->indexRelation,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -184,7 +177,6 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	Assert(read_stream_next_buffer(stream, NULL) == InvalidBuffer);
 	read_stream_end(stream);
-	FreeAccessStrategy(bas);
 
 	return ntids;
 }
diff --git a/contrib/bloom/blutils.c b/contrib/bloom/blutils.c
index 5111cdc6dd6..6c1ec15c59c 100644
--- a/contrib/bloom/blutils.c
+++ b/contrib/bloom/blutils.c
@@ -392,7 +392,7 @@ BloomNewBuffer(Relation index)
 	}
 
 	/* Must extend the file */
-	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM,
 							   EB_LOCK_FIRST);
 
 	return buffer;
@@ -460,7 +460,7 @@ BloomInitMetapage(Relation index, ForkNumber forknum)
 	 * block number 0 (BLOOM_METAPAGE_BLKNO).  No need to hold the extension
 	 * lock because there cannot be concurrent inserters yet.
 	 */
-	metaBuffer = ReadBufferExtended(index, forknum, P_NEW, RBM_NORMAL, NULL);
+	metaBuffer = ReadBufferExtended(index, forknum, P_NEW, RBM_NORMAL);
 	LockBuffer(metaBuffer, BUFFER_LOCK_EXCLUSIVE);
 	Assert(BufferGetBlockNumber(metaBuffer) == BLOOM_METAPAGE_BLKNO);
 
diff --git a/contrib/bloom/blvacuum.c b/contrib/bloom/blvacuum.c
index 6beb1c20ebb..08d7705e365 100644
--- a/contrib/bloom/blvacuum.c
+++ b/contrib/bloom/blvacuum.c
@@ -66,7 +66,6 @@ blbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										index,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -219,7 +218,6 @@ blvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										index,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/contrib/pageinspect/rawpage.c b/contrib/pageinspect/rawpage.c
index d136593edb2..bbe8edf9161 100644
--- a/contrib/pageinspect/rawpage.c
+++ b/contrib/pageinspect/rawpage.c
@@ -188,7 +188,7 @@ get_raw_page_internal(text *relname, ForkNumber forknum, BlockNumber blkno)
 
 	/* Take a verbatim copy of the page */
 
-	buf = ReadBufferExtended(rel, forknum, blkno, RBM_NORMAL, NULL);
+	buf = ReadBufferExtended(rel, forknum, blkno, RBM_NORMAL);
 	LockBuffer(buf, BUFFER_LOCK_SHARE);
 
 	memcpy(raw_page_data, BufferGetPage(buf), BLCKSZ);
diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index deb4c2671b5..33ad88ea8b1 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -630,7 +630,6 @@ autoprewarm_database_main(Datum main_arg)
 			stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 												READ_STREAM_DEFAULT |
 												READ_STREAM_USE_BATCHING,
-												NULL,
 												rel,
 												p.forknum,
 												apw_read_stream_next_block,
diff --git a/contrib/pg_prewarm/pg_prewarm.c b/contrib/pg_prewarm/pg_prewarm.c
index c2716086693..716a6754a7e 100644
--- a/contrib/pg_prewarm/pg_prewarm.c
+++ b/contrib/pg_prewarm/pg_prewarm.c
@@ -251,7 +251,6 @@ pg_prewarm(PG_FUNCTION_ARGS)
 		stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 											READ_STREAM_FULL |
 											READ_STREAM_USE_BATCHING,
-											NULL,
 											rel,
 											forkNumber,
 											block_range_read_stream_cb,
diff --git a/contrib/pg_visibility/pg_visibility.c b/contrib/pg_visibility/pg_visibility.c
index dfab0b64cf5..ccb829a09eb 100644
--- a/contrib/pg_visibility/pg_visibility.c
+++ b/contrib/pg_visibility/pg_visibility.c
@@ -488,7 +488,6 @@ collect_visibility_data(Oid relid, bool include_pd)
 	vbits	   *info;
 	BlockNumber blkno;
 	Buffer		vmbuffer = InvalidBuffer;
-	BufferAccessStrategy bstrategy = GetAccessStrategy(BAS_BULKREAD);
 	BlockRangeReadStreamPrivate p;
 	ReadStream *stream = NULL;
 
@@ -514,7 +513,6 @@ collect_visibility_data(Oid relid, bool include_pd)
 		 */
 		stream = read_stream_begin_relation(READ_STREAM_FULL |
 											READ_STREAM_USE_BATCHING,
-											bstrategy,
 											rel,
 											MAIN_FORKNUM,
 											block_range_read_stream_cb,
@@ -538,8 +536,7 @@ collect_visibility_data(Oid relid, bool include_pd)
 
 		/*
 		 * Page-level data requires reading every block, so only get it if the
-		 * caller needs it.  Use a buffer access strategy, too, to prevent
-		 * cache-trashing.
+		 * caller needs it.
 		 */
 		if (include_pd)
 		{
@@ -700,7 +697,6 @@ collect_corrupt_items(Oid relid, bool all_visible, bool all_frozen)
 	Relation	rel;
 	corrupt_items *items;
 	Buffer		vmbuffer = InvalidBuffer;
-	BufferAccessStrategy bstrategy = GetAccessStrategy(BAS_BULKREAD);
 	TransactionId OldestXmin = InvalidTransactionId;
 	struct collect_corrupt_items_read_stream_private p;
 	ReadStream *stream;
@@ -734,7 +730,6 @@ collect_corrupt_items(Oid relid, bool all_visible, bool all_frozen)
 	p.all_frozen = all_frozen;
 	p.all_visible = all_visible;
 	stream = read_stream_begin_relation(READ_STREAM_FULL,
-										bstrategy,
 										rel,
 										MAIN_FORKNUM,
 										collect_corrupt_items_read_stream_next_block,
diff --git a/contrib/pgstattuple/pgstatapprox.c b/contrib/pgstattuple/pgstatapprox.c
index 21e0b50fb4b..8e17d48991e 100644
--- a/contrib/pgstattuple/pgstatapprox.c
+++ b/contrib/pgstattuple/pgstatapprox.c
@@ -116,13 +116,11 @@ static void
 statapprox_heap(Relation rel, output_type *stat)
 {
 	BlockNumber nblocks;
-	BufferAccessStrategy bstrategy;
 	TransactionId OldestXmin;
 	StatApproxReadStreamPrivate p;
 	ReadStream *stream;
 
 	OldestXmin = GetOldestNonRemovableTransactionId(rel);
-	bstrategy = GetAccessStrategy(BAS_BULKREAD);
 
 	nblocks = RelationGetNumberOfBlocks(rel);
 
@@ -141,7 +139,6 @@ statapprox_heap(Relation rel, output_type *stat)
 	 * caution.
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_FULL,
-										bstrategy,
 										rel,
 										MAIN_FORKNUM,
 										statapprox_heap_read_stream_next,
diff --git a/contrib/pgstattuple/pgstatindex.c b/contrib/pgstattuple/pgstatindex.c
index 8951ad0aac4..a0922b386a0 100644
--- a/contrib/pgstattuple/pgstatindex.c
+++ b/contrib/pgstattuple/pgstatindex.c
@@ -217,7 +217,6 @@ pgstatindex_impl(Relation rel, FunctionCallInfo fcinfo)
 	BlockNumber nblocks;
 	BlockNumber blkno;
 	BTIndexStat indexStat;
-	BufferAccessStrategy bstrategy = GetAccessStrategy(BAS_BULKREAD);
 	BlockRangeReadStreamPrivate p;
 	ReadStream *stream;
 	BlockNumber startblk;
@@ -254,7 +253,7 @@ pgstatindex_impl(Relation rel, FunctionCallInfo fcinfo)
 	 * Read metapage
 	 */
 	{
-		Buffer		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, 0, RBM_NORMAL, bstrategy);
+		Buffer		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, 0, RBM_NORMAL);
 		Page		page = BufferGetPage(buffer);
 		BTMetaPageData *metad = BTPageGetMeta(page);
 
@@ -291,7 +290,6 @@ pgstatindex_impl(Relation rel, FunctionCallInfo fcinfo)
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										bstrategy,
 										rel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -612,7 +610,6 @@ pgstathashindex(PG_FUNCTION_ARGS)
 	BlockNumber blkno;
 	Relation	rel;
 	HashIndexStat stats;
-	BufferAccessStrategy bstrategy;
 	HeapTuple	tuple;
 	TupleDesc	tupleDesc;
 	Datum		values[8];
@@ -665,9 +662,6 @@ pgstathashindex(PG_FUNCTION_ARGS)
 	/* Get the current relation length */
 	nblocks = RelationGetNumberOfBlocks(rel);
 
-	/* prepare access strategy for this index */
-	bstrategy = GetAccessStrategy(BAS_BULKREAD);
-
 	/* Scan all blocks except the metapage (0th page) using streaming reads */
 	startblk = HASH_METAPAGE + 1;
 
@@ -680,7 +674,6 @@ pgstathashindex(PG_FUNCTION_ARGS)
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										bstrategy,
 										rel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/contrib/pgstattuple/pgstattuple.c b/contrib/pgstattuple/pgstattuple.c
index 6a7f8cb4a7c..90f7938e953 100644
--- a/contrib/pgstattuple/pgstattuple.c
+++ b/contrib/pgstattuple/pgstattuple.c
@@ -64,22 +64,18 @@ typedef struct pgstattuple_type
 	uint64		free_space;		/* free/reusable space in bytes */
 } pgstattuple_type;
 
-typedef void (*pgstat_page) (pgstattuple_type *, Relation, BlockNumber,
-							 BufferAccessStrategy);
+typedef void (*pgstat_page) (pgstattuple_type *, Relation, BlockNumber);
 
 static Datum build_pgstattuple_type(pgstattuple_type *stat,
 									FunctionCallInfo fcinfo);
 static Datum pgstat_relation(Relation rel, FunctionCallInfo fcinfo);
 static Datum pgstat_heap(Relation rel, FunctionCallInfo fcinfo);
 static void pgstat_btree_page(pgstattuple_type *stat,
-							  Relation rel, BlockNumber blkno,
-							  BufferAccessStrategy bstrategy);
+							  Relation rel, BlockNumber blkno);
 static void pgstat_hash_page(pgstattuple_type *stat,
-							 Relation rel, BlockNumber blkno,
-							 BufferAccessStrategy bstrategy);
+							 Relation rel, BlockNumber blkno);
 static void pgstat_gist_page(pgstattuple_type *stat,
-							 Relation rel, BlockNumber blkno,
-							 BufferAccessStrategy bstrategy);
+							 Relation rel, BlockNumber blkno);
 static Datum pgstat_index(Relation rel, BlockNumber start,
 						  pgstat_page pagefn, FunctionCallInfo fcinfo);
 static void pgstat_index_page(pgstattuple_type *stat, Page page,
@@ -376,7 +372,7 @@ pgstat_heap(Relation rel, FunctionCallInfo fcinfo)
 			CHECK_FOR_INTERRUPTS();
 
 			buffer = ReadBufferExtended(rel, MAIN_FORKNUM, block,
-										RBM_NORMAL, hscan->rs_strategy);
+										RBM_NORMAL);
 			LockBuffer(buffer, BUFFER_LOCK_SHARE);
 			stat.free_space += PageGetExactFreeSpace(BufferGetPage(buffer));
 			UnlockReleaseBuffer(buffer);
@@ -389,7 +385,7 @@ pgstat_heap(Relation rel, FunctionCallInfo fcinfo)
 		CHECK_FOR_INTERRUPTS();
 
 		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, block,
-									RBM_NORMAL, hscan->rs_strategy);
+									RBM_NORMAL);
 		LockBuffer(buffer, BUFFER_LOCK_SHARE);
 		stat.free_space += PageGetExactFreeSpace(BufferGetPage(buffer));
 		UnlockReleaseBuffer(buffer);
@@ -408,13 +404,12 @@ pgstat_heap(Relation rel, FunctionCallInfo fcinfo)
  * pgstat_btree_page -- check tuples in a btree page
  */
 static void
-pgstat_btree_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
-				  BufferAccessStrategy bstrategy)
+pgstat_btree_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno)
 {
 	Buffer		buf;
 	Page		page;
 
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, bstrategy);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 	LockBuffer(buf, BT_READ);
 	page = BufferGetPage(buf);
 
@@ -452,13 +447,12 @@ pgstat_btree_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
  * pgstat_hash_page -- check tuples in a hash page
  */
 static void
-pgstat_hash_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
-				 BufferAccessStrategy bstrategy)
+pgstat_hash_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno)
 {
 	Buffer		buf;
 	Page		page;
 
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, bstrategy);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 	LockBuffer(buf, HASH_READ);
 	page = BufferGetPage(buf);
 
@@ -500,13 +494,12 @@ pgstat_hash_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
  * pgstat_gist_page -- check tuples in a gist page
  */
 static void
-pgstat_gist_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
-				 BufferAccessStrategy bstrategy)
+pgstat_gist_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno)
 {
 	Buffer		buf;
 	Page		page;
 
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, bstrategy);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 	LockBuffer(buf, GIST_SHARE);
 	page = BufferGetPage(buf);
 	if (PageIsNew(page))
@@ -539,12 +532,8 @@ pgstat_index(Relation rel, BlockNumber start, pgstat_page pagefn,
 {
 	BlockNumber nblocks;
 	BlockNumber blkno;
-	BufferAccessStrategy bstrategy;
 	pgstattuple_type stat = {0};
 
-	/* prepare access strategy for this index */
-	bstrategy = GetAccessStrategy(BAS_BULKREAD);
-
 	blkno = start;
 	for (;;)
 	{
@@ -565,7 +554,7 @@ pgstat_index(Relation rel, BlockNumber start, pgstat_page pagefn,
 		{
 			CHECK_FOR_INTERRUPTS();
 
-			pagefn(&stat, rel, blkno, bstrategy);
+			pagefn(&stat, rel, blkno);
 		}
 	}
 
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0848c18d329..7cb8839245c 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2107,36 +2107,6 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
-     <varlistentry id="guc-vacuum-buffer-usage-limit" xreflabel="vacuum_buffer_usage_limit">
-      <term>
-       <varname>vacuum_buffer_usage_limit</varname> (<type>integer</type>)
-       <indexterm>
-        <primary><varname>vacuum_buffer_usage_limit</varname> configuration parameter</primary>
-       </indexterm>
-      </term>
-      <listitem>
-       <para>
-        Specifies the size of the
-        <glossterm linkend="glossary-buffer-access-strategy">Buffer Access Strategy</glossterm>
-        used by the <command>VACUUM</command> and <command>ANALYZE</command>
-        commands.  A setting of <literal>0</literal> will allow the operation
-        to use any number of <varname>shared_buffers</varname>.  Otherwise
-        valid sizes range from <literal>128 kB</literal> to
-        <literal>16 GB</literal>.  If the specified size would exceed 1/8 the
-        size of <varname>shared_buffers</varname>, the size is silently capped
-        to that value.  The default value is <literal>2MB</literal>.  If
-        this value is specified without units, it is taken as kilobytes.  This
-        parameter can be set at any time.  It can be overridden for
-        <xref linkend="sql-vacuum"/> and <xref linkend="sql-analyze"/>
-        when passing the <option>BUFFER_USAGE_LIMIT</option> option.  Higher
-        settings can allow <command>VACUUM</command> and
-        <command>ANALYZE</command> to run more quickly, but having too large a
-        setting may cause too many other useful pages to be evicted from
-        shared buffers.
-       </para>
-      </listitem>
-     </varlistentry>
-
      <varlistentry id="guc-logical-decoding-work-mem" xreflabel="logical_decoding_work_mem">
       <term><varname>logical_decoding_work_mem</varname> (<type>integer</type>)
       <indexterm>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index d1a20d001e9..18d17540430 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -2993,28 +2993,6 @@ description | Waiting for a newly initialized WAL file to reach durable storage
           <literal>init</literal>.
          </para>
         </listitem>
-        <listitem>
-         <para>
-          <literal>vacuum</literal>: I/O operations performed outside of shared
-          buffers while vacuuming and analyzing permanent relations. Temporary
-          table vacuums use the same local buffer pool as other temporary table
-          I/O operations and are tracked in <varname>context</varname>
-          <literal>normal</literal>.
-         </para>
-        </listitem>
-        <listitem>
-         <para>
-          <literal>bulkread</literal>: Certain large read I/O operations
-          done outside of shared buffers, for example, a sequential scan of a
-          large table.
-         </para>
-        </listitem>
-        <listitem>
-         <para>
-          <literal>bulkwrite</literal>: Certain large write I/O operations
-          done outside of shared buffers, such as <command>COPY</command>.
-         </para>
-        </listitem>
        </itemizedlist>
       </entry>
      </row>
@@ -3180,13 +3158,8 @@ description | Waiting for a newly initialized WAL file to reach durable storage
         buffer in order to make it available for another use.
        </para>
        <para>
-        In <varname>context</varname> <literal>normal</literal>, this counts
-        the number of times a block was evicted from a buffer and replaced with
-        another block. In <varname>context</varname>s
-        <literal>bulkwrite</literal>, <literal>bulkread</literal>, and
-        <literal>vacuum</literal>, this counts the number of times a block was
-        evicted from shared buffers in order to add the shared buffer to a
-        separate, size-limited ring buffer for use in a bulk I/O operation.
+        This counts the number of times a block was evicted from a buffer and
+        replaced with another block.
         </para>
       </entry>
      </row>
@@ -3197,10 +3170,9 @@ description | Waiting for a newly initialized WAL file to reach durable storage
         <structfield>reuses</structfield> <type>bigint</type>
        </para>
        <para>
-        The number of times an existing buffer in a size-limited ring buffer
-        outside of shared buffers was reused as part of an I/O operation in the
-        <literal>bulkread</literal>, <literal>bulkwrite</literal>, or
-        <literal>vacuum</literal> <varname>context</varname>s.
+        Always zero.  This column previously counted reuses of buffers in a
+        size-limited ring buffer (buffer access strategy); ring buffers have
+        been removed, so no reuses are tracked.
        </para>
       </entry>
      </row>
diff --git a/doc/src/sgml/ref/analyze.sgml b/doc/src/sgml/ref/analyze.sgml
index ec81f00fecf..7f5159111d8 100644
--- a/doc/src/sgml/ref/analyze.sgml
+++ b/doc/src/sgml/ref/analyze.sgml
@@ -27,7 +27,6 @@ ANALYZE [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] [ <r
 
     VERBOSE [ <replaceable class="parameter">boolean</replaceable> ]
     SKIP_LOCKED [ <replaceable class="parameter">boolean</replaceable> ]
-    BUFFER_USAGE_LIMIT <replaceable class="parameter">size</replaceable>
 
 <phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase>
 
@@ -88,26 +87,6 @@ ANALYZE [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] [ <r
     </listitem>
    </varlistentry>
 
-   <varlistentry>
-    <term><literal>BUFFER_USAGE_LIMIT</literal></term>
-    <listitem>
-     <para>
-      Specifies the
-      <glossterm linkend="glossary-buffer-access-strategy">Buffer Access Strategy</glossterm>
-      ring buffer size for <command>ANALYZE</command>.  This size is used to
-      calculate the number of shared buffers which will be reused as part of
-      this strategy.  <literal>0</literal> disables use of a
-      <literal>Buffer Access Strategy</literal>.   When this option is not
-      specified, <command>ANALYZE</command> uses the value from
-      <xref linkend="guc-vacuum-buffer-usage-limit"/>.  Higher settings can
-      allow <command>ANALYZE</command> to run more quickly, but having too
-      large a setting may cause too many other useful pages to be evicted from
-      shared buffers.  The minimum value is <literal>128 kB</literal> and the
-      maximum value is <literal>16 GB</literal>.
-     </para>
-    </listitem>
-   </varlistentry>
-
    <varlistentry>
     <term><replaceable class="parameter">boolean</replaceable></term>
     <listitem>
diff --git a/doc/src/sgml/ref/vacuum.sgml b/doc/src/sgml/ref/vacuum.sgml
index 38ee973ea05..d44a49f8efe 100644
--- a/doc/src/sgml/ref/vacuum.sgml
+++ b/doc/src/sgml/ref/vacuum.sgml
@@ -37,7 +37,6 @@ VACUUM [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] [ <re
     PARALLEL <replaceable class="parameter">integer</replaceable>
     SKIP_DATABASE_STATS [ <replaceable class="parameter">boolean</replaceable> ]
     ONLY_DATABASE_STATS [ <replaceable class="parameter">boolean</replaceable> ]
-    BUFFER_USAGE_LIMIT <replaceable class="parameter">size</replaceable>
     FULL [ <replaceable class="parameter">boolean</replaceable> ]
 
 <phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase>
@@ -309,30 +308,6 @@ VACUUM [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] [ <re
     </listitem>
    </varlistentry>
 
-   <varlistentry>
-    <term><literal>BUFFER_USAGE_LIMIT</literal></term>
-    <listitem>
-     <para>
-      Specifies the
-      <glossterm linkend="glossary-buffer-access-strategy">Buffer Access Strategy</glossterm>
-      ring buffer size for <command>VACUUM</command>.  This size is used to
-      calculate the number of shared buffers which will be reused as part of
-      this strategy.  <literal>0</literal> disables use of a
-      <literal>Buffer Access Strategy</literal>.  If <option>ANALYZE</option>
-      is also specified, the <option>BUFFER_USAGE_LIMIT</option> value is used
-      for both the vacuum and analyze stages.  This option can't be used with
-      the <option>FULL</option> option except if <option>ANALYZE</option> is
-      also specified.  When this option is not specified,
-      <command>VACUUM</command> uses the value from
-      <xref linkend="guc-vacuum-buffer-usage-limit"/>.  Higher settings can
-      allow <command>VACUUM</command> to run more quickly, but having too
-      large a setting may cause too many other useful pages to be evicted from
-      shared buffers.  The minimum value is <literal>128 kB</literal> and the
-      maximum value is <literal>16 GB</literal>.
-     </para>
-    </listitem>
-   </varlistentry>
-
    <varlistentry>
     <term><literal>FULL</literal></term>
     <listitem>
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index bdb30752e09..5d6539f5483 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -224,7 +224,7 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void form_and_spill_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
-static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
+static void brin_vacuum_scan(Relation idxrel);
 static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
 								BrinMemTuple *dtup, const Datum *values, const bool *nulls);
 static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
@@ -1129,7 +1129,7 @@ brinbuild(Relation heap, Relation index, IndexInfo *indexInfo)
 	 * whole relation will be rolled back.
 	 */
 
-	meta = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM, NULL,
+	meta = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM,
 							 EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 	Assert(BufferGetBlockNumber(meta) == BRIN_METAPAGE_BLKNO);
 
@@ -1281,7 +1281,7 @@ brinbuildempty(Relation index)
 	Buffer		metabuf;
 
 	/* An empty BRIN index has a metapage only. */
-	metabuf = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+	metabuf = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM,
 								EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 
 	/* Initialize and xlog metabuffer. */
@@ -1336,7 +1336,7 @@ brinvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
 	heapRel = table_open(IndexGetRelation(RelationGetRelid(info->index), false),
 						 AccessShareLock);
 
-	brin_vacuum_scan(info->index, info->strategy);
+	brin_vacuum_scan(info->index);
 
 	brinsummarize(info->index, heapRel, BRIN_ALL_BLOCKRANGES, false,
 				  &stats->num_index_tuples, &stats->num_index_tuples);
@@ -2171,7 +2171,7 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
  * and such.
  */
 static void
-brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
+brin_vacuum_scan(Relation idxrel)
 {
 	BlockRangeReadStreamPrivate p;
 	ReadStream *stream;
@@ -2187,7 +2187,6 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										strategy,
 										idxrel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/src/backend/access/brin/brin_revmap.c b/src/backend/access/brin/brin_revmap.c
index 233355cb2d5..01c4104ac88 100644
--- a/src/backend/access/brin/brin_revmap.c
+++ b/src/backend/access/brin/brin_revmap.c
@@ -559,7 +559,7 @@ revmap_physical_extend(BrinRevmap *revmap)
 	}
 	else
 	{
-		buf = ExtendBufferedRel(BMR_REL(irel), MAIN_FORKNUM, NULL,
+		buf = ExtendBufferedRel(BMR_REL(irel), MAIN_FORKNUM,
 								EB_LOCK_FIRST);
 		if (BufferGetBlockNumber(buf) != mapBlk)
 		{
diff --git a/src/backend/access/gin/gininsert.c b/src/backend/access/gin/gininsert.c
index cb9ed3b563c..32ad65cc95f 100644
--- a/src/backend/access/gin/gininsert.c
+++ b/src/backend/access/gin/gininsert.c
@@ -815,9 +815,9 @@ ginbuildempty(Relation index)
 				MetaBuffer;
 
 	/* An empty GIN index has two pages. */
-	MetaBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+	MetaBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM,
 								   EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
-	RootBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+	RootBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM,
 								   EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 
 	/* Initialize and xlog metabuffer and root buffer. */
diff --git a/src/backend/access/gin/ginutil.c b/src/backend/access/gin/ginutil.c
index e7cba81d477..2d7394aedd5 100644
--- a/src/backend/access/gin/ginutil.c
+++ b/src/backend/access/gin/ginutil.c
@@ -336,7 +336,7 @@ GinNewBuffer(Relation index)
 	}
 
 	/* Must extend the file */
-	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM,
 							   EB_LOCK_FIRST);
 
 	return buffer;
diff --git a/src/backend/access/gin/ginvacuum.c b/src/backend/access/gin/ginvacuum.c
index 840543eb664..86538de39d6 100644
--- a/src/backend/access/gin/ginvacuum.c
+++ b/src/backend/access/gin/ginvacuum.c
@@ -32,7 +32,6 @@ struct GinVacuumState
 	IndexBulkDeleteCallback callback;
 	void	   *callback_state;
 	GinState	ginstate;
-	BufferAccessStrategy strategy;
 	MemoryContext tmpCxt;
 };
 
@@ -289,7 +288,7 @@ ginScanPostingTreeToDelete(GinVacuumState *gvs, DataPageDeleteStack *myStackItem
 			childBuffer = ReadBufferExtended(gvs->index,
 											 MAIN_FORKNUM,
 											 PostingItemGetBlockNumber(pitem),
-											 RBM_NORMAL, gvs->strategy);
+											 RBM_NORMAL);
 			LockBuffer(childBuffer, GIN_EXCLUSIVE);
 
 			/* Allocate a child stack entry on first use; reuse thereafter */
@@ -389,7 +388,7 @@ ginVacuumPostingTreeLeaves(GinVacuumState *gvs, BlockNumber blkno)
 		PostingItem *pitem;
 
 		buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, gvs->strategy);
+									RBM_NORMAL);
 		LockBuffer(buffer, GIN_SHARE);
 		page = BufferGetPage(buffer);
 
@@ -430,7 +429,7 @@ ginVacuumPostingTreeLeaves(GinVacuumState *gvs, BlockNumber blkno)
 			break;
 
 		buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, gvs->strategy);
+									RBM_NORMAL);
 		LockBuffer(buffer, GIN_EXCLUSIVE);
 		page = BufferGetPage(buffer);
 	}
@@ -454,7 +453,7 @@ ginVacuumPostingTree(GinVacuumState *gvs, BlockNumber rootBlkno)
 		bool		deleted PG_USED_FOR_ASSERTS_ONLY;
 
 		buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, rootBlkno,
-									RBM_NORMAL, gvs->strategy);
+									RBM_NORMAL);
 
 		/*
 		 * Lock posting tree root for cleanup to ensure there are no
@@ -615,7 +614,6 @@ ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	gvs.index = index;
 	gvs.callback = callback;
 	gvs.callback_state = callback_state;
-	gvs.strategy = info->strategy;
 	initGinState(&gvs.ginstate, index);
 
 	/* first time through? */
@@ -636,7 +634,7 @@ ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	gvs.result = stats;
 
 	buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
-								RBM_NORMAL, info->strategy);
+								RBM_NORMAL);
 
 	/* find leaf page */
 	for (;;)
@@ -669,7 +667,7 @@ ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 
 		UnlockReleaseBuffer(buffer);
 		buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, info->strategy);
+									RBM_NORMAL);
 	}
 
 	/* right now we found leftmost page in entry's BTree */
@@ -712,7 +710,7 @@ ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 			break;
 
 		buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, info->strategy);
+									RBM_NORMAL);
 		LockBuffer(buffer, GIN_EXCLUSIVE);
 	}
 
@@ -794,7 +792,6 @@ ginvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										index,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/src/backend/access/gist/gist.c b/src/backend/access/gist/gist.c
index 8565e225be7..ae494742a4b 100644
--- a/src/backend/access/gist/gist.c
+++ b/src/backend/access/gist/gist.c
@@ -142,7 +142,7 @@ gistbuildempty(Relation index)
 	Buffer		buffer;
 
 	/* Initialize the root page */
-	buffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM,
 							   EB_SKIP_EXTENSION_LOCK | EB_LOCK_FIRST);
 
 	/* Initialize and xlog buffer */
diff --git a/src/backend/access/gist/gistutil.c b/src/backend/access/gist/gistutil.c
index 0f58f61879f..170174f62fd 100644
--- a/src/backend/access/gist/gistutil.c
+++ b/src/backend/access/gist/gistutil.c
@@ -877,7 +877,7 @@ gistNewBuffer(Relation r, Relation heaprel)
 	}
 
 	/* Must extend the file */
-	buffer = ExtendBufferedRel(BMR_REL(r), MAIN_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(r), MAIN_FORKNUM,
 							   EB_LOCK_FIRST);
 
 	return buffer;
diff --git a/src/backend/access/gist/gistvacuum.c b/src/backend/access/gist/gistvacuum.c
index 686a0418054..c366ed35ea8 100644
--- a/src/backend/access/gist/gistvacuum.c
+++ b/src/backend/access/gist/gistvacuum.c
@@ -218,7 +218,6 @@ gistvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										rel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -491,8 +490,7 @@ restart:
 		/* check for vacuum delay while not holding any buffer lock */
 		vacuum_delay_point(false);
 
-		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL,
-									info->strategy);
+		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 		goto restart;
 	}
 }
@@ -524,8 +522,7 @@ gistvacuum_delete_empty_pages(IndexVacuumInfo *info, GistVacState *vstate)
 		int			ntodelete;
 		int			deleted;
 
-		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, (BlockNumber) blkno,
-									RBM_NORMAL, info->strategy);
+		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, (BlockNumber) blkno, RBM_NORMAL);
 
 		LockBuffer(buffer, GIST_SHARE);
 		page = BufferGetPage(buffer);
@@ -590,8 +587,7 @@ gistvacuum_delete_empty_pages(IndexVacuumInfo *info, GistVacState *vstate)
 			if (PageGetMaxOffsetNumber(page) == FirstOffsetNumber)
 				break;
 
-			leafbuf = ReadBufferExtended(rel, MAIN_FORKNUM, leafs_to_delete[i],
-										 RBM_NORMAL, info->strategy);
+			leafbuf = ReadBufferExtended(rel, MAIN_FORKNUM, leafs_to_delete[i], RBM_NORMAL);
 			LockBuffer(leafbuf, GIST_EXCLUSIVE);
 			gistcheckpage(rel, leafbuf);
 
diff --git a/src/backend/access/hash/hash.c b/src/backend/access/hash/hash.c
index 8d8cd30dc38..1bbb7224bfc 100644
--- a/src/backend/access/hash/hash.c
+++ b/src/backend/access/hash/hash.c
@@ -542,7 +542,6 @@ hashbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										rel,
 										MAIN_FORKNUM,
 										hash_bulkdelete_read_stream_cb,
@@ -616,7 +615,7 @@ bucket_loop:
 
 		bucket_buf = buf;
 
-		hashbucketcleanup(rel, cur_bucket, bucket_buf, blkno, info->strategy,
+		hashbucketcleanup(rel, cur_bucket, bucket_buf, blkno,
 						  cachedmetap->hashm_maxbucket,
 						  cachedmetap->hashm_highmask,
 						  cachedmetap->hashm_lowmask, &tuples_removed,
@@ -765,7 +764,7 @@ hashvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
  */
 void
 hashbucketcleanup(Relation rel, Bucket cur_bucket, Buffer bucket_buf,
-				  BlockNumber bucket_blkno, BufferAccessStrategy bstrategy,
+				  BlockNumber bucket_blkno,
 				  uint32 maxbucket, uint32 highmask, uint32 lowmask,
 				  double *tuples_removed, double *num_index_tuples,
 				  bool split_cleanup,
@@ -935,9 +934,8 @@ hashbucketcleanup(Relation rel, Bucket cur_bucket, Buffer bucket_buf,
 		if (!BlockNumberIsValid(blkno))
 			break;
 
-		next_buf = _hash_getbuf_with_strategy(rel, blkno, HASH_WRITE,
-											  LH_OVERFLOW_PAGE,
-											  bstrategy);
+		next_buf = _hash_getbuf(rel, blkno, HASH_WRITE,
+								LH_OVERFLOW_PAGE);
 
 		/*
 		 * release the lock on previous page after acquiring the lock on next
@@ -1004,8 +1002,7 @@ hashbucketcleanup(Relation rel, Bucket cur_bucket, Buffer bucket_buf,
 	 * ordering of tuples for a scan that has started before it.
 	 */
 	if (bucket_dirty && IsBufferCleanupOK(bucket_buf))
-		_hash_squeezebucket(rel, cur_bucket, bucket_blkno, bucket_buf,
-							bstrategy);
+		_hash_squeezebucket(rel, cur_bucket, bucket_blkno, bucket_buf);
 	else
 		LockBuffer(bucket_buf, BUFFER_LOCK_UNLOCK);
 }
diff --git a/src/backend/access/hash/hashovfl.c b/src/backend/access/hash/hashovfl.c
index dbc57ef958c..5b35fed33f9 100644
--- a/src/backend/access/hash/hashovfl.c
+++ b/src/backend/access/hash/hashovfl.c
@@ -491,8 +491,7 @@ _hash_firstfreebit(uint32 map)
 BlockNumber
 _hash_freeovflpage(Relation rel, Buffer bucketbuf, Buffer ovflbuf,
 				   Buffer wbuf, IndexTuple *itups, OffsetNumber *itup_offsets,
-				   Size *tups_size, uint16 nitups,
-				   BufferAccessStrategy bstrategy)
+				   Size *tups_size, uint16 nitups)
 {
 	HashMetaPage metap;
 	Buffer		metabuf;
@@ -539,20 +538,16 @@ _hash_freeovflpage(Relation rel, Buffer bucketbuf, Buffer ovflbuf,
 		if (prevblkno == writeblkno)
 			prevbuf = wbuf;
 		else
-			prevbuf = _hash_getbuf_with_strategy(rel,
-												 prevblkno,
-												 HASH_WRITE,
-												 LH_BUCKET_PAGE | LH_OVERFLOW_PAGE,
-												 bstrategy);
+			prevbuf = _hash_getbuf(rel,
+								   prevblkno,
+								   HASH_WRITE,
+								   LH_BUCKET_PAGE | LH_OVERFLOW_PAGE);
 	}
 	if (BlockNumberIsValid(nextblkno))
-		nextbuf = _hash_getbuf_with_strategy(rel,
-											 nextblkno,
-											 HASH_WRITE,
-											 LH_OVERFLOW_PAGE,
-											 bstrategy);
-
-	/* Note: bstrategy is intentionally not used for metapage and bitmap */
+		nextbuf = _hash_getbuf(rel,
+							   nextblkno,
+							   HASH_WRITE,
+							   LH_OVERFLOW_PAGE);
 
 	/* Read the metapage so we can determine which bitmap page to use */
 	metabuf = _hash_getbuf(rel, HASH_METAPAGE, HASH_READ, LH_META_PAGE);
@@ -843,8 +838,7 @@ void
 _hash_squeezebucket(Relation rel,
 					Bucket bucket,
 					BlockNumber bucket_blkno,
-					Buffer bucket_buf,
-					BufferAccessStrategy bstrategy)
+					Buffer bucket_buf)
 {
 	BlockNumber wblkno;
 	BlockNumber rblkno;
@@ -886,11 +880,10 @@ _hash_squeezebucket(Relation rel,
 		rblkno = ropaque->hasho_nextblkno;
 		if (rbuf != InvalidBuffer)
 			_hash_relbuf(rel, rbuf);
-		rbuf = _hash_getbuf_with_strategy(rel,
-										  rblkno,
-										  HASH_WRITE,
-										  LH_OVERFLOW_PAGE,
-										  bstrategy);
+		rbuf = _hash_getbuf(rel,
+							rblkno,
+							HASH_WRITE,
+							LH_OVERFLOW_PAGE);
 		rpage = BufferGetPage(rbuf);
 		ropaque = HashPageGetOpaque(rpage);
 		Assert(ropaque->hasho_bucket == bucket);
@@ -952,11 +945,10 @@ readpage:
 
 				/* don't need to move to next page if we reached the read page */
 				if (wblkno != rblkno)
-					next_wbuf = _hash_getbuf_with_strategy(rel,
-														   wblkno,
-														   HASH_WRITE,
-														   LH_OVERFLOW_PAGE,
-														   bstrategy);
+					next_wbuf = _hash_getbuf(rel,
+											 wblkno,
+											 HASH_WRITE,
+											 LH_OVERFLOW_PAGE);
 
 				if (nitups > 0)
 				{
@@ -1098,7 +1090,7 @@ readpage:
 
 		/* free this overflow page (releases rbuf) */
 		_hash_freeovflpage(rel, bucket_buf, rbuf, wbuf, itups, itup_offsets,
-						   tups_size, nitups, bstrategy);
+						   tups_size, nitups);
 
 		/* be tidy */
 		for (i = 0; i < nitups; i++)
@@ -1115,11 +1107,10 @@ readpage:
 			return;
 		}
 
-		rbuf = _hash_getbuf_with_strategy(rel,
-										  rblkno,
-										  HASH_WRITE,
-										  LH_OVERFLOW_PAGE,
-										  bstrategy);
+		rbuf = _hash_getbuf(rel,
+							rblkno,
+							HASH_WRITE,
+							LH_OVERFLOW_PAGE);
 		rpage = BufferGetPage(rbuf);
 		ropaque = HashPageGetOpaque(rpage);
 		Assert(ropaque->hasho_bucket == bucket);
diff --git a/src/backend/access/hash/hashpage.c b/src/backend/access/hash/hashpage.c
index 8099b0d021f..bdf1a255f1f 100644
--- a/src/backend/access/hash/hashpage.c
+++ b/src/backend/access/hash/hashpage.c
@@ -139,8 +139,7 @@ _hash_getinitbuf(Relation rel, BlockNumber blkno)
 	if (blkno == P_NEW)
 		elog(ERROR, "hash AM does not use P_NEW");
 
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_ZERO_AND_LOCK,
-							 NULL);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_ZERO_AND_LOCK);
 
 	/* ref count and lock type are correct */
 
@@ -209,7 +208,7 @@ _hash_getnewbuf(Relation rel, BlockNumber blkno, ForkNumber forkNum)
 	/* smgr insists we explicitly extend the relation */
 	if (blkno == nblocks)
 	{
-		buf = ExtendBufferedRel(BMR_REL(rel), forkNum, NULL,
+		buf = ExtendBufferedRel(BMR_REL(rel), forkNum,
 								EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 		if (BufferGetBlockNumber(buf) != blkno)
 			elog(ERROR, "unexpected hash relation size: %u, should be %u",
@@ -217,8 +216,7 @@ _hash_getnewbuf(Relation rel, BlockNumber blkno, ForkNumber forkNum)
 	}
 	else
 	{
-		buf = ReadBufferExtended(rel, forkNum, blkno, RBM_ZERO_AND_LOCK,
-								 NULL);
+		buf = ReadBufferExtended(rel, forkNum, blkno, RBM_ZERO_AND_LOCK);
 	}
 
 	/* ref count and lock type are correct */
@@ -229,34 +227,6 @@ _hash_getnewbuf(Relation rel, BlockNumber blkno, ForkNumber forkNum)
 	return buf;
 }
 
-/*
- *	_hash_getbuf_with_strategy() -- Get a buffer with nondefault strategy.
- *
- *		This is identical to _hash_getbuf() but also allows a buffer access
- *		strategy to be specified.  We use this for VACUUM operations.
- */
-Buffer
-_hash_getbuf_with_strategy(Relation rel, BlockNumber blkno,
-						   int access, int flags,
-						   BufferAccessStrategy bstrategy)
-{
-	Buffer		buf;
-
-	if (blkno == P_NEW)
-		elog(ERROR, "hash AM does not use P_NEW");
-
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, bstrategy);
-
-	if (access != HASH_NOLOCK)
-		LockBuffer(buf, access);
-
-	/* ref count and lock type are correct */
-
-	_hash_checkpage(rel, buf, flags);
-
-	return buf;
-}
-
 /*
  *	_hash_relbuf() -- release a locked buffer.
  *
@@ -758,7 +728,7 @@ restart_expand:
 		/* Release the metapage lock. */
 		LockBuffer(metabuf, BUFFER_LOCK_UNLOCK);
 
-		hashbucketcleanup(rel, old_bucket, buf_oblkno, start_oblkno, NULL,
+		hashbucketcleanup(rel, old_bucket, buf_oblkno, start_oblkno,
 						  maxbucket, highmask, lowmask, NULL, NULL, true,
 						  NULL, NULL);
 
@@ -1333,7 +1303,7 @@ _hash_splitbucket(Relation rel,
 	{
 		LockBuffer(bucket_nbuf, BUFFER_LOCK_UNLOCK);
 		hashbucketcleanup(rel, obucket, bucket_obuf,
-						  BufferGetBlockNumber(bucket_obuf), NULL,
+						  BufferGetBlockNumber(bucket_obuf),
 						  maxbucket, highmask, lowmask, NULL, NULL, true,
 						  NULL, NULL);
 	}
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index abfd8e8970a..4331ab53437 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -359,7 +359,6 @@ static void
 initscan(HeapScanDesc scan, ScanKey key, bool keep_startblock)
 {
 	ParallelBlockTableScanDesc bpscan = NULL;
-	bool		allow_strat;
 	bool		allow_sync;
 
 	/*
@@ -396,24 +395,10 @@ initscan(HeapScanDesc scan, ScanKey key, bool keep_startblock)
 	if (!RelationUsesLocalBuffers(scan->rs_base.rs_rd) &&
 		scan->rs_nblocks > NBuffers / 4)
 	{
-		allow_strat = (scan->rs_base.rs_flags & SO_ALLOW_STRAT) != 0;
 		allow_sync = (scan->rs_base.rs_flags & SO_ALLOW_SYNC) != 0;
 	}
 	else
-		allow_strat = allow_sync = false;
-
-	if (allow_strat)
-	{
-		/* During a rescan, keep the previous strategy object. */
-		if (scan->rs_strategy == NULL)
-			scan->rs_strategy = GetAccessStrategy(BAS_BULKREAD);
-	}
-	else
-	{
-		if (scan->rs_strategy != NULL)
-			FreeAccessStrategy(scan->rs_strategy);
-		scan->rs_strategy = NULL;
-	}
+		allow_sync = false;
 
 	if (scan->rs_base.rs_parallel != NULL)
 	{
@@ -1202,7 +1187,6 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 	scan->rs_base.rs_flags = flags;
 	scan->rs_base.rs_parallel = parallel_scan;
 	scan->rs_base.rs_instrument = NULL;
-	scan->rs_strategy = NULL;	/* set in initscan */
 	scan->rs_cbuf = InvalidBuffer;
 
 	/*
@@ -1273,8 +1257,7 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 
 	/*
 	 * Set up a read stream for sequential scans and TID range scans. This
-	 * should be done after initscan() because initscan() allocates the
-	 * BufferAccessStrategy object passed to the read stream API.
+	 * should be done after initscan().
 	 */
 	if (scan->rs_base.rs_flags & SO_TYPE_SEQSCAN ||
 		scan->rs_base.rs_flags & SO_TYPE_TIDRANGESCAN)
@@ -1295,7 +1278,6 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 		 */
 		scan->rs_read_stream = read_stream_begin_relation(READ_STREAM_SEQUENTIAL |
 														  READ_STREAM_USE_BATCHING,
-														  scan->rs_strategy,
 														  scan->rs_base.rs_rd,
 														  MAIN_FORKNUM,
 														  cb,
@@ -1306,7 +1288,6 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 	{
 		scan->rs_read_stream = read_stream_begin_relation(READ_STREAM_DEFAULT |
 														  READ_STREAM_USE_BATCHING,
-														  scan->rs_strategy,
 														  scan->rs_base.rs_rd,
 														  MAIN_FORKNUM,
 														  bitmapheap_stream_read_next,
@@ -1402,9 +1383,6 @@ heap_endscan(TableScanDesc sscan)
 	if (BufferIsValid(scan->rs_vmbuffer))
 		ReleaseBuffer(scan->rs_vmbuffer);
 
-	/*
-	 * Must free the read stream before freeing the BufferAccessStrategy.
-	 */
 	if (scan->rs_read_stream)
 		read_stream_end(scan->rs_read_stream);
 
@@ -1416,9 +1394,6 @@ heap_endscan(TableScanDesc sscan)
 	if (scan->rs_base.rs_key)
 		pfree(scan->rs_base.rs_key);
 
-	if (scan->rs_strategy != NULL)
-		FreeAccessStrategy(scan->rs_strategy);
-
 	if (scan->rs_parallelworkerdata != NULL)
 		pfree(scan->rs_parallelworkerdata);
 
@@ -1939,7 +1914,6 @@ GetBulkInsertState(void)
 	BulkInsertState bistate;
 
 	bistate = (BulkInsertState) palloc_object(BulkInsertStateData);
-	bistate->strategy = GetAccessStrategy(BAS_BULKWRITE);
 	bistate->current_buf = InvalidBuffer;
 	bistate->next_free = InvalidBlockNumber;
 	bistate->last_free = InvalidBlockNumber;
@@ -1955,7 +1929,6 @@ FreeBulkInsertState(BulkInsertState bistate)
 {
 	if (bistate->current_buf != InvalidBuffer)
 		ReleaseBuffer(bistate->current_buf);
-	FreeAccessStrategy(bistate->strategy);
 	pfree(bistate);
 }
 
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index bf87430cf01..d14658d7218 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -2213,9 +2213,9 @@ heapam_scan_sample_next_block(TableScanDesc scan, SampleScanState *scanstate)
 	 */
 	CHECK_FOR_INTERRUPTS();
 
-	/* Read page using selected strategy */
+	/* Read page */
 	hscan->rs_cbuf = ReadBufferExtended(hscan->rs_base.rs_rd, MAIN_FORKNUM,
-										blockno, RBM_NORMAL, hscan->rs_strategy);
+										blockno, RBM_NORMAL);
 
 	/* in pagemode, prune the page and determine visible tuple offsets */
 	if (hscan->rs_base.rs_flags & SO_ALLOW_PAGEMODE)
diff --git a/src/backend/access/heap/hio.c b/src/backend/access/heap/hio.c
index e96e0f77d92..9bf7abc247f 100644
--- a/src/backend/access/heap/hio.c
+++ b/src/backend/access/heap/hio.c
@@ -80,7 +80,8 @@ RelationPutHeapTuple(Relation relation,
 }
 
 /*
- * Read in a buffer in mode, using bulk-insert strategy if bistate isn't NULL.
+ * Read in a buffer in mode, using the bistate's pinned target page if
+ * available.
  */
 static Buffer
 ReadBufferBI(Relation relation, BlockNumber targetBlock,
@@ -91,7 +92,7 @@ ReadBufferBI(Relation relation, BlockNumber targetBlock,
 	/* If not bulk-insert, exactly like ReadBuffer */
 	if (!bistate)
 		return ReadBufferExtended(relation, MAIN_FORKNUM, targetBlock,
-								  mode, NULL);
+								  mode);
 
 	/* If we have the desired block already pinned, re-pin and return it */
 	if (bistate->current_buf != InvalidBuffer)
@@ -113,9 +114,9 @@ ReadBufferBI(Relation relation, BlockNumber targetBlock,
 		bistate->current_buf = InvalidBuffer;
 	}
 
-	/* Perform a read using the buffer strategy */
+	/* Perform the read */
 	buffer = ReadBufferExtended(relation, MAIN_FORKNUM, targetBlock,
-								mode, bistate->strategy);
+								mode);
 
 	/* Save the selected block as target for future inserts */
 	IncrBufferRefCount(buffer);
@@ -337,7 +338,6 @@ RelationAddBlocks(Relation relation, BulkInsertState bistate,
 	 * way larger.
 	 */
 	first_block = ExtendBufferedRelBy(BMR_REL(relation), MAIN_FORKNUM,
-									  bistate ? bistate->strategy : NULL,
 									  EB_LOCK_FIRST,
 									  extend_by_pages,
 									  victim_buffers,
@@ -484,8 +484,7 @@ RelationAddBlocks(Relation relation, BulkInsertState bistate,
  *
  *	The caller can also provide a BulkInsertState object to optimize many
  *	insertions into the same relation.  This keeps a pin on the current
- *	insertion target page (to save pin/unpin cycles) and also passes a
- *	BULKWRITE buffer selection strategy object to the buffer manager.
+ *	insertion target page (to save pin/unpin cycles).
  *	Passing NULL for bistate selects the default behavior.
  *
  *	We don't fill existing pages further than the fillfactor, except for large
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 39395aed0d5..8bc8e470af3 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -257,7 +257,6 @@ typedef struct LVRelState
 	int			nindexes;
 
 	/* Buffer access strategy and parallel vacuum state */
-	BufferAccessStrategy bstrategy;
 	ParallelVacuumState *pvs;
 
 	/* Aggressive VACUUM? (must set relfrozenxid >= FreezeLimit) */
@@ -621,8 +620,7 @@ heap_vacuum_eager_scan_setup(LVRelState *vacrel, const VacuumParams *params)
  *		and locked the relation.
  */
 void
-heap_vacuum_rel(Relation rel, const VacuumParams *params,
-				BufferAccessStrategy bstrategy)
+heap_vacuum_rel(Relation rel, const VacuumParams *params)
 {
 	LVRelState *vacrel;
 	bool		verbose,
@@ -699,7 +697,6 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
 	vacrel->rel = rel;
 	vac_open_indexes(vacrel->rel, RowExclusiveLock, &vacrel->nindexes,
 					 &vacrel->indrels);
-	vacrel->bstrategy = bstrategy;
 	if (instrument && vacrel->nindexes > 0)
 	{
 		/* Copy index names used by instrumentation (not error reporting) */
@@ -1311,7 +1308,6 @@ lazy_scan_heap(LVRelState *vacrel)
 	 * explicit work in heap_vac_scan_next_block.
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE,
-										vacrel->bstrategy,
 										vacrel->rel,
 										MAIN_FORKNUM,
 										heap_vac_scan_next_block,
@@ -2670,7 +2666,6 @@ lazy_vacuum_heap_rel(LVRelState *vacrel)
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_USE_BATCHING,
-										vacrel->bstrategy,
 										vacrel->rel,
 										MAIN_FORKNUM,
 										vacuum_reap_lp_read_stream_next,
@@ -2904,13 +2899,6 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel)
 
 		VacuumFailsafeActive = true;
 
-		/*
-		 * Abandon use of a buffer access strategy to allow use of all of
-		 * shared buffers.  We assume the caller who allocated the memory for
-		 * the BufferAccessStrategy will free it.
-		 */
-		vacrel->bstrategy = NULL;
-
 		/* Disable index vacuuming, index cleanup, and heap rel truncation */
 		vacrel->do_index_vacuuming = false;
 		vacrel->do_index_cleanup = false;
@@ -3023,7 +3011,6 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	ivinfo.estimated_count = true;
 	ivinfo.message_level = DEBUG2;
 	ivinfo.num_heap_tuples = reltuples;
-	ivinfo.strategy = vacrel->bstrategy;
 
 	/*
 	 * Update error traceback information.
@@ -3074,7 +3061,6 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	ivinfo.message_level = DEBUG2;
 
 	ivinfo.num_heap_tuples = reltuples;
-	ivinfo.strategy = vacrel->bstrategy;
 
 	/*
 	 * Update error traceback information.
@@ -3354,8 +3340,7 @@ count_nondeletable_pages(LVRelState *vacrel, bool *lock_waiter_detected)
 			prefetchedUntil = prefetchStart;
 		}
 
-		buf = ReadBufferExtended(vacrel->rel, MAIN_FORKNUM, blkno, RBM_NORMAL,
-								 vacrel->bstrategy);
+		buf = ReadBufferExtended(vacrel->rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 
 		/* In this phase we only need shared access to the buffer */
 		LockBuffer(buf, BUFFER_LOCK_SHARE);
@@ -3446,8 +3431,7 @@ dead_items_alloc(LVRelState *vacrel, int nworkers)
 			vacrel->pvs = parallel_vacuum_init(vacrel->rel, vacrel->indrels,
 											   vacrel->nindexes, nworkers,
 											   vac_work_mem,
-											   vacrel->verbose ? INFO : DEBUG2,
-											   vacrel->bstrategy);
+											   vacrel->verbose ? INFO : DEBUG2);
 
 		/*
 		 * If parallel mode started, dead_items and dead_items_info spaces are
diff --git a/src/backend/access/heap/visibilitymap.c b/src/backend/access/heap/visibilitymap.c
index 4fd470702aa..b2612b1f710 100644
--- a/src/backend/access/heap/visibilitymap.c
+++ b/src/backend/access/heap/visibilitymap.c
@@ -575,7 +575,7 @@ vm_readbuf(Relation rel, BlockNumber blkno, bool extend)
 	}
 	else
 		buf = ReadBufferExtended(rel, VISIBILITYMAP_FORKNUM, blkno,
-								 RBM_ZERO_ON_ERROR, NULL);
+								 RBM_ZERO_ON_ERROR);
 
 	/*
 	 * Initializing the page when needed is trickier than it looks, because of
@@ -611,7 +611,7 @@ vm_extend(Relation rel, BlockNumber vm_nblocks)
 {
 	Buffer		buf;
 
-	buf = ExtendBufferedRelTo(BMR_REL(rel), VISIBILITYMAP_FORKNUM, NULL,
+	buf = ExtendBufferedRelTo(BMR_REL(rel), VISIBILITYMAP_FORKNUM,
 							  EB_CREATE_FORK_IF_NEEDED |
 							  EB_CLEAR_SIZE_CACHE,
 							  vm_nblocks,
diff --git a/src/backend/access/nbtree/nbtpage.c b/src/backend/access/nbtree/nbtpage.c
index 406088dcb57..23f31f07f17 100644
--- a/src/backend/access/nbtree/nbtpage.c
+++ b/src/backend/access/nbtree/nbtpage.c
@@ -960,7 +960,7 @@ _bt_allocbuf(Relation rel, Relation heaprel)
 	 * otherwise would make, as we can't use _bt_lockbuf() without introducing
 	 * a race.
 	 */
-	buf = ExtendBufferedRel(BMR_REL(rel), MAIN_FORKNUM, NULL, EB_LOCK_FIRST);
+	buf = ExtendBufferedRel(BMR_REL(rel), MAIN_FORKNUM, EB_LOCK_FIRST);
 	if (!RelationUsesLocalBuffers(rel))
 		VALGRIND_MAKE_MEM_DEFINED(BufferGetPage(buf), BLCKSZ);
 
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 3df2c752ead..379a2d37def 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -1324,7 +1324,6 @@ btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										rel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -1730,8 +1729,7 @@ backtrack:
 		 * recycle all-zero pages, not fail.  Also, we want to use a
 		 * nondefault buffer access strategy.
 		 */
-		buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL,
-								 info->strategy);
+		buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 		goto backtrack;
 	}
 
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index f2ee333f60d..785e385aab6 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -432,7 +432,7 @@ SpGistNewBuffer(Relation index)
 		ReleaseBuffer(buffer);
 	}
 
-	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM,
 							   EB_LOCK_FIRST);
 
 	return buffer;
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index c461f8dc02d..9f644edaf82 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -704,8 +704,7 @@ spgprocesspending(spgBulkDeleteState *bds)
 
 		/* examine the referenced page */
 		blkno = ItemPointerGetBlockNumber(&pitem->tid);
-		buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, bds->info->strategy);
+		buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno, RBM_NORMAL);
 		LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE);
 		page = BufferGetPage(buffer);
 
@@ -830,7 +829,6 @@ spgvacuumscan(spgBulkDeleteState *bds)
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										bds->info->strategy,
 										index,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/src/backend/access/transam/xloginsert.c b/src/backend/access/transam/xloginsert.c
index f2e10b82b7d..0c16b7fef0f 100644
--- a/src/backend/access/transam/xloginsert.c
+++ b/src/backend/access/transam/xloginsert.c
@@ -1348,7 +1348,7 @@ log_newpage_range(Relation rel, ForkNumber forknum,
 		while (nbufs < XLR_MAX_BLOCK_ID && blkno < endblk)
 		{
 			Buffer		buf = ReadBufferExtended(rel, forknum, blkno,
-												 RBM_NORMAL, NULL);
+												 RBM_NORMAL);
 
 			LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
 
diff --git a/src/backend/access/transam/xlogutils.c b/src/backend/access/transam/xlogutils.c
index d8c179c5dcc..e5be49f8b5d 100644
--- a/src/backend/access/transam/xlogutils.c
+++ b/src/backend/access/transam/xlogutils.c
@@ -519,7 +519,7 @@ XLogReadBufferExtended(RelFileLocator rlocator, ForkNumber forknum,
 	{
 		/* page exists in file */
 		buffer = ReadBufferWithoutRelcache(rlocator, forknum, blkno,
-										   mode, NULL, true);
+										   mode, true);
 	}
 	else
 	{
@@ -536,7 +536,6 @@ XLogReadBufferExtended(RelFileLocator rlocator, ForkNumber forknum,
 		Assert(InRecovery);
 		buffer = ExtendBufferedRelTo(BMR_SMGR(smgr, RELPERSISTENCE_PERMANENT),
 									 forknum,
-									 NULL,
 									 EB_PERFORMING_RECOVERY |
 									 EB_SKIP_EXTENSION_LOCK,
 									 blkno + 1,
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 81bba4beac7..b0a7b8a33cd 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -3431,7 +3431,6 @@ validate_index(Oid heapId, Oid indexId, Snapshot snapshot)
 	ivinfo.estimated_count = true;
 	ivinfo.message_level = DEBUG2;
 	ivinfo.num_heap_tuples = heapRelation->rd_rel->reltuples;
-	ivinfo.strategy = NULL;
 
 	/*
 	 * Encode TIDs as int8 values for the sort, rather than directly sorting
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index f66e80b757c..9b117a53124 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -72,7 +72,6 @@ int			default_statistics_target = 100;
 
 /* A few variables that don't seem worth passing around as parameters */
 static MemoryContext anl_context = NULL;
-static BufferAccessStrategy vac_strategy;
 
 
 static void do_analyze_rel(Relation onerel,
@@ -108,8 +107,7 @@ static Datum ind_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
  */
 void
 analyze_rel(Oid relid, RangeVar *relation,
-			const VacuumParams *params, List *va_cols, bool in_outer_xact,
-			BufferAccessStrategy bstrategy)
+			const VacuumParams *params, List *va_cols, bool in_outer_xact)
 {
 	Relation	onerel;
 	int			elevel;
@@ -124,7 +122,6 @@ analyze_rel(Oid relid, RangeVar *relation,
 		elevel = DEBUG2;
 
 	/* Set up static variables */
-	vac_strategy = bstrategy;
 
 	/*
 	 * Check for user-requested abort.
@@ -730,7 +727,6 @@ do_analyze_rel(Relation onerel, const VacuumParams *params,
 			ivinfo.estimated_count = true;
 			ivinfo.message_level = elevel;
 			ivinfo.num_heap_tuples = onerel->rd_rel->reltuples;
-			ivinfo.strategy = vac_strategy;
 
 			stats = index_vacuum_cleanup(&ivinfo, NULL);
 
@@ -1302,7 +1298,6 @@ acquire_sample_rows(Relation onerel, int elevel,
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_USE_BATCHING,
-										vac_strategy,
 										scan->rs_rd,
 										MAIN_FORKNUM,
 										block_sampling_read_stream_next,
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index f0819d15ab7..6f65d48b6bd 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -262,7 +262,6 @@ ScanSourceDatabasePgClass(Oid tbid, Oid dbid, char *srcpath)
 	LockRelId	relid;
 	Snapshot	snapshot;
 	SMgrRelation smgr;
-	BufferAccessStrategy bstrategy;
 
 	/* Get pg_class relfilenumber. */
 	relfilenumber = RelationMapOidToFilenumberForDatabase(srcpath,
@@ -282,9 +281,6 @@ ScanSourceDatabasePgClass(Oid tbid, Oid dbid, char *srcpath)
 	nblocks = smgrnblocks(smgr, MAIN_FORKNUM);
 	smgrclose(smgr);
 
-	/* Use a buffer access strategy since this is a bulk read operation. */
-	bstrategy = GetAccessStrategy(BAS_BULKREAD);
-
 	/*
 	 * As explained in the function header comments, we need a snapshot that
 	 * will see all committed transactions as committed, and our transaction
@@ -299,7 +295,7 @@ ScanSourceDatabasePgClass(Oid tbid, Oid dbid, char *srcpath)
 		CHECK_FOR_INTERRUPTS();
 
 		buf = ReadBufferWithoutRelcache(rlocator, MAIN_FORKNUM, blkno,
-										RBM_NORMAL, bstrategy, true);
+										RBM_NORMAL, true);
 
 		LockBuffer(buf, BUFFER_LOCK_SHARE);
 		page = BufferGetPage(buf);
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 02883fe34a4..d8c15a8b1bc 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -2447,7 +2447,7 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 			if (params->options & CLUOPT_VERBOSE)
 				vac_params.options |= VACOPT_VERBOSE;
 			analyze_rel(tableOid, NULL, &vac_params,
-						stmt->relation->va_cols, true, NULL);
+						stmt->relation->va_cols, true);
 			PopActiveSnapshot();
 			CommandCounterIncrement();
 		}
diff --git a/src/backend/commands/sequence.c b/src/backend/commands/sequence.c
index 551667650ba..ac1ab948211 100644
--- a/src/backend/commands/sequence.c
+++ b/src/backend/commands/sequence.c
@@ -358,7 +358,7 @@ fill_seq_fork_with_data(Relation rel, HeapTuple tuple, ForkNumber forkNum)
 
 	/* Initialize first page of relation with special magic number */
 
-	buf = ExtendBufferedRel(BMR_REL(rel), forkNum, NULL,
+	buf = ExtendBufferedRel(BMR_REL(rel), forkNum,
 							EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 	Assert(BufferGetBlockNumber(buf) == 0);
 
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 38539a6fd3d..66acd27bd48 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -127,32 +127,11 @@ static void vac_truncate_clog(TransactionId frozenXID,
 							  TransactionId lastSaneFrozenXid,
 							  MultiXactId lastSaneMinMulti);
 static bool vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
-					   BufferAccessStrategy bstrategy, bool isTopLevel);
+					   bool isTopLevel);
 static double compute_parallel_delay(void);
 static VacOptValue get_vacoptval_from_boolean(DefElem *def);
 static bool vac_tid_reaped(ItemPointer itemptr, void *state);
 
-/*
- * GUC check function to ensure GUC value specified is within the allowable
- * range.
- */
-bool
-check_vacuum_buffer_usage_limit(int *newval, void **extra,
-								GucSource source)
-{
-	/* Value upper and lower hard limits are inclusive */
-	if (*newval == 0 || (*newval >= MIN_BAS_VAC_RING_SIZE_KB &&
-						 *newval <= MAX_BAS_VAC_RING_SIZE_KB))
-		return true;
-
-	/* Value does not fall within any allowable range */
-	GUC_check_errdetail("\"%s\" must be 0 or between %d kB and %d kB.",
-						"vacuum_buffer_usage_limit",
-						MIN_BAS_VAC_RING_SIZE_KB, MAX_BAS_VAC_RING_SIZE_KB);
-
-	return false;
-}
-
 /*
  * Primary entry point for manual VACUUM and ANALYZE commands
  *
@@ -163,7 +142,6 @@ void
 ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 {
 	VacuumParams params;
-	BufferAccessStrategy bstrategy = NULL;
 	bool		verbose = false;
 	bool		skip_locked = false;
 	bool		analyze = false;
@@ -172,7 +150,6 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 	bool		disable_page_skipping = false;
 	bool		process_main = true;
 	bool		process_toast = true;
-	int			ring_size;
 	bool		skip_database_stats = false;
 	bool		only_database_stats = false;
 	MemoryContext vac_context;
@@ -188,12 +165,6 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 	/* Will be set later if we recurse to a TOAST table. */
 	params.toast_parent = InvalidOid;
 
-	/*
-	 * Set this to an invalid value so it is clear whether or not a
-	 * BUFFER_USAGE_LIMIT was specified when making the access strategy.
-	 */
-	ring_size = -1;
-
 	/* Parse options list */
 	foreach(lc, vacstmt->options)
 	{
@@ -204,32 +175,6 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 			verbose = defGetBoolean(opt);
 		else if (strcmp(opt->defname, "skip_locked") == 0)
 			skip_locked = defGetBoolean(opt);
-		else if (strcmp(opt->defname, "buffer_usage_limit") == 0)
-		{
-			const char *hintmsg;
-			int			result;
-			char	   *vac_buffer_size;
-
-			vac_buffer_size = defGetString(opt);
-
-			/*
-			 * Check that the specified value is valid and the size falls
-			 * within the hard upper and lower limits if it is not 0.
-			 */
-			if (!parse_int(vac_buffer_size, &result, GUC_UNIT_KB, &hintmsg) ||
-				(result != 0 &&
-				 (result < MIN_BAS_VAC_RING_SIZE_KB || result > MAX_BAS_VAC_RING_SIZE_KB)))
-			{
-				ereport(ERROR,
-						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-						 errmsg("%s option must be 0 or between %d kB and %d kB",
-								"BUFFER_USAGE_LIMIT",
-								MIN_BAS_VAC_RING_SIZE_KB, MAX_BAS_VAC_RING_SIZE_KB),
-						 hintmsg ? errhint_internal("%s", _(hintmsg)) : 0));
-			}
-
-			ring_size = result;
-		}
 		else if (!vacstmt->is_vacuumcmd)
 			ereport(ERROR,
 					(errcode(ERRCODE_SYNTAX_ERROR),
@@ -325,17 +270,6 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("VACUUM FULL cannot be performed in parallel")));
 
-	/*
-	 * BUFFER_USAGE_LIMIT does nothing for VACUUM (FULL) so just raise an
-	 * ERROR for that case.  VACUUM (FULL, ANALYZE) does make use of it, so
-	 * we'll permit that.
-	 */
-	if (ring_size != -1 && (params.options & VACOPT_FULL) &&
-		!(params.options & VACOPT_ANALYZE))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("BUFFER_USAGE_LIMIT cannot be specified for VACUUM FULL")));
-
 	/*
 	 * Make sure VACOPT_ANALYZE is specified if any column lists are present.
 	 */
@@ -431,38 +365,8 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 										"Vacuum",
 										ALLOCSET_DEFAULT_SIZES);
 
-	/*
-	 * Make a buffer strategy object in the cross-transaction memory context.
-	 * We needn't bother making this for VACUUM (FULL) or VACUUM
-	 * (ONLY_DATABASE_STATS) as they'll not make use of it.  VACUUM (FULL,
-	 * ANALYZE) is possible, so we'd better ensure that we make a strategy
-	 * when we see ANALYZE.
-	 */
-	if ((params.options & (VACOPT_ONLY_DATABASE_STATS |
-						   VACOPT_FULL)) == 0 ||
-		(params.options & VACOPT_ANALYZE) != 0)
-	{
-
-		MemoryContext old_context = MemoryContextSwitchTo(vac_context);
-
-		Assert(ring_size >= -1);
-
-		/*
-		 * If BUFFER_USAGE_LIMIT was specified by the VACUUM or ANALYZE
-		 * command, it overrides the value of VacuumBufferUsageLimit.  Either
-		 * value may be 0, in which case GetAccessStrategyWithSize() will
-		 * return NULL, effectively allowing full use of shared buffers.
-		 */
-		if (ring_size == -1)
-			ring_size = VacuumBufferUsageLimit;
-
-		bstrategy = GetAccessStrategyWithSize(BAS_VACUUM, ring_size);
-
-		MemoryContextSwitchTo(old_context);
-	}
-
 	/* Now go through the common routine */
-	vacuum(vacstmt->rels, &params, bstrategy, vac_context, isTopLevel);
+	vacuum(vacstmt->rels, &params, vac_context, isTopLevel);
 
 	/* Finally, clean up the vacuum memory context */
 	MemoryContextDelete(vac_context);
@@ -479,19 +383,13 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
  * params contains a set of parameters that can be used to customize the
  * behavior.
  *
- * bstrategy may be passed in as NULL when the caller does not want to
- * restrict the number of shared_buffers that VACUUM / ANALYZE can use,
- * otherwise, the caller must build a BufferAccessStrategy with the number of
- * shared_buffers that VACUUM / ANALYZE should try to limit themselves to
- * using.
- *
  * isTopLevel should be passed down from ProcessUtility.
  *
  * It is the caller's responsibility that all parameters are allocated in a
  * memory context that will not disappear at transaction commit.
  */
 void
-vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrategy,
+vacuum(List *relations, const VacuumParams *params,
 	   MemoryContext vac_context, bool isTopLevel)
 {
 	static bool in_vacuum = false;
@@ -630,7 +528,7 @@ vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrate
 
 			if (params->options & VACOPT_VACUUM)
 			{
-				if (!vacuum_rel(vrel->oid, vrel->relation, *params, bstrategy,
+				if (!vacuum_rel(vrel->oid, vrel->relation, *params,
 								isTopLevel))
 					continue;
 			}
@@ -649,7 +547,7 @@ vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrate
 				}
 
 				analyze_rel(vrel->oid, vrel->relation, params,
-							vrel->va_cols, in_outer_xact, bstrategy);
+							vrel->va_cols, in_outer_xact);
 
 				if (use_own_xacts)
 				{
@@ -2010,7 +1908,7 @@ vac_truncate_clog(TransactionId frozenXID,
  */
 static bool
 vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
-		   BufferAccessStrategy bstrategy, bool isTopLevel)
+		   bool isTopLevel)
 {
 	LOCKMODE	lmode;
 	Relation	rel;
@@ -2307,7 +2205,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 			rel = NULL;
 		}
 		else
-			table_relation_vacuum(rel, &params, bstrategy);
+			table_relation_vacuum(rel, &params);
 	}
 
 	/* Roll back any GUC changes executed by index functions */
@@ -2344,7 +2242,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 		toast_vacuum_params.options |= VACOPT_PROCESS_MAIN;
 		toast_vacuum_params.toast_parent = relid;
 
-		vacuum_rel(toast_relid, NULL, toast_vacuum_params, bstrategy,
+		vacuum_rel(toast_relid, NULL, toast_vacuum_params,
 				   isTopLevel);
 	}
 
diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c
index 41cefcfde54..8b4b835db9b 100644
--- a/src/backend/commands/vacuumparallel.c
+++ b/src/backend/commands/vacuumparallel.c
@@ -125,12 +125,6 @@ typedef struct PVShared
 	 */
 	int			maintenance_work_mem_worker;
 
-	/*
-	 * The number of buffers each worker's Buffer Access Strategy ring should
-	 * contain.
-	 */
-	int			ring_nbuffers;
-
 	/*
 	 * Shared vacuum cost balance.  During parallel vacuum,
 	 * VacuumSharedCostBalance points to this value and it accumulates the
@@ -257,9 +251,6 @@ struct ParallelVacuumState
 	int			nindexes_parallel_cleanup;
 	int			nindexes_parallel_condcleanup;
 
-	/* Buffer access strategy used by leader process */
-	BufferAccessStrategy bstrategy;
-
 	/*
 	 * Error reporting state.  The error callback is set only for workers
 	 * processes during parallel index vacuum.
@@ -304,7 +295,7 @@ static void parallel_vacuum_dsm_detach(dsm_segment *seg, Datum arg);
 ParallelVacuumState *
 parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes,
 					 int nrequested_workers, int vac_work_mem,
-					 int elevel, BufferAccessStrategy bstrategy)
+					 int elevel)
 {
 	ParallelVacuumState *pvs;
 	ParallelContext *pcxt;
@@ -345,7 +336,6 @@ parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes,
 	pvs->indrels = indrels;
 	pvs->nindexes = nindexes;
 	pvs->will_parallel_vacuum = will_parallel_vacuum;
-	pvs->bstrategy = bstrategy;
 	pvs->heaprel = rel;
 
 	EnterParallelMode();
@@ -447,9 +437,6 @@ parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes,
 	shared->dead_items_handle = TidStoreGetHandle(dead_items);
 	shared->dead_items_dsa_handle = dsa_get_handle(TidStoreGetDSA(dead_items));
 
-	/* Use the same buffer size for all workers */
-	shared->ring_nbuffers = GetAccessStrategyBufferCount(bstrategy);
-
 	pg_atomic_init_u32(&(shared->cost_balance), 0);
 	pg_atomic_init_u32(&(shared->active_nworkers), 0);
 	pg_atomic_init_u32(&(shared->idx), 0);
@@ -1091,7 +1078,6 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
 	ivinfo.message_level = DEBUG2;
 	ivinfo.estimated_count = pvs->shared->estimated_count;
 	ivinfo.num_heap_tuples = pvs->shared->reltuples;
-	ivinfo.strategy = pvs->bstrategy;
 
 	/* Update error traceback information */
 	pvs->indname = pstrdup(RelationGetRelationName(indrel));
@@ -1294,10 +1280,6 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
 	pvs.indname = NULL;
 	pvs.status = PARALLEL_INDVAC_STATUS_INITIAL;
 
-	/* Each parallel VACUUM worker gets its own access strategy. */
-	pvs.bstrategy = GetAccessStrategyWithSize(BAS_VACUUM,
-											  shared->ring_nbuffers * (BLCKSZ / 1024));
-
 	/* Setup error traceback support for ereport() */
 	errcallback.callback = parallel_vacuum_error_callback;
 	errcallback.arg = &pvs;
@@ -1328,7 +1310,6 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
 
 	vac_close_indexes(nindexes, indrels, RowExclusiveLock);
 	table_close(rel, ShareUpdateExclusiveLock);
-	FreeAccessStrategy(pvs.bstrategy);
 
 	if (shared->is_autovacuum)
 		pv_shared_cost_params = NULL;
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 45abf48768a..e021e798805 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -386,8 +386,7 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
 									  bool *dovacuum, bool *doanalyze, bool *wraparound,
 									  AutoVacuumScores *scores);
 
-static void autovacuum_do_vac_analyze(autovac_table *tab,
-									  BufferAccessStrategy bstrategy);
+static void autovacuum_do_vac_analyze(autovac_table *tab);
 static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
 										 TupleDesc pg_class_desc);
 static void perform_work_item(AutoVacuumWorkItem *workitem);
@@ -1936,7 +1935,6 @@ do_autovacuum(void)
 	HASHCTL		ctl;
 	HTAB	   *table_toast_map;
 	ListCell   *volatile cell;
-	BufferAccessStrategy bstrategy;
 	ScanKeyData key;
 	TupleDesc	pg_class_desc;
 	int			effective_multixact_freeze_max_age;
@@ -2322,23 +2320,6 @@ do_autovacuum(void)
 		autovacuum_analyze_score_weight != 0.0)
 		list_sort(tables_to_process, TableToProcessComparator);
 
-	/*
-	 * Optionally, create a buffer access strategy object for VACUUM to use.
-	 * We use the same BufferAccessStrategy object for all tables VACUUMed by
-	 * this worker to prevent autovacuum from blowing out shared buffers.
-	 *
-	 * VacuumBufferUsageLimit being set to 0 results in
-	 * GetAccessStrategyWithSize returning NULL, effectively meaning we can
-	 * use up to all of shared buffers.
-	 *
-	 * If we later enter failsafe mode on any of the tables being vacuumed, we
-	 * will cease use of the BufferAccessStrategy only for that table.
-	 *
-	 * XXX should we consider adding code to adjust the size of this if
-	 * VacuumBufferUsageLimit changes?
-	 */
-	bstrategy = GetAccessStrategyWithSize(BAS_VACUUM, VacuumBufferUsageLimit);
-
 	/*
 	 * create a memory context to act as fake PortalContext, so that the
 	 * contexts created in the vacuum code are cleaned up for each table.
@@ -2516,7 +2497,7 @@ do_autovacuum(void)
 			MemoryContextSwitchTo(PortalContext);
 
 			/* have at it */
-			autovacuum_do_vac_analyze(tab, bstrategy);
+			autovacuum_do_vac_analyze(tab);
 
 			/*
 			 * Clear a possible query-cancel signal, to avoid a late reaction
@@ -2636,8 +2617,6 @@ deleted:
 #ifdef USE_VALGRIND
 	hash_destroy(table_toast_map);
 	FreeTupleDesc(pg_class_desc);
-	if (bstrategy)
-		pfree(bstrategy);
 #endif
 
 	/* Run the rest in xact context, mainly to avoid Valgrind leak warnings */
@@ -3348,7 +3327,7 @@ relation_needs_vacanalyze(Oid relid,
  * disappear at transaction commit.
  */
 static void
-autovacuum_do_vac_analyze(autovac_table *tab, BufferAccessStrategy bstrategy)
+autovacuum_do_vac_analyze(autovac_table *tab)
 {
 	RangeVar   *rangevar;
 	VacuumRelation *rel;
@@ -3371,7 +3350,7 @@ autovacuum_do_vac_analyze(autovac_table *tab, BufferAccessStrategy bstrategy)
 	rel_list = list_make1(rel);
 	MemoryContextSwitchTo(old_context);
 
-	vacuum(rel_list, &tab->at_params, bstrategy, vac_context, true);
+	vacuum(rel_list, &tab->at_params, vac_context, true);
 
 	MemoryContextDelete(vac_context);
 }
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 7f29551202f..57322001355 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -390,7 +390,7 @@ static List *BuildRelationList(bool temp_relations, bool include_shared);
 static void FreeDatabaseList(List *dblist);
 static DataChecksumsWorkerResult ProcessDatabase(DataChecksumsWorkerDatabase *db);
 static bool ProcessAllDatabases(void);
-static bool ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy);
+static bool ProcessSingleRelationFork(Relation reln, ForkNumber forkNum);
 static void ResetDataChecksumsProgressCounters(void);
 static void launcher_cancel_handler(SIGNAL_ARGS);
 static void WaitForAllTransactionsToFinish(void);
@@ -687,7 +687,7 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
  * error is raised in the lower levels.
  */
 static bool
-ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy)
+ProcessSingleRelationFork(Relation reln, ForkNumber forkNum)
 {
 	BlockNumber numblocks = RelationGetNumberOfBlocksInFork(reln, forkNum);
 	char		activity[NAMEDATALEN * 2 + 128];
@@ -722,7 +722,7 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg
 	 */
 	for (BlockNumber blknum = 0; blknum < numblocks; blknum++)
 	{
-		Buffer		buf = ReadBufferExtended(reln, forkNum, blknum, RBM_NORMAL, strategy);
+		Buffer		buf = ReadBufferExtended(reln, forkNum, blknum, RBM_NORMAL);
 
 		/* Need to get an exclusive lock to mark the buffer as dirty */
 		LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
@@ -808,7 +808,7 @@ ResetDataChecksumsProgressCounters(void)
  * error is raised in the lower levels.
  */
 static bool
-ProcessSingleRelationByOid(Oid relationId, BufferAccessStrategy strategy)
+ProcessSingleRelationByOid(Oid relationId)
 {
 	Relation	rel;
 	bool		aborted = false;
@@ -835,7 +835,7 @@ ProcessSingleRelationByOid(Oid relationId, BufferAccessStrategy strategy)
 	{
 		if (smgrexists(rel->rd_smgr, fnum))
 		{
-			if (!ProcessSingleRelationFork(rel, fnum, strategy))
+			if (!ProcessSingleRelationFork(rel, fnum))
 			{
 				aborted = true;
 				break;
@@ -1582,7 +1582,6 @@ DataChecksumsWorkerMain(Datum arg)
 	Oid			dboid;
 	List	   *RelationList = NIL;
 	List	   *InitialTempTableList = NIL;
-	BufferAccessStrategy strategy;
 	bool		aborted = false;
 	int64		rels_done;
 	bool		process_shared;
@@ -1650,11 +1649,6 @@ DataChecksumsWorkerMain(Datum arg)
 	VacuumUpdateCosts();
 	VacuumCostBalance = 0;
 
-	/*
-	 * Create and set the vacuum strategy as our buffer strategy.
-	 */
-	strategy = GetAccessStrategy(BAS_VACUUM);
-
 	RelationList = BuildRelationList(false, process_shared);
 
 	/* Update the total number of relations to be processed in this DB. */
@@ -1676,9 +1670,7 @@ DataChecksumsWorkerMain(Datum arg)
 	rels_done = 0;
 	foreach_oid(reloid, RelationList)
 	{
-		bool		costs_updated = false;
-
-		if (!ProcessSingleRelationByOid(reloid, strategy))
+		if (!ProcessSingleRelationByOid(reloid))
 		{
 			aborted = true;
 			break;
@@ -1694,8 +1686,7 @@ DataChecksumsWorkerMain(Datum arg)
 
 		/*
 		 * Check if the cost settings changed during runtime and if so, update
-		 * to reflect the new values and signal that the access strategy needs
-		 * to be refreshed.
+		 * to reflect the new values.
 		 */
 		LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
 		if (DataChecksumState->worker_invocation != worker_invocation)
@@ -1706,7 +1697,6 @@ DataChecksumsWorkerMain(Datum arg)
 		if ((DataChecksumState->launch_cost_delay != DataChecksumState->cost_delay)
 			|| (DataChecksumState->launch_cost_limit != DataChecksumState->cost_limit))
 		{
-			costs_updated = true;
 			VacuumCostDelay = DataChecksumState->launch_cost_delay;
 			VacuumCostLimit = DataChecksumState->launch_cost_limit;
 			VacuumUpdateCosts();
@@ -1714,19 +1704,10 @@ DataChecksumsWorkerMain(Datum arg)
 			DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
 			DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
 		}
-		else
-			costs_updated = false;
 		LWLockRelease(DataChecksumsWorkerLock);
-
-		if (costs_updated)
-		{
-			FreeAccessStrategy(strategy);
-			strategy = GetAccessStrategy(BAS_VACUUM);
-		}
 	}
 
 	list_free(RelationList);
-	FreeAccessStrategy(strategy);
 
 	if (aborted || abort_requested)
 	{
diff --git a/src/backend/storage/aio/read_stream.c b/src/backend/storage/aio/read_stream.c
index a318539e56c..08d344f063d 100644
--- a/src/backend/storage/aio/read_stream.c
+++ b/src/backend/storage/aio/read_stream.c
@@ -757,7 +757,6 @@ read_stream_look_ahead(ReadStream *stream)
  */
 static ReadStream *
 read_stream_begin_impl(int flags,
-					   BufferAccessStrategy strategy,
 					   Relation rel,
 					   SMgrRelation smgr,
 					   char persistence,
@@ -771,7 +770,6 @@ read_stream_begin_impl(int flags,
 	int16		queue_size;
 	int16		queue_overflow;
 	int			max_ios;
-	int			strategy_pin_limit;
 	uint32		max_pinned_buffers;
 	uint32		max_possible_buffer_limit;
 	Oid			tablespace_id;
@@ -835,10 +833,6 @@ read_stream_begin_impl(int flags,
 	max_pinned_buffers = Min(max_pinned_buffers,
 							 PG_INT16_MAX - queue_overflow - 1);
 
-	/* Give the strategy a chance to limit the number of buffers we pin. */
-	strategy_pin_limit = GetAccessStrategyPinLimit(strategy);
-	max_pinned_buffers = Min(strategy_pin_limit, max_pinned_buffers);
-
 	/*
 	 * Also limit our queue to the maximum number of pins we could ever be
 	 * allowed to acquire according to the buffer manager.  We may not really
@@ -962,7 +956,6 @@ read_stream_begin_impl(int flags,
 		stream->ios[i].op.smgr = smgr;
 		stream->ios[i].op.persistence = persistence;
 		stream->ios[i].op.forknum = forknum;
-		stream->ios[i].op.strategy = strategy;
 	}
 
 	return stream;
@@ -974,7 +967,6 @@ read_stream_begin_impl(int flags,
  */
 ReadStream *
 read_stream_begin_relation(int flags,
-						   BufferAccessStrategy strategy,
 						   Relation rel,
 						   ForkNumber forknum,
 						   ReadStreamBlockNumberCB callback,
@@ -982,7 +974,6 @@ read_stream_begin_relation(int flags,
 						   size_t per_buffer_data_size)
 {
 	return read_stream_begin_impl(flags,
-								  strategy,
 								  rel,
 								  RelationGetSmgr(rel),
 								  rel->rd_rel->relpersistence,
@@ -998,7 +989,6 @@ read_stream_begin_relation(int flags,
  */
 ReadStream *
 read_stream_begin_smgr_relation(int flags,
-								BufferAccessStrategy strategy,
 								SMgrRelation smgr,
 								char smgr_persistence,
 								ForkNumber forknum,
@@ -1007,7 +997,6 @@ read_stream_begin_smgr_relation(int flags,
 								size_t per_buffer_data_size)
 {
 	return read_stream_begin_impl(flags,
-								  strategy,
 								  NULL,
 								  smgr,
 								  smgr_persistence,
@@ -1370,13 +1359,11 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data)
  * Transitional support for code that would like to perform or skip reads
  * itself, without using the stream.  Returns, and consumes, the next block
  * number that would be read by the stream's look-ahead algorithm, or
- * InvalidBlockNumber if the end of the stream is reached.  Also reports the
- * strategy that would be used to read it.
+ * InvalidBlockNumber if the end of the stream is reached.
  */
 BlockNumber
-read_stream_next_block(ReadStream *stream, BufferAccessStrategy *strategy)
+read_stream_next_block(ReadStream *stream)
 {
-	*strategy = stream->ios[0].op.strategy;
 	return read_stream_get_block(stream, NULL);
 }
 
diff --git a/src/backend/storage/buffer/README b/src/backend/storage/buffer/README
index b332e002ba1..58df6bd011a 100644
--- a/src/backend/storage/buffer/README
+++ b/src/backend/storage/buffer/README
@@ -203,50 +203,6 @@ have to give up and try another buffer.  This however is not a concern
 of the basic select-a-victim-buffer algorithm.)
 
 
-Buffer Ring Replacement Strategy
----------------------------------
-
-When running a query that needs to access a large number of pages just once,
-such as VACUUM or a large sequential scan, a different strategy is used.
-A page that has been touched only by such a scan is unlikely to be needed
-again soon, so instead of running the normal clock-sweep algorithm and
-blowing out the entire buffer cache, a small ring of buffers is allocated
-using the normal clock-sweep algorithm and those buffers are reused for the
-whole scan.  This also implies that much of the write traffic caused by such
-a statement will be done by the backend itself and not pushed off onto other
-processes.
-
-For sequential scans, a 256KB ring is used. That's small enough to fit in L2
-cache, which makes transferring pages from OS cache to shared buffer cache
-efficient.  Even less would often be enough, but the ring must be big enough
-to accommodate all pages in the scan that are pinned concurrently.  256KB
-should also be enough to leave a small cache trail for other backends to
-join in a synchronized seq scan.  If a ring buffer is dirtied and its LSN
-updated, we would normally have to write and flush WAL before we could
-re-use the buffer; in this case we instead discard the buffer from the ring
-and (later) choose a replacement using the normal clock-sweep algorithm.
-Hence this strategy works best for scans that are read-only (or at worst
-update hint bits).  In a scan that modifies every page in the scan, like a
-bulk UPDATE or DELETE, the buffers in the ring will always be dirtied and
-the ring strategy effectively degrades to the normal strategy.
-
-VACUUM uses a ring like sequential scans, however, the size of this ring is
-controlled by the vacuum_buffer_usage_limit GUC.  Dirty pages are not removed
-from the ring.  Instead, the WAL is flushed if needed to allow reuse of the
-buffers.  Before introducing the buffer ring strategy in 8.3, VACUUM's buffers
-were sent to the freelist, which was effectively a buffer ring of 1 buffer,
-resulting in excessive WAL flushing.
-
-Bulk writes work similarly to VACUUM.  Currently this applies only to
-COPY IN and CREATE TABLE AS SELECT.  (Might it be interesting to make
-seqscan UPDATE and DELETE use the bulkwrite strategy?)  For bulk writes
-we use a ring size of 16MB (but not more than 1/8th of shared_buffers).
-Smaller sizes have been shown to result in the COPY blocking too often
-for WAL flushes.  While it's okay for a background vacuum to be slowed by
-doing its own WAL flushing, we'd prefer that COPY not be subject to that,
-so we let it use up a bit more of the buffer arena.
-
-
 Background Writer's Processing
 ------------------------------
 
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 1173b50b7c1..30f2443e577 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -612,10 +612,9 @@ ForgetPrivateRefCountEntry(PrivateRefCountEntry *ref)
 static Buffer ReadBuffer_common(Relation rel,
 								SMgrRelation smgr, char smgr_persistence,
 								ForkNumber forkNum, BlockNumber blockNum,
-								ReadBufferMode mode, BufferAccessStrategy strategy);
+								ReadBufferMode mode);
 static BlockNumber ExtendBufferedRelCommon(BufferManagerRelation bmr,
 										   ForkNumber fork,
-										   BufferAccessStrategy strategy,
 										   uint32 flags,
 										   uint32 extend_by,
 										   BlockNumber extend_upto,
@@ -623,13 +622,12 @@ static BlockNumber ExtendBufferedRelCommon(BufferManagerRelation bmr,
 										   uint32 *extended_by);
 static BlockNumber ExtendBufferedRelShared(BufferManagerRelation bmr,
 										   ForkNumber fork,
-										   BufferAccessStrategy strategy,
 										   uint32 flags,
 										   uint32 extend_by,
 										   BlockNumber extend_upto,
 										   Buffer *buffers,
 										   uint32 *extended_by);
-static bool PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy,
+static bool PinBuffer(BufferDesc *buf,
 					  bool skip_if_not_valid);
 static void PinBuffer_Locked(BufferDesc *buf);
 static void UnpinBuffer(BufferDesc *buf);
@@ -645,7 +643,6 @@ static inline BufferDesc *BufferAlloc(SMgrRelation smgr,
 									  char relpersistence,
 									  ForkNumber forkNum,
 									  BlockNumber blockNum,
-									  BufferAccessStrategy strategy,
 									  bool *foundPtr, IOContext io_context);
 static bool AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress);
 static void CheckReadBuffersOperation(ReadBuffersOperation *operation, bool is_complete);
@@ -654,7 +651,7 @@ static pg_attribute_always_inline void TrackBufferHit(IOObject io_object,
 													  IOContext io_context,
 													  Relation rel, char persistence, SMgrRelation smgr,
 													  ForkNumber forknum, BlockNumber blocknum);
-static Buffer GetVictimBuffer(BufferAccessStrategy strategy, IOContext io_context);
+static Buffer GetVictimBuffer(IOContext io_context);
 static void FlushUnlockedBuffer(BufferDesc *buf, SMgrRelation reln,
 								IOObject io_object, IOContext io_context);
 static void FlushBuffer(BufferDesc *buf, SMgrRelation reln,
@@ -858,7 +855,7 @@ ReadRecentBuffer(RelFileLocator rlocator, ForkNumber forkNum, BlockNumber blockN
 		 * pin.
 		 */
 		if (BufferTagsEqual(&tag, &bufHdr->tag) &&
-			PinBuffer(bufHdr, NULL, true))
+			PinBuffer(bufHdr, true))
 		{
 			if (BufferTagsEqual(&tag, &bufHdr->tag))
 			{
@@ -874,12 +871,12 @@ ReadRecentBuffer(RelFileLocator rlocator, ForkNumber forkNum, BlockNumber blockN
 
 /*
  * ReadBuffer -- a shorthand for ReadBufferExtended, for reading from main
- *		fork with RBM_NORMAL mode and default strategy.
+ *		fork with RBM_NORMAL mode.
  */
 Buffer
 ReadBuffer(Relation reln, BlockNumber blockNum)
 {
-	return ReadBufferExtended(reln, MAIN_FORKNUM, blockNum, RBM_NORMAL, NULL);
+	return ReadBufferExtended(reln, MAIN_FORKNUM, blockNum, RBM_NORMAL);
 }
 
 /*
@@ -919,13 +916,10 @@ ReadBuffer(Relation reln, BlockNumber blockNum)
  * a cleanup-strength lock on the page.
  *
  * RBM_NORMAL_NO_LOG mode is treated the same as RBM_NORMAL here.
- *
- * If strategy is not NULL, a nondefault buffer access strategy is used.
- * See buffer/README for details.
  */
 inline Buffer
 ReadBufferExtended(Relation reln, ForkNumber forkNum, BlockNumber blockNum,
-				   ReadBufferMode mode, BufferAccessStrategy strategy)
+				   ReadBufferMode mode)
 {
 	Buffer		buf;
 
@@ -935,7 +929,7 @@ ReadBufferExtended(Relation reln, ForkNumber forkNum, BlockNumber blockNum,
 	 * ReadBuffer_common().
 	 */
 	buf = ReadBuffer_common(reln, RelationGetSmgr(reln), 0,
-							forkNum, blockNum, mode, strategy);
+							forkNum, blockNum, mode);
 
 	return buf;
 }
@@ -954,14 +948,14 @@ ReadBufferExtended(Relation reln, ForkNumber forkNum, BlockNumber blockNum,
 Buffer
 ReadBufferWithoutRelcache(RelFileLocator rlocator, ForkNumber forkNum,
 						  BlockNumber blockNum, ReadBufferMode mode,
-						  BufferAccessStrategy strategy, bool permanent)
+						  bool permanent)
 {
 	SMgrRelation smgr = smgropen(rlocator, INVALID_PROC_NUMBER);
 
 	return ReadBuffer_common(NULL, smgr,
 							 permanent ? RELPERSISTENCE_PERMANENT : RELPERSISTENCE_UNLOGGED,
 							 forkNum, blockNum,
-							 mode, strategy);
+							 mode);
 }
 
 /*
@@ -970,13 +964,12 @@ ReadBufferWithoutRelcache(RelFileLocator rlocator, ForkNumber forkNum,
 Buffer
 ExtendBufferedRel(BufferManagerRelation bmr,
 				  ForkNumber forkNum,
-				  BufferAccessStrategy strategy,
 				  uint32 flags)
 {
 	Buffer		buf;
 	uint32		extend_by = 1;
 
-	ExtendBufferedRelBy(bmr, forkNum, strategy, flags, extend_by,
+	ExtendBufferedRelBy(bmr, forkNum, flags, extend_by,
 						&buf, &extend_by);
 
 	return buf;
@@ -1002,7 +995,6 @@ ExtendBufferedRel(BufferManagerRelation bmr,
 BlockNumber
 ExtendBufferedRelBy(BufferManagerRelation bmr,
 					ForkNumber fork,
-					BufferAccessStrategy strategy,
 					uint32 flags,
 					uint32 extend_by,
 					Buffer *buffers,
@@ -1015,7 +1007,7 @@ ExtendBufferedRelBy(BufferManagerRelation bmr,
 	if (bmr.relpersistence == '\0')
 		bmr.relpersistence = bmr.rel->rd_rel->relpersistence;
 
-	return ExtendBufferedRelCommon(bmr, fork, strategy, flags,
+	return ExtendBufferedRelCommon(bmr, fork, flags,
 								   extend_by, InvalidBlockNumber,
 								   buffers, extended_by);
 }
@@ -1031,7 +1023,6 @@ ExtendBufferedRelBy(BufferManagerRelation bmr,
 Buffer
 ExtendBufferedRelTo(BufferManagerRelation bmr,
 					ForkNumber fork,
-					BufferAccessStrategy strategy,
 					uint32 flags,
 					BlockNumber extend_to,
 					ReadBufferMode mode)
@@ -1097,7 +1088,7 @@ ExtendBufferedRelTo(BufferManagerRelation bmr,
 		if ((uint64) current_size + num_pages > extend_to)
 			num_pages = extend_to - current_size;
 
-		first_block = ExtendBufferedRelCommon(bmr, fork, strategy, flags,
+		first_block = ExtendBufferedRelCommon(bmr, fork, flags,
 											  num_pages, extend_to,
 											  buffers, &extended_by);
 
@@ -1123,7 +1114,7 @@ ExtendBufferedRelTo(BufferManagerRelation bmr,
 	{
 		Assert(extended_by == 0);
 		buffer = ReadBuffer_common(bmr.rel, BMR_GET_SMGR(bmr), bmr.relpersistence,
-								   fork, extend_to - 1, mode, strategy);
+								   fork, extend_to - 1, mode);
 	}
 
 	return buffer;
@@ -1226,7 +1217,6 @@ PinBufferForBlock(Relation rel,
 				  char persistence,
 				  ForkNumber forkNum,
 				  BlockNumber blockNum,
-				  BufferAccessStrategy strategy,
 				  IOObject io_object,
 				  IOContext io_context,
 				  bool *foundPtr)
@@ -1250,7 +1240,7 @@ PinBufferForBlock(Relation rel,
 		bufHdr = LocalBufferAlloc(smgr, forkNum, blockNum, foundPtr);
 	else
 		bufHdr = BufferAlloc(smgr, persistence, forkNum, blockNum,
-							 strategy, foundPtr, io_context);
+							 foundPtr, io_context);
 
 	if (*foundPtr)
 		TrackBufferHit(io_object, io_context, rel, persistence, smgr, forkNum, blockNum);
@@ -1276,8 +1266,7 @@ PinBufferForBlock(Relation rel,
 static pg_attribute_always_inline Buffer
 ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
 				  ForkNumber forkNum,
-				  BlockNumber blockNum, ReadBufferMode mode,
-				  BufferAccessStrategy strategy)
+				  BlockNumber blockNum, ReadBufferMode mode)
 {
 	ReadBuffersOperation operation;
 	Buffer		buffer;
@@ -1313,7 +1302,7 @@ ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
 		if (mode == RBM_ZERO_AND_LOCK || mode == RBM_ZERO_AND_CLEANUP_LOCK)
 			flags |= EB_LOCK_FIRST;
 
-		return ExtendBufferedRel(BMR_REL(rel), forkNum, strategy, flags);
+		return ExtendBufferedRel(BMR_REL(rel), forkNum, flags);
 	}
 
 	if (rel)
@@ -1335,12 +1324,12 @@ ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
 		}
 		else
 		{
-			io_context = IOContextForStrategy(strategy);
+			io_context = IOCONTEXT_NORMAL;
 			io_object = IOOBJECT_RELATION;
 		}
 
 		buffer = PinBufferForBlock(rel, smgr, persistence,
-								   forkNum, blockNum, strategy,
+								   forkNum, blockNum,
 								   io_object, io_context, &found);
 		ZeroAndLockBuffer(buffer, mode, found);
 		return buffer;
@@ -1358,7 +1347,6 @@ ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
 	operation.rel = rel;
 	operation.persistence = persistence;
 	operation.forknum = forkNum;
-	operation.strategy = strategy;
 	if (StartReadBuffer(&operation,
 						&buffer,
 						blockNum,
@@ -1399,7 +1387,7 @@ StartReadBuffersImpl(ReadBuffersOperation *operation,
 	}
 	else
 	{
-		io_context = IOContextForStrategy(operation->strategy);
+		io_context = IOCONTEXT_NORMAL;
 		io_object = IOOBJECT_RELATION;
 	}
 
@@ -1450,7 +1438,6 @@ StartReadBuffersImpl(ReadBuffersOperation *operation,
 										   operation->persistence,
 										   operation->forknum,
 										   blockNum + i,
-										   operation->strategy,
 										   io_object, io_context,
 										   &found);
 		}
@@ -1771,7 +1758,7 @@ WaitReadBuffers(ReadBuffersOperation *operation)
 	}
 	else
 	{
-		io_context = IOContextForStrategy(operation->strategy);
+		io_context = IOCONTEXT_NORMAL;
 		io_object = IOOBJECT_RELATION;
 	}
 
@@ -1961,7 +1948,7 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress)
 	}
 	else
 	{
-		io_context = IOContextForStrategy(operation->strategy);
+		io_context = IOCONTEXT_NORMAL;
 		io_object = IOOBJECT_RELATION;
 	}
 
@@ -2180,24 +2167,18 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress)
  *		buffer.  If no buffer exists already, selects a replacement victim and
  *		evicts the old page, but does NOT read in new page.
  *
- * "strategy" can be a buffer replacement strategy object, or NULL for
- * the default strategy.  The selected buffer's usage_count is advanced when
- * using the default strategy, but otherwise possibly not (see PinBuffer).
- *
  * The returned buffer is pinned and is already marked as holding the
  * desired page.  If it already did have the desired page, *foundPtr is
  * set true.  Otherwise, *foundPtr is set false.
  *
- * io_context is passed as an output parameter to avoid calling
- * IOContextForStrategy() when there is a shared buffers hit and no IO
- * statistics need be captured.
+ * io_context is passed as an output parameter to avoid capturing IO
+ * statistics when there is a shared buffers hit and no IO occurs.
  *
  * No locks are held either at entry or exit.
  */
 static pg_attribute_always_inline BufferDesc *
 BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 			BlockNumber blockNum,
-			BufferAccessStrategy strategy,
 			bool *foundPtr, IOContext io_context)
 {
 	BufferTag	newTag;			/* identity of requested block */
@@ -2235,7 +2216,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 		 */
 		buf = GetBufferDescriptor(existing_buf_id);
 
-		valid = PinBuffer(buf, strategy, false);
+		valid = PinBuffer(buf, false);
 
 		/* Can release the mapping lock as soon as we've pinned it */
 		LWLockRelease(newPartitionLock);
@@ -2266,7 +2247,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	 * don't hold any conflicting locks. If so we'll have to undo our work
 	 * later.
 	 */
-	victim_buffer = GetVictimBuffer(strategy, io_context);
+	victim_buffer = GetVictimBuffer(io_context);
 	victim_buf_hdr = GetBufferDescriptor(victim_buffer - 1);
 
 	/*
@@ -2297,7 +2278,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 
 		existing_buf_hdr = GetBufferDescriptor(existing_buf_id);
 
-		valid = PinBuffer(existing_buf_hdr, strategy, false);
+		valid = PinBuffer(existing_buf_hdr, false);
 
 		/* Can release the mapping lock as soon as we've pinned it */
 		LWLockRelease(newPartitionLock);
@@ -2335,9 +2316,12 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	 * just like permanent relations.
 	 */
 	set_bits |= BM_TAG_VALID;
-	/* Admit the newly loaded page COOL (probation); a second access via
+
+	/*
+	 * Admit the newly loaded page COOL (probation); a second access via
 	 * PinBuffer promotes it to HOT.  This is what makes a one-touch scan
-	 * self-evicting -- see the cooling-state notes in buf_internals.h. */
+	 * self-evicting -- see the cooling-state notes in buf_internals.h.
+	 */
 	if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM)
 		set_bits |= BM_PERMANENT;
 
@@ -2549,12 +2533,11 @@ InvalidateVictimBuffer(BufferDesc *buf_hdr)
 }
 
 static Buffer
-GetVictimBuffer(BufferAccessStrategy strategy, IOContext io_context)
+GetVictimBuffer(IOContext io_context)
 {
 	BufferDesc *buf_hdr;
 	Buffer		buf;
 	uint64		buf_state;
-	bool		from_ring;
 
 	/*
 	 * Ensure, before we pin a victim buffer, that there's a free refcount
@@ -2570,7 +2553,7 @@ again:
 	 * Select a victim buffer.  The buffer is returned pinned and owned by
 	 * this backend.
 	 */
-	buf_hdr = StrategyGetBuffer(strategy, &buf_state, &from_ring);
+	buf_hdr = StrategyGetBuffer(&buf_state);
 	buf = BufferDescriptorGetBuffer(buf_hdr);
 
 	/*
@@ -2614,26 +2597,6 @@ again:
 			goto again;
 		}
 
-		/*
-		 * If using a nondefault strategy, and this victim came from the
-		 * strategy ring, let the strategy decide whether to reject it when
-		 * reusing it would require a WAL flush.  This only applies to
-		 * permanent buffers; unlogged buffers can have fake LSNs, so
-		 * XLogNeedsFlush() is not meaningful for them.
-		 *
-		 * We need to hold the content lock in at least share-exclusive mode
-		 * to safely inspect the page LSN, so this couldn't have been done
-		 * inside StrategyGetBuffer().
-		 */
-		if (strategy && from_ring &&
-			buf_state & BM_PERMANENT &&
-			XLogNeedsFlush(BufferGetLSN(buf_hdr)) &&
-			StrategyRejectBuffer(strategy, buf_hdr, from_ring))
-		{
-			UnlockReleaseBuffer(buf);
-			goto again;
-		}
-
 		/* OK, do the I/O */
 		FlushBuffer(buf_hdr, NULL, IOOBJECT_RELATION, io_context);
 		LockBuffer(buf, BUFFER_LOCK_UNLOCK);
@@ -2646,23 +2609,15 @@ again:
 	if (buf_state & BM_VALID)
 	{
 		/*
-		 * When a BufferAccessStrategy is in use, blocks evicted from shared
-		 * buffers are counted as IOOP_EVICT in the corresponding context
-		 * (e.g. IOCONTEXT_BULKWRITE). Shared buffers are evicted by a
-		 * strategy in two cases: 1) while initially claiming buffers for the
-		 * strategy ring 2) to replace an existing strategy ring buffer
-		 * because it is pinned or in use and cannot be reused.
+		 * Blocks evicted from shared buffers are counted as IOOP_EVICT.
 		 *
-		 * Blocks evicted from buffers already in the strategy ring are
-		 * counted as IOOP_REUSE in the corresponding strategy context.
-		 *
-		 * At this point, we can accurately count evictions and reuses,
-		 * because we have successfully claimed the valid buffer. Previously,
-		 * we may have been forced to release the buffer due to concurrent
-		 * pinners or erroring out.
+		 * At this point, we can accurately count evictions, because we have
+		 * successfully claimed the valid buffer. Previously, we may have been
+		 * forced to release the buffer due to concurrent pinners or erroring
+		 * out.
 		 */
 		pgstat_count_io_op(IOOBJECT_RELATION, io_context,
-						   from_ring ? IOOP_REUSE : IOOP_EVICT, 1, 0);
+						   IOOP_EVICT, 1, 0);
 	}
 
 	/*
@@ -2754,7 +2709,6 @@ LimitAdditionalPins(uint32 *additional_pins)
 static BlockNumber
 ExtendBufferedRelCommon(BufferManagerRelation bmr,
 						ForkNumber fork,
-						BufferAccessStrategy strategy,
 						uint32 flags,
 						uint32 extend_by,
 						BlockNumber extend_upto,
@@ -2789,7 +2743,7 @@ ExtendBufferedRelCommon(BufferManagerRelation bmr,
 											 buffers, &extend_by);
 	}
 	else
-		first_block = ExtendBufferedRelShared(bmr, fork, strategy, flags,
+		first_block = ExtendBufferedRelShared(bmr, fork, flags,
 											  extend_by, extend_upto,
 											  buffers, &extend_by);
 	*extended_by = extend_by;
@@ -2812,7 +2766,6 @@ ExtendBufferedRelCommon(BufferManagerRelation bmr,
 static BlockNumber
 ExtendBufferedRelShared(BufferManagerRelation bmr,
 						ForkNumber fork,
-						BufferAccessStrategy strategy,
 						uint32 flags,
 						uint32 extend_by,
 						BlockNumber extend_upto,
@@ -2820,7 +2773,7 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 						uint32 *extended_by)
 {
 	BlockNumber first_block;
-	IOContext	io_context = IOContextForStrategy(strategy);
+	IOContext	io_context = IOCONTEXT_NORMAL;
 	instr_time	io_start;
 
 	LimitAdditionalPins(&extend_by);
@@ -2839,7 +2792,7 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 	{
 		Block		buf_block;
 
-		buffers[i] = GetVictimBuffer(strategy, io_context);
+		buffers[i] = GetVictimBuffer(io_context);
 		buf_block = BufHdrGetBlock(GetBufferDescriptor(buffers[i] - 1));
 
 		/* new buffers are zero-filled */
@@ -2957,7 +2910,7 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 			 * Pin the existing buffer before releasing the partition lock,
 			 * preventing it from being evicted.
 			 */
-			valid = PinBuffer(existing_hdr, strategy, false);
+			valid = PinBuffer(existing_hdr, false);
 
 			LWLockRelease(partition_lock);
 			UnpinBuffer(victim_buf_hdr);
@@ -3007,8 +2960,11 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 			victim_buf_hdr->tag = tag;
 
 			set_bits |= BM_TAG_VALID;
-			/* Admit COOL (probation); see the comment at the other admission
-			 * site and the cooling-state notes in buf_internals.h. */
+
+			/*
+			 * Admit COOL (probation); see the comment at the other admission
+			 * site and the cooling-state notes in buf_internals.h.
+			 */
 			if (bmr.relpersistence == RELPERSISTENCE_PERMANENT || fork == INIT_FORKNUM)
 				set_bits |= BM_PERMANENT;
 
@@ -3275,13 +3231,8 @@ ReleaseAndReadBuffer(Buffer buffer,
 /*
  * PinBuffer -- make buffer unavailable for replacement.
  *
- * For the default access strategy, the buffer's usage_count is incremented
- * when we first pin it; for other strategies we just make sure the usage_count
- * isn't zero.  (The idea of the latter is that we don't want synchronized
- * heap scans to inflate the count, but we need it to not be zero to discourage
- * other backends from stealing buffers from our ring.  As long as we cycle
- * through the ring faster than the global clock-sweep cycles, buffers in
- * our ring won't be chosen as victims for replacement by other backends.)
+ * The buffer's cooling state is promoted to HOT (the 2Q rescue) when we pin
+ * it; see the cooling-state notes in buf_internals.h.
  *
  * This should be applied only to shared buffers, never local ones.
  *
@@ -3298,7 +3249,7 @@ ReleaseAndReadBuffer(Buffer buffer,
  * (recently) invalid and has not been pinned.
  */
 static bool
-PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy,
+PinBuffer(BufferDesc *buf,
 		  bool skip_if_not_valid)
 {
 	Buffer		b = BufferDescriptorGetBuffer(buf);
@@ -3396,7 +3347,7 @@ PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy,
  * ResourceOwnerEnlarge(CurrentResourceOwner);
  *
  * Currently, no callers of this function want to modify the buffer's
- * usage_count at all, so there's no need for a strategy parameter.
+ * cooling state at all.
  * Also we don't bother with a BM_VALID test (the caller could check that for
  * itself).
  *
@@ -4659,22 +4610,8 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object,
 			  false);
 
 	/*
-	 * When a strategy is in use, only flushes of dirty buffers already in the
-	 * strategy ring are counted as strategy writes (IOCONTEXT
-	 * [BULKREAD|BULKWRITE|VACUUM] IOOP_WRITE) for the purpose of IO
-	 * statistics tracking.
-	 *
-	 * If a shared buffer initially added to the ring must be flushed before
-	 * being used, this is counted as an IOCONTEXT_NORMAL IOOP_WRITE.
-	 *
-	 * If a shared buffer which was added to the ring later because the
-	 * current strategy buffer is pinned or in use or because all strategy
-	 * buffers were dirty and rejected (for BAS_BULKREAD operations only)
-	 * requires flushing, this is counted as an IOCONTEXT_NORMAL IOOP_WRITE
-	 * (from_ring will be false).
-	 *
-	 * When a strategy is not in use, the write can only be a "regular" write
-	 * of a dirty shared buffer (IOCONTEXT_NORMAL IOOP_WRITE).
+	 * All writes of a dirty shared buffer are counted as an IOCONTEXT_NORMAL
+	 * IOOP_WRITE for the purpose of IO statistics tracking.
 	 */
 	pgstat_count_io_op_time(io_object, io_context,
 							IOOP_WRITE, io_start, 1, BLCKSZ);
@@ -5435,8 +5372,6 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 	BlockNumber nblocks;
 	BlockNumber blkno;
 	PGIOAlignedBlock buf;
-	BufferAccessStrategy bstrategy_src;
-	BufferAccessStrategy bstrategy_dst;
 	BlockRangeReadStreamPrivate p;
 	ReadStream *src_stream;
 	SMgrRelation src_smgr;
@@ -5464,10 +5399,6 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 	smgrextend(smgropen(dstlocator, INVALID_PROC_NUMBER), forkNum, nblocks - 1,
 			   buf.data, true);
 
-	/* This is a bulk operation, so use buffer access strategies. */
-	bstrategy_src = GetAccessStrategy(BAS_BULKREAD);
-	bstrategy_dst = GetAccessStrategy(BAS_BULKWRITE);
-
 	/* Initialize streaming read */
 	p.current_blocknum = 0;
 	p.last_exclusive = nblocks;
@@ -5479,7 +5410,6 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 	 */
 	src_stream = read_stream_begin_smgr_relation(READ_STREAM_FULL |
 												 READ_STREAM_USE_BATCHING,
-												 bstrategy_src,
 												 src_smgr,
 												 permanent ? RELPERSISTENCE_PERMANENT : RELPERSISTENCE_UNLOGGED,
 												 forkNum,
@@ -5499,7 +5429,7 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 
 		dstBuf = ReadBufferWithoutRelcache(dstlocator, forkNum,
 										   BufferGetBlockNumber(srcBuf),
-										   RBM_ZERO_AND_LOCK, bstrategy_dst,
+										   RBM_ZERO_AND_LOCK,
 										   permanent);
 		dstPage = BufferGetPage(dstBuf);
 
@@ -5520,9 +5450,6 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 	}
 	Assert(read_stream_next_buffer(src_stream, NULL) == InvalidBuffer);
 	read_stream_end(src_stream);
-
-	FreeAccessStrategy(bstrategy_src);
-	FreeAccessStrategy(bstrategy_dst);
 }
 
 /* ---------------------------------------------------------------------
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index cc77144224c..63b2aa9b607 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -77,40 +77,6 @@ const ShmemCallbacks StrategyCtlShmemCallbacks = {
 	.init_fn = StrategyCtlShmemInit,
 };
 
-/*
- * Private (non-shared) state for managing a ring of shared buffers to re-use.
- * This is currently the only kind of BufferAccessStrategy object, but someday
- * we might have more kinds.
- */
-typedef struct BufferAccessStrategyData
-{
-	/* Overall strategy type */
-	BufferAccessStrategyType btype;
-	/* Number of elements in buffers[] array */
-	int			nbuffers;
-
-	/*
-	 * Index of the "current" slot in the ring, ie, the one most recently
-	 * returned by GetBufferFromRing.
-	 */
-	int			current;
-
-	/*
-	 * Array of buffer numbers.  InvalidBuffer (that is, zero) indicates we
-	 * have not yet selected a buffer for this ring slot.  For allocation
-	 * simplicity this is palloc'd together with the fixed fields of the
-	 * struct.
-	 */
-	Buffer		buffers[FLEXIBLE_ARRAY_MEMBER];
-}			BufferAccessStrategyData;
-
-
-/* Prototypes for internal functions */
-static BufferDesc *GetBufferFromRing(BufferAccessStrategy strategy,
-									 uint64 *buf_state);
-static void AddBufferToRing(BufferAccessStrategy strategy,
-							BufferDesc *buf);
-
 /*
  * Per-backend state for the batched clock sweep.  Each backend claims a run
  * of consecutive clock-hand values with a single atomic fetch-add and then
@@ -157,8 +123,8 @@ ClockSweepTick(void)
 			 * must hold the spinlock so StrategySyncStart() can read
 			 * nextVictimBuffer and completePasses consistently.
 			 *
-			 * With batching, multiple backends may each land a fetch-add
-			 * that returns a value past NBuffers in the same pass.  After
+			 * With batching, multiple backends may each land a fetch-add that
+			 * returns a value past NBuffers in the same pass.  After
 			 * acquiring the spinlock we re-read the counter: if another
 			 * backend already wrapped it below NBuffers we are done.
 			 */
@@ -196,8 +162,6 @@ ClockSweepTick(void)
  *	GetVictimBuffer(). The only hard requirement GetVictimBuffer() has is that
  *	the selected buffer must not currently be pinned by anyone.
  *
- *	strategy is a BufferAccessStrategy object, or NULL for default strategy.
- *
  *	It is the callers responsibility to ensure the buffer ownership can be
  *	tracked via TrackNewBufferPin().
  *
@@ -205,29 +169,13 @@ ClockSweepTick(void)
  *	before returning.
  */
 BufferDesc *
-StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_ring)
+StrategyGetBuffer(uint64 *buf_state)
 {
 	BufferDesc *buf;
 	int			bgwprocno;
 	int			trycounter;
 	bool		force_cool;
 
-	*from_ring = false;
-
-	/*
-	 * If given a strategy object, see whether it can select a buffer. We
-	 * assume strategy objects don't need buffer_strategy_lock.
-	 */
-	if (strategy != NULL)
-	{
-		buf = GetBufferFromRing(strategy, buf_state);
-		if (buf != NULL)
-		{
-			*from_ring = true;
-			return buf;
-		}
-	}
-
 	/*
 	 * If asked, we need to waken the bgwriter. Since we don't want to rely on
 	 * a spinlock for this we force a read from shared memory once, and then
@@ -256,8 +204,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 
 	/*
 	 * We count buffer allocation requests so that the bgwriter can estimate
-	 * the rate of buffer consumption.  Note that buffers recycled by a
-	 * strategy object are intentionally not counted here.
+	 * the rate of buffer consumption.
 	 */
 	pg_atomic_fetch_add_u32(&StrategyControl->numBufferAllocs, 1);
 
@@ -323,7 +270,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 				if (!force_cool)
 				{
 					no_progress = true;
-					break;			/* advance the hand, look for COOL */
+					break;		/* advance the hand, look for COOL */
 				}
 
 				local_buf_state &= ~BUF_USAGECOUNT_MASK;	/* HOT -> COOL, clear ref bit */
@@ -356,8 +303,6 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 												   local_buf_state))
 				{
 					/* Found a usable buffer */
-					if (strategy != NULL)
-						AddBufferToRing(strategy, buf);
 					*buf_state = local_buf_state;
 
 					TrackNewBufferPin(BufferDescriptorGetBuffer(buf));
@@ -368,11 +313,12 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 		}
 
 		/*
-		 * A tick that made no progress toward a victim counts down trycounter.
-		 * A full unproductive pass escalates to force_cool (cool HOT buffers
-		 * into victims); a second unproductive full pass means everything is
-		 * pinned, so fail rather than spin forever.  (A failed CAS above is
-		 * neither progress nor a full miss: we simply retry the same buffer.)
+		 * A tick that made no progress toward a victim counts down
+		 * trycounter. A full unproductive pass escalates to force_cool (cool
+		 * HOT buffers into victims); a second unproductive full pass means
+		 * everything is pinned, so fail rather than spin forever.  (A failed
+		 * CAS above is neither progress nor a full miss: we simply retry the
+		 * same buffer.)
 		 */
 		if (no_progress && --trycounter == 0)
 		{
@@ -500,361 +446,3 @@ StrategyCtlShmemInit(void *arg)
 	else
 		StrategyControl->batchSize = 1;
 }
-
-
-/* ----------------------------------------------------------------
- *				Backend-private buffer ring management
- * ----------------------------------------------------------------
- */
-
-
-/*
- * GetAccessStrategy -- create a BufferAccessStrategy object
- *
- * The object is allocated in the current memory context.
- */
-BufferAccessStrategy
-GetAccessStrategy(BufferAccessStrategyType btype)
-{
-	int			ring_size_kb;
-
-	/*
-	 * Select ring size to use.  See buffer/README for rationales.
-	 *
-	 * Note: if you change the ring size for BAS_BULKREAD, see also
-	 * SYNC_SCAN_REPORT_INTERVAL in access/heap/syncscan.c.
-	 */
-	switch (btype)
-	{
-		case BAS_NORMAL:
-			/* if someone asks for NORMAL, just give 'em a "default" object */
-			return NULL;
-
-		case BAS_BULKREAD:
-			{
-				int			ring_max_kb;
-
-				/*
-				 * The ring always needs to be large enough to allow some
-				 * separation in time between providing a buffer to the user
-				 * of the strategy and that buffer being reused. Otherwise the
-				 * user's pin will prevent reuse of the buffer, even without
-				 * concurrent activity.
-				 *
-				 * We also need to ensure the ring always is large enough for
-				 * SYNC_SCAN_REPORT_INTERVAL, as noted above.
-				 *
-				 * Thus we start out a minimal size and increase the size
-				 * further if appropriate.
-				 */
-				ring_size_kb = 256;
-
-				/*
-				 * There's no point in a larger ring if we won't be allowed to
-				 * pin sufficiently many buffers.  But we never limit to less
-				 * than the minimal size above.
-				 */
-				ring_max_kb = GetPinLimit() * (BLCKSZ / 1024);
-				ring_max_kb = Max(ring_size_kb, ring_max_kb);
-
-				/*
-				 * We would like the ring to additionally have space for the
-				 * configured degree of IO concurrency. While being read in,
-				 * buffers can obviously not yet be reused.
-				 *
-				 * Each IO can be up to io_combine_limit blocks large, and we
-				 * want to start up to effective_io_concurrency IOs.
-				 *
-				 * Note that effective_io_concurrency may be 0, which disables
-				 * AIO.
-				 */
-				ring_size_kb += (BLCKSZ / 1024) *
-					io_combine_limit * effective_io_concurrency;
-
-				if (ring_size_kb > ring_max_kb)
-					ring_size_kb = ring_max_kb;
-				break;
-			}
-		case BAS_BULKWRITE:
-			ring_size_kb = 16 * 1024;
-			break;
-		case BAS_VACUUM:
-			ring_size_kb = 2048;
-			break;
-
-		default:
-			elog(ERROR, "unrecognized buffer access strategy: %d",
-				 (int) btype);
-			return NULL;		/* keep compiler quiet */
-	}
-
-	return GetAccessStrategyWithSize(btype, ring_size_kb);
-}
-
-/*
- * GetAccessStrategyWithSize -- create a BufferAccessStrategy object with a
- *		number of buffers equivalent to the passed in size.
- *
- * If the given ring size is 0, no BufferAccessStrategy will be created and
- * the function will return NULL.  ring_size_kb must not be negative.
- */
-BufferAccessStrategy
-GetAccessStrategyWithSize(BufferAccessStrategyType btype, int ring_size_kb)
-{
-	int			ring_buffers;
-	BufferAccessStrategy strategy;
-
-	Assert(ring_size_kb >= 0);
-
-	/* Figure out how many buffers ring_size_kb is */
-	ring_buffers = ring_size_kb / (BLCKSZ / 1024);
-
-	/* 0 means unlimited, so no BufferAccessStrategy required */
-	if (ring_buffers == 0)
-		return NULL;
-
-	/* Cap to 1/8th of shared_buffers */
-	ring_buffers = Min(NBuffers / 8, ring_buffers);
-
-	/* NBuffers should never be less than 16, so this shouldn't happen */
-	Assert(ring_buffers > 0);
-
-	/* Allocate the object and initialize all elements to zeroes */
-	strategy = (BufferAccessStrategy)
-		palloc0(offsetof(BufferAccessStrategyData, buffers) +
-				ring_buffers * sizeof(Buffer));
-
-	/* Set fields that don't start out zero */
-	strategy->btype = btype;
-	strategy->nbuffers = ring_buffers;
-
-	return strategy;
-}
-
-/*
- * GetAccessStrategyBufferCount -- an accessor for the number of buffers in
- *		the ring
- *
- * Returns 0 on NULL input to match behavior of GetAccessStrategyWithSize()
- * returning NULL with 0 size.
- */
-int
-GetAccessStrategyBufferCount(BufferAccessStrategy strategy)
-{
-	if (strategy == NULL)
-		return 0;
-
-	return strategy->nbuffers;
-}
-
-/*
- * GetAccessStrategyPinLimit -- get cap of number of buffers that should be pinned
- *
- * When pinning extra buffers to look ahead, users of a ring-based strategy are
- * in danger of pinning too much of the ring at once while performing look-ahead.
- * For some strategies, that means "escaping" from the ring, and in others it
- * means forcing dirty data to disk very frequently with associated WAL
- * flushing.  Since external code has no insight into any of that, allow
- * individual strategy types to expose a clamp that should be applied when
- * deciding on a maximum number of buffers to pin at once.
- *
- * Callers should combine this number with other relevant limits and take the
- * minimum.
- */
-int
-GetAccessStrategyPinLimit(BufferAccessStrategy strategy)
-{
-	if (strategy == NULL)
-		return NBuffers;
-
-	switch (strategy->btype)
-	{
-		case BAS_BULKREAD:
-
-			/*
-			 * Since BAS_BULKREAD uses StrategyRejectBuffer(), dirty buffers
-			 * shouldn't be a problem and the caller is free to pin up to the
-			 * entire ring at once.
-			 */
-			return strategy->nbuffers;
-
-		default:
-
-			/*
-			 * Tell caller not to pin more than half the buffers in the ring.
-			 * This is a trade-off between look ahead distance and deferring
-			 * writeback and associated WAL traffic.
-			 */
-			return strategy->nbuffers / 2;
-	}
-}
-
-/*
- * FreeAccessStrategy -- release a BufferAccessStrategy object
- *
- * A simple pfree would do at the moment, but we would prefer that callers
- * don't assume that much about the representation of BufferAccessStrategy.
- */
-void
-FreeAccessStrategy(BufferAccessStrategy strategy)
-{
-	/* don't crash if called on a "default" strategy */
-	if (strategy != NULL)
-		pfree(strategy);
-}
-
-/*
- * GetBufferFromRing -- returns a buffer from the ring, or NULL if the
- *		ring is empty / not usable.
- *
- * The buffer is pinned and marked as owned, using TrackNewBufferPin(), before
- * returning.
- */
-static BufferDesc *
-GetBufferFromRing(BufferAccessStrategy strategy, uint64 *buf_state)
-{
-	BufferDesc *buf;
-	Buffer		bufnum;
-	uint64		old_buf_state;
-	uint64		local_buf_state;	/* to avoid repeated (de-)referencing */
-
-
-	/* Advance to next ring slot */
-	if (++strategy->current >= strategy->nbuffers)
-		strategy->current = 0;
-
-	/*
-	 * If the slot hasn't been filled yet, tell the caller to allocate a new
-	 * buffer with the normal allocation strategy.  He will then fill this
-	 * slot by calling AddBufferToRing with the new buffer.
-	 */
-	bufnum = strategy->buffers[strategy->current];
-	if (bufnum == InvalidBuffer)
-		return NULL;
-
-	buf = GetBufferDescriptor(bufnum - 1);
-
-	/*
-	 * Check whether the buffer can be used and pin it if so. Do this using a
-	 * CAS loop, to avoid having to lock the buffer header.
-	 */
-	old_buf_state = pg_atomic_read_u64(&buf->state);
-	for (;;)
-	{
-		local_buf_state = old_buf_state;
-
-		/*
-		 * If the buffer is pinned we cannot use it under any circumstances.
-		 *
-		 * With the cooling-state replacement the field holds only COOL or HOT,
-		 * so the stock "usage_count > 1 means another backend touched it"
-		 * heuristic no longer applies: a ring element is reusable whenever it
-		 * is unpinned.  (The whole ring mechanism is removed in a later patch;
-		 * scan resistance is now intrinsic to the sweep.)
-		 */
-		if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0)
-			break;
-
-		/* See equivalent code in PinBuffer() */
-		if (unlikely(local_buf_state & BM_LOCKED))
-		{
-			old_buf_state = WaitBufHdrUnlocked(buf);
-			continue;
-		}
-
-		/* pin the buffer if the CAS succeeds */
-		local_buf_state += BUF_REFCOUNT_ONE;
-
-		if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
-										   local_buf_state))
-		{
-			*buf_state = local_buf_state;
-
-			TrackNewBufferPin(BufferDescriptorGetBuffer(buf));
-			return buf;
-		}
-	}
-
-	/*
-	 * Tell caller to allocate a new buffer with the normal allocation
-	 * strategy.  He'll then replace this ring element via AddBufferToRing.
-	 */
-	return NULL;
-}
-
-/*
- * AddBufferToRing -- add a buffer to the buffer ring
- *
- * Caller must hold the buffer header spinlock on the buffer.  Since this
- * is called with the spinlock held, it had better be quite cheap.
- */
-static void
-AddBufferToRing(BufferAccessStrategy strategy, BufferDesc *buf)
-{
-	strategy->buffers[strategy->current] = BufferDescriptorGetBuffer(buf);
-}
-
-/*
- * Utility function returning the IOContext of a given BufferAccessStrategy's
- * strategy ring.
- */
-IOContext
-IOContextForStrategy(BufferAccessStrategy strategy)
-{
-	if (!strategy)
-		return IOCONTEXT_NORMAL;
-
-	switch (strategy->btype)
-	{
-		case BAS_NORMAL:
-
-			/*
-			 * Currently, GetAccessStrategy() returns NULL for
-			 * BufferAccessStrategyType BAS_NORMAL, so this case is
-			 * unreachable.
-			 */
-			pg_unreachable();
-			return IOCONTEXT_NORMAL;
-		case BAS_BULKREAD:
-			return IOCONTEXT_BULKREAD;
-		case BAS_BULKWRITE:
-			return IOCONTEXT_BULKWRITE;
-		case BAS_VACUUM:
-			return IOCONTEXT_VACUUM;
-	}
-
-	elog(ERROR, "unrecognized BufferAccessStrategyType: %d", strategy->btype);
-	pg_unreachable();
-}
-
-/*
- * StrategyRejectBuffer -- consider rejecting a dirty buffer
- *
- * When a nondefault strategy is used, the buffer manager calls this function
- * when it turns out that the buffer selected by StrategyGetBuffer needs to
- * be written out and doing so would require flushing WAL too.  This gives us
- * a chance to choose a different victim.
- *
- * Returns true if buffer manager should ask for a new victim, and false
- * if this buffer should be written and re-used.
- */
-bool
-StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf, bool from_ring)
-{
-	/* We only do this in bulkread mode */
-	if (strategy->btype != BAS_BULKREAD)
-		return false;
-
-	/* Don't muck with behavior of normal buffer-replacement strategy */
-	if (!from_ring ||
-		strategy->buffers[strategy->current] != BufferDescriptorGetBuffer(buf))
-		return false;
-
-	/*
-	 * Remove the dirty buffer from the ring; necessary to prevent infinite
-	 * loop if all ring members are dirty.
-	 */
-	strategy->buffers[strategy->current] = InvalidBuffer;
-
-	return true;
-}
diff --git a/src/backend/storage/freespace/freespace.c b/src/backend/storage/freespace/freespace.c
index 006edab9d77..29cb2d70f66 100644
--- a/src/backend/storage/freespace/freespace.c
+++ b/src/backend/storage/freespace/freespace.c
@@ -603,7 +603,7 @@ fsm_readbuf(Relation rel, FSMAddress addr, bool extend)
 			return InvalidBuffer;
 	}
 	else
-		buf = ReadBufferExtended(rel, FSM_FORKNUM, blkno, RBM_ZERO_ON_ERROR, NULL);
+		buf = ReadBufferExtended(rel, FSM_FORKNUM, blkno, RBM_ZERO_ON_ERROR);
 
 	/*
 	 * Initializing the page when needed is trickier than it looks, because of
@@ -638,7 +638,7 @@ fsm_readbuf(Relation rel, FSMAddress addr, bool extend)
 static Buffer
 fsm_extend(Relation rel, BlockNumber fsm_nblocks)
 {
-	return ExtendBufferedRelTo(BMR_REL(rel), FSM_FORKNUM, NULL,
+	return ExtendBufferedRelTo(BMR_REL(rel), FSM_FORKNUM,
 							   EB_CREATE_FORK_IF_NEEDED |
 							   EB_CLEAR_SIZE_CACHE,
 							   fsm_nblocks,
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 718c1cfc0f9..3c3e35ab8a8 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -1540,15 +1540,8 @@ register_dirty_segment(SMgrRelation reln, ForkNumber forknum, MdfdVec *seg)
 							FilePathName(seg->mdfd_vfd))));
 
 		/*
-		 * We have no way of knowing if the current IOContext is
-		 * IOCONTEXT_NORMAL or IOCONTEXT_[BULKREAD, BULKWRITE, VACUUM] at this
-		 * point, so count the fsync as being in the IOCONTEXT_NORMAL
-		 * IOContext. This is probably okay, because the number of backend
-		 * fsyncs doesn't say anything about the efficacy of the
-		 * BufferAccessStrategy. And counting both fsyncs done in
-		 * IOCONTEXT_NORMAL and IOCONTEXT_[BULKREAD, BULKWRITE, VACUUM] under
-		 * IOCONTEXT_NORMAL is likely clearer when investigating the number of
-		 * backend fsyncs.
+		 * Count the fsync in the IOCONTEXT_NORMAL IOContext, the only
+		 * IOContext relations are read/written under.
 		 */
 		pgstat_count_io_op_time(IOOBJECT_RELATION, IOCONTEXT_NORMAL,
 								IOOP_FSYNC, io_start, 1, 0);
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 38bae7b15d2..d66a23749d5 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -241,16 +241,10 @@ pgstat_get_io_context_name(IOContext io_context)
 {
 	switch (io_context)
 	{
-		case IOCONTEXT_BULKREAD:
-			return "bulkread";
-		case IOCONTEXT_BULKWRITE:
-			return "bulkwrite";
 		case IOCONTEXT_INIT:
 			return "init";
 		case IOCONTEXT_NORMAL:
 			return "normal";
-		case IOCONTEXT_VACUUM:
-			return "vacuum";
 	}
 
 	elog(ERROR, "unrecognized IOContext value: %d", io_context);
@@ -451,18 +445,6 @@ pgstat_tracks_io_object(BackendType bktype, IOObject io_object,
 	 * IOContexts, and, while it may not be inherently incorrect for them to
 	 * do so, excluding those rows from the view makes the view easier to use.
 	 */
-	if ((bktype == B_CHECKPOINTER || bktype == B_BG_WRITER) &&
-		(io_context == IOCONTEXT_BULKREAD ||
-		 io_context == IOCONTEXT_BULKWRITE ||
-		 io_context == IOCONTEXT_VACUUM))
-		return false;
-
-	if (bktype == B_AUTOVAC_LAUNCHER && io_context == IOCONTEXT_VACUUM)
-		return false;
-
-	if ((bktype == B_AUTOVAC_WORKER || bktype == B_AUTOVAC_LAUNCHER) &&
-		io_context == IOCONTEXT_BULKWRITE)
-		return false;
 
 	return true;
 }
@@ -479,8 +461,6 @@ bool
 pgstat_tracks_io_op(BackendType bktype, IOObject io_object,
 					IOContext io_context, IOOp io_op)
 {
-	bool		strategy_io_context;
-
 	/* if (io_context, io_object) will never collect stats, we're done */
 	if (!pgstat_tracks_io_object(bktype, io_object, io_context))
 		return false;
@@ -521,17 +501,13 @@ pgstat_tracks_io_op(BackendType bktype, IOObject io_object,
 	/*
 	 * Some IOOps are not valid in certain IOContexts and some IOOps are only
 	 * valid in certain contexts.
+	 *
+	 * IOOP_REUSE was only relevant when a BufferAccessStrategy was in use.
+	 * Buffer access strategies (ring buffers) have been removed -- scan
+	 * resistance is now intrinsic to the cooling-stage clock sweep -- so
+	 * IOOP_REUSE never occurs.
 	 */
-	if (io_context == IOCONTEXT_BULKREAD && io_op == IOOP_EXTEND)
-		return false;
-
-	strategy_io_context = io_context == IOCONTEXT_BULKREAD ||
-		io_context == IOCONTEXT_BULKWRITE || io_context == IOCONTEXT_VACUUM;
-
-	/*
-	 * IOOP_REUSE is only relevant when a BufferAccessStrategy is in use.
-	 */
-	if (!strategy_io_context && io_op == IOOP_REUSE)
+	if (io_op == IOOP_REUSE)
 		return false;
 
 	/*
@@ -545,14 +521,5 @@ pgstat_tracks_io_op(BackendType bktype, IOObject io_object,
 		!(io_op == IOOP_WRITE || io_op == IOOP_READ || io_op == IOOP_FSYNC))
 		return false;
 
-	/*
-	 * IOOP_FSYNC IOOps done by a backend using a BufferAccessStrategy are
-	 * counted in the IOCONTEXT_NORMAL IOContext. See comment in
-	 * register_dirty_segment() for more details.
-	 */
-	if (strategy_io_context && io_op == IOOP_FSYNC)
-		return false;
-
-
 	return true;
 }
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index bbd28d14d99..555c04272f5 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -149,7 +149,6 @@ int			autovacuum_max_parallel_workers = 0;
 int			MaxBackends = 0;
 
 /* GUC parameters for vacuum */
-int			VacuumBufferUsageLimit = 2048;
 
 int			VacuumCostPageHit = 1;
 int			VacuumCostPageMiss = 2;
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index d421cdbde76..b1ea81f536c 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -3326,16 +3326,6 @@
   boot_val => 'DEFAULT_UPDATE_PROCESS_TITLE',
 },
 
-{ name => 'vacuum_buffer_usage_limit', type => 'int', context => 'PGC_USERSET', group => 'RESOURCES_MEM',
-  short_desc => 'Sets the buffer pool size for VACUUM, ANALYZE, and autovacuum.',
-  flags => 'GUC_UNIT_KB',
-  variable => 'VacuumBufferUsageLimit',
-  boot_val => '2048',
-  min => '0',
-  max => 'MAX_BAS_VAC_RING_SIZE_KB',
-  check_hook => 'check_vacuum_buffer_usage_limit',
-},
-
 { name => 'vacuum_cost_delay', type => 'real', context => 'PGC_USERSET', group => 'VACUUM_COST_DELAY',
   short_desc => 'Vacuum cost delay in milliseconds.',
   flags => 'GUC_UNIT_MS',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 7958653077b..cb1eb8ef3ff 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -164,9 +164,6 @@
                                         #   mmap
                                         # (change requires restart)
 #min_dynamic_shared_memory = 0MB        # (change requires restart)
-#vacuum_buffer_usage_limit = 2MB        # size of vacuum and analyze buffer access strategy ring;
-                                        # 0 to disable vacuum buffer access strategy;
-                                        # range 128kB to 16GB
 
 # SLRU buffers (change requires restart)
 #commit_timestamp_buffers = 0           # memory for pg_commit_ts (0 = auto)
diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index f8158ca6a78..f78e57ff2c3 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -57,7 +57,6 @@ main(int argc, char *argv[])
 		{"no-truncate", no_argument, NULL, 10},
 		{"no-process-toast", no_argument, NULL, 11},
 		{"no-process-main", no_argument, NULL, 12},
-		{"buffer-usage-limit", required_argument, NULL, 13},
 		{"missing-stats-only", no_argument, NULL, 14},
 		{"dry-run", no_argument, NULL, 15},
 		{NULL, 0, NULL, 0}
@@ -202,9 +201,6 @@ main(int argc, char *argv[])
 			case 12:
 				vacopts.process_main = false;
 				break;
-			case 13:
-				vacopts.buffer_usage_limit = escape_quotes(optarg);
-				break;
 			case 14:
 				vacopts.missing_stats_only = true;
 				break;
@@ -290,14 +286,6 @@ main(int argc, char *argv[])
 		pg_fatal("cannot use the \"%s\" option with the \"%s\" option",
 				 "no-index-cleanup", "force-index-cleanup");
 
-	/*
-	 * buffer-usage-limit is not allowed with VACUUM FULL unless ANALYZE is
-	 * included too.
-	 */
-	if (vacopts.buffer_usage_limit && vacopts.full && !vacopts.and_analyze)
-		pg_fatal("cannot use the \"%s\" option with the \"%s\" option",
-				 "buffer-usage-limit", "full");
-
 	/*
 	 * Prohibit --missing-stats-only without --analyze-only or
 	 * --analyze-in-stages.
@@ -352,7 +340,6 @@ help(const char *progname)
 	printf(_("  %s [OPTION]... [DBNAME]\n"), progname);
 	printf(_("\nOptions:\n"));
 	printf(_("  -a, --all                       vacuum all databases\n"));
-	printf(_("      --buffer-usage-limit=SIZE   size of ring buffer used for vacuum\n"));
 	printf(_("  -d, --dbname=DBNAME             database to vacuum\n"));
 	printf(_("      --disable-page-skipping     disable all page-skipping behavior\n"));
 	printf(_("      --dry-run                   show the commands that would be sent to the server\n"));
diff --git a/src/bin/scripts/vacuuming.c b/src/bin/scripts/vacuuming.c
index 855a5754c98..078a3bbb516 100644
--- a/src/bin/scripts/vacuuming.c
+++ b/src/bin/scripts/vacuuming.c
@@ -264,13 +264,6 @@ vacuum_one_database(ConnParams *cparams,
 				 "--parallel", "13");
 	}
 
-	if (vacopts->buffer_usage_limit && PQserverVersion(conn) < 160000)
-	{
-		PQfinish(conn);
-		pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s",
-				 "--buffer-usage-limit", "16");
-	}
-
 	if (vacopts->missing_stats_only && PQserverVersion(conn) < 150000)
 	{
 		PQfinish(conn);
@@ -865,13 +858,6 @@ prepare_vacuum_command(PGconn *conn, PQExpBuffer sql,
 				appendPQExpBuffer(sql, "%sVERBOSE", sep);
 				sep = comma;
 			}
-			if (vacopts->buffer_usage_limit)
-			{
-				Assert(serverVersion >= 160000);
-				appendPQExpBuffer(sql, "%sBUFFER_USAGE_LIMIT '%s'", sep,
-								  vacopts->buffer_usage_limit);
-				sep = comma;
-			}
 			if (sep != paren)
 				appendPQExpBufferChar(sql, ')');
 		}
@@ -974,13 +960,6 @@ prepare_vacuum_command(PGconn *conn, PQExpBuffer sql,
 								  vacopts->parallel_workers);
 				sep = comma;
 			}
-			if (vacopts->buffer_usage_limit)
-			{
-				Assert(serverVersion >= 160000);
-				appendPQExpBuffer(sql, "%sBUFFER_USAGE_LIMIT '%s'", sep,
-								  vacopts->buffer_usage_limit);
-				sep = comma;
-			}
 			if (sep != paren)
 				appendPQExpBufferChar(sql, ')');
 		}
diff --git a/src/bin/scripts/vacuuming.h b/src/bin/scripts/vacuuming.h
index 5a491db2526..83a2469a6aa 100644
--- a/src/bin/scripts/vacuuming.h
+++ b/src/bin/scripts/vacuuming.h
@@ -49,7 +49,6 @@ typedef struct vacuumingOptions
 	bool		process_main;
 	bool		process_toast;
 	bool		skip_database_stats;
-	char	   *buffer_usage_limit;
 	bool		missing_stats_only;
 	bool		echo;
 	bool		quiet;
diff --git a/src/include/access/genam.h b/src/include/access/genam.h
index 68bfe405db3..bc87f6f7cc0 100644
--- a/src/include/access/genam.h
+++ b/src/include/access/genam.h
@@ -58,7 +58,6 @@ typedef struct IndexVacuumInfo
 	bool		estimated_count;	/* num_heap_tuples is an estimate */
 	int			message_level;	/* ereport level for progress messages */
 	double		num_heap_tuples;	/* tuples remaining in heap */
-	BufferAccessStrategy strategy;	/* access strategy for reads */
 } IndexVacuumInfo;
 
 /*
diff --git a/src/include/access/hash.h b/src/include/access/hash.h
index a8702f0e5ea..7c9268fe953 100644
--- a/src/include/access/hash.h
+++ b/src/include/access/hash.h
@@ -405,12 +405,11 @@ extern void _hash_pgaddmultitup(Relation rel, Buffer buf, IndexTuple *itups,
 extern Buffer _hash_addovflpage(Relation rel, Buffer metabuf, Buffer buf, bool retain_pin);
 extern BlockNumber _hash_freeovflpage(Relation rel, Buffer bucketbuf, Buffer ovflbuf,
 									  Buffer wbuf, IndexTuple *itups, OffsetNumber *itup_offsets,
-									  Size *tups_size, uint16 nitups, BufferAccessStrategy bstrategy);
+									  Size *tups_size, uint16 nitups);
 extern void _hash_initbitmapbuffer(Buffer buf, uint16 bmsize, bool initpage);
 extern void _hash_squeezebucket(Relation rel,
 								Bucket bucket, BlockNumber bucket_blkno,
-								Buffer bucket_buf,
-								BufferAccessStrategy bstrategy);
+								Buffer bucket_buf);
 extern uint32 _hash_ovflblkno_to_bitno(HashMetaPage metap, BlockNumber ovflblkno);
 
 /* hashpage.c */
@@ -428,9 +427,6 @@ extern void _hash_initbuf(Buffer buf, uint32 max_bucket, uint32 num_bucket,
 						  uint32 flag, bool initpage);
 extern Buffer _hash_getnewbuf(Relation rel, BlockNumber blkno,
 							  ForkNumber forkNum);
-extern Buffer _hash_getbuf_with_strategy(Relation rel, BlockNumber blkno,
-										 int access, int flags,
-										 BufferAccessStrategy bstrategy);
 extern void _hash_relbuf(Relation rel, Buffer buf);
 extern void _hash_dropbuf(Relation rel, Buffer buf);
 extern void _hash_dropscanbuf(Relation rel, HashScanOpaque so);
@@ -481,7 +477,6 @@ extern void _hash_kill_items(IndexScanDesc scan);
 /* hash.c */
 extern void hashbucketcleanup(Relation rel, Bucket cur_bucket,
 							  Buffer bucket_buf, BlockNumber bucket_blkno,
-							  BufferAccessStrategy bstrategy,
 							  uint32 maxbucket, uint32 highmask, uint32 lowmask,
 							  double *tuples_removed, double *num_index_tuples,
 							  bool split_cleanup,
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 5176478c295..0967b659794 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -72,8 +72,6 @@ typedef struct HeapScanDescData
 	Buffer		rs_cbuf;		/* current buffer in scan, if any */
 	/* NB: if rs_cbuf is not InvalidBuffer, we hold a pin on that buffer */
 
-	BufferAccessStrategy rs_strategy;	/* access strategy for reads */
-
 	HeapTupleData rs_ctup;		/* current tuple in scan, if any */
 
 	/* For scans that stream reads */
@@ -466,7 +464,7 @@ extern void log_heap_prune_and_freeze(Relation relation, Buffer buffer,
 
 /* in heap/vacuumlazy.c */
 extern void heap_vacuum_rel(Relation rel,
-							const VacuumParams *params, BufferAccessStrategy bstrategy);
+							const VacuumParams *params);
 #ifdef USE_ASSERT_CHECKING
 extern bool heap_page_is_all_visible(Relation rel, Buffer buf,
 									 GlobalVisState *vistest,
diff --git a/src/include/access/hio.h b/src/include/access/hio.h
index 60cfc375fd5..63bfcc884ee 100644
--- a/src/include/access/hio.h
+++ b/src/include/access/hio.h
@@ -28,7 +28,6 @@
  */
 typedef struct BulkInsertStateData
 {
-	BufferAccessStrategy strategy;	/* our BULKWRITE strategy object */
 	Buffer		current_buf;	/* current insertion target page */
 
 	/*
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index f2c36696bca..83ba0f3ca73 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -689,8 +689,7 @@ typedef struct TableAmRoutine
 	 * integrate with autovacuum's scheduling.
 	 */
 	void		(*relation_vacuum) (Relation rel,
-									const VacuumParams *params,
-									BufferAccessStrategy bstrategy);
+									const VacuumParams *params);
 
 	/*
 	 * Prepare to analyze block `blockno` of `scan`. The scan has been started
@@ -1774,10 +1773,9 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
  * routine, even if (for ANALYZE) it is part of the same VACUUM command.
  */
 static inline void
-table_relation_vacuum(Relation rel, const VacuumParams *params,
-					  BufferAccessStrategy bstrategy)
+table_relation_vacuum(Relation rel, const VacuumParams *params)
 {
-	rel->rd_tableam->relation_vacuum(rel, params, bstrategy);
+	rel->rd_tableam->relation_vacuum(rel, params);
 }
 
 /*
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 956d9cea36d..14a9b530bcf 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -363,7 +363,7 @@ extern PGDLLIMPORT int64 parallel_vacuum_worker_delay_ns;
 /* in commands/vacuum.c */
 extern void ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel);
 extern void vacuum(List *relations, const VacuumParams *params,
-				   BufferAccessStrategy bstrategy, MemoryContext vac_context,
+				   MemoryContext vac_context,
 				   bool isTopLevel);
 extern void vac_open_indexes(Relation relation, LOCKMODE lockmode,
 							 int *nindexes, Relation **Irel);
@@ -407,8 +407,7 @@ extern void VacuumUpdateCosts(void);
 /* in commands/vacuumparallel.c */
 extern ParallelVacuumState *parallel_vacuum_init(Relation rel, Relation *indrels,
 												 int nindexes, int nrequested_workers,
-												 int vac_work_mem, int elevel,
-												 BufferAccessStrategy bstrategy);
+												 int vac_work_mem, int elevel);
 extern void parallel_vacuum_end(ParallelVacuumState *pvs, IndexBulkDeleteResult **istats);
 extern TidStore *parallel_vacuum_get_dead_items(ParallelVacuumState *pvs,
 												VacDeadItemsInfo **dead_items_info_p);
@@ -428,8 +427,7 @@ extern void parallel_vacuum_main(dsm_segment *seg, shm_toc *toc);
 
 /* in commands/analyze.c */
 extern void analyze_rel(Oid relid, RangeVar *relation,
-						const VacuumParams *params, List *va_cols, bool in_outer_xact,
-						BufferAccessStrategy bstrategy);
+						const VacuumParams *params, List *va_cols, bool in_outer_xact);
 extern bool attribute_is_analyzable(Relation onerel, int attnum, Form_pg_attribute attr,
 									int *p_attstattarget);
 extern bool std_typanalyze(VacAttrStats *stats);
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 7170a4bff98..c5d582b152b 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -273,15 +273,6 @@ extern PGDLLIMPORT double hash_mem_multiplier;
 extern PGDLLIMPORT int maintenance_work_mem;
 extern PGDLLIMPORT int max_parallel_maintenance_workers;
 
-/*
- * Upper and lower hard limits for the buffer access strategy ring size
- * specified by the VacuumBufferUsageLimit GUC and BUFFER_USAGE_LIMIT option
- * to VACUUM and ANALYZE.
- */
-#define MIN_BAS_VAC_RING_SIZE_KB 128
-#define MAX_BAS_VAC_RING_SIZE_KB (16 * 1024 * 1024)
-
-extern PGDLLIMPORT int VacuumBufferUsageLimit;
 extern PGDLLIMPORT int VacuumCostPageHit;
 extern PGDLLIMPORT int VacuumCostPageMiss;
 extern PGDLLIMPORT int VacuumCostPageDirty;
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..68b4e5093a2 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -287,14 +287,11 @@ typedef enum IOObject
 
 typedef enum IOContext
 {
-	IOCONTEXT_BULKREAD,
-	IOCONTEXT_BULKWRITE,
 	IOCONTEXT_INIT,
 	IOCONTEXT_NORMAL,
-	IOCONTEXT_VACUUM,
 } IOContext;
 
-#define IOCONTEXT_NUM_TYPES (IOCONTEXT_VACUUM + 1)
+#define IOCONTEXT_NUM_TYPES (IOCONTEXT_NORMAL + 1)
 
 /*
  * Enumeration of IO operations.
diff --git a/src/include/storage/buf.h b/src/include/storage/buf.h
index b21445522b1..fa942a64f05 100644
--- a/src/include/storage/buf.h
+++ b/src/include/storage/buf.h
@@ -36,11 +36,4 @@ typedef int Buffer;
  */
 #define BufferIsLocal(buffer)	((buffer) < 0)
 
-/*
- * Buffer access strategy objects.
- *
- * BufferAccessStrategyData is private to freelist.c
- */
-typedef struct BufferAccessStrategyData *BufferAccessStrategy;
-
 #endif							/* BUF_H */
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index 89b11051577..de5139323c0 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -627,11 +627,7 @@ extern void TerminateBufferIO(BufferDesc *buf, bool clear_dirty, uint64 set_flag
 
 
 /* freelist.c */
-extern IOContext IOContextForStrategy(BufferAccessStrategy strategy);
-extern BufferDesc *StrategyGetBuffer(BufferAccessStrategy strategy,
-									 uint64 *buf_state, bool *from_ring);
-extern bool StrategyRejectBuffer(BufferAccessStrategy strategy,
-								 BufferDesc *buf, bool from_ring);
+extern BufferDesc *StrategyGetBuffer(uint64 *buf_state);
 
 extern int	StrategySyncStart(uint32 *complete_passes, uint32 *num_buf_alloc);
 extern void StrategyNotifyBgWriter(int bgwprocno);
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 6837b35fc6d..36023792f36 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -25,21 +25,6 @@
 
 typedef void *Block;
 
-/*
- * Possible arguments for GetAccessStrategy().
- *
- * If adding a new BufferAccessStrategyType, also add a new IOContext so
- * IO statistics using this strategy are tracked.
- */
-typedef enum BufferAccessStrategyType
-{
-	BAS_NORMAL,					/* Normal random access */
-	BAS_BULKREAD,				/* Large read-only scan (hint bit updates are
-								 * ok) */
-	BAS_BULKWRITE,				/* Large multi-block write (e.g. COPY IN) */
-	BAS_VACUUM,					/* VACUUM */
-} BufferAccessStrategyType;
-
 /* Possible modes for ReadBufferExtended() */
 typedef enum
 {
@@ -135,7 +120,6 @@ struct ReadBuffersOperation
 	SMgrRelation smgr;
 	char		persistence;
 	ForkNumber	forknum;
-	BufferAccessStrategy strategy;
 
 	/*
 	 * The following private members are private state for communication
@@ -235,11 +219,10 @@ extern bool ReadRecentBuffer(RelFileLocator rlocator, ForkNumber forkNum,
 							 BlockNumber blockNum, Buffer recent_buffer);
 extern Buffer ReadBuffer(Relation reln, BlockNumber blockNum);
 extern Buffer ReadBufferExtended(Relation reln, ForkNumber forkNum,
-								 BlockNumber blockNum, ReadBufferMode mode,
-								 BufferAccessStrategy strategy);
+								 BlockNumber blockNum, ReadBufferMode mode);
 extern Buffer ReadBufferWithoutRelcache(RelFileLocator rlocator,
 										ForkNumber forkNum, BlockNumber blockNum,
-										ReadBufferMode mode, BufferAccessStrategy strategy,
+										ReadBufferMode mode,
 										bool permanent);
 
 extern bool StartReadBuffer(ReadBuffersOperation *operation,
@@ -266,18 +249,15 @@ extern Buffer ReleaseAndReadBuffer(Buffer buffer, Relation relation,
 
 extern Buffer ExtendBufferedRel(BufferManagerRelation bmr,
 								ForkNumber forkNum,
-								BufferAccessStrategy strategy,
 								uint32 flags);
 extern BlockNumber ExtendBufferedRelBy(BufferManagerRelation bmr,
 									   ForkNumber fork,
-									   BufferAccessStrategy strategy,
 									   uint32 flags,
 									   uint32 extend_by,
 									   Buffer *buffers,
 									   uint32 *extended_by);
 extern Buffer ExtendBufferedRelTo(BufferManagerRelation bmr,
 								  ForkNumber fork,
-								  BufferAccessStrategy strategy,
 								  uint32 flags,
 								  BlockNumber extend_to,
 								  ReadBufferMode mode);
@@ -376,14 +356,6 @@ extern void AtProcExit_LocalBuffers(void);
 
 /* in freelist.c */
 
-extern BufferAccessStrategy GetAccessStrategy(BufferAccessStrategyType btype);
-extern BufferAccessStrategy GetAccessStrategyWithSize(BufferAccessStrategyType btype,
-													  int ring_size_kb);
-extern int	GetAccessStrategyBufferCount(BufferAccessStrategy strategy);
-extern int	GetAccessStrategyPinLimit(BufferAccessStrategy strategy);
-
-extern void FreeAccessStrategy(BufferAccessStrategy strategy);
-
 
 /* inline functions */
 
diff --git a/src/include/storage/read_stream.h b/src/include/storage/read_stream.h
index 48995c6d534..9e5fbbed9e8 100644
--- a/src/include/storage/read_stream.h
+++ b/src/include/storage/read_stream.h
@@ -83,17 +83,14 @@ extern BlockNumber block_range_read_stream_cb(ReadStream *stream,
 											  void *callback_private_data,
 											  void *per_buffer_data);
 extern ReadStream *read_stream_begin_relation(int flags,
-											  BufferAccessStrategy strategy,
 											  Relation rel,
 											  ForkNumber forknum,
 											  ReadStreamBlockNumberCB callback,
 											  void *callback_private_data,
 											  size_t per_buffer_data_size);
 extern Buffer read_stream_next_buffer(ReadStream *stream, void **per_buffer_data);
-extern BlockNumber read_stream_next_block(ReadStream *stream,
-										  BufferAccessStrategy *strategy);
+extern BlockNumber read_stream_next_block(ReadStream *stream);
 extern ReadStream *read_stream_begin_smgr_relation(int flags,
-												   BufferAccessStrategy strategy,
 												   SMgrRelation smgr,
 												   char smgr_persistence,
 												   ForkNumber forknum,
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 307f4fbaefe..3f28558d47c 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -31,8 +31,6 @@ extern void assign_application_name(const char *newval, void *extra);
 extern const char *show_archive_command(void);
 extern bool check_autovacuum_work_mem(int *newval, void **extra,
 									  GucSource source);
-extern bool check_vacuum_buffer_usage_limit(int *newval, void **extra,
-											GucSource source);
 extern bool check_backtrace_functions(char **newval, void **extra,
 									  GucSource source);
 extern void assign_backtrace_functions(const char *newval, void *extra);
diff --git a/src/test/modules/test_aio/test_aio.c b/src/test/modules/test_aio/test_aio.c
index 35efba1a5e3..f60153f801f 100644
--- a/src/test/modules/test_aio/test_aio.c
+++ b/src/test/modules/test_aio/test_aio.c
@@ -194,7 +194,6 @@ grow_rel(PG_FUNCTION_ARGS)
 
 		ExtendBufferedRelBy(BMR_REL(rel),
 							MAIN_FORKNUM,
-							NULL,
 							0,
 							extend_by_pages,
 							victim_buffers,
@@ -231,7 +230,7 @@ modify_rel_block(PG_FUNCTION_ARGS)
 	rel = relation_open(relid, AccessExclusiveLock);
 
 	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno,
-							 RBM_ZERO_ON_ERROR, NULL);
+							 RBM_ZERO_ON_ERROR);
 
 	LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
 
@@ -331,7 +330,7 @@ create_toy_buffer(Relation rel, BlockNumber blkno)
 	uint64		unset_bits = 0;
 
 	/* place buffer in shared buffers without erroring out */
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_ZERO_AND_LOCK, NULL);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_ZERO_AND_LOCK);
 	LockBuffer(buf, BUFFER_LOCK_UNLOCK);
 
 	if (RelationUsesLocalBuffers(rel))
@@ -737,7 +736,6 @@ read_buffers(PG_FUNCTION_ARGS)
 		operation->rel = rel;
 		operation->smgr = smgr;
 		operation->persistence = rel->rd_rel->relpersistence;
-		operation->strategy = NULL;
 		operation->forknum = MAIN_FORKNUM;
 
 		io_reqds[nios] = StartReadBuffers(operation,
@@ -879,7 +877,6 @@ read_stream_for_blocks(PG_FUNCTION_ARGS)
 	rel = relation_open(relid, AccessShareLock);
 
 	stream = read_stream_begin_relation(READ_STREAM_FULL,
-										NULL,
 										rel,
 										MAIN_FORKNUM,
 										read_stream_for_blocks_cb,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index 03cbc1cdef5..e4b9de0b4a8 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -16,22 +16,16 @@ SHOW track_counts;  -- must be on
 SELECT backend_type, object, context FROM pg_stat_io
   ORDER BY backend_type COLLATE "C", object COLLATE "C", context COLLATE "C";
 backend_type|object|context
-autovacuum launcher|relation|bulkread
 autovacuum launcher|relation|init
 autovacuum launcher|relation|normal
 autovacuum launcher|wal|init
 autovacuum launcher|wal|normal
-autovacuum worker|relation|bulkread
 autovacuum worker|relation|init
 autovacuum worker|relation|normal
-autovacuum worker|relation|vacuum
 autovacuum worker|wal|init
 autovacuum worker|wal|normal
-background worker|relation|bulkread
-background worker|relation|bulkwrite
 background worker|relation|init
 background worker|relation|normal
-background worker|relation|vacuum
 background worker|temp relation|normal
 background worker|wal|init
 background worker|wal|normal
@@ -43,67 +37,43 @@ checkpointer|relation|init
 checkpointer|relation|normal
 checkpointer|wal|init
 checkpointer|wal|normal
-client backend|relation|bulkread
-client backend|relation|bulkwrite
 client backend|relation|init
 client backend|relation|normal
-client backend|relation|vacuum
 client backend|temp relation|normal
 client backend|wal|init
 client backend|wal|normal
-datachecksums launcher|relation|bulkread
-datachecksums launcher|relation|bulkwrite
 datachecksums launcher|relation|init
 datachecksums launcher|relation|normal
-datachecksums launcher|relation|vacuum
 datachecksums launcher|temp relation|normal
 datachecksums launcher|wal|init
 datachecksums launcher|wal|normal
-datachecksums worker|relation|bulkread
-datachecksums worker|relation|bulkwrite
 datachecksums worker|relation|init
 datachecksums worker|relation|normal
-datachecksums worker|relation|vacuum
 datachecksums worker|temp relation|normal
 datachecksums worker|wal|init
 datachecksums worker|wal|normal
-io worker|relation|bulkread
-io worker|relation|bulkwrite
 io worker|relation|init
 io worker|relation|normal
-io worker|relation|vacuum
 io worker|temp relation|normal
 io worker|wal|init
 io worker|wal|normal
-slotsync worker|relation|bulkread
-slotsync worker|relation|bulkwrite
 slotsync worker|relation|init
 slotsync worker|relation|normal
-slotsync worker|relation|vacuum
 slotsync worker|temp relation|normal
 slotsync worker|wal|init
 slotsync worker|wal|normal
-standalone backend|relation|bulkread
-standalone backend|relation|bulkwrite
 standalone backend|relation|init
 standalone backend|relation|normal
-standalone backend|relation|vacuum
 standalone backend|wal|init
 standalone backend|wal|normal
-startup|relation|bulkread
-startup|relation|bulkwrite
 startup|relation|init
 startup|relation|normal
-startup|relation|vacuum
 startup|wal|init
 startup|wal|normal
 walreceiver|wal|init
 walreceiver|wal|normal
-walsender|relation|bulkread
-walsender|relation|bulkwrite
 walsender|relation|init
 walsender|relation|normal
-walsender|relation|vacuum
 walsender|temp relation|normal
 walsender|wal|init
 walsender|wal|normal
@@ -111,7 +81,7 @@ walsummarizer|wal|init
 walsummarizer|wal|normal
 walwriter|wal|init
 walwriter|wal|normal
-(95 rows)
+(65 rows)
 \a
 -- List of registered statistics kinds.
 SELECT id, name, fixed_amount,
@@ -1757,70 +1727,6 @@ SELECT :io_sum_local_new_tblspc_writes > :io_sum_local_after_writes;
 (1 row)
 
 RESET temp_buffers;
--- Test that reuse of strategy buffers and reads of blocks into these reused
--- buffers while VACUUMing are tracked in pg_stat_io. If there is sufficient
--- demand for shared buffers from concurrent queries, some buffers may be
--- pinned by other backends before they can be reused. In such cases, the
--- backend will evict a buffer from outside the ring and add it to the
--- ring. This is considered an eviction and not a reuse.
--- Set wal_skip_threshold smaller than the expected size of
--- test_io_vac_strategy so that, even if wal_level is minimal, VACUUM FULL will
--- fsync the newly rewritten test_io_vac_strategy instead of writing it to WAL.
--- Writing it to WAL will result in the newly written relation pages being in
--- shared buffers -- preventing us from testing BAS_VACUUM BufferAccessStrategy
--- reads.
-SET wal_skip_threshold = '1 kB';
-SELECT sum(reuses) AS reuses, sum(reads) AS reads, sum(evictions) AS evictions
-  FROM pg_stat_io WHERE context = 'vacuum' \gset io_sum_vac_strategy_before_
-CREATE TABLE test_io_vac_strategy(a int, b int) WITH (autovacuum_enabled = 'false');
-INSERT INTO test_io_vac_strategy SELECT i, i from generate_series(1, 4500)i;
--- Ensure that the next VACUUM will need to perform IO by rewriting the table
--- first with VACUUM (FULL).
-VACUUM (FULL) test_io_vac_strategy;
--- Use the minimum BUFFER_USAGE_LIMIT to cause reuses or evictions with the
--- smallest table possible.
-VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) test_io_vac_strategy;
-SELECT pg_stat_force_next_flush();
- pg_stat_force_next_flush 
---------------------------
- 
-(1 row)
-
-SELECT sum(reuses) AS reuses, sum(reads) AS reads, sum(evictions) AS evictions
-  FROM pg_stat_io WHERE context = 'vacuum' \gset io_sum_vac_strategy_after_
-SELECT :io_sum_vac_strategy_after_reads > :io_sum_vac_strategy_before_reads;
- ?column? 
-----------
- t
-(1 row)
-
-SELECT (:io_sum_vac_strategy_after_reuses + :io_sum_vac_strategy_after_evictions) >
-  (:io_sum_vac_strategy_before_reuses + :io_sum_vac_strategy_before_evictions);
- ?column? 
-----------
- t
-(1 row)
-
-RESET wal_skip_threshold;
--- Test that extends done by a CTAS, which uses a BAS_BULKWRITE
--- BufferAccessStrategy, are tracked in pg_stat_io.
-SELECT sum(extends) AS io_sum_bulkwrite_strategy_extends_before
-  FROM pg_stat_io WHERE context = 'bulkwrite' \gset
-CREATE TABLE test_io_bulkwrite_strategy AS SELECT i FROM generate_series(1,100)i;
-SELECT pg_stat_force_next_flush();
- pg_stat_force_next_flush 
---------------------------
- 
-(1 row)
-
-SELECT sum(extends) AS io_sum_bulkwrite_strategy_extends_after
-  FROM pg_stat_io WHERE context = 'bulkwrite' \gset
-SELECT :io_sum_bulkwrite_strategy_extends_after > :io_sum_bulkwrite_strategy_extends_before;
- ?column? 
-----------
- t
-(1 row)
-
 -- Test IO stats reset
 SELECT pg_stat_have_stats('io', 0, 0);
  pg_stat_have_stats 
@@ -1841,20 +1747,16 @@ SELECT pg_stat_reset_shared('io');
 SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS io_stats_post_reset
   FROM pg_stat_io \gset
 SELECT :io_stats_post_reset < :io_stats_pre_reset;
- ?column? 
-----------
- t
-(1 row)
-
+ERROR:  syntax error at or near ":"
+LINE 1: SELECT :io_stats_post_reset < :io_stats_pre_reset;
+               ^
 SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_post_reset
   FROM pg_stat_get_backend_io(pg_backend_pid()) \gset
 -- pg_stat_reset_shared() did not reset backend IO stats
 SELECT :my_io_stats_pre_reset <= :my_io_stats_post_reset;
- ?column? 
-----------
- t
-(1 row)
-
+ERROR:  syntax error at or near ":"
+LINE 1: SELECT :my_io_stats_pre_reset <= :my_io_stats_post_reset;
+               ^
 -- but pg_stat_reset_backend_stats() does
 SELECT pg_stat_reset_backend_stats(pg_backend_pid());
  pg_stat_reset_backend_stats 
@@ -1865,11 +1767,9 @@ SELECT pg_stat_reset_backend_stats(pg_backend_pid());
 SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_post_backend_reset
   FROM pg_stat_get_backend_io(pg_backend_pid()) \gset
 SELECT :my_io_stats_pre_reset > :my_io_stats_post_backend_reset;
- ?column? 
-----------
- t
-(1 row)
-
+ERROR:  syntax error at or near ":"
+LINE 1: SELECT :my_io_stats_pre_reset > :my_io_stats_post_backend_re...
+               ^
 -- Check invalid input for pg_stat_get_backend_io()
 SELECT pg_stat_get_backend_io(NULL);
  pg_stat_get_backend_io 
diff --git a/src/test/regress/expected/vacuum.out b/src/test/regress/expected/vacuum.out
index d4696bc3325..85d06907cec 100644
--- a/src/test/regress/expected/vacuum.out
+++ b/src/test/regress/expected/vacuum.out
@@ -526,25 +526,6 @@ SELECT t.relfilenode = :toast_filenode AS is_same_toast_filenode
  f
 (1 row)
 
--- BUFFER_USAGE_LIMIT option
-VACUUM (BUFFER_USAGE_LIMIT '512 kB') vac_option_tab;
-ANALYZE (BUFFER_USAGE_LIMIT '512 kB') vac_option_tab;
--- try disabling the buffer usage limit
-VACUUM (BUFFER_USAGE_LIMIT 0) vac_option_tab;
-ANALYZE (BUFFER_USAGE_LIMIT 0) vac_option_tab;
--- value exceeds max size error
-VACUUM (BUFFER_USAGE_LIMIT 16777220) vac_option_tab;
-ERROR:  BUFFER_USAGE_LIMIT option must be 0 or between 128 kB and 16777216 kB
--- value is less than min size error
-VACUUM (BUFFER_USAGE_LIMIT 120) vac_option_tab;
-ERROR:  BUFFER_USAGE_LIMIT option must be 0 or between 128 kB and 16777216 kB
--- integer overflow error
-VACUUM (BUFFER_USAGE_LIMIT 10000000000) vac_option_tab;
-ERROR:  BUFFER_USAGE_LIMIT option must be 0 or between 128 kB and 16777216 kB
-HINT:  Value exceeds integer range.
--- incompatible with VACUUM FULL error
-VACUUM (BUFFER_USAGE_LIMIT '512 kB', FULL) vac_option_tab;
-ERROR:  BUFFER_USAGE_LIMIT cannot be specified for VACUUM FULL
 -- SKIP_DATABASE_STATS option
 VACUUM (SKIP_DATABASE_STATS) vactst;
 -- ONLY_DATABASE_STATS option
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index 4c265d1245c..7a049d88a77 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -814,48 +814,6 @@ SELECT sum(writes) AS io_sum_local_new_tblspc_writes
 SELECT :io_sum_local_new_tblspc_writes > :io_sum_local_after_writes;
 RESET temp_buffers;
 
--- Test that reuse of strategy buffers and reads of blocks into these reused
--- buffers while VACUUMing are tracked in pg_stat_io. If there is sufficient
--- demand for shared buffers from concurrent queries, some buffers may be
--- pinned by other backends before they can be reused. In such cases, the
--- backend will evict a buffer from outside the ring and add it to the
--- ring. This is considered an eviction and not a reuse.
-
--- Set wal_skip_threshold smaller than the expected size of
--- test_io_vac_strategy so that, even if wal_level is minimal, VACUUM FULL will
--- fsync the newly rewritten test_io_vac_strategy instead of writing it to WAL.
--- Writing it to WAL will result in the newly written relation pages being in
--- shared buffers -- preventing us from testing BAS_VACUUM BufferAccessStrategy
--- reads.
-SET wal_skip_threshold = '1 kB';
-SELECT sum(reuses) AS reuses, sum(reads) AS reads, sum(evictions) AS evictions
-  FROM pg_stat_io WHERE context = 'vacuum' \gset io_sum_vac_strategy_before_
-CREATE TABLE test_io_vac_strategy(a int, b int) WITH (autovacuum_enabled = 'false');
-INSERT INTO test_io_vac_strategy SELECT i, i from generate_series(1, 4500)i;
--- Ensure that the next VACUUM will need to perform IO by rewriting the table
--- first with VACUUM (FULL).
-VACUUM (FULL) test_io_vac_strategy;
--- Use the minimum BUFFER_USAGE_LIMIT to cause reuses or evictions with the
--- smallest table possible.
-VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) test_io_vac_strategy;
-SELECT pg_stat_force_next_flush();
-SELECT sum(reuses) AS reuses, sum(reads) AS reads, sum(evictions) AS evictions
-  FROM pg_stat_io WHERE context = 'vacuum' \gset io_sum_vac_strategy_after_
-SELECT :io_sum_vac_strategy_after_reads > :io_sum_vac_strategy_before_reads;
-SELECT (:io_sum_vac_strategy_after_reuses + :io_sum_vac_strategy_after_evictions) >
-  (:io_sum_vac_strategy_before_reuses + :io_sum_vac_strategy_before_evictions);
-RESET wal_skip_threshold;
-
--- Test that extends done by a CTAS, which uses a BAS_BULKWRITE
--- BufferAccessStrategy, are tracked in pg_stat_io.
-SELECT sum(extends) AS io_sum_bulkwrite_strategy_extends_before
-  FROM pg_stat_io WHERE context = 'bulkwrite' \gset
-CREATE TABLE test_io_bulkwrite_strategy AS SELECT i FROM generate_series(1,100)i;
-SELECT pg_stat_force_next_flush();
-SELECT sum(extends) AS io_sum_bulkwrite_strategy_extends_after
-  FROM pg_stat_io WHERE context = 'bulkwrite' \gset
-SELECT :io_sum_bulkwrite_strategy_extends_after > :io_sum_bulkwrite_strategy_extends_before;
-
 -- Test IO stats reset
 SELECT pg_stat_have_stats('io', 0, 0);
 SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS io_stats_pre_reset
diff --git a/src/test/regress/sql/vacuum.sql b/src/test/regress/sql/vacuum.sql
index 247b8e23b23..506c4389765 100644
--- a/src/test/regress/sql/vacuum.sql
+++ b/src/test/regress/sql/vacuum.sql
@@ -390,21 +390,6 @@ SELECT t.relfilenode = :toast_filenode AS is_same_toast_filenode
   FROM pg_class c, pg_class t
   WHERE c.reltoastrelid = t.oid AND c.relname = 'vac_option_tab';
 
--- BUFFER_USAGE_LIMIT option
-VACUUM (BUFFER_USAGE_LIMIT '512 kB') vac_option_tab;
-ANALYZE (BUFFER_USAGE_LIMIT '512 kB') vac_option_tab;
--- try disabling the buffer usage limit
-VACUUM (BUFFER_USAGE_LIMIT 0) vac_option_tab;
-ANALYZE (BUFFER_USAGE_LIMIT 0) vac_option_tab;
--- value exceeds max size error
-VACUUM (BUFFER_USAGE_LIMIT 16777220) vac_option_tab;
--- value is less than min size error
-VACUUM (BUFFER_USAGE_LIMIT 120) vac_option_tab;
--- integer overflow error
-VACUUM (BUFFER_USAGE_LIMIT 10000000000) vac_option_tab;
--- incompatible with VACUUM FULL error
-VACUUM (BUFFER_USAGE_LIMIT '512 kB', FULL) vac_option_tab;
-
 -- SKIP_DATABASE_STATS option
 VACUUM (SKIP_DATABASE_STATS) vactst;
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 56c1f997f88..064a9878e72 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -355,8 +355,6 @@ BtreeLevel
 Bucket
 BufFile
 Buffer
-BufferAccessStrategy
-BufferAccessStrategyType
 BufferCacheOsPagesContext
 BufferCacheOsPagesRec
 BufferDesc
-- 
2.51.2



^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention
  2026-04-25 20:08 [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-04-27 14:15 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-04-28 14:30   ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 14:00     ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 18:07       ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
@ 2026-07-11 15:54         ` Greg Burd <greg@burd.me>
  2026-07-14 17:53           ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  0 siblings, 1 reply; 21+ messages in thread

From: Greg Burd @ 2026-07-11 15:54 UTC (permalink / raw)
  To: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; +Cc: Tomas Vondra <tomas@vondra.me>; Nathan Bossart <nathandbossart@gmail.com>; Andres Freund <andres@anarazel.de>


On Fri, Jul 10, 2026, at 2:07 PM, Greg Burd wrote:
> On Fri, Jul 10, 2026, at 10:00 AM, Greg Burd wrote:
>> Hello again,
>>
>> Removing the freelist [1] turned out to be a good idea, so why not remove
>> and simplify more?
>>
>> Included is a three-patch series (v3, on 2e6578292a9 cf-7000) that
>> replaces the 0..5 usage_count clock sweep with a cooling-stage clock,
>> and because this evictor algorithm is scan resistance 0003 removes
>> the BufferAccessStrategy ring buffers entirely.  The net is slightly
>> faster (TPS), hotter buffers (fewer misses) and a lot less code to
>> maintain (no special case for scan resistant work) going forward.
>>
>>   0001  Batch the clock sweep to reduce nextVictimBuffer atomic contention
>>   0002  Replace the usage_count clock sweep with a cooling-stage evictor
>>   0003  Remove BufferAccessStrategy; scan resistance is now intrinsic
>>
>> Net it is +510/-1642 over 83 files; almost all of the deletion is 0003.
>>
>> On a 6-NUMA-node r8i.metal-96xl under continuous eviction, this change is
>> a consistent +4–5% on read-heavy pgbench with equal-or-better hit ratios.
>> The same test on a 2-node box is flat, which is the tell that the win
>> is the cross-socket clock-hand contention the batched sweep removes. Under
>> real storage IO (working set > RAM on local NVMe), the scan-resistance
>> path is +1.6–3.9% with measurably fewer heap reads.
>>
>> I'll put the design decisions and the benchmark methodology on the table
>> first, then talk through the results because if the model is wrong on
>> principle, or the benchmarks are measuring the wrong thing, I would much
>> rather hear it now.
>>
>> Contention arises on high core-count systems from the of cost advancing
>> nextVictimBuffer in StrategyGetBuffer() via a fetch-add of 1 per tick.
>> This contention is exacerbated on a multi-socket box.  That one cache
>> line bounces over the interconnect on every eviction; under pressure
>> with hundreds of backends it is the dominant cost of the sweep.  This is
>> why we're investing time on NUMA optimizations.
>>
>>
>> == What each patch does, and why ==
>>
>> 0001 -- Batch the clock sweep.
>>
>>   This is the setup/baseline commit and is an incremental improvement
>>   over the existing batched approach, but it is not the final goal and
>>   should not be committed without 0002.
>>
>>   Each backend will claim a run of consecutive buffer IDs with a single
>>   fetch-add and iterate them privately.  Global sweep order is preserved
>>   -- each buffer is visited exactly once per pass -- only the intra-pass
>>   ordering of visits changes, which the algorithm does not depend on.
>>   The atomic fires ~1/batch as often.
>>
>>   The batch is one cache line's worth of hand values (PG_CACHE_LINE_SIZE
>>   / sizeof(uint32)), capped at NBuffers.  It is gated on multi-node NUMA
>>   (pg_numa_get_max_node() >= 1); on a single socket the batch is 1 and
>>   the path is byte-identical to today.  This is essentially Jim's patch;
>>   the only substantive change from his v1/v2 is deriving the batch size
>>   from the cache-line size rather than a fixed 64/tiered constant.
>>
>> 0002 -- Replace usage_count with a cooling-stage evictor.
>>
>>   This is the heart of the series and where I most want the design
>>   attacked.
>>
>>   A buffer is HOT (recently used) or COOL (an eviction candidate);
>>   "pinned" is the existing refcount.  There is no per-buffer 0..5
>>   counter.  A demand-loaded page is admitted COOL (probationary); a
>>   second access promotes it COOL->HOT.  So a page touched once -- as is
>>   the case with a sequential scan -- fills and drains the COOL stage and
>>   is evicted from it without displacing the HOT working set.  This is
>>   the LeanStore / 2Q-A1 idea, and it is the whole reason 0003 can exist:
>>   scan resistance stops being a ring-buffer bolt-on and becomes a
>>   property of the replacement algorithm.
>>
>>   The 4-bit usage_count field is reinterpreted in place -- bit 0
>>   HOT/COOL, bit 1 a reference bit, the top 2 bits unused -- so the 64-bit
>>   buffer-state layout, its refcount / flag / lock offsets.  The only
>>   change to the StaticAsserts is a new one asserting the field is at
>>   least 2 bits wide, since we now use two of its bits.  The eviction
>>   claim (COOL, unpinned -> pinned) stays a CAS so a racing PinBuffer
>>   always wins; promotion and demotion are single-bit transitions.
>>
>>   The one non-obvious decision, which I got wrong first and want to flag
>>   loudly: *Who* demotes HOT->COOL.  My first cut was "prefer-COOL": the
>>   foreground sweep skips HOT buffers hunting for an already-COOL victim
>>   and only cools HOT buffers once a full pass finds none.  That
>>   collapses under an ordinary OLTP workload where the working set is
>>   larger than shared_buffers (no scan at all).  Every access promotes
>>   its buffer to HOT, so there are almost no COOL buffers, the "cool a
>>   full pass" fallback fires on nearly every allocation, and each victim
>>   search becomes a ~2x full-pool scan of scattered BufferDescs.  I
>>   measured 3-17x throughput loss vs stock -- a cliff.
>>
>>   HOT->COOL demotion is done during the background writer's existing LRU
>>   scan, which already runs ahead of the clock hand.  It demotes just
>>   enough HOT buffers to keep a supply of COOL victims (bounded by the
>>   predicted next-cycle allocation, so it does not cool the whole pool),
>>   under the buffer header lock it already holds.  A single reference bit
>>   gives a recently-accessed buffer one reprieve before it is cooled,
>>   which keeps the genuinely-hot set out of the COOL stage under scan
>>   pressure.  With that, the foreground finds a victim in a single pass
>>   and the cliff is gone (data below).
>>
>>   I chose prefer-COOL-plus-bgwriter-precooling because it can protect a
>>   hot buffer slightly longer (the pre-cooler, tuned to a budget, decides
>>   when to demote rather than demoting on contact), which should help hit
>>   ratio on a stable hot set -- IF the pre-cooler keeps up.  When it lags
>>   (bgwriter off or behind), the foreground force_cool is still there as
>>   a correctness fallback, but it is the expensive path.
>>
>> 0003 -- Remove BufferAccessStrategy.
>>
>>   With scan resistance intrinsic, the BAS_BULKREAD/BULKWRITE/VACUUM
>>   rings are dead weight, so this removes them end to end: the type and
>>   enum, the ring machinery in freelist.c, the strategy parameter
>>   threaded through ReadBufferExtended / the ExtendBufferedRel* family /
>>   read_stream / every scan/vacuum/analyze/index-AM caller, the strategy
>>   fields on the scan and bulk-insert descriptors, and
>>   _hash_getbuf_with_strategy.  pg_stat_io's per-strategy IO contexts
>>   collapse to normal/init (IOOP_REUSE only ever happened while recycling
>>   a ring buffer, so it is gone), and the vacuum_buffer_usage_limit GUC /
>>   VACUUM (BUFFER_USAGE_LIMIT ...) option / vacuumdb --buffer-usage-limit
>>   go with it.
>>
>>   This is the patch most likely to be contentious for reasons unrelated
>>   to the sweep: it removes a user-visible GUC and changes pg_stat_io's
>>   shape.  I have kept it as its own commit precisely so 0001+0002 can be
>>   judged on the algorithm without swallowing the removal.  If the
>>   consensus is that the cooling model is fine but the ring machinery
>>   should stay for other reasons (BUFFER_USAGE_LIMIT as an operator
>>   control, say), 0003 can simply be dropped and 0002 still stands -- the
>>   rings just become redundant rather than removed.  I would like to know
>>   if that is where people land.
>>
>>
>> == Benchmarks ==
>>
>> I want to be careful here, because Andres's central criticism of the
>> original batched-sweep numbers [2] was that they measured the wrong
>> thing -- an all-in-page-cache config where the only bottleneck left is
>> the atomic, which is not how anyone runs a 384-vCPU box.  That criticism
>> is correct and I have tried to design around it, but I have almost
>> certainly not fully escaped it, so the methodology is laid out below in
>> enough detail to shoot at.
>>
>> I refer to the COOL/HOT approach (the changes in this patch set) as
>> "bcs", I've forgotten what that stands for... "buffer cache solution?"
>> I really don't remember (ha!).
>>
>> Hardware / method (both instances bare-metal, Amazon Linux 2023):
>>
>> - m6i.metal      -- 128 vCPU, 2 sockets, 2 NUMA nodes (distances 10/20), 503GB
>> - r8i.metal-96xl -- 384 vCPU, 2 sockets, 6 NUMA nodes via SNC3, 3TB
>>
>> Builds: --buildtype=debugoptimized (-O2), cassert off, --with-libnuma,
>> both branches from the same tree.  Postmaster pinned with numactl
>> --cpunodebind=0 --membind=0 (without it stock TPS varied ~30% by launch
>> node -- worth flagging for anyone reproducing).  pgbench dataset loaded
>> once per build into its own datadir.
>>
>> The regime I settled on: a dataset that fits fully in OS page cache but
>> exceeds shared_buffers, warmed before each cell, caches NOT dropped
>> between cells.  The point is a sweep that runs continuously with
>> sub-millisecond read latency and NO storage IO in the critical path --
>> so what I am measuring is the eviction machinery itself, not the disk
>> (as best as I can tell).
>>
>> I sweep the ratio (working-set / shared_buffers) from 0.8 (fits, no
>> eviction, a control) up to 8x by varying shared_buffers against a fixed
>> 63GB dataset.
>>
>> 3 iterations per cell, medians reported, 256 clients on m6i / 384 on r8i.
>>
>> Two workloads: uniform pgbench -S (the pure eviction-churn case, and the
>> worst case for the cooling model -- no hot set to protect), and
>> "hotscan" (a Zipfian hot set plus a handful of clients running large
>> range scans -- the scan-resistance case).
>>
>> I want the regime itself critiqued.  It deliberately removes storage IO
>> to expose the sweep; the flip side is it is not a production
>> configuration, and the read-path win (fewer misses under scan
>> resistance) shows up as read count, not TPS, because a "miss" here is an
>> OS-cache memcpy, not a device read.
>>
>> r8i.metal-96xl, uniform pgbench -S, 384 clients, TPS (median of 3):
>>
>>   ratio  SB(GB)   stock       bcs        delta   stock/bcs hit%
>>   0.8    ~400     1,623,536   1,692,090   +4.2%   99.2 / 99.9
>>   1.25   ~320     1,443,343   1,519,290   +5.3%   94.8 / 95.2
>>   1.5    ~266     1,344,051   1,417,462   +5.5%   91.9 / 92.1
>>   2      ~200     1,293,871   1,358,550   +5.0%   87.9 / 87.8
>>   4      ~100     1,194,405   1,240,334   +3.8%   77.1 / 78.5
>>   8      ~50      1,090,873   1,140,839   +4.6%   69.7 / 70.6
>>
>> Consistent +4-5% across the eviction range, growing with pressure, with
>> equal-or-better hit ratio (better at 4x/8x).  bcs also showed lower
>> cache-miss rate in perf stat (~28-35% vs ~31-37%), which is the batched
>> sweep's reduced cross-node line bouncing showing through.
>>
>> r8i, hotscan (Zipfian hot set + range scanners), TPS / hit% / heap reads:
>>
>>   ratio  stock TPS   bcs TPS    stock hit  bcs hit   stock reads  bcs reads
>>   1.25   1,833,675   1,871,289   99.43     99.57     7,309,457    5,503,927
>>   1.5    1,876,846   1,942,877   99.35     99.42     8,335,144    7,565,667
>>   2      1,868,591   1,853,547   99.12     99.15    11,173,831   10,987,371
>>
>> The scan-resistance signal: at 1.25-1.5x, bcs holds a higher hit ratio
>> and does up to 25% fewer heap reads (24.7% at 1.25x, ~9% at 1.5x) -- it
>> is keeping the hot set resident through the scans where stock lets them
>> flush it.  Muted in TPS only because everything is in OS cache (a miss
>> is cheap); on real storage this read reduction is where the win would
>> land.  At 2x it washes out (enough pressure that both evict heavily).
>>
>> m6i.metal (2 nodes), uniform, 256 clients -- the smaller box, for contrast:
>> essentially parity, bcs -2% to +1% across ratios.  The 2-node box barely
>> exercises the atomic, so 0001's contention win does not appear; that it does
>> not regress is the result that matters here.
>>
>> Huge pages on vs off (r8i, uniform, medians): I ran this because the
>> original thread flagged it as uncharacterized.  bcs won by +3-7% both
>> ways, no regression without huge pages -- the win is from cutting the
>> frequency of atomic ops on the counter line, which does not depend on
>> where the descriptors physically live.  (This is why the batching gate
>> is NUMA-only and not also huge-pages-gated.)
>>
>> The "prefer-COOL cliff" I mentioned under 0002, so the failure mode is
>> on the record: BEFORE moving cooling into the bgwriter, the m6i uniform
>> run at 256 clients was bcs 274K vs stock 840K at ratio 2 (-67%), and
>> 42.8K vs 762K at 8x (-94%), with cache-miss rate exploding to ~40%.
>> That is the shape of getting the demotion policy wrong; the r8i +5%
>> table above is after the fix.
>>
>> Reproduction: the whole harness (instance launch, OS tuning, per-build
>> load, the ratio sweep, perf stat capture) is scripted; I will attach it
>> as a DO-NOT-MERGE commit / put it in the CF entry so the methodology can
>> be reproduced and picked apart rather than taken on faith.  Raw per-run
>> CSVs and perf output likewise.
>>
>>
>> A Real IO (working set > RAM, evictions hitting storage) Benchmark
>>
>> This is the regime Andres asked for, and the one I flagged earlier as not yet
>> done cleanly.  The earlier attempt was EBS-latency-bound; this one uses local
>> NVMe so eviction reads hit real storage at ~microsecond, not ~15ms, latency --
>> during the run the array sat at 100% utilization and ~145K read IOPS, so the
>> eviction path is genuinely storage-bound, not cache-served.
>>
>> m6id.metal -- 128 vCPU, 2 sockets, 2 NUMA nodes, 503GB, 4x1.9TB local NVMe in
>> RAID0.  Dataset ~700GB (pgbench scale 47000), i.e. LARGER than RAM, so the
>> working set cannot sit in the OS page cache.  shared_buffers is a small window
>> over it -- 63GB (11x) and 31GB (22x) -- caches dropped per cell, 256 clients, 3
>> iterations, medians.  Same builds/method as the in-cache runs otherwise.
>>
>> hotscan (Zipfian hot set + range scanners), median of 3:
>>
>>   ratio  SB(GB)   stock TPS   bcs TPS     dTPS    stock/bcs hit   reads d
>>   11     63       877,357     911,363    +3.9%    96.30 / 96.43   -2.2%
>>   22     31       873,880     888,010    +1.6%    94.02 / 94.22   -1.5%
>>
>> This is the result the in-cache runs could only hint at: under real storage IO
>> the scan-resistance read reduction converts to throughput.  The bcs approach
>> keeps a higher hit ratio and does 1.5-2.2% fewer heap reads, and here -- unlike
>> in cache, where a miss is a cheap memcpy -- a read it avoids is an NVMe round
>> trip, so the read reduction shows up as +1.6-3.9% TPS.
>>
>> uniform pgbench -S (pure eviction churn, no hot set to protect), median of 3:
>>
>>   ratio  SB(GB)   stock TPS   bcs TPS     dTPS    stock/bcs hit
>>   11     63       605,656     616,247    +1.7%    67.0 / 67.3
>>   22     31       605,517     611,312    +1.0%    63.5 / 63.6
>>
>> Hit ratio here is 63-67% -- a third of accesses miss and hit NVMe (190M-220M
>> evictions per run), so this is deep, genuinely storage-bound churn.  bcs is
>> +1-1.7%, i.e. flat-to-slightly-positive, which is the honest production picture:
>> with no hot set to protect, scan resistance has nothing to do, and the win is
>> just the batched sweep's reduced contention showing faintly through the IO wait.
>> Notably bcs does not regress even when its policy has no advantage to exploit.
>>
>>
>> == Side note for the curious... ==
>>
>> Separately, Dhruv Aron has proposed restructuring the shared-buffer lookup table
>> [2], replacing dynahash with a flat two-array structure. That attacks the
>> other hot cost on a buffer miss — resolving a page to its buffer — where this
>> series attacks the eviction that a miss triggers. They touch buf_table.c and a
>> lock-ordering change in InvalidateBuffer(); this series touches freelist.c and
>> the per-buffer replacement state, and removes BufferAccessStrategy. The two are
>> complementary and should compound on the miss path; the only overlap is
>> InvalidateBuffer()/GetVictimBuffer(), where their extended buffer-header-lock
>> hold and this series' CAS-claim + bgwriter pre-cooling both take that lock, and
>> would want reconciling if both land. I have not benchmarked them together (yet).
>>
>> == What I have not done, honestly ==
>>
>>   - Hardening the foreground force_cool fallback to be cheap when it
>>     fires, rather than relying on the bgwriter pre-cooler never lagging.
>>
>>   - Anything on single-socket beyond "does not regress"; the design is
>>     not trying to help there.
>>
>>
>> == The ask ==
>>
>>   1. 0002's demotion policy: is prefer-COOL + bgwriter pre-cooling the
>>      right call, or is the other team's cool-in-place the more robust
>>      default given it has no cliff and no background-process dependency?
>>      This is the decision everything else hangs on.
>>
>>   2. Is admitting demand-loaded pages COOL (probationary,
>>      promote-on-second- touch) an acceptable basis for scan resistance
>>      in the core buffer manager, i.e. is it OK to make scan resistance
>>      an algorithm property and retire the strategy rings (0003)?  Or
>>      should the rings stay and 0002 ride alongside them?
>>
>>   3. The benchmark methodology: where is the in-OS-cache regime
>>      misleading, and what would you want measured instead?  I am most
>>      worried I am flattering the sweep by removing the IO that would
>>      otherwise hide it.
>>
>>   4. Reinterpreting the usage_count field as {HOT/COOL, ref} bits and
>>      collapsing pg_stat_io's contexts -- acceptable, or is there a
>>      cleaner representation the project would want before this is worth
>>      pursuing?
>>
>> I have measured that the 0..5 count is overhead and provides no
>> meaningful signal at all, that a HOT/COLD approach provides a simpler
>> more stable and better performing eviction model for the buffer pool.
>> If you dispute that, let's dig in and compare notes. :)
>>
>> I would be remiss if I didn't point out the thread [3] by Tomas et. al.,
>> whose NUMA investigation targets the same bottlenecks, and inspired the
>> work that led to this set of ideas. 
>>
>> Thanks for reading this far.  I look forward to the critique.
>>
>> best.
>>
>> -greg
>>
>> [1] Reconsidering the freelist
>>     
>> https://www.postgresql.org/message-id/f0e3c02e-e217-4f04-8dab-1e7e80a228c0%40burd.me
>> [2] Re: Restructured Shared Buffer Hash Table
>>     
>> https://www.postgresql.org/message-id/dbbd1998-19ff-4ac2-b4b1-a39f4ec1b0f5@iki.fi
>> [3] Adding basic NUMA awareness (Tomas Vondra)
>>     
>> https://www.postgresql.org/message-id/099b9433-2855-4f1b-b421-d078a5d82017%40vondra.me
>> Attachments:
>> * v3-0001-Batch-the-clock-sweep-to-reduce-nextVictimBuffer-.patch
>> * v3-0002-Replace-the-usage_count-clock-sweep-with-a-coolin.patch
>> * v3-0003-Remove-BufferAccessStrategy-scan-resistance-is-no.patch
>
> Rebased v4 onto c71d43025d7.
>
> -greg
> Attachments:
> * v4-0001-Batch-the-clock-sweep-to-reduce-nextVictimBuffer-.patch
> * v4-0002-Replace-the-usage_count-clock-sweep-with-a-coolin.patch
> * v4-0003-Remove-BufferAccessStrategy-scan-resistance-is-no.patch

Rebased v5 onto 5f14f82280d to keep the CF-bot happy (and everyone else, I'd imagine).

-greg

Attachments:

  [text/x-patch] v5-0001-Batch-the-clock-sweep-to-reduce-nextVictimBuffer-.patch (8.9K, ../../9cfdf3f2-b799-485a-8b5a-5159c00ff2df@app.fastmail.com/2-v5-0001-Batch-the-clock-sweep-to-reduce-nextVictimBuffer-.patch)
  download | inline diff:
From 6cb67e3346fd496b4b735cd47fea9019cbf232c4 Mon Sep 17 00:00:00 2001
From: Greg Burd <greg@burd.me>
Date: Mon, 6 Jul 2026 17:25:00 -0400
Subject: [PATCH v5 1/3] Batch the clock sweep to reduce nextVictimBuffer
 atomic contention

StrategyGetBuffer() advances the shared clock hand, nextVictimBuffer, with a
pg_atomic_fetch_add_u32(..., 1) on every tick.  On a multi-socket system the
cache line holding that counter has to travel over the interconnect on each
operation, pushing a sweep tick from ~20ns (same-socket, line warm in L1/L2)
into the ~100-200ns range.  Under eviction pressure with hundreds of backends
in StrategyGetBuffer() concurrently, that single cache line becomes the
dominant cost of the sweep, visible as elevated bus-cycles and cache-misses in
a perf profile.

Have each backend claim a run of consecutive buffer IDs from the shared hand
with a single fetch-add and then iterate through them privately.  The sweep
still advances through the pool in order, each buffer is still visited exactly
once per complete pass, and the meaning of the clock state is unchanged; only
the temporal ordering of visits within a pass changes, which the algorithm
does not depend on.  The contended atomic now fires roughly once per batch
rather than once per buffer.

The batch is one cache line's worth of clock-hand values --
PG_CACHE_LINE_SIZE / sizeof(uint32) -- capped at NBuffers so a claim can never
wrap the pool more than once.  Batching only helps when the counter's cache
line actually bounces between sockets, so it is enabled only on multi-node
NUMA hardware (pg_numa_get_max_node() >= 1); on a single socket, or where
libnuma is unavailable, the batch size stays 1 and the code path is
byte-identical to the stock clock sweep.

Wraparound handling is adjusted: with batching, several backends can each see
a fetch-add return a value past NBuffers within the same pass.  Any such
backend takes buffer_strategy_lock, re-reads the counter, and if it is still
out of range wraps it with a single CAS and increments completePasses.
StrategySyncStart() continues to see a consistent (nextVictimBuffer,
completePasses) pair.

This is the batched-clock-sweep idea from Jim Mlodgenski's pgsql-hackers
thread, adapted to derive the batch size from the platform cache-line size.

Co-authored-by: Jim Mlodgenski <mlodj@amazon.com>
---
 src/backend/storage/buffer/freelist.c | 134 +++++++++++++++++---------
 1 file changed, 91 insertions(+), 43 deletions(-)

diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index fdb5bad7910..93fc4dd758b 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -22,6 +22,7 @@
 #include "storage/proc.h"
 #include "storage/shmem.h"
 #include "storage/subsystems.h"
+#include "port/pg_numa.h"
 
 #define INT_ACCESS_ONCE(var)	((int)(*((volatile int *)&(var))))
 
@@ -100,68 +101,92 @@ static BufferDesc *GetBufferFromRing(BufferAccessStrategy strategy,
 static void AddBufferToRing(BufferAccessStrategy strategy,
 							BufferDesc *buf);
 
+/*
+ * Per-backend state for the batched clock sweep.  Each backend claims a run
+ * of consecutive clock-hand values with a single atomic fetch-add and then
+ * iterates through them privately, so the contended nextVictimBuffer cache
+ * line is touched roughly 1/batch as often.  MyBatchPos is the next hand
+ * value to hand out; MyBatchEnd is one past the end of the claimed run.  Both
+ * are absolute (monotonically increasing) hand values; the buffer id is the
+ * value modulo NBuffers.
+ */
+static uint32 MyBatchPos = 0;
+static uint32 MyBatchEnd = 0;
+
+/*
+ * Number of clock-hand values a backend claims per atomic fetch-add,
+ * computed once at startup (see StrategyCtlShmemInit).  When batching is
+ * enabled it is one cache line's worth of hand advance, so concurrent
+ * backends sweep non-overlapping, cache-line-sized runs of the pool; the
+ * global sweep order is preserved (each buffer is still visited exactly once
+ * per pass).  Batching is enabled only on multi-node NUMA hardware; otherwise
+ * this stays 1 and the sweep is byte-identical to the stock clock.
+ */
+static uint32 ClockSweepBatchSize = 1;
+
 /*
  * ClockSweepTick - Helper routine for StrategyGetBuffer()
  *
- * Move the clock hand one buffer ahead of its current position and return the
- * id of the buffer now under the hand.
+ * Return the next buffer to consider for eviction.  Backends claim batches of
+ * consecutive buffer IDs from the shared clock hand, then iterate through
+ * them locally without further atomic operations.  This preserves the global
+ * sweep order while reducing contention on the shared counter.
  */
 static inline uint32
 ClockSweepTick(void)
 {
 	uint32		victim;
 
-	/*
-	 * Atomically move hand ahead one buffer - if there's several processes
-	 * doing this, this can lead to buffers being returned slightly out of
-	 * apparent order.
-	 */
-	victim =
-		pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer, 1);
-
-	if (victim >= NBuffers)
+	if (MyBatchPos >= MyBatchEnd)
 	{
-		uint32		originalVictim = victim;
-
-		/* always wrap what we look up in BufferDescriptors */
-		victim = victim % NBuffers;
-
 		/*
-		 * If we're the one that just caused a wraparound, force
-		 * completePasses to be incremented while holding the spinlock. We
-		 * need the spinlock so StrategySyncStart() can return a consistent
-		 * value consisting of nextVictimBuffer and completePasses.
+		 * Claim a fresh batch from the shared clock hand.  This is the only
+		 * atomic operation per batch, reducing contention by the batch size.
 		 */
-		if (victim == 0)
-		{
-			uint32		expected;
-			uint32		wrapped;
-			bool		success = false;
+		uint32		start;
+		uint32		batch_size = ClockSweepBatchSize;
 
-			expected = originalVictim + 1;
+		start = pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer,
+										batch_size);
 
-			while (!success)
-			{
-				/*
-				 * Acquire the spinlock while increasing completePasses. That
-				 * allows other readers to read nextVictimBuffer and
-				 * completePasses in a consistent manner which is required for
-				 * StrategySyncStart().  In theory delaying the increment
-				 * could lead to an overflow of nextVictimBuffers, but that's
-				 * highly unlikely and wouldn't be particularly harmful.
-				 */
-				SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
+		if (start >= (uint32) NBuffers)
+		{
+			start = start % NBuffers;
 
-				wrapped = expected % NBuffers;
+			/*
+			 * The counter has grown past NBuffers; try to wrap it back.  We
+			 * must hold the spinlock so StrategySyncStart() can read
+			 * nextVictimBuffer and completePasses consistently.
+			 *
+			 * With batching, multiple backends may each land a fetch-add
+			 * that returns a value past NBuffers in the same pass.  After
+			 * acquiring the spinlock we re-read the counter: if another
+			 * backend already wrapped it below NBuffers we are done.
+			 */
+			SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
+			{
+				uint32		current;
+				uint32		wrapped;
 
-				success = pg_atomic_compare_exchange_u32(&StrategyControl->nextVictimBuffer,
-														 &expected, wrapped);
-				if (success)
-					StrategyControl->completePasses++;
-				SpinLockRelease(&StrategyControl->buffer_strategy_lock);
+				current = pg_atomic_read_u32(&StrategyControl->nextVictimBuffer);
+				if (current >= (uint32) NBuffers)
+				{
+					wrapped = current % NBuffers;
+					if (pg_atomic_compare_exchange_u32(&StrategyControl->nextVictimBuffer,
+													   &current, wrapped))
+						StrategyControl->completePasses++;
+				}
 			}
+			SpinLockRelease(&StrategyControl->buffer_strategy_lock);
 		}
+
+		MyBatchPos = start;
+		MyBatchEnd = start + batch_size;
 	}
+
+	victim = MyBatchPos % NBuffers;
+	MyBatchPos++;
+
 	return victim;
 }
 
@@ -408,6 +433,29 @@ StrategyCtlShmemInit(void *arg)
 
 	/* No pending notification */
 	StrategyControl->bgwprocno = -1;
+
+	/*
+	 * Decide whether to batch the clock sweep.
+	 *
+	 * Batching claims a run of consecutive buffer IDs per atomic fetch-add so
+	 * concurrent backends touch the shared nextVictimBuffer cache line ~1/batch
+	 * as often -- a win only when that line actually bounces across sockets,
+	 * i.e. on multi-node NUMA hardware.  On a single socket the atomic is
+	 * already node-local and batching would only make backends skip ahead for
+	 * no benefit, so we fall back to batch size 1 there (byte-identical to the
+	 * stock one-buffer-at-a-time clock sweep).
+	 *
+	 * Batch (> 1) only when libnuma reports more than one node
+	 * (pg_numa_get_max_node() >= 1); pg_numa_init() returns -1 when NUMA is
+	 * unavailable.  (Benchmarking showed the win holds with or without huge
+	 * pages, so huge-page availability is intentionally not part of the gate.)
+	 */
+	if (pg_numa_init() != -1 &&
+		pg_numa_get_max_node() >= 1)
+		ClockSweepBatchSize = Min(PG_CACHE_LINE_SIZE / (uint32) sizeof(uint32),
+								  (uint32) NBuffers);
+	else
+		ClockSweepBatchSize = 1;
 }
 
 
-- 
2.51.2



  [text/x-patch] v5-0002-Replace-the-usage_count-clock-sweep-with-a-coolin.patch (27.5K, ../../9cfdf3f2-b799-485a-8b5a-5159c00ff2df@app.fastmail.com/3-v5-0002-Replace-the-usage_count-clock-sweep-with-a-coolin.patch)
  download | inline diff:
From e8429f51618e8932e2b8912822dec89d73e46ed2 Mon Sep 17 00:00:00 2001
From: Greg Burd <greg@burd.me>
Date: Mon, 6 Jul 2026 19:22:45 -0400
Subject: [PATCH v5 2/3] Replace the usage_count clock sweep with a
 cooling-stage evictor

Replace the 0..5 usage_count buffer-replacement policy with a cooling-stage
clock (the LeanStore / 2Q-A1 model): a buffer is simply HOT (recently used)
or COOL (an eviction candidate), with "pinned" being the existing refcount.
There is no per-buffer access counter.

  - A demand-loaded page is admitted COOL (probationary), not HOT.  A second
    access via PinBuffer promotes it COOL -> HOT (the rescue).  So a page
    touched once -- a sequential scan -- fills and drains the COOL stage and
    is evicted from it without ever displacing the HOT working set.  Scan
    resistance is intrinsic to the replacement algorithm, which is what lets a
    later commit remove the BufferAccessStrategy ring buffers entirely.

  - The foreground sweep in StrategyGetBuffer() reclaims an already-COOL,
    unpinned buffer, pinning it with a CAS so a racing PinBuffer always wins.
    Promotion (COOL -> HOT) and demotion (HOT -> COOL) are single-bit
    transitions; only the eviction claim is a CAS.

  - The background writer maintains the supply of COOL victims.  As its LRU
    scan runs ahead of the clock hand it demotes HOT buffers to COOL, so the
    foreground finds a victim in a single pass rather than having to cool
    buffers itself.  The demotion is demand-driven -- bounded by the predicted
    allocation for the next cycle -- so it stages just enough COOL buffers
    without cooling the whole pool, and it is done under the buffer header
    lock the scan already holds.  A single second-chance reference bit
    (set by PinBuffer, cleared on the bgwriter's first pass over a HOT buffer)
    spares a recently-accessed buffer one cooling pass, keeping the genuinely
    hot set out of the COOL stage under scan pressure.  BgBufferSync also
    tracks the reusable-buffer density it directly observes on a shorter
    smoothing window, so a burst of probationary/scan COOL pages is followed
    promptly rather than averaged away.

The 4-bit usage_count field is reinterpreted in place: bit 0 is the HOT/COOL
state (BUF_COOLSTATE_ONE), bit 1 the reference bit (BUF_REFBIT).  The 64-bit
buffer-state layout -- refcount, flag and lock offsets and their StaticAsserts
-- is unchanged; only the meaning of the field and the instructions that touch
it change.  A StaticAssert requires the field to be at least 2 bits wide so a
future width change cannot push the reference bit into the flag bits.
BM_MAX_USAGE_COUNT becomes BUF_COOLSTATE_HOT (1), so the pin fast path
saturates at HOT.  Local (temp-table) buffers get the same two-state
treatment; being single-backend they need no background cooler.

Because the reference bit shares the field, the full 4-bit value can be 0..3;
readers that mean "the cooling state" must use BUF_STATE_GET_COOLSTATE(), which
masks to bit 0 and returns only 0 (COOL) or 1 (HOT), never the raw field.
contrib/pg_buffercache is updated accordingly: its usagecount column and the
pg_buffercache_summary average report the cooling state (0 = COOL, 1 = HOT),
and pg_buffercache_usage_counts() buckets on it.  Using the raw 4-bit getter
there would index its BM_MAX_USAGE_COUNT+1 = 2-element arrays with values up
to 3 and overrun the stack; masking to the cooling bit keeps the index in
range.  The reference bit is deliberately not exposed as usagecount.

Depends on the batched clock sweep from the previous commit.
---
 contrib/pg_buffercache/pg_buffercache_pages.c |   6 +-
 src/backend/storage/buffer/bufmgr.c           | 103 ++++++++++----
 src/backend/storage/buffer/freelist.c         | 128 ++++++++++++------
 src/backend/storage/buffer/localbuf.c         |  13 +-
 src/include/storage/buf_internals.h           |  63 +++++++--
 5 files changed, 229 insertions(+), 84 deletions(-)

diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index 510455998aa..b94e6f6fda8 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -161,7 +161,7 @@ pg_buffercache_pages(PG_FUNCTION_ARGS)
 		reldatabase = bufHdr->tag.dbOid;
 		forknum = BufTagGetForkNum(&bufHdr->tag);
 		blocknum = bufHdr->tag.blockNum;
-		usagecount = BUF_STATE_GET_USAGECOUNT(buf_state);
+		usagecount = BUF_STATE_GET_COOLSTATE(buf_state);
 		pinning_backends = BUF_STATE_GET_REFCOUNT(buf_state);
 
 		if (buf_state & BM_DIRTY)
@@ -605,7 +605,7 @@ pg_buffercache_summary(PG_FUNCTION_ARGS)
 		if (buf_state & BM_VALID)
 		{
 			buffers_used++;
-			usagecount_total += BUF_STATE_GET_USAGECOUNT(buf_state);
+			usagecount_total += BUF_STATE_GET_COOLSTATE(buf_state);
 
 			if (buf_state & BM_DIRTY)
 				buffers_dirty++;
@@ -655,7 +655,7 @@ pg_buffercache_usage_counts(PG_FUNCTION_ARGS)
 
 		CHECK_FOR_INTERRUPTS();
 
-		usage_count = BUF_STATE_GET_USAGECOUNT(buf_state);
+		usage_count = BUF_STATE_GET_COOLSTATE(buf_state);
 		usage_counts[usage_count]++;
 
 		if (buf_state & BM_DIRTY)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 9ab282a76d1..1173b50b7c1 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -83,6 +83,7 @@
 /* Bits in SyncOneBuffer's return value */
 #define BUF_WRITTEN				0x01
 #define BUF_REUSABLE			0x02
+#define BUF_COOLED				0x04
 
 #define RELS_BSEARCH_THRESHOLD		20
 
@@ -634,7 +635,7 @@ static void PinBuffer_Locked(BufferDesc *buf);
 static void UnpinBuffer(BufferDesc *buf);
 static void UnpinBufferNoOwner(BufferDesc *buf);
 static void BufferSync(int flags);
-static int	SyncOneBuffer(int buf_id, bool skip_recently_used,
+static int	SyncOneBuffer(int buf_id, bool skip_recently_used, bool cool_if_hot,
 						  WritebackContext *wb_context);
 static void WaitIO(BufferDesc *buf);
 static void AbortBufferIO(Buffer buffer);
@@ -2333,7 +2334,10 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	 * checkpoints, except for their "init" forks, which need to be treated
 	 * just like permanent relations.
 	 */
-	set_bits |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
+	set_bits |= BM_TAG_VALID;
+	/* Admit the newly loaded page COOL (probation); a second access via
+	 * PinBuffer promotes it to HOT.  This is what makes a one-touch scan
+	 * self-evicting -- see the cooling-state notes in buf_internals.h. */
 	if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM)
 		set_bits |= BM_PERMANENT;
 
@@ -3002,7 +3006,9 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 
 			victim_buf_hdr->tag = tag;
 
-			set_bits |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
+			set_bits |= BM_TAG_VALID;
+			/* Admit COOL (probation); see the comment at the other admission
+			 * site and the cooling-state notes in buf_internals.h. */
 			if (bmr.relpersistence == RELPERSISTENCE_PERMANENT || fork == INIT_FORKNUM)
 				set_bits |= BM_PERMANENT;
 
@@ -3332,21 +3338,17 @@ PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy,
 			/* increase refcount */
 			buf_state += BUF_REFCOUNT_ONE;
 
-			if (strategy == NULL)
-			{
-				/* Default case: increase usagecount unless already max. */
-				if (BUF_STATE_GET_USAGECOUNT(buf_state) < BM_MAX_USAGE_COUNT)
-					buf_state += BUF_USAGECOUNT_ONE;
-			}
-			else
-			{
-				/*
-				 * Ring buffers shouldn't evict others from pool.  Thus we
-				 * don't make usagecount more than 1.
-				 */
-				if (BUF_STATE_GET_USAGECOUNT(buf_state) == 0)
-					buf_state += BUF_USAGECOUNT_ONE;
-			}
+			/*
+			 * Accessing a resident buffer promotes it to HOT (the 2Q rescue):
+			 * a page loaded COOL on probation becomes part of the hot working
+			 * set on its second touch.  BM_MAX_USAGE_COUNT is
+			 * BUF_COOLSTATE_HOT (1), so this saturates at HOT and never
+			 * overflows the field.  We also set the second-chance ref bit so
+			 * the bgwriter's next cooling pass spares this recently-used buffer.
+			 */
+			if (BUF_STATE_GET_COOLSTATE(buf_state) < BUF_COOLSTATE_HOT)
+				buf_state += BUF_COOLSTATE_ONE;
+			buf_state |= BUF_REFBIT;
 
 			if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
 											   buf_state))
@@ -3785,7 +3787,7 @@ BufferSync(int flags)
 		 */
 		if (pg_atomic_read_u64(&bufHdr->state) & BM_CHECKPOINT_NEEDED)
 		{
-			if (SyncOneBuffer(buf_id, false, &wb_context) & BUF_WRITTEN)
+			if (SyncOneBuffer(buf_id, false, false, &wb_context) & BUF_WRITTEN)
 			{
 				TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
 				PendingCheckpointerStats.buffers_written++;
@@ -3876,6 +3878,19 @@ BgBufferSync(WritebackContext *wb_context)
 	float		smoothing_samples = 16;
 	float		scan_whole_pool_milliseconds = 120000.0;
 
+	/*
+	 * The cleaner scan directly observes the reusable (COOL, unpinned) buffer
+	 * density over the region it walks, which -- with the cooling-stage
+	 * evictor -- is exactly the sweep's victim predicate.  That observation is
+	 * ground truth for the buffers about to be reused, whereas the strategy
+	 * scan's positional proxy (strategy_delta/recent_alloc) blurs a pool whose
+	 * COOL population is spatially clustered (a scan burst leaves whole regions
+	 * COOL, hot OLTP regions not).  So we let the cleaner's own sample adapt on
+	 * a shorter window than the strategy proxy, tracking a burst of
+	 * probationary/scan COOL pages within a cycle or two instead of lagging it.
+	 */
+	float		cleaner_smoothing_samples = 4;
+
 	/* Used to compute how far we scan ahead */
 	long		strategy_delta;
 	int			bufs_to_lap;
@@ -4073,7 +4088,7 @@ BgBufferSync(WritebackContext *wb_context)
 	/* Execute the LRU scan */
 	while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
 	{
-		int			sync_state = SyncOneBuffer(next_to_clean, true,
+		int			sync_state = SyncOneBuffer(next_to_clean, true, true,
 											   wb_context);
 
 		if (++next_to_clean >= NBuffers)
@@ -4121,7 +4136,7 @@ BgBufferSync(WritebackContext *wb_context)
 	{
 		scans_per_alloc = (float) new_strategy_delta / (float) new_recent_alloc;
 		smoothed_density += (scans_per_alloc - smoothed_density) /
-			smoothing_samples;
+			cleaner_smoothing_samples;
 
 #ifdef BGW_DEBUG
 		elog(DEBUG2, "bgwriter: cleaner density alloc=%u scan=%ld density=%.2f new smoothed=%.2f",
@@ -4140,16 +4155,26 @@ BgBufferSync(WritebackContext *wb_context)
  * If skip_recently_used is true, we don't write currently-pinned buffers, nor
  * buffers marked recently used, as these are not replacement candidates.
  *
+ * If cool_if_hot is true (the bgwriter's LRU scan), an unpinned HOT buffer is
+ * demoted HOT -> COOL as we pass it, pre-staging eviction candidates so the
+ * foreground clock sweep finds a COOL victim in a single pass instead of
+ * having to cool buffers itself (force_cool).  The demotion is done under the
+ * buffer header lock we already hold, so it needs no CAS and cannot race a
+ * concurrent demotion.  A concurrent PinBuffer promotes it back to HOT, which
+ * is the intended 2Q behavior (a re-accessed buffer is rescued).
+ *
  * Returns a bitmask containing the following flag bits:
  *	BUF_WRITTEN: we wrote the buffer.
  *	BUF_REUSABLE: buffer is available for replacement, ie, it has
- *		pin count 0 and usage count 0.
+ *		pin count 0 and is COOL (an eviction candidate).
+ *	BUF_COOLED: we demoted this buffer HOT -> COOL this call.
  *
  * (BUF_WRITTEN could be set in error if FlushBuffer finds the buffer clean
  * after locking it, but we don't care all that much.)
  */
 static int
-SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
+SyncOneBuffer(int buf_id, bool skip_recently_used, bool cool_if_hot,
+			  WritebackContext *wb_context)
 {
 	BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
 	int			result = 0;
@@ -4171,8 +4196,38 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 	 */
 	buf_state = LockBufHdr(bufHdr);
 
+	/*
+	 * Pre-cool with a second chance: if asked, act on an unpinned HOT buffer.
+	 * If its ref bit is set (accessed since our last pass), clear the ref bit
+	 * and leave it HOT -- a recently-used buffer earns one reprieve, keeping
+	 * the hot working set out of the COOL stage under scan pressure.  Only a
+	 * HOT buffer whose ref bit is already clear is demoted HOT -> COOL,
+	 * pre-staging it as an eviction candidate for the foreground sweep.  We
+	 * hold the header lock, so each transition is a plain masked store applied
+	 * atomically by UnlockBufHdrExt; we re-lock to continue the dirty-write
+	 * inspection below.
+	 */
+	if (cool_if_hot &&
+		BUF_STATE_GET_REFCOUNT(buf_state) == 0 &&
+		BUF_STATE_GET_COOLSTATE(buf_state) != BUF_COOLSTATE_COOL)
+	{
+		if (BUF_STATE_GET_REFBIT(buf_state))
+		{
+			/* second chance: consume the ref bit, stay HOT */
+			UnlockBufHdrExt(bufHdr, buf_state, 0, BUF_REFBIT, 0);
+			buf_state = LockBufHdr(bufHdr);
+		}
+		else
+		{
+			/* not re-accessed since last pass: demote to COOL */
+			UnlockBufHdrExt(bufHdr, buf_state, 0, BUF_USAGECOUNT_MASK, 0);
+			buf_state = LockBufHdr(bufHdr);
+			result |= BUF_COOLED;
+		}
+	}
+
 	if (BUF_STATE_GET_REFCOUNT(buf_state) == 0 &&
-		BUF_STATE_GET_USAGECOUNT(buf_state) == 0)
+		BUF_STATE_GET_COOLSTATE(buf_state) == BUF_COOLSTATE_COOL)
 	{
 		result |= BUF_REUSABLE;
 	}
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index 93fc4dd758b..cc77144224c 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -49,6 +49,16 @@ typedef struct
 	uint32		completePasses; /* Complete cycles of the clock-sweep */
 	pg_atomic_uint32 numBufferAllocs;	/* Buffers allocated since last reset */
 
+	/*
+	 * Number of clock-hand values a backend claims per atomic fetch-add.
+	 * Computed once at startup (see StrategyCtlShmemInit).  Kept in shared
+	 * memory rather than a backend-local static so that EXEC_BACKEND children
+	 * (which do not inherit the postmaster's statics) see the same value; a
+	 * backend-local copy would silently reset to 1 there, disabling batching
+	 * on Windows.
+	 */
+	uint32		batchSize;
+
 	/*
 	 * Bgworker process to be notified upon activity or -1 if none. See
 	 * StrategyNotifyBgWriter.
@@ -113,17 +123,6 @@ static void AddBufferToRing(BufferAccessStrategy strategy,
 static uint32 MyBatchPos = 0;
 static uint32 MyBatchEnd = 0;
 
-/*
- * Number of clock-hand values a backend claims per atomic fetch-add,
- * computed once at startup (see StrategyCtlShmemInit).  When batching is
- * enabled it is one cache line's worth of hand advance, so concurrent
- * backends sweep non-overlapping, cache-line-sized runs of the pool; the
- * global sweep order is preserved (each buffer is still visited exactly once
- * per pass).  Batching is enabled only on multi-node NUMA hardware; otherwise
- * this stays 1 and the sweep is byte-identical to the stock clock.
- */
-static uint32 ClockSweepBatchSize = 1;
-
 /*
  * ClockSweepTick - Helper routine for StrategyGetBuffer()
  *
@@ -144,7 +143,7 @@ ClockSweepTick(void)
 		 * atomic operation per batch, reducing contention by the batch size.
 		 */
 		uint32		start;
-		uint32		batch_size = ClockSweepBatchSize;
+		uint32		batch_size = StrategyControl->batchSize;
 
 		start = pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer,
 										batch_size);
@@ -211,6 +210,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 	BufferDesc *buf;
 	int			bgwprocno;
 	int			trycounter;
+	bool		force_cool;
 
 	*from_ring = false;
 
@@ -261,12 +261,32 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 	 */
 	pg_atomic_fetch_add_u32(&StrategyControl->numBufferAllocs, 1);
 
-	/* Use the "clock sweep" algorithm to find a free buffer */
+	/*
+	 * Use the cooling-stage clock sweep to find a victim.
+	 *
+	 * A buffer is HOT (recently used) or COOL (an eviction candidate).  We
+	 * prefer to reclaim an already-COOL buffer and demote a HOT buffer to
+	 * COOL only once a full sweep has found no COOL victim (force_cool) -- so
+	 * an abundant supply of COOL/probationary pages (e.g. a scan) is drained
+	 * before the hot working set is cooled.  Newly loaded pages are admitted
+	 * COOL (see BufferAlloc), so scan resistance falls out of the algorithm.
+	 *
+	 * trycounter bounds the search.  Any tick that does not produce a victim
+	 * and does not make progress -- a pinned buffer, or a HOT buffer skipped
+	 * on a prefer-COOL pass -- decrements it.  Cooling a HOT buffer (under
+	 * force_cool) is progress and resets it.  When a full pass (NBuffers)
+	 * makes no progress we escalate to force_cool so the next pass cools HOT
+	 * buffers into victims; if a force_cool pass ALSO makes no progress every
+	 * buffer is pinned and we fail, matching the stock "no unpinned buffers
+	 * available" contract.
+	 */
 	trycounter = NBuffers;
+	force_cool = false;
 	for (;;)
 	{
 		uint64		old_buf_state;
 		uint64		local_buf_state;
+		bool		no_progress = false;
 
 		buf = GetBufferDescriptor(ClockSweepTick());
 
@@ -279,25 +299,10 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 		{
 			local_buf_state = old_buf_state;
 
-			/*
-			 * If the buffer is pinned or has a nonzero usage_count, we cannot
-			 * use it; decrement the usage_count (unless pinned) and keep
-			 * scanning.
-			 */
-
+			/* If the buffer is pinned we cannot use it; keep scanning. */
 			if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0)
 			{
-				if (--trycounter == 0)
-				{
-					/*
-					 * We've scanned all the buffers without making any state
-					 * changes, so all the buffers are pinned (or were when we
-					 * looked at them). We could hope that someone will free
-					 * one eventually, but it's probably better to fail than
-					 * to risk getting stuck in an infinite loop.
-					 */
-					elog(ERROR, "no unpinned buffers available");
-				}
+				no_progress = true;
 				break;
 			}
 
@@ -308,20 +313,43 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 				continue;
 			}
 
-			if (BUF_STATE_GET_USAGECOUNT(local_buf_state) != 0)
+			if (BUF_STATE_GET_COOLSTATE(local_buf_state) != BUF_COOLSTATE_COOL)
 			{
-				local_buf_state -= BUF_USAGECOUNT_ONE;
+				/*
+				 * HOT buffer.  Prefer a COOL victim: on a normal pass just
+				 * advance the hand (no progress).  Under force_cool demote it
+				 * HOT -> COOL -- the cooling tick, which IS progress.
+				 */
+				if (!force_cool)
+				{
+					no_progress = true;
+					break;			/* advance the hand, look for COOL */
+				}
+
+				local_buf_state &= ~BUF_USAGECOUNT_MASK;	/* HOT -> COOL, clear ref bit */
 
 				if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
 												   local_buf_state))
 				{
+					/*
+					 * Cooling a buffer is progress toward a victim, so reset the
+					 * counter and drop back out of force_cool.  Because we clear
+					 * force_cool after a single demotion, an all-HOT pool can take
+					 * up to ~2 full passes to yield a victim (one to discover no
+					 * COOL buffer exists, one to cool + reclaim).  That is still
+					 * within the stock clock's worst case (up to
+					 * BM_MAX_USAGE_COUNT+1 passes), and in practice the bgwriter
+					 * pre-cooling keeps a COOL victim available so force_cool
+					 * rarely fires at all.
+					 */
 					trycounter = NBuffers;
+					force_cool = false;
 					break;
 				}
 			}
 			else
 			{
-				/* pin the buffer if the CAS succeeds */
+				/* COOL and unpinned: claim it.  Pin if the CAS succeeds. */
 				local_buf_state += BUF_REFCOUNT_ONE;
 
 				if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
@@ -338,6 +366,21 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 				}
 			}
 		}
+
+		/*
+		 * A tick that made no progress toward a victim counts down trycounter.
+		 * A full unproductive pass escalates to force_cool (cool HOT buffers
+		 * into victims); a second unproductive full pass means everything is
+		 * pinned, so fail rather than spin forever.  (A failed CAS above is
+		 * neither progress nor a full miss: we simply retry the same buffer.)
+		 */
+		if (no_progress && --trycounter == 0)
+		{
+			if (force_cool)
+				elog(ERROR, "no unpinned buffers available");
+			force_cool = true;
+			trycounter = NBuffers;
+		}
 	}
 }
 
@@ -452,10 +495,10 @@ StrategyCtlShmemInit(void *arg)
 	 */
 	if (pg_numa_init() != -1 &&
 		pg_numa_get_max_node() >= 1)
-		ClockSweepBatchSize = Min(PG_CACHE_LINE_SIZE / (uint32) sizeof(uint32),
-								  (uint32) NBuffers);
+		StrategyControl->batchSize = Min(PG_CACHE_LINE_SIZE / (uint32) sizeof(uint32),
+										 (uint32) NBuffers);
 	else
-		ClockSweepBatchSize = 1;
+		StrategyControl->batchSize = 1;
 }
 
 
@@ -703,14 +746,13 @@ GetBufferFromRing(BufferAccessStrategy strategy, uint64 *buf_state)
 		/*
 		 * If the buffer is pinned we cannot use it under any circumstances.
 		 *
-		 * If usage_count is 0 or 1 then the buffer is fair game (we expect 1,
-		 * since our own previous usage of the ring element would have left it
-		 * there, but it might've been decremented by clock-sweep since then).
-		 * A higher usage_count indicates someone else has touched the buffer,
-		 * so we shouldn't re-use it.
+		 * With the cooling-state replacement the field holds only COOL or HOT,
+		 * so the stock "usage_count > 1 means another backend touched it"
+		 * heuristic no longer applies: a ring element is reusable whenever it
+		 * is unpinned.  (The whole ring mechanism is removed in a later patch;
+		 * scan resistance is now intrinsic to the sweep.)
 		 */
-		if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0
-			|| BUF_STATE_GET_USAGECOUNT(local_buf_state) > 1)
+		if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0)
 			break;
 
 		/* See equivalent code in PinBuffer() */
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 4870c8e13d0..21c08091c49 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -167,7 +167,7 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
 
 		buf_state = pg_atomic_read_u64(&bufHdr->state);
 		buf_state &= ~(BUF_FLAG_MASK | BUF_USAGECOUNT_MASK);
-		buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
+		buf_state |= BM_TAG_VALID;	/* admit COOL (probation) */
 		pg_atomic_unlocked_write_u64(&bufHdr->state, buf_state);
 
 		*foundPtr = false;
@@ -248,9 +248,10 @@ GetLocalVictimBuffer(void)
 		{
 			uint64		buf_state = pg_atomic_read_u64(&bufHdr->state);
 
-			if (BUF_STATE_GET_USAGECOUNT(buf_state) > 0)
+			if (BUF_STATE_GET_COOLSTATE(buf_state) != BUF_COOLSTATE_COOL)
 			{
-				buf_state -= BUF_USAGECOUNT_ONE;
+				/* HOT: give it a second chance, cool it and keep scanning. */
+				buf_state -= BUF_COOLSTATE_ONE;
 				pg_atomic_unlocked_write_u64(&bufHdr->state, buf_state);
 				trycounter = NLocBuffer;
 			}
@@ -454,7 +455,7 @@ ExtendBufferedRelLocal(BufferManagerRelation bmr,
 
 			victim_buf_hdr->tag = tag;
 
-			buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
+			buf_state |= BM_TAG_VALID;	/* admit COOL (probation) */
 
 			pg_atomic_unlocked_write_u64(&victim_buf_hdr->state, buf_state);
 
@@ -839,9 +840,9 @@ PinLocalBuffer(BufferDesc *buf_hdr, bool adjust_usagecount)
 		NLocalPinnedBuffers++;
 		buf_state += BUF_REFCOUNT_ONE;
 		if (adjust_usagecount &&
-			BUF_STATE_GET_USAGECOUNT(buf_state) < BM_MAX_USAGE_COUNT)
+			BUF_STATE_GET_COOLSTATE(buf_state) < BUF_COOLSTATE_HOT)
 		{
-			buf_state += BUF_USAGECOUNT_ONE;
+			buf_state += BUF_COOLSTATE_ONE;
 		}
 		pg_atomic_unlocked_write_u64(&buf_hdr->state, buf_state);
 
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index e4ff5619b79..131df126cda 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -67,6 +67,50 @@ StaticAssertDecl(BUF_REFCOUNT_BITS + BUF_USAGECOUNT_BITS + BUF_FLAG_BITS + BUF_L
 #define BUF_USAGECOUNT_ONE \
 	(UINT64CONST(1) << BUF_REFCOUNT_BITS)
 
+/*
+ * Cooling state (LeanStore / 2Q-A1 cooling-stage clock sweep).
+ *
+ * The field historically used for the 0..5 usage_count now holds a single
+ * cooling-state bit: HOT (recently accessed, not an eviction candidate) or
+ * COOL (an eviction candidate).  We reuse BUF_USAGECOUNT_ONE as the unit so
+ * the buffer-state bit geography -- refcount, flag, and lock offsets, and the
+ * 64-bit StaticAsserts -- is unchanged; only the meaning of the field and the
+ * instructions that touch it change.
+ *
+ * A demand-loaded page is admitted COOL (probation); a second access promotes
+ * it to HOT (the rescue).  The sweep prefers COOL victims and demotes HOT to
+ * COOL only when a full pass finds no COOL victim.  So a one-touch scan fills
+ * and drains the COOL stage without displacing the HOT working set -- scan
+ * resistance intrinsic to the replacement algorithm.
+ */
+#define BUF_COOLSTATE_COOL	0
+#define BUF_COOLSTATE_HOT	1
+#define BUF_COOLSTATE_ONE	BUF_USAGECOUNT_ONE
+
+/*
+ * Second-chance reference bit, bit 1 of the (former usagecount) field, one
+ * position above the cooling-state bit.  PinBuffer sets it on every access.
+ * The bgwriter's pre-cooling gives a HOT buffer a second chance: the first
+ * time it passes a HOT buffer whose ref bit is set, it clears the ref bit and
+ * leaves the buffer HOT; only a HOT buffer whose ref bit is already clear (not
+ * re-accessed since the previous bgwriter pass) is demoted to COOL.  This
+ * keeps genuinely-hot pages out of the COOL stage (protecting the working set
+ * from being cooled under scan pressure) while leaving the foreground sweep a
+ * single-pass search over the pre-staged COOL buffers.  A separate bit (not a
+ * count) so it stays a plain masked store under the header lock.
+ */
+#define BUF_REFBIT			(UINT64CONST(2) << BUF_REFCOUNT_BITS)
+#define BUF_STATE_GET_REFBIT(state)	(((state) & BUF_REFBIT) != 0)
+
+/*
+ * The cooling state occupies bit 0 and the reference bit occupies bit 1 of
+ * the (former usagecount) field, so the field must be at least 2 bits wide.
+ * Assert it here so a future change to BUF_USAGECOUNT_BITS cannot silently
+ * push BUF_REFBIT up into the flag bits.
+ */
+StaticAssertDecl(BUF_USAGECOUNT_BITS >= 2,
+				 "cooling state + reference bit need at least 2 bits in the usagecount field");
+
 /* flags related definitions */
 #define BUF_FLAG_SHIFT \
 	(BUF_REFCOUNT_BITS + BUF_USAGECOUNT_BITS)
@@ -92,6 +136,11 @@ StaticAssertDecl(BUF_REFCOUNT_BITS + BUF_USAGECOUNT_BITS + BUF_FLAG_BITS + BUF_L
 #define BUF_STATE_GET_USAGECOUNT(state) \
 	((uint32)(((state) & BUF_USAGECOUNT_MASK) >> BUF_USAGECOUNT_SHIFT))
 
+/* Cooling state (HOT/COOL) from buffer state -- bit 0 of the field only, so
+ * the second-chance ref bit (bit 1) does not perturb the HOT/COOL test. */
+#define BUF_STATE_GET_COOLSTATE(state) \
+	((uint32) (((state) >> BUF_USAGECOUNT_SHIFT) & 1))
+
 /*
  * Flags for buffer descriptors
  *
@@ -134,17 +183,15 @@ StaticAssertDecl(MAX_BACKENDS_BITS <= (BUF_LOCK_BITS - 2),
 
 
 /*
- * The maximum allowed value of usage_count represents a tradeoff between
- * accuracy and speed of the clock-sweep buffer management algorithm.  A
- * large value (comparable to NBuffers) would approximate LRU semantics.
- * But it can take as many as BM_MAX_USAGE_COUNT+1 complete cycles of the
- * clock-sweep hand to find a free buffer, so in practice we don't want the
- * value to be very large.
+ * The cooling state is a single bit (HOT/COOL); the maximum value stored in
+ * the field is therefore BUF_COOLSTATE_HOT.  Retained under the historical
+ * name BM_MAX_USAGE_COUNT so the pin fast path ("promote unless already at
+ * max") reads naturally.
  */
-#define BM_MAX_USAGE_COUNT	5
+#define BM_MAX_USAGE_COUNT	BUF_COOLSTATE_HOT
 
 StaticAssertDecl(BM_MAX_USAGE_COUNT < (UINT64CONST(1) << BUF_USAGECOUNT_BITS),
-				 "BM_MAX_USAGE_COUNT doesn't fit in BUF_USAGECOUNT_BITS bits");
+				 "cooling state doesn't fit in BUF_USAGECOUNT_BITS bits");
 
 /*
  * Buffer tag identifies which disk block the buffer contains.
-- 
2.51.2



  [text/x-patch] v5-0003-Remove-BufferAccessStrategy-scan-resistance-is-no.patch (177.4K, ../../9cfdf3f2-b799-485a-8b5a-5159c00ff2df@app.fastmail.com/4-v5-0003-Remove-BufferAccessStrategy-scan-resistance-is-no.patch)
  download | inline diff:
From d656fe3e321a6896b834a3c2d8d7ce3c0abbce34 Mon Sep 17 00:00:00 2001
From: Greg Burd <greg@burd.me>
Date: Tue, 7 Jul 2026 08:45:39 -0400
Subject: [PATCH v5 3/3] Remove BufferAccessStrategy; scan resistance is now
 intrinsic

The cooling-stage evictor admits demand-loaded pages COOL and promotes them to
HOT only on a second access, so a one-touch sequential scan fills and drains
the COOL stage without displacing the hot working set.  Scan resistance is
therefore a property of the replacement algorithm itself, and the
BufferAccessStrategy ring buffers that previously provided it are dead weight.
Remove them end to end.

Deleted:
  - the BufferAccessStrategy type and the BufferAccessStrategyType enum
    (BAS_NORMAL/BULKREAD/BULKWRITE/VACUUM);
  - the ring machinery in freelist.c (GetAccessStrategy[WithSize],
    GetAccessStrategyBufferCount, GetAccessStrategyPinLimit,
    FreeAccessStrategy, GetBufferFromRing, AddBufferToRing,
    StrategyRejectBuffer, IOContextForStrategy);
  - the strategy parameter from ReadBufferExtended, ReadBufferWithoutRelcache,
    the ExtendBufferedRel* family, StrategyGetBuffer, read_stream_begin_*,
    and every scan/vacuum/analyze/index-AM caller;
  - the strategy fields on HeapScanDescData, IndexScanDescData,
    BulkInsertStateData, ReadBuffersOperation, and ReadStream;
  - _hash_getbuf_with_strategy (identical to _hash_getbuf without a strategy).

pg_stat_io's per-strategy IO contexts collapse: IOCONTEXT_BULKREAD,
IOCONTEXT_BULKWRITE and IOCONTEXT_VACUUM are removed, leaving IOCONTEXT_INIT
and IOCONTEXT_NORMAL.  IOOP_REUSE only ever occurred while recycling a ring
buffer, so it is no longer tracked; GetVictimBuffer counts IOOP_EVICT only.

The vacuum_buffer_usage_limit GUC and the VACUUM/ANALYZE (BUFFER_USAGE_LIMIT
...) option are removed, along with the VacuumBufferUsageLimit global, the
ring-size plumbing through VacuumParams and parallel vacuum, and vacuumdb's
--buffer-usage-limit client option.  read_stream's per-backend pin budget is
now enforced solely by GetPinLimit()/GetLocalPinLimit(), which already applied
and is unchanged for the (formerly universal) no-strategy case.

Documentation and the stats/amcheck regression tests are updated to drop the
removed contexts and options.
---
 contrib/amcheck/expected/check_heap.out       |  16 +-
 contrib/amcheck/sql/check_heap.sql            |  16 +-
 contrib/amcheck/verify_gin.c                  |  18 +-
 contrib/amcheck/verify_heapam.c               |   3 -
 contrib/amcheck/verify_nbtree.c               |  11 +-
 contrib/bloom/blscan.c                        |   8 -
 contrib/bloom/blutils.c                       |   4 +-
 contrib/bloom/blvacuum.c                      |   2 -
 contrib/pageinspect/rawpage.c                 |   2 +-
 contrib/pg_prewarm/autoprewarm.c              |   1 -
 contrib/pg_prewarm/pg_prewarm.c               |   1 -
 contrib/pg_visibility/pg_visibility.c         |   7 +-
 contrib/pgstattuple/pgstatapprox.c            |   3 -
 contrib/pgstattuple/pgstatindex.c             |   9 +-
 contrib/pgstattuple/pgstattuple.c             |  37 +-
 doc/src/sgml/config.sgml                      |  30 --
 doc/src/sgml/monitoring.sgml                  |  38 +-
 doc/src/sgml/ref/analyze.sgml                 |  21 -
 doc/src/sgml/ref/vacuum.sgml                  |  25 -
 src/backend/access/brin/brin.c                |  11 +-
 src/backend/access/brin/brin_revmap.c         |   2 +-
 src/backend/access/gin/gininsert.c            |   4 +-
 src/backend/access/gin/ginutil.c              |   2 +-
 src/backend/access/gin/ginvacuum.c            |  17 +-
 src/backend/access/gist/gist.c                |   2 +-
 src/backend/access/gist/gistutil.c            |   2 +-
 src/backend/access/gist/gistvacuum.c          |  10 +-
 src/backend/access/hash/hash.c                |  13 +-
 src/backend/access/hash/hashovfl.c            |  55 +--
 src/backend/access/hash/hashpage.c            |  40 +-
 src/backend/access/heap/heapam.c              |  31 +-
 src/backend/access/heap/heapam_handler.c      |   4 +-
 src/backend/access/heap/hio.c                 |  13 +-
 src/backend/access/heap/vacuumlazy.c          |  22 +-
 src/backend/access/heap/visibilitymap.c       |   4 +-
 src/backend/access/nbtree/nbtpage.c           |   2 +-
 src/backend/access/nbtree/nbtree.c            |   4 +-
 src/backend/access/spgist/spgutils.c          |   2 +-
 src/backend/access/spgist/spgvacuum.c         |   4 +-
 src/backend/access/transam/xloginsert.c       |   2 +-
 src/backend/access/transam/xlogutils.c        |   3 +-
 src/backend/catalog/index.c                   |   1 -
 src/backend/commands/analyze.c                |   7 +-
 src/backend/commands/dbcommands.c             |   6 +-
 src/backend/commands/repack.c                 |   2 +-
 src/backend/commands/sequence.c               |   2 +-
 src/backend/commands/vacuum.c                 | 118 +----
 src/backend/commands/vacuumparallel.c         |  21 +-
 src/backend/postmaster/autovacuum.c           |  29 +-
 src/backend/postmaster/datachecksum_state.c   |  33 +-
 src/backend/storage/aio/read_stream.c         |  17 +-
 src/backend/storage/buffer/README             |  44 --
 src/backend/storage/buffer/bufmgr.c           | 185 +++-----
 src/backend/storage/buffer/freelist.c         | 434 +-----------------
 src/backend/storage/freespace/freespace.c     |   4 +-
 src/backend/storage/smgr/md.c                 |  11 +-
 src/backend/utils/activity/pgstat_io.c        |  45 +-
 src/backend/utils/init/globals.c              |   1 -
 src/backend/utils/misc/guc_parameters.dat     |  10 -
 src/backend/utils/misc/postgresql.conf.sample |   3 -
 src/bin/scripts/vacuumdb.c                    |  13 -
 src/bin/scripts/vacuuming.c                   |  21 -
 src/bin/scripts/vacuuming.h                   |   1 -
 src/include/access/genam.h                    |   1 -
 src/include/access/hash.h                     |   9 +-
 src/include/access/heapam.h                   |   4 +-
 src/include/access/hio.h                      |   1 -
 src/include/access/tableam.h                  |   8 +-
 src/include/commands/vacuum.h                 |   8 +-
 src/include/miscadmin.h                       |   9 -
 src/include/pgstat.h                          |   5 +-
 src/include/storage/buf.h                     |   7 -
 src/include/storage/buf_internals.h           |   6 +-
 src/include/storage/bufmgr.h                  |  32 +-
 src/include/storage/read_stream.h             |   5 +-
 src/include/utils/guc_hooks.h                 |   2 -
 src/test/modules/test_aio/test_aio.c          |   7 +-
 src/test/regress/expected/stats.out           | 120 +----
 src/test/regress/expected/vacuum.out          |  19 -
 src/test/regress/sql/stats.sql                |  42 --
 src/test/regress/sql/vacuum.sql               |  15 -
 src/tools/pgindent/typedefs.list              |   2 -
 82 files changed, 252 insertions(+), 1559 deletions(-)

diff --git a/contrib/amcheck/expected/check_heap.out b/contrib/amcheck/expected/check_heap.out
index 979e5e84e72..569b0202f1c 100644
--- a/contrib/amcheck/expected/check_heap.out
+++ b/contrib/amcheck/expected/check_heap.out
@@ -67,11 +67,11 @@ INSERT INTO heaptest (a, b)
 	(SELECT gs, repeat('x', gs)
 		FROM generate_series(1,50) gs);
 -- pg_stat_io test:
--- verify_heapam always uses a BAS_BULKREAD BufferAccessStrategy, whereas a
--- sequential scan does so only if the table is large enough when compared to
--- shared buffers (see initscan()). CREATE DATABASE ... also unconditionally
--- uses a BAS_BULKREAD strategy, but we have chosen to use a tablespace and
--- verify_heapam to provide coverage instead of adding another expensive
+-- verify_heapam reads the heap through the buffer manager; with the
+-- cooling-stage clock sweep there are no per-strategy IO contexts, so the
+-- reads are counted in the 'normal' context.  CREATE DATABASE ... likewise
+-- reads through the normal context, but we have chosen to use a tablespace
+-- and verify_heapam to provide coverage instead of adding another expensive
 -- operation to the main regression test suite.
 --
 -- Create an alternative tablespace and move the heaptest table to it, causing
@@ -83,7 +83,7 @@ INSERT INTO heaptest (a, b)
 SET allow_in_place_tablespaces = true;
 CREATE TABLESPACE regress_test_stats_tblspc LOCATION '';
 SELECT sum(reads) AS stats_bulkreads_before
-  FROM pg_stat_io WHERE context = 'bulkread' \gset
+  FROM pg_stat_io WHERE context = 'normal' AND object = 'relation' \gset
 BEGIN;
 ALTER TABLE heaptest SET TABLESPACE regress_test_stats_tblspc;
 -- Check that valid options are not rejected nor corruption reported
@@ -111,7 +111,7 @@ SELECT * FROM verify_heapam(relation := 'heaptest', startblock := 0, endblock :=
 COMMIT;
 -- verify_heapam should have read in the page written out by
 --   ALTER TABLE ... SET TABLESPACE ...
--- causing an additional bulkread, which should be reflected in pg_stat_io.
+-- causing additional reads, which should be reflected in pg_stat_io.
 SELECT pg_stat_force_next_flush();
  pg_stat_force_next_flush 
 --------------------------
@@ -119,7 +119,7 @@ SELECT pg_stat_force_next_flush();
 (1 row)
 
 SELECT sum(reads) AS stats_bulkreads_after
-  FROM pg_stat_io WHERE context = 'bulkread' \gset
+  FROM pg_stat_io WHERE context = 'normal' AND object = 'relation' \gset
 SELECT :stats_bulkreads_after > :stats_bulkreads_before;
  ?column? 
 ----------
diff --git a/contrib/amcheck/sql/check_heap.sql b/contrib/amcheck/sql/check_heap.sql
index 1745bae634e..61f710dbeda 100644
--- a/contrib/amcheck/sql/check_heap.sql
+++ b/contrib/amcheck/sql/check_heap.sql
@@ -27,11 +27,11 @@ INSERT INTO heaptest (a, b)
 		FROM generate_series(1,50) gs);
 
 -- pg_stat_io test:
--- verify_heapam always uses a BAS_BULKREAD BufferAccessStrategy, whereas a
--- sequential scan does so only if the table is large enough when compared to
--- shared buffers (see initscan()). CREATE DATABASE ... also unconditionally
--- uses a BAS_BULKREAD strategy, but we have chosen to use a tablespace and
--- verify_heapam to provide coverage instead of adding another expensive
+-- verify_heapam reads the heap through the buffer manager; with the
+-- cooling-stage clock sweep there are no per-strategy IO contexts, so the
+-- reads are counted in the 'normal' context.  CREATE DATABASE ... likewise
+-- reads through the normal context, but we have chosen to use a tablespace
+-- and verify_heapam to provide coverage instead of adding another expensive
 -- operation to the main regression test suite.
 --
 -- Create an alternative tablespace and move the heaptest table to it, causing
@@ -43,7 +43,7 @@ INSERT INTO heaptest (a, b)
 SET allow_in_place_tablespaces = true;
 CREATE TABLESPACE regress_test_stats_tblspc LOCATION '';
 SELECT sum(reads) AS stats_bulkreads_before
-  FROM pg_stat_io WHERE context = 'bulkread' \gset
+  FROM pg_stat_io WHERE context = 'normal' AND object = 'relation' \gset
 BEGIN;
 ALTER TABLE heaptest SET TABLESPACE regress_test_stats_tblspc;
 -- Check that valid options are not rejected nor corruption reported
@@ -56,10 +56,10 @@ COMMIT;
 
 -- verify_heapam should have read in the page written out by
 --   ALTER TABLE ... SET TABLESPACE ...
--- causing an additional bulkread, which should be reflected in pg_stat_io.
+-- causing additional reads, which should be reflected in pg_stat_io.
 SELECT pg_stat_force_next_flush();
 SELECT sum(reads) AS stats_bulkreads_after
-  FROM pg_stat_io WHERE context = 'bulkread' \gset
+  FROM pg_stat_io WHERE context = 'normal' AND object = 'relation' \gset
 SELECT :stats_bulkreads_after > :stats_bulkreads_before;
 
 CREATE ROLE regress_heaptest_role;
diff --git a/contrib/amcheck/verify_gin.c b/contrib/amcheck/verify_gin.c
index fa06689ed5b..ef2e64475c6 100644
--- a/contrib/amcheck/verify_gin.c
+++ b/contrib/amcheck/verify_gin.c
@@ -63,8 +63,7 @@ static void gin_check_parent_keys_consistency(Relation rel,
 static void check_index_page(Relation rel, Buffer buffer, BlockNumber blockNo);
 static IndexTuple gin_refind_parent(Relation rel,
 									BlockNumber parentblkno,
-									BlockNumber childblkno,
-									BufferAccessStrategy strategy);
+									BlockNumber childblkno);
 static ItemId PageGetItemIdCareful(Relation rel, BlockNumber block, Page page,
 								   OffsetNumber offset);
 
@@ -133,7 +132,6 @@ ginReadTupleWithoutState(IndexTuple itup, int *nitems)
 static void
 gin_check_posting_tree_parent_keys_consistency(Relation rel, BlockNumber posting_tree_root)
 {
-	BufferAccessStrategy strategy = GetAccessStrategy(BAS_BULKREAD);
 	GinPostingTreeScanItem *stack;
 	MemoryContext mctx;
 	MemoryContext oldcontext;
@@ -171,8 +169,7 @@ gin_check_posting_tree_parent_keys_consistency(Relation rel, BlockNumber posting
 
 		CHECK_FOR_INTERRUPTS();
 
-		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno,
-									RBM_NORMAL, strategy);
+		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno, RBM_NORMAL);
 		LockBuffer(buffer, GIN_SHARE);
 		page = BufferGetPage(buffer);
 
@@ -391,7 +388,6 @@ gin_check_parent_keys_consistency(Relation rel,
 								  void *callback_state,
 								  bool readonly)
 {
-	BufferAccessStrategy strategy = GetAccessStrategy(BAS_BULKREAD);
 	GinScanItem *stack;
 	MemoryContext mctx;
 	MemoryContext oldcontext;
@@ -430,8 +426,7 @@ gin_check_parent_keys_consistency(Relation rel,
 
 		CHECK_FOR_INTERRUPTS();
 
-		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno,
-									RBM_NORMAL, strategy);
+		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno, RBM_NORMAL);
 		LockBuffer(buffer, GIN_SHARE);
 		page = BufferGetPage(buffer);
 		maxoff = PageGetMaxOffsetNumber(page);
@@ -567,7 +562,7 @@ gin_check_parent_keys_consistency(Relation rel,
 					 */
 					pfree(stack->parenttup);
 					stack->parenttup = gin_refind_parent(rel, stack->parentblk,
-														 stack->blkno, strategy);
+														 stack->blkno);
 
 					/* We found it - make a final check before failing */
 					if (!stack->parenttup)
@@ -719,7 +714,7 @@ check_index_page(Relation rel, Buffer buffer, BlockNumber blockNo)
  */
 static IndexTuple
 gin_refind_parent(Relation rel, BlockNumber parentblkno,
-				  BlockNumber childblkno, BufferAccessStrategy strategy)
+				  BlockNumber childblkno)
 {
 	Buffer		parentbuf;
 	Page		parentpage;
@@ -727,8 +722,7 @@ gin_refind_parent(Relation rel, BlockNumber parentblkno,
 				parent_maxoff;
 	IndexTuple	result = NULL;
 
-	parentbuf = ReadBufferExtended(rel, MAIN_FORKNUM, parentblkno, RBM_NORMAL,
-								   strategy);
+	parentbuf = ReadBufferExtended(rel, MAIN_FORKNUM, parentblkno, RBM_NORMAL);
 
 	LockBuffer(parentbuf, GIN_SHARE);
 	parentpage = BufferGetPage(parentbuf);
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 20ff58aa782..e4336eb5fc2 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -126,7 +126,6 @@ typedef struct HeapCheckContext
 	 * recent block in the buffer yielded by the read stream API.
 	 */
 	BlockNumber blkno;
-	BufferAccessStrategy bstrategy;
 	Buffer		buffer;
 	Page		page;
 
@@ -374,7 +373,6 @@ verify_heapam(PG_FUNCTION_ARGS)
 		PG_RETURN_NULL();
 	}
 
-	ctx.bstrategy = GetAccessStrategy(BAS_BULKREAD);
 	ctx.buffer = InvalidBuffer;
 	ctx.page = NULL;
 
@@ -472,7 +470,6 @@ verify_heapam(PG_FUNCTION_ARGS)
 	}
 
 	stream = read_stream_begin_relation(stream_flags,
-										ctx.bstrategy,
 										ctx.rel,
 										MAIN_FORKNUM,
 										stream_cb,
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 3ef2d66f826..18cd8928fdd 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -88,8 +88,6 @@ typedef struct BtreeCheckState
 	bool		checkunique;
 	/* Per-page context */
 	MemoryContext targetcontext;
-	/* Buffer access strategy */
-	BufferAccessStrategy checkstrategy;
 
 	/*
 	 * Info for uniqueness checking. Fill this field and the one below once
@@ -490,7 +488,6 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	state->targetcontext = AllocSetContextCreate(CurrentMemoryContext,
 												 "amcheck context",
 												 ALLOCSET_DEFAULT_SIZES);
-	state->checkstrategy = GetAccessStrategy(BAS_BULKREAD);
 
 	/* Get true root block from meta-page */
 	metapage = palloc_btree_page(state, BTREE_METAPAGE);
@@ -1115,7 +1112,7 @@ bt_recheck_sibling_links(BtreeCheckState *state,
 
 		/* Couple locks in the usual order for nbtree:  Left to right */
 		lbuf = ReadBufferExtended(state->rel, MAIN_FORKNUM, leftcurrent,
-								  RBM_NORMAL, state->checkstrategy);
+								  RBM_NORMAL);
 		LockBuffer(lbuf, BT_READ);
 		_bt_checkpage(state->rel, lbuf);
 		page = BufferGetPage(lbuf);
@@ -1138,8 +1135,7 @@ bt_recheck_sibling_links(BtreeCheckState *state,
 		if (newtargetblock != leftcurrent)
 		{
 			newtargetbuf = ReadBufferExtended(state->rel, MAIN_FORKNUM,
-											  newtargetblock, RBM_NORMAL,
-											  state->checkstrategy);
+											  newtargetblock, RBM_NORMAL);
 			LockBuffer(newtargetbuf, BT_READ);
 			_bt_checkpage(state->rel, newtargetbuf);
 			page = BufferGetPage(newtargetbuf);
@@ -3300,8 +3296,7 @@ palloc_btree_page(BtreeCheckState *state, BlockNumber blocknum)
 	 * We copy the page into local storage to avoid holding pin on the buffer
 	 * longer than we must.
 	 */
-	buffer = ReadBufferExtended(state->rel, MAIN_FORKNUM, blocknum, RBM_NORMAL,
-								state->checkstrategy);
+	buffer = ReadBufferExtended(state->rel, MAIN_FORKNUM, blocknum, RBM_NORMAL);
 	LockBuffer(buffer, BT_READ);
 
 	/*
diff --git a/contrib/bloom/blscan.c b/contrib/bloom/blscan.c
index 1a0e42021ec..ed62d5c04e6 100644
--- a/contrib/bloom/blscan.c
+++ b/contrib/bloom/blscan.c
@@ -80,7 +80,6 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BlockNumber blkno,
 				npages;
 	int			i;
-	BufferAccessStrategy bas;
 	BloomScanOpaque so = (BloomScanOpaque) scan->opaque;
 	BlockRangeReadStreamPrivate p;
 	ReadStream *stream;
@@ -113,11 +112,6 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		}
 	}
 
-	/*
-	 * We're going to read the whole index. This is why we use appropriate
-	 * buffer access strategy.
-	 */
-	bas = GetAccessStrategy(BAS_BULKREAD);
 	npages = RelationGetNumberOfBlocks(scan->indexRelation);
 	pgstat_count_index_scan(scan->indexRelation);
 	if (scan->instrument)
@@ -133,7 +127,6 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										bas,
 										scan->indexRelation,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -184,7 +177,6 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	Assert(read_stream_next_buffer(stream, NULL) == InvalidBuffer);
 	read_stream_end(stream);
-	FreeAccessStrategy(bas);
 
 	return ntids;
 }
diff --git a/contrib/bloom/blutils.c b/contrib/bloom/blutils.c
index 5111cdc6dd6..6c1ec15c59c 100644
--- a/contrib/bloom/blutils.c
+++ b/contrib/bloom/blutils.c
@@ -392,7 +392,7 @@ BloomNewBuffer(Relation index)
 	}
 
 	/* Must extend the file */
-	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM,
 							   EB_LOCK_FIRST);
 
 	return buffer;
@@ -460,7 +460,7 @@ BloomInitMetapage(Relation index, ForkNumber forknum)
 	 * block number 0 (BLOOM_METAPAGE_BLKNO).  No need to hold the extension
 	 * lock because there cannot be concurrent inserters yet.
 	 */
-	metaBuffer = ReadBufferExtended(index, forknum, P_NEW, RBM_NORMAL, NULL);
+	metaBuffer = ReadBufferExtended(index, forknum, P_NEW, RBM_NORMAL);
 	LockBuffer(metaBuffer, BUFFER_LOCK_EXCLUSIVE);
 	Assert(BufferGetBlockNumber(metaBuffer) == BLOOM_METAPAGE_BLKNO);
 
diff --git a/contrib/bloom/blvacuum.c b/contrib/bloom/blvacuum.c
index 6beb1c20ebb..08d7705e365 100644
--- a/contrib/bloom/blvacuum.c
+++ b/contrib/bloom/blvacuum.c
@@ -66,7 +66,6 @@ blbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										index,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -219,7 +218,6 @@ blvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										index,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/contrib/pageinspect/rawpage.c b/contrib/pageinspect/rawpage.c
index d136593edb2..bbe8edf9161 100644
--- a/contrib/pageinspect/rawpage.c
+++ b/contrib/pageinspect/rawpage.c
@@ -188,7 +188,7 @@ get_raw_page_internal(text *relname, ForkNumber forknum, BlockNumber blkno)
 
 	/* Take a verbatim copy of the page */
 
-	buf = ReadBufferExtended(rel, forknum, blkno, RBM_NORMAL, NULL);
+	buf = ReadBufferExtended(rel, forknum, blkno, RBM_NORMAL);
 	LockBuffer(buf, BUFFER_LOCK_SHARE);
 
 	memcpy(raw_page_data, BufferGetPage(buf), BLCKSZ);
diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index deb4c2671b5..33ad88ea8b1 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -630,7 +630,6 @@ autoprewarm_database_main(Datum main_arg)
 			stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 												READ_STREAM_DEFAULT |
 												READ_STREAM_USE_BATCHING,
-												NULL,
 												rel,
 												p.forknum,
 												apw_read_stream_next_block,
diff --git a/contrib/pg_prewarm/pg_prewarm.c b/contrib/pg_prewarm/pg_prewarm.c
index c2716086693..716a6754a7e 100644
--- a/contrib/pg_prewarm/pg_prewarm.c
+++ b/contrib/pg_prewarm/pg_prewarm.c
@@ -251,7 +251,6 @@ pg_prewarm(PG_FUNCTION_ARGS)
 		stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 											READ_STREAM_FULL |
 											READ_STREAM_USE_BATCHING,
-											NULL,
 											rel,
 											forkNumber,
 											block_range_read_stream_cb,
diff --git a/contrib/pg_visibility/pg_visibility.c b/contrib/pg_visibility/pg_visibility.c
index dfab0b64cf5..ccb829a09eb 100644
--- a/contrib/pg_visibility/pg_visibility.c
+++ b/contrib/pg_visibility/pg_visibility.c
@@ -488,7 +488,6 @@ collect_visibility_data(Oid relid, bool include_pd)
 	vbits	   *info;
 	BlockNumber blkno;
 	Buffer		vmbuffer = InvalidBuffer;
-	BufferAccessStrategy bstrategy = GetAccessStrategy(BAS_BULKREAD);
 	BlockRangeReadStreamPrivate p;
 	ReadStream *stream = NULL;
 
@@ -514,7 +513,6 @@ collect_visibility_data(Oid relid, bool include_pd)
 		 */
 		stream = read_stream_begin_relation(READ_STREAM_FULL |
 											READ_STREAM_USE_BATCHING,
-											bstrategy,
 											rel,
 											MAIN_FORKNUM,
 											block_range_read_stream_cb,
@@ -538,8 +536,7 @@ collect_visibility_data(Oid relid, bool include_pd)
 
 		/*
 		 * Page-level data requires reading every block, so only get it if the
-		 * caller needs it.  Use a buffer access strategy, too, to prevent
-		 * cache-trashing.
+		 * caller needs it.
 		 */
 		if (include_pd)
 		{
@@ -700,7 +697,6 @@ collect_corrupt_items(Oid relid, bool all_visible, bool all_frozen)
 	Relation	rel;
 	corrupt_items *items;
 	Buffer		vmbuffer = InvalidBuffer;
-	BufferAccessStrategy bstrategy = GetAccessStrategy(BAS_BULKREAD);
 	TransactionId OldestXmin = InvalidTransactionId;
 	struct collect_corrupt_items_read_stream_private p;
 	ReadStream *stream;
@@ -734,7 +730,6 @@ collect_corrupt_items(Oid relid, bool all_visible, bool all_frozen)
 	p.all_frozen = all_frozen;
 	p.all_visible = all_visible;
 	stream = read_stream_begin_relation(READ_STREAM_FULL,
-										bstrategy,
 										rel,
 										MAIN_FORKNUM,
 										collect_corrupt_items_read_stream_next_block,
diff --git a/contrib/pgstattuple/pgstatapprox.c b/contrib/pgstattuple/pgstatapprox.c
index 21e0b50fb4b..8e17d48991e 100644
--- a/contrib/pgstattuple/pgstatapprox.c
+++ b/contrib/pgstattuple/pgstatapprox.c
@@ -116,13 +116,11 @@ static void
 statapprox_heap(Relation rel, output_type *stat)
 {
 	BlockNumber nblocks;
-	BufferAccessStrategy bstrategy;
 	TransactionId OldestXmin;
 	StatApproxReadStreamPrivate p;
 	ReadStream *stream;
 
 	OldestXmin = GetOldestNonRemovableTransactionId(rel);
-	bstrategy = GetAccessStrategy(BAS_BULKREAD);
 
 	nblocks = RelationGetNumberOfBlocks(rel);
 
@@ -141,7 +139,6 @@ statapprox_heap(Relation rel, output_type *stat)
 	 * caution.
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_FULL,
-										bstrategy,
 										rel,
 										MAIN_FORKNUM,
 										statapprox_heap_read_stream_next,
diff --git a/contrib/pgstattuple/pgstatindex.c b/contrib/pgstattuple/pgstatindex.c
index 8951ad0aac4..a0922b386a0 100644
--- a/contrib/pgstattuple/pgstatindex.c
+++ b/contrib/pgstattuple/pgstatindex.c
@@ -217,7 +217,6 @@ pgstatindex_impl(Relation rel, FunctionCallInfo fcinfo)
 	BlockNumber nblocks;
 	BlockNumber blkno;
 	BTIndexStat indexStat;
-	BufferAccessStrategy bstrategy = GetAccessStrategy(BAS_BULKREAD);
 	BlockRangeReadStreamPrivate p;
 	ReadStream *stream;
 	BlockNumber startblk;
@@ -254,7 +253,7 @@ pgstatindex_impl(Relation rel, FunctionCallInfo fcinfo)
 	 * Read metapage
 	 */
 	{
-		Buffer		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, 0, RBM_NORMAL, bstrategy);
+		Buffer		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, 0, RBM_NORMAL);
 		Page		page = BufferGetPage(buffer);
 		BTMetaPageData *metad = BTPageGetMeta(page);
 
@@ -291,7 +290,6 @@ pgstatindex_impl(Relation rel, FunctionCallInfo fcinfo)
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										bstrategy,
 										rel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -612,7 +610,6 @@ pgstathashindex(PG_FUNCTION_ARGS)
 	BlockNumber blkno;
 	Relation	rel;
 	HashIndexStat stats;
-	BufferAccessStrategy bstrategy;
 	HeapTuple	tuple;
 	TupleDesc	tupleDesc;
 	Datum		values[8];
@@ -665,9 +662,6 @@ pgstathashindex(PG_FUNCTION_ARGS)
 	/* Get the current relation length */
 	nblocks = RelationGetNumberOfBlocks(rel);
 
-	/* prepare access strategy for this index */
-	bstrategy = GetAccessStrategy(BAS_BULKREAD);
-
 	/* Scan all blocks except the metapage (0th page) using streaming reads */
 	startblk = HASH_METAPAGE + 1;
 
@@ -680,7 +674,6 @@ pgstathashindex(PG_FUNCTION_ARGS)
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										bstrategy,
 										rel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/contrib/pgstattuple/pgstattuple.c b/contrib/pgstattuple/pgstattuple.c
index 6a7f8cb4a7c..90f7938e953 100644
--- a/contrib/pgstattuple/pgstattuple.c
+++ b/contrib/pgstattuple/pgstattuple.c
@@ -64,22 +64,18 @@ typedef struct pgstattuple_type
 	uint64		free_space;		/* free/reusable space in bytes */
 } pgstattuple_type;
 
-typedef void (*pgstat_page) (pgstattuple_type *, Relation, BlockNumber,
-							 BufferAccessStrategy);
+typedef void (*pgstat_page) (pgstattuple_type *, Relation, BlockNumber);
 
 static Datum build_pgstattuple_type(pgstattuple_type *stat,
 									FunctionCallInfo fcinfo);
 static Datum pgstat_relation(Relation rel, FunctionCallInfo fcinfo);
 static Datum pgstat_heap(Relation rel, FunctionCallInfo fcinfo);
 static void pgstat_btree_page(pgstattuple_type *stat,
-							  Relation rel, BlockNumber blkno,
-							  BufferAccessStrategy bstrategy);
+							  Relation rel, BlockNumber blkno);
 static void pgstat_hash_page(pgstattuple_type *stat,
-							 Relation rel, BlockNumber blkno,
-							 BufferAccessStrategy bstrategy);
+							 Relation rel, BlockNumber blkno);
 static void pgstat_gist_page(pgstattuple_type *stat,
-							 Relation rel, BlockNumber blkno,
-							 BufferAccessStrategy bstrategy);
+							 Relation rel, BlockNumber blkno);
 static Datum pgstat_index(Relation rel, BlockNumber start,
 						  pgstat_page pagefn, FunctionCallInfo fcinfo);
 static void pgstat_index_page(pgstattuple_type *stat, Page page,
@@ -376,7 +372,7 @@ pgstat_heap(Relation rel, FunctionCallInfo fcinfo)
 			CHECK_FOR_INTERRUPTS();
 
 			buffer = ReadBufferExtended(rel, MAIN_FORKNUM, block,
-										RBM_NORMAL, hscan->rs_strategy);
+										RBM_NORMAL);
 			LockBuffer(buffer, BUFFER_LOCK_SHARE);
 			stat.free_space += PageGetExactFreeSpace(BufferGetPage(buffer));
 			UnlockReleaseBuffer(buffer);
@@ -389,7 +385,7 @@ pgstat_heap(Relation rel, FunctionCallInfo fcinfo)
 		CHECK_FOR_INTERRUPTS();
 
 		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, block,
-									RBM_NORMAL, hscan->rs_strategy);
+									RBM_NORMAL);
 		LockBuffer(buffer, BUFFER_LOCK_SHARE);
 		stat.free_space += PageGetExactFreeSpace(BufferGetPage(buffer));
 		UnlockReleaseBuffer(buffer);
@@ -408,13 +404,12 @@ pgstat_heap(Relation rel, FunctionCallInfo fcinfo)
  * pgstat_btree_page -- check tuples in a btree page
  */
 static void
-pgstat_btree_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
-				  BufferAccessStrategy bstrategy)
+pgstat_btree_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno)
 {
 	Buffer		buf;
 	Page		page;
 
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, bstrategy);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 	LockBuffer(buf, BT_READ);
 	page = BufferGetPage(buf);
 
@@ -452,13 +447,12 @@ pgstat_btree_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
  * pgstat_hash_page -- check tuples in a hash page
  */
 static void
-pgstat_hash_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
-				 BufferAccessStrategy bstrategy)
+pgstat_hash_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno)
 {
 	Buffer		buf;
 	Page		page;
 
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, bstrategy);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 	LockBuffer(buf, HASH_READ);
 	page = BufferGetPage(buf);
 
@@ -500,13 +494,12 @@ pgstat_hash_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
  * pgstat_gist_page -- check tuples in a gist page
  */
 static void
-pgstat_gist_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
-				 BufferAccessStrategy bstrategy)
+pgstat_gist_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno)
 {
 	Buffer		buf;
 	Page		page;
 
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, bstrategy);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 	LockBuffer(buf, GIST_SHARE);
 	page = BufferGetPage(buf);
 	if (PageIsNew(page))
@@ -539,12 +532,8 @@ pgstat_index(Relation rel, BlockNumber start, pgstat_page pagefn,
 {
 	BlockNumber nblocks;
 	BlockNumber blkno;
-	BufferAccessStrategy bstrategy;
 	pgstattuple_type stat = {0};
 
-	/* prepare access strategy for this index */
-	bstrategy = GetAccessStrategy(BAS_BULKREAD);
-
 	blkno = start;
 	for (;;)
 	{
@@ -565,7 +554,7 @@ pgstat_index(Relation rel, BlockNumber start, pgstat_page pagefn,
 		{
 			CHECK_FOR_INTERRUPTS();
 
-			pagefn(&stat, rel, blkno, bstrategy);
+			pagefn(&stat, rel, blkno);
 		}
 	}
 
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0848c18d329..7cb8839245c 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2107,36 +2107,6 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
-     <varlistentry id="guc-vacuum-buffer-usage-limit" xreflabel="vacuum_buffer_usage_limit">
-      <term>
-       <varname>vacuum_buffer_usage_limit</varname> (<type>integer</type>)
-       <indexterm>
-        <primary><varname>vacuum_buffer_usage_limit</varname> configuration parameter</primary>
-       </indexterm>
-      </term>
-      <listitem>
-       <para>
-        Specifies the size of the
-        <glossterm linkend="glossary-buffer-access-strategy">Buffer Access Strategy</glossterm>
-        used by the <command>VACUUM</command> and <command>ANALYZE</command>
-        commands.  A setting of <literal>0</literal> will allow the operation
-        to use any number of <varname>shared_buffers</varname>.  Otherwise
-        valid sizes range from <literal>128 kB</literal> to
-        <literal>16 GB</literal>.  If the specified size would exceed 1/8 the
-        size of <varname>shared_buffers</varname>, the size is silently capped
-        to that value.  The default value is <literal>2MB</literal>.  If
-        this value is specified without units, it is taken as kilobytes.  This
-        parameter can be set at any time.  It can be overridden for
-        <xref linkend="sql-vacuum"/> and <xref linkend="sql-analyze"/>
-        when passing the <option>BUFFER_USAGE_LIMIT</option> option.  Higher
-        settings can allow <command>VACUUM</command> and
-        <command>ANALYZE</command> to run more quickly, but having too large a
-        setting may cause too many other useful pages to be evicted from
-        shared buffers.
-       </para>
-      </listitem>
-     </varlistentry>
-
      <varlistentry id="guc-logical-decoding-work-mem" xreflabel="logical_decoding_work_mem">
       <term><varname>logical_decoding_work_mem</varname> (<type>integer</type>)
       <indexterm>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index d1a20d001e9..18d17540430 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -2993,28 +2993,6 @@ description | Waiting for a newly initialized WAL file to reach durable storage
           <literal>init</literal>.
          </para>
         </listitem>
-        <listitem>
-         <para>
-          <literal>vacuum</literal>: I/O operations performed outside of shared
-          buffers while vacuuming and analyzing permanent relations. Temporary
-          table vacuums use the same local buffer pool as other temporary table
-          I/O operations and are tracked in <varname>context</varname>
-          <literal>normal</literal>.
-         </para>
-        </listitem>
-        <listitem>
-         <para>
-          <literal>bulkread</literal>: Certain large read I/O operations
-          done outside of shared buffers, for example, a sequential scan of a
-          large table.
-         </para>
-        </listitem>
-        <listitem>
-         <para>
-          <literal>bulkwrite</literal>: Certain large write I/O operations
-          done outside of shared buffers, such as <command>COPY</command>.
-         </para>
-        </listitem>
        </itemizedlist>
       </entry>
      </row>
@@ -3180,13 +3158,8 @@ description | Waiting for a newly initialized WAL file to reach durable storage
         buffer in order to make it available for another use.
        </para>
        <para>
-        In <varname>context</varname> <literal>normal</literal>, this counts
-        the number of times a block was evicted from a buffer and replaced with
-        another block. In <varname>context</varname>s
-        <literal>bulkwrite</literal>, <literal>bulkread</literal>, and
-        <literal>vacuum</literal>, this counts the number of times a block was
-        evicted from shared buffers in order to add the shared buffer to a
-        separate, size-limited ring buffer for use in a bulk I/O operation.
+        This counts the number of times a block was evicted from a buffer and
+        replaced with another block.
         </para>
       </entry>
      </row>
@@ -3197,10 +3170,9 @@ description | Waiting for a newly initialized WAL file to reach durable storage
         <structfield>reuses</structfield> <type>bigint</type>
        </para>
        <para>
-        The number of times an existing buffer in a size-limited ring buffer
-        outside of shared buffers was reused as part of an I/O operation in the
-        <literal>bulkread</literal>, <literal>bulkwrite</literal>, or
-        <literal>vacuum</literal> <varname>context</varname>s.
+        Always zero.  This column previously counted reuses of buffers in a
+        size-limited ring buffer (buffer access strategy); ring buffers have
+        been removed, so no reuses are tracked.
        </para>
       </entry>
      </row>
diff --git a/doc/src/sgml/ref/analyze.sgml b/doc/src/sgml/ref/analyze.sgml
index ec81f00fecf..7f5159111d8 100644
--- a/doc/src/sgml/ref/analyze.sgml
+++ b/doc/src/sgml/ref/analyze.sgml
@@ -27,7 +27,6 @@ ANALYZE [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] [ <r
 
     VERBOSE [ <replaceable class="parameter">boolean</replaceable> ]
     SKIP_LOCKED [ <replaceable class="parameter">boolean</replaceable> ]
-    BUFFER_USAGE_LIMIT <replaceable class="parameter">size</replaceable>
 
 <phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase>
 
@@ -88,26 +87,6 @@ ANALYZE [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] [ <r
     </listitem>
    </varlistentry>
 
-   <varlistentry>
-    <term><literal>BUFFER_USAGE_LIMIT</literal></term>
-    <listitem>
-     <para>
-      Specifies the
-      <glossterm linkend="glossary-buffer-access-strategy">Buffer Access Strategy</glossterm>
-      ring buffer size for <command>ANALYZE</command>.  This size is used to
-      calculate the number of shared buffers which will be reused as part of
-      this strategy.  <literal>0</literal> disables use of a
-      <literal>Buffer Access Strategy</literal>.   When this option is not
-      specified, <command>ANALYZE</command> uses the value from
-      <xref linkend="guc-vacuum-buffer-usage-limit"/>.  Higher settings can
-      allow <command>ANALYZE</command> to run more quickly, but having too
-      large a setting may cause too many other useful pages to be evicted from
-      shared buffers.  The minimum value is <literal>128 kB</literal> and the
-      maximum value is <literal>16 GB</literal>.
-     </para>
-    </listitem>
-   </varlistentry>
-
    <varlistentry>
     <term><replaceable class="parameter">boolean</replaceable></term>
     <listitem>
diff --git a/doc/src/sgml/ref/vacuum.sgml b/doc/src/sgml/ref/vacuum.sgml
index 38ee973ea05..d44a49f8efe 100644
--- a/doc/src/sgml/ref/vacuum.sgml
+++ b/doc/src/sgml/ref/vacuum.sgml
@@ -37,7 +37,6 @@ VACUUM [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] [ <re
     PARALLEL <replaceable class="parameter">integer</replaceable>
     SKIP_DATABASE_STATS [ <replaceable class="parameter">boolean</replaceable> ]
     ONLY_DATABASE_STATS [ <replaceable class="parameter">boolean</replaceable> ]
-    BUFFER_USAGE_LIMIT <replaceable class="parameter">size</replaceable>
     FULL [ <replaceable class="parameter">boolean</replaceable> ]
 
 <phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase>
@@ -309,30 +308,6 @@ VACUUM [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] [ <re
     </listitem>
    </varlistentry>
 
-   <varlistentry>
-    <term><literal>BUFFER_USAGE_LIMIT</literal></term>
-    <listitem>
-     <para>
-      Specifies the
-      <glossterm linkend="glossary-buffer-access-strategy">Buffer Access Strategy</glossterm>
-      ring buffer size for <command>VACUUM</command>.  This size is used to
-      calculate the number of shared buffers which will be reused as part of
-      this strategy.  <literal>0</literal> disables use of a
-      <literal>Buffer Access Strategy</literal>.  If <option>ANALYZE</option>
-      is also specified, the <option>BUFFER_USAGE_LIMIT</option> value is used
-      for both the vacuum and analyze stages.  This option can't be used with
-      the <option>FULL</option> option except if <option>ANALYZE</option> is
-      also specified.  When this option is not specified,
-      <command>VACUUM</command> uses the value from
-      <xref linkend="guc-vacuum-buffer-usage-limit"/>.  Higher settings can
-      allow <command>VACUUM</command> to run more quickly, but having too
-      large a setting may cause too many other useful pages to be evicted from
-      shared buffers.  The minimum value is <literal>128 kB</literal> and the
-      maximum value is <literal>16 GB</literal>.
-     </para>
-    </listitem>
-   </varlistentry>
-
    <varlistentry>
     <term><literal>FULL</literal></term>
     <listitem>
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index bdb30752e09..5d6539f5483 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -224,7 +224,7 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void form_and_spill_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
-static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
+static void brin_vacuum_scan(Relation idxrel);
 static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
 								BrinMemTuple *dtup, const Datum *values, const bool *nulls);
 static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
@@ -1129,7 +1129,7 @@ brinbuild(Relation heap, Relation index, IndexInfo *indexInfo)
 	 * whole relation will be rolled back.
 	 */
 
-	meta = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM, NULL,
+	meta = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM,
 							 EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 	Assert(BufferGetBlockNumber(meta) == BRIN_METAPAGE_BLKNO);
 
@@ -1281,7 +1281,7 @@ brinbuildempty(Relation index)
 	Buffer		metabuf;
 
 	/* An empty BRIN index has a metapage only. */
-	metabuf = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+	metabuf = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM,
 								EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 
 	/* Initialize and xlog metabuffer. */
@@ -1336,7 +1336,7 @@ brinvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
 	heapRel = table_open(IndexGetRelation(RelationGetRelid(info->index), false),
 						 AccessShareLock);
 
-	brin_vacuum_scan(info->index, info->strategy);
+	brin_vacuum_scan(info->index);
 
 	brinsummarize(info->index, heapRel, BRIN_ALL_BLOCKRANGES, false,
 				  &stats->num_index_tuples, &stats->num_index_tuples);
@@ -2171,7 +2171,7 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
  * and such.
  */
 static void
-brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
+brin_vacuum_scan(Relation idxrel)
 {
 	BlockRangeReadStreamPrivate p;
 	ReadStream *stream;
@@ -2187,7 +2187,6 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										strategy,
 										idxrel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/src/backend/access/brin/brin_revmap.c b/src/backend/access/brin/brin_revmap.c
index 233355cb2d5..01c4104ac88 100644
--- a/src/backend/access/brin/brin_revmap.c
+++ b/src/backend/access/brin/brin_revmap.c
@@ -559,7 +559,7 @@ revmap_physical_extend(BrinRevmap *revmap)
 	}
 	else
 	{
-		buf = ExtendBufferedRel(BMR_REL(irel), MAIN_FORKNUM, NULL,
+		buf = ExtendBufferedRel(BMR_REL(irel), MAIN_FORKNUM,
 								EB_LOCK_FIRST);
 		if (BufferGetBlockNumber(buf) != mapBlk)
 		{
diff --git a/src/backend/access/gin/gininsert.c b/src/backend/access/gin/gininsert.c
index cb9ed3b563c..32ad65cc95f 100644
--- a/src/backend/access/gin/gininsert.c
+++ b/src/backend/access/gin/gininsert.c
@@ -815,9 +815,9 @@ ginbuildempty(Relation index)
 				MetaBuffer;
 
 	/* An empty GIN index has two pages. */
-	MetaBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+	MetaBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM,
 								   EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
-	RootBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+	RootBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM,
 								   EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 
 	/* Initialize and xlog metabuffer and root buffer. */
diff --git a/src/backend/access/gin/ginutil.c b/src/backend/access/gin/ginutil.c
index e7cba81d477..2d7394aedd5 100644
--- a/src/backend/access/gin/ginutil.c
+++ b/src/backend/access/gin/ginutil.c
@@ -336,7 +336,7 @@ GinNewBuffer(Relation index)
 	}
 
 	/* Must extend the file */
-	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM,
 							   EB_LOCK_FIRST);
 
 	return buffer;
diff --git a/src/backend/access/gin/ginvacuum.c b/src/backend/access/gin/ginvacuum.c
index 840543eb664..86538de39d6 100644
--- a/src/backend/access/gin/ginvacuum.c
+++ b/src/backend/access/gin/ginvacuum.c
@@ -32,7 +32,6 @@ struct GinVacuumState
 	IndexBulkDeleteCallback callback;
 	void	   *callback_state;
 	GinState	ginstate;
-	BufferAccessStrategy strategy;
 	MemoryContext tmpCxt;
 };
 
@@ -289,7 +288,7 @@ ginScanPostingTreeToDelete(GinVacuumState *gvs, DataPageDeleteStack *myStackItem
 			childBuffer = ReadBufferExtended(gvs->index,
 											 MAIN_FORKNUM,
 											 PostingItemGetBlockNumber(pitem),
-											 RBM_NORMAL, gvs->strategy);
+											 RBM_NORMAL);
 			LockBuffer(childBuffer, GIN_EXCLUSIVE);
 
 			/* Allocate a child stack entry on first use; reuse thereafter */
@@ -389,7 +388,7 @@ ginVacuumPostingTreeLeaves(GinVacuumState *gvs, BlockNumber blkno)
 		PostingItem *pitem;
 
 		buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, gvs->strategy);
+									RBM_NORMAL);
 		LockBuffer(buffer, GIN_SHARE);
 		page = BufferGetPage(buffer);
 
@@ -430,7 +429,7 @@ ginVacuumPostingTreeLeaves(GinVacuumState *gvs, BlockNumber blkno)
 			break;
 
 		buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, gvs->strategy);
+									RBM_NORMAL);
 		LockBuffer(buffer, GIN_EXCLUSIVE);
 		page = BufferGetPage(buffer);
 	}
@@ -454,7 +453,7 @@ ginVacuumPostingTree(GinVacuumState *gvs, BlockNumber rootBlkno)
 		bool		deleted PG_USED_FOR_ASSERTS_ONLY;
 
 		buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, rootBlkno,
-									RBM_NORMAL, gvs->strategy);
+									RBM_NORMAL);
 
 		/*
 		 * Lock posting tree root for cleanup to ensure there are no
@@ -615,7 +614,6 @@ ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	gvs.index = index;
 	gvs.callback = callback;
 	gvs.callback_state = callback_state;
-	gvs.strategy = info->strategy;
 	initGinState(&gvs.ginstate, index);
 
 	/* first time through? */
@@ -636,7 +634,7 @@ ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	gvs.result = stats;
 
 	buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
-								RBM_NORMAL, info->strategy);
+								RBM_NORMAL);
 
 	/* find leaf page */
 	for (;;)
@@ -669,7 +667,7 @@ ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 
 		UnlockReleaseBuffer(buffer);
 		buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, info->strategy);
+									RBM_NORMAL);
 	}
 
 	/* right now we found leftmost page in entry's BTree */
@@ -712,7 +710,7 @@ ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 			break;
 
 		buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, info->strategy);
+									RBM_NORMAL);
 		LockBuffer(buffer, GIN_EXCLUSIVE);
 	}
 
@@ -794,7 +792,6 @@ ginvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										index,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/src/backend/access/gist/gist.c b/src/backend/access/gist/gist.c
index 8565e225be7..ae494742a4b 100644
--- a/src/backend/access/gist/gist.c
+++ b/src/backend/access/gist/gist.c
@@ -142,7 +142,7 @@ gistbuildempty(Relation index)
 	Buffer		buffer;
 
 	/* Initialize the root page */
-	buffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM,
 							   EB_SKIP_EXTENSION_LOCK | EB_LOCK_FIRST);
 
 	/* Initialize and xlog buffer */
diff --git a/src/backend/access/gist/gistutil.c b/src/backend/access/gist/gistutil.c
index 0f58f61879f..170174f62fd 100644
--- a/src/backend/access/gist/gistutil.c
+++ b/src/backend/access/gist/gistutil.c
@@ -877,7 +877,7 @@ gistNewBuffer(Relation r, Relation heaprel)
 	}
 
 	/* Must extend the file */
-	buffer = ExtendBufferedRel(BMR_REL(r), MAIN_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(r), MAIN_FORKNUM,
 							   EB_LOCK_FIRST);
 
 	return buffer;
diff --git a/src/backend/access/gist/gistvacuum.c b/src/backend/access/gist/gistvacuum.c
index 686a0418054..c366ed35ea8 100644
--- a/src/backend/access/gist/gistvacuum.c
+++ b/src/backend/access/gist/gistvacuum.c
@@ -218,7 +218,6 @@ gistvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										rel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -491,8 +490,7 @@ restart:
 		/* check for vacuum delay while not holding any buffer lock */
 		vacuum_delay_point(false);
 
-		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL,
-									info->strategy);
+		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 		goto restart;
 	}
 }
@@ -524,8 +522,7 @@ gistvacuum_delete_empty_pages(IndexVacuumInfo *info, GistVacState *vstate)
 		int			ntodelete;
 		int			deleted;
 
-		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, (BlockNumber) blkno,
-									RBM_NORMAL, info->strategy);
+		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, (BlockNumber) blkno, RBM_NORMAL);
 
 		LockBuffer(buffer, GIST_SHARE);
 		page = BufferGetPage(buffer);
@@ -590,8 +587,7 @@ gistvacuum_delete_empty_pages(IndexVacuumInfo *info, GistVacState *vstate)
 			if (PageGetMaxOffsetNumber(page) == FirstOffsetNumber)
 				break;
 
-			leafbuf = ReadBufferExtended(rel, MAIN_FORKNUM, leafs_to_delete[i],
-										 RBM_NORMAL, info->strategy);
+			leafbuf = ReadBufferExtended(rel, MAIN_FORKNUM, leafs_to_delete[i], RBM_NORMAL);
 			LockBuffer(leafbuf, GIST_EXCLUSIVE);
 			gistcheckpage(rel, leafbuf);
 
diff --git a/src/backend/access/hash/hash.c b/src/backend/access/hash/hash.c
index 8d8cd30dc38..1bbb7224bfc 100644
--- a/src/backend/access/hash/hash.c
+++ b/src/backend/access/hash/hash.c
@@ -542,7 +542,6 @@ hashbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										rel,
 										MAIN_FORKNUM,
 										hash_bulkdelete_read_stream_cb,
@@ -616,7 +615,7 @@ bucket_loop:
 
 		bucket_buf = buf;
 
-		hashbucketcleanup(rel, cur_bucket, bucket_buf, blkno, info->strategy,
+		hashbucketcleanup(rel, cur_bucket, bucket_buf, blkno,
 						  cachedmetap->hashm_maxbucket,
 						  cachedmetap->hashm_highmask,
 						  cachedmetap->hashm_lowmask, &tuples_removed,
@@ -765,7 +764,7 @@ hashvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
  */
 void
 hashbucketcleanup(Relation rel, Bucket cur_bucket, Buffer bucket_buf,
-				  BlockNumber bucket_blkno, BufferAccessStrategy bstrategy,
+				  BlockNumber bucket_blkno,
 				  uint32 maxbucket, uint32 highmask, uint32 lowmask,
 				  double *tuples_removed, double *num_index_tuples,
 				  bool split_cleanup,
@@ -935,9 +934,8 @@ hashbucketcleanup(Relation rel, Bucket cur_bucket, Buffer bucket_buf,
 		if (!BlockNumberIsValid(blkno))
 			break;
 
-		next_buf = _hash_getbuf_with_strategy(rel, blkno, HASH_WRITE,
-											  LH_OVERFLOW_PAGE,
-											  bstrategy);
+		next_buf = _hash_getbuf(rel, blkno, HASH_WRITE,
+								LH_OVERFLOW_PAGE);
 
 		/*
 		 * release the lock on previous page after acquiring the lock on next
@@ -1004,8 +1002,7 @@ hashbucketcleanup(Relation rel, Bucket cur_bucket, Buffer bucket_buf,
 	 * ordering of tuples for a scan that has started before it.
 	 */
 	if (bucket_dirty && IsBufferCleanupOK(bucket_buf))
-		_hash_squeezebucket(rel, cur_bucket, bucket_blkno, bucket_buf,
-							bstrategy);
+		_hash_squeezebucket(rel, cur_bucket, bucket_blkno, bucket_buf);
 	else
 		LockBuffer(bucket_buf, BUFFER_LOCK_UNLOCK);
 }
diff --git a/src/backend/access/hash/hashovfl.c b/src/backend/access/hash/hashovfl.c
index dbc57ef958c..5b35fed33f9 100644
--- a/src/backend/access/hash/hashovfl.c
+++ b/src/backend/access/hash/hashovfl.c
@@ -491,8 +491,7 @@ _hash_firstfreebit(uint32 map)
 BlockNumber
 _hash_freeovflpage(Relation rel, Buffer bucketbuf, Buffer ovflbuf,
 				   Buffer wbuf, IndexTuple *itups, OffsetNumber *itup_offsets,
-				   Size *tups_size, uint16 nitups,
-				   BufferAccessStrategy bstrategy)
+				   Size *tups_size, uint16 nitups)
 {
 	HashMetaPage metap;
 	Buffer		metabuf;
@@ -539,20 +538,16 @@ _hash_freeovflpage(Relation rel, Buffer bucketbuf, Buffer ovflbuf,
 		if (prevblkno == writeblkno)
 			prevbuf = wbuf;
 		else
-			prevbuf = _hash_getbuf_with_strategy(rel,
-												 prevblkno,
-												 HASH_WRITE,
-												 LH_BUCKET_PAGE | LH_OVERFLOW_PAGE,
-												 bstrategy);
+			prevbuf = _hash_getbuf(rel,
+								   prevblkno,
+								   HASH_WRITE,
+								   LH_BUCKET_PAGE | LH_OVERFLOW_PAGE);
 	}
 	if (BlockNumberIsValid(nextblkno))
-		nextbuf = _hash_getbuf_with_strategy(rel,
-											 nextblkno,
-											 HASH_WRITE,
-											 LH_OVERFLOW_PAGE,
-											 bstrategy);
-
-	/* Note: bstrategy is intentionally not used for metapage and bitmap */
+		nextbuf = _hash_getbuf(rel,
+							   nextblkno,
+							   HASH_WRITE,
+							   LH_OVERFLOW_PAGE);
 
 	/* Read the metapage so we can determine which bitmap page to use */
 	metabuf = _hash_getbuf(rel, HASH_METAPAGE, HASH_READ, LH_META_PAGE);
@@ -843,8 +838,7 @@ void
 _hash_squeezebucket(Relation rel,
 					Bucket bucket,
 					BlockNumber bucket_blkno,
-					Buffer bucket_buf,
-					BufferAccessStrategy bstrategy)
+					Buffer bucket_buf)
 {
 	BlockNumber wblkno;
 	BlockNumber rblkno;
@@ -886,11 +880,10 @@ _hash_squeezebucket(Relation rel,
 		rblkno = ropaque->hasho_nextblkno;
 		if (rbuf != InvalidBuffer)
 			_hash_relbuf(rel, rbuf);
-		rbuf = _hash_getbuf_with_strategy(rel,
-										  rblkno,
-										  HASH_WRITE,
-										  LH_OVERFLOW_PAGE,
-										  bstrategy);
+		rbuf = _hash_getbuf(rel,
+							rblkno,
+							HASH_WRITE,
+							LH_OVERFLOW_PAGE);
 		rpage = BufferGetPage(rbuf);
 		ropaque = HashPageGetOpaque(rpage);
 		Assert(ropaque->hasho_bucket == bucket);
@@ -952,11 +945,10 @@ readpage:
 
 				/* don't need to move to next page if we reached the read page */
 				if (wblkno != rblkno)
-					next_wbuf = _hash_getbuf_with_strategy(rel,
-														   wblkno,
-														   HASH_WRITE,
-														   LH_OVERFLOW_PAGE,
-														   bstrategy);
+					next_wbuf = _hash_getbuf(rel,
+											 wblkno,
+											 HASH_WRITE,
+											 LH_OVERFLOW_PAGE);
 
 				if (nitups > 0)
 				{
@@ -1098,7 +1090,7 @@ readpage:
 
 		/* free this overflow page (releases rbuf) */
 		_hash_freeovflpage(rel, bucket_buf, rbuf, wbuf, itups, itup_offsets,
-						   tups_size, nitups, bstrategy);
+						   tups_size, nitups);
 
 		/* be tidy */
 		for (i = 0; i < nitups; i++)
@@ -1115,11 +1107,10 @@ readpage:
 			return;
 		}
 
-		rbuf = _hash_getbuf_with_strategy(rel,
-										  rblkno,
-										  HASH_WRITE,
-										  LH_OVERFLOW_PAGE,
-										  bstrategy);
+		rbuf = _hash_getbuf(rel,
+							rblkno,
+							HASH_WRITE,
+							LH_OVERFLOW_PAGE);
 		rpage = BufferGetPage(rbuf);
 		ropaque = HashPageGetOpaque(rpage);
 		Assert(ropaque->hasho_bucket == bucket);
diff --git a/src/backend/access/hash/hashpage.c b/src/backend/access/hash/hashpage.c
index 8099b0d021f..bdf1a255f1f 100644
--- a/src/backend/access/hash/hashpage.c
+++ b/src/backend/access/hash/hashpage.c
@@ -139,8 +139,7 @@ _hash_getinitbuf(Relation rel, BlockNumber blkno)
 	if (blkno == P_NEW)
 		elog(ERROR, "hash AM does not use P_NEW");
 
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_ZERO_AND_LOCK,
-							 NULL);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_ZERO_AND_LOCK);
 
 	/* ref count and lock type are correct */
 
@@ -209,7 +208,7 @@ _hash_getnewbuf(Relation rel, BlockNumber blkno, ForkNumber forkNum)
 	/* smgr insists we explicitly extend the relation */
 	if (blkno == nblocks)
 	{
-		buf = ExtendBufferedRel(BMR_REL(rel), forkNum, NULL,
+		buf = ExtendBufferedRel(BMR_REL(rel), forkNum,
 								EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 		if (BufferGetBlockNumber(buf) != blkno)
 			elog(ERROR, "unexpected hash relation size: %u, should be %u",
@@ -217,8 +216,7 @@ _hash_getnewbuf(Relation rel, BlockNumber blkno, ForkNumber forkNum)
 	}
 	else
 	{
-		buf = ReadBufferExtended(rel, forkNum, blkno, RBM_ZERO_AND_LOCK,
-								 NULL);
+		buf = ReadBufferExtended(rel, forkNum, blkno, RBM_ZERO_AND_LOCK);
 	}
 
 	/* ref count and lock type are correct */
@@ -229,34 +227,6 @@ _hash_getnewbuf(Relation rel, BlockNumber blkno, ForkNumber forkNum)
 	return buf;
 }
 
-/*
- *	_hash_getbuf_with_strategy() -- Get a buffer with nondefault strategy.
- *
- *		This is identical to _hash_getbuf() but also allows a buffer access
- *		strategy to be specified.  We use this for VACUUM operations.
- */
-Buffer
-_hash_getbuf_with_strategy(Relation rel, BlockNumber blkno,
-						   int access, int flags,
-						   BufferAccessStrategy bstrategy)
-{
-	Buffer		buf;
-
-	if (blkno == P_NEW)
-		elog(ERROR, "hash AM does not use P_NEW");
-
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, bstrategy);
-
-	if (access != HASH_NOLOCK)
-		LockBuffer(buf, access);
-
-	/* ref count and lock type are correct */
-
-	_hash_checkpage(rel, buf, flags);
-
-	return buf;
-}
-
 /*
  *	_hash_relbuf() -- release a locked buffer.
  *
@@ -758,7 +728,7 @@ restart_expand:
 		/* Release the metapage lock. */
 		LockBuffer(metabuf, BUFFER_LOCK_UNLOCK);
 
-		hashbucketcleanup(rel, old_bucket, buf_oblkno, start_oblkno, NULL,
+		hashbucketcleanup(rel, old_bucket, buf_oblkno, start_oblkno,
 						  maxbucket, highmask, lowmask, NULL, NULL, true,
 						  NULL, NULL);
 
@@ -1333,7 +1303,7 @@ _hash_splitbucket(Relation rel,
 	{
 		LockBuffer(bucket_nbuf, BUFFER_LOCK_UNLOCK);
 		hashbucketcleanup(rel, obucket, bucket_obuf,
-						  BufferGetBlockNumber(bucket_obuf), NULL,
+						  BufferGetBlockNumber(bucket_obuf),
 						  maxbucket, highmask, lowmask, NULL, NULL, true,
 						  NULL, NULL);
 	}
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index de31a204c9f..479e45bf33c 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -359,7 +359,6 @@ static void
 initscan(HeapScanDesc scan, ScanKey key, bool keep_startblock)
 {
 	ParallelBlockTableScanDesc bpscan = NULL;
-	bool		allow_strat;
 	bool		allow_sync;
 
 	/*
@@ -396,24 +395,10 @@ initscan(HeapScanDesc scan, ScanKey key, bool keep_startblock)
 	if (!RelationUsesLocalBuffers(scan->rs_base.rs_rd) &&
 		scan->rs_nblocks > NBuffers / 4)
 	{
-		allow_strat = (scan->rs_base.rs_flags & SO_ALLOW_STRAT) != 0;
 		allow_sync = (scan->rs_base.rs_flags & SO_ALLOW_SYNC) != 0;
 	}
 	else
-		allow_strat = allow_sync = false;
-
-	if (allow_strat)
-	{
-		/* During a rescan, keep the previous strategy object. */
-		if (scan->rs_strategy == NULL)
-			scan->rs_strategy = GetAccessStrategy(BAS_BULKREAD);
-	}
-	else
-	{
-		if (scan->rs_strategy != NULL)
-			FreeAccessStrategy(scan->rs_strategy);
-		scan->rs_strategy = NULL;
-	}
+		allow_sync = false;
 
 	if (scan->rs_base.rs_parallel != NULL)
 	{
@@ -1202,7 +1187,6 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 	scan->rs_base.rs_flags = flags;
 	scan->rs_base.rs_parallel = parallel_scan;
 	scan->rs_base.rs_instrument = NULL;
-	scan->rs_strategy = NULL;	/* set in initscan */
 	scan->rs_cbuf = InvalidBuffer;
 
 	/*
@@ -1273,8 +1257,7 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 
 	/*
 	 * Set up a read stream for sequential scans and TID range scans. This
-	 * should be done after initscan() because initscan() allocates the
-	 * BufferAccessStrategy object passed to the read stream API.
+	 * should be done after initscan().
 	 */
 	if (scan->rs_base.rs_flags & SO_TYPE_SEQSCAN ||
 		scan->rs_base.rs_flags & SO_TYPE_TIDRANGESCAN)
@@ -1295,7 +1278,6 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 		 */
 		scan->rs_read_stream = read_stream_begin_relation(READ_STREAM_SEQUENTIAL |
 														  READ_STREAM_USE_BATCHING,
-														  scan->rs_strategy,
 														  scan->rs_base.rs_rd,
 														  MAIN_FORKNUM,
 														  cb,
@@ -1306,7 +1288,6 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 	{
 		scan->rs_read_stream = read_stream_begin_relation(READ_STREAM_DEFAULT |
 														  READ_STREAM_USE_BATCHING,
-														  scan->rs_strategy,
 														  scan->rs_base.rs_rd,
 														  MAIN_FORKNUM,
 														  bitmapheap_stream_read_next,
@@ -1402,9 +1383,6 @@ heap_endscan(TableScanDesc sscan)
 	if (BufferIsValid(scan->rs_vmbuffer))
 		ReleaseBuffer(scan->rs_vmbuffer);
 
-	/*
-	 * Must free the read stream before freeing the BufferAccessStrategy.
-	 */
 	if (scan->rs_read_stream)
 		read_stream_end(scan->rs_read_stream);
 
@@ -1416,9 +1394,6 @@ heap_endscan(TableScanDesc sscan)
 	if (scan->rs_base.rs_key)
 		pfree(scan->rs_base.rs_key);
 
-	if (scan->rs_strategy != NULL)
-		FreeAccessStrategy(scan->rs_strategy);
-
 	if (scan->rs_parallelworkerdata != NULL)
 		pfree(scan->rs_parallelworkerdata);
 
@@ -1939,7 +1914,6 @@ GetBulkInsertState(void)
 	BulkInsertState bistate;
 
 	bistate = (BulkInsertState) palloc_object(BulkInsertStateData);
-	bistate->strategy = GetAccessStrategy(BAS_BULKWRITE);
 	bistate->current_buf = InvalidBuffer;
 	bistate->next_free = InvalidBlockNumber;
 	bistate->last_free = InvalidBlockNumber;
@@ -1955,7 +1929,6 @@ FreeBulkInsertState(BulkInsertState bistate)
 {
 	if (bistate->current_buf != InvalidBuffer)
 		ReleaseBuffer(bistate->current_buf);
-	FreeAccessStrategy(bistate->strategy);
 	pfree(bistate);
 }
 
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index bf87430cf01..d14658d7218 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -2213,9 +2213,9 @@ heapam_scan_sample_next_block(TableScanDesc scan, SampleScanState *scanstate)
 	 */
 	CHECK_FOR_INTERRUPTS();
 
-	/* Read page using selected strategy */
+	/* Read page */
 	hscan->rs_cbuf = ReadBufferExtended(hscan->rs_base.rs_rd, MAIN_FORKNUM,
-										blockno, RBM_NORMAL, hscan->rs_strategy);
+										blockno, RBM_NORMAL);
 
 	/* in pagemode, prune the page and determine visible tuple offsets */
 	if (hscan->rs_base.rs_flags & SO_ALLOW_PAGEMODE)
diff --git a/src/backend/access/heap/hio.c b/src/backend/access/heap/hio.c
index e96e0f77d92..9bf7abc247f 100644
--- a/src/backend/access/heap/hio.c
+++ b/src/backend/access/heap/hio.c
@@ -80,7 +80,8 @@ RelationPutHeapTuple(Relation relation,
 }
 
 /*
- * Read in a buffer in mode, using bulk-insert strategy if bistate isn't NULL.
+ * Read in a buffer in mode, using the bistate's pinned target page if
+ * available.
  */
 static Buffer
 ReadBufferBI(Relation relation, BlockNumber targetBlock,
@@ -91,7 +92,7 @@ ReadBufferBI(Relation relation, BlockNumber targetBlock,
 	/* If not bulk-insert, exactly like ReadBuffer */
 	if (!bistate)
 		return ReadBufferExtended(relation, MAIN_FORKNUM, targetBlock,
-								  mode, NULL);
+								  mode);
 
 	/* If we have the desired block already pinned, re-pin and return it */
 	if (bistate->current_buf != InvalidBuffer)
@@ -113,9 +114,9 @@ ReadBufferBI(Relation relation, BlockNumber targetBlock,
 		bistate->current_buf = InvalidBuffer;
 	}
 
-	/* Perform a read using the buffer strategy */
+	/* Perform the read */
 	buffer = ReadBufferExtended(relation, MAIN_FORKNUM, targetBlock,
-								mode, bistate->strategy);
+								mode);
 
 	/* Save the selected block as target for future inserts */
 	IncrBufferRefCount(buffer);
@@ -337,7 +338,6 @@ RelationAddBlocks(Relation relation, BulkInsertState bistate,
 	 * way larger.
 	 */
 	first_block = ExtendBufferedRelBy(BMR_REL(relation), MAIN_FORKNUM,
-									  bistate ? bistate->strategy : NULL,
 									  EB_LOCK_FIRST,
 									  extend_by_pages,
 									  victim_buffers,
@@ -484,8 +484,7 @@ RelationAddBlocks(Relation relation, BulkInsertState bistate,
  *
  *	The caller can also provide a BulkInsertState object to optimize many
  *	insertions into the same relation.  This keeps a pin on the current
- *	insertion target page (to save pin/unpin cycles) and also passes a
- *	BULKWRITE buffer selection strategy object to the buffer manager.
+ *	insertion target page (to save pin/unpin cycles).
  *	Passing NULL for bistate selects the default behavior.
  *
  *	We don't fill existing pages further than the fillfactor, except for large
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 39395aed0d5..8bc8e470af3 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -257,7 +257,6 @@ typedef struct LVRelState
 	int			nindexes;
 
 	/* Buffer access strategy and parallel vacuum state */
-	BufferAccessStrategy bstrategy;
 	ParallelVacuumState *pvs;
 
 	/* Aggressive VACUUM? (must set relfrozenxid >= FreezeLimit) */
@@ -621,8 +620,7 @@ heap_vacuum_eager_scan_setup(LVRelState *vacrel, const VacuumParams *params)
  *		and locked the relation.
  */
 void
-heap_vacuum_rel(Relation rel, const VacuumParams *params,
-				BufferAccessStrategy bstrategy)
+heap_vacuum_rel(Relation rel, const VacuumParams *params)
 {
 	LVRelState *vacrel;
 	bool		verbose,
@@ -699,7 +697,6 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
 	vacrel->rel = rel;
 	vac_open_indexes(vacrel->rel, RowExclusiveLock, &vacrel->nindexes,
 					 &vacrel->indrels);
-	vacrel->bstrategy = bstrategy;
 	if (instrument && vacrel->nindexes > 0)
 	{
 		/* Copy index names used by instrumentation (not error reporting) */
@@ -1311,7 +1308,6 @@ lazy_scan_heap(LVRelState *vacrel)
 	 * explicit work in heap_vac_scan_next_block.
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE,
-										vacrel->bstrategy,
 										vacrel->rel,
 										MAIN_FORKNUM,
 										heap_vac_scan_next_block,
@@ -2670,7 +2666,6 @@ lazy_vacuum_heap_rel(LVRelState *vacrel)
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_USE_BATCHING,
-										vacrel->bstrategy,
 										vacrel->rel,
 										MAIN_FORKNUM,
 										vacuum_reap_lp_read_stream_next,
@@ -2904,13 +2899,6 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel)
 
 		VacuumFailsafeActive = true;
 
-		/*
-		 * Abandon use of a buffer access strategy to allow use of all of
-		 * shared buffers.  We assume the caller who allocated the memory for
-		 * the BufferAccessStrategy will free it.
-		 */
-		vacrel->bstrategy = NULL;
-
 		/* Disable index vacuuming, index cleanup, and heap rel truncation */
 		vacrel->do_index_vacuuming = false;
 		vacrel->do_index_cleanup = false;
@@ -3023,7 +3011,6 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	ivinfo.estimated_count = true;
 	ivinfo.message_level = DEBUG2;
 	ivinfo.num_heap_tuples = reltuples;
-	ivinfo.strategy = vacrel->bstrategy;
 
 	/*
 	 * Update error traceback information.
@@ -3074,7 +3061,6 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	ivinfo.message_level = DEBUG2;
 
 	ivinfo.num_heap_tuples = reltuples;
-	ivinfo.strategy = vacrel->bstrategy;
 
 	/*
 	 * Update error traceback information.
@@ -3354,8 +3340,7 @@ count_nondeletable_pages(LVRelState *vacrel, bool *lock_waiter_detected)
 			prefetchedUntil = prefetchStart;
 		}
 
-		buf = ReadBufferExtended(vacrel->rel, MAIN_FORKNUM, blkno, RBM_NORMAL,
-								 vacrel->bstrategy);
+		buf = ReadBufferExtended(vacrel->rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 
 		/* In this phase we only need shared access to the buffer */
 		LockBuffer(buf, BUFFER_LOCK_SHARE);
@@ -3446,8 +3431,7 @@ dead_items_alloc(LVRelState *vacrel, int nworkers)
 			vacrel->pvs = parallel_vacuum_init(vacrel->rel, vacrel->indrels,
 											   vacrel->nindexes, nworkers,
 											   vac_work_mem,
-											   vacrel->verbose ? INFO : DEBUG2,
-											   vacrel->bstrategy);
+											   vacrel->verbose ? INFO : DEBUG2);
 
 		/*
 		 * If parallel mode started, dead_items and dead_items_info spaces are
diff --git a/src/backend/access/heap/visibilitymap.c b/src/backend/access/heap/visibilitymap.c
index 4fd470702aa..b2612b1f710 100644
--- a/src/backend/access/heap/visibilitymap.c
+++ b/src/backend/access/heap/visibilitymap.c
@@ -575,7 +575,7 @@ vm_readbuf(Relation rel, BlockNumber blkno, bool extend)
 	}
 	else
 		buf = ReadBufferExtended(rel, VISIBILITYMAP_FORKNUM, blkno,
-								 RBM_ZERO_ON_ERROR, NULL);
+								 RBM_ZERO_ON_ERROR);
 
 	/*
 	 * Initializing the page when needed is trickier than it looks, because of
@@ -611,7 +611,7 @@ vm_extend(Relation rel, BlockNumber vm_nblocks)
 {
 	Buffer		buf;
 
-	buf = ExtendBufferedRelTo(BMR_REL(rel), VISIBILITYMAP_FORKNUM, NULL,
+	buf = ExtendBufferedRelTo(BMR_REL(rel), VISIBILITYMAP_FORKNUM,
 							  EB_CREATE_FORK_IF_NEEDED |
 							  EB_CLEAR_SIZE_CACHE,
 							  vm_nblocks,
diff --git a/src/backend/access/nbtree/nbtpage.c b/src/backend/access/nbtree/nbtpage.c
index 109017d6b52..7b5fc16d75c 100644
--- a/src/backend/access/nbtree/nbtpage.c
+++ b/src/backend/access/nbtree/nbtpage.c
@@ -960,7 +960,7 @@ _bt_allocbuf(Relation rel, Relation heaprel)
 	 * otherwise would make, as we can't use _bt_lockbuf() without introducing
 	 * a race.
 	 */
-	buf = ExtendBufferedRel(BMR_REL(rel), MAIN_FORKNUM, NULL, EB_LOCK_FIRST);
+	buf = ExtendBufferedRel(BMR_REL(rel), MAIN_FORKNUM, EB_LOCK_FIRST);
 	if (!RelationUsesLocalBuffers(rel))
 		VALGRIND_MAKE_MEM_DEFINED(BufferGetPage(buf), BLCKSZ);
 
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 3df2c752ead..379a2d37def 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -1324,7 +1324,6 @@ btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										rel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -1730,8 +1729,7 @@ backtrack:
 		 * recycle all-zero pages, not fail.  Also, we want to use a
 		 * nondefault buffer access strategy.
 		 */
-		buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL,
-								 info->strategy);
+		buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 		goto backtrack;
 	}
 
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index f2ee333f60d..785e385aab6 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -432,7 +432,7 @@ SpGistNewBuffer(Relation index)
 		ReleaseBuffer(buffer);
 	}
 
-	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM,
 							   EB_LOCK_FIRST);
 
 	return buffer;
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index c461f8dc02d..9f644edaf82 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -704,8 +704,7 @@ spgprocesspending(spgBulkDeleteState *bds)
 
 		/* examine the referenced page */
 		blkno = ItemPointerGetBlockNumber(&pitem->tid);
-		buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, bds->info->strategy);
+		buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno, RBM_NORMAL);
 		LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE);
 		page = BufferGetPage(buffer);
 
@@ -830,7 +829,6 @@ spgvacuumscan(spgBulkDeleteState *bds)
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										bds->info->strategy,
 										index,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/src/backend/access/transam/xloginsert.c b/src/backend/access/transam/xloginsert.c
index f2e10b82b7d..0c16b7fef0f 100644
--- a/src/backend/access/transam/xloginsert.c
+++ b/src/backend/access/transam/xloginsert.c
@@ -1348,7 +1348,7 @@ log_newpage_range(Relation rel, ForkNumber forknum,
 		while (nbufs < XLR_MAX_BLOCK_ID && blkno < endblk)
 		{
 			Buffer		buf = ReadBufferExtended(rel, forknum, blkno,
-												 RBM_NORMAL, NULL);
+												 RBM_NORMAL);
 
 			LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
 
diff --git a/src/backend/access/transam/xlogutils.c b/src/backend/access/transam/xlogutils.c
index d8c179c5dcc..e5be49f8b5d 100644
--- a/src/backend/access/transam/xlogutils.c
+++ b/src/backend/access/transam/xlogutils.c
@@ -519,7 +519,7 @@ XLogReadBufferExtended(RelFileLocator rlocator, ForkNumber forknum,
 	{
 		/* page exists in file */
 		buffer = ReadBufferWithoutRelcache(rlocator, forknum, blkno,
-										   mode, NULL, true);
+										   mode, true);
 	}
 	else
 	{
@@ -536,7 +536,6 @@ XLogReadBufferExtended(RelFileLocator rlocator, ForkNumber forknum,
 		Assert(InRecovery);
 		buffer = ExtendBufferedRelTo(BMR_SMGR(smgr, RELPERSISTENCE_PERMANENT),
 									 forknum,
-									 NULL,
 									 EB_PERFORMING_RECOVERY |
 									 EB_SKIP_EXTENSION_LOCK,
 									 blkno + 1,
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 81bba4beac7..b0a7b8a33cd 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -3431,7 +3431,6 @@ validate_index(Oid heapId, Oid indexId, Snapshot snapshot)
 	ivinfo.estimated_count = true;
 	ivinfo.message_level = DEBUG2;
 	ivinfo.num_heap_tuples = heapRelation->rd_rel->reltuples;
-	ivinfo.strategy = NULL;
 
 	/*
 	 * Encode TIDs as int8 values for the sort, rather than directly sorting
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index f66e80b757c..9b117a53124 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -72,7 +72,6 @@ int			default_statistics_target = 100;
 
 /* A few variables that don't seem worth passing around as parameters */
 static MemoryContext anl_context = NULL;
-static BufferAccessStrategy vac_strategy;
 
 
 static void do_analyze_rel(Relation onerel,
@@ -108,8 +107,7 @@ static Datum ind_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
  */
 void
 analyze_rel(Oid relid, RangeVar *relation,
-			const VacuumParams *params, List *va_cols, bool in_outer_xact,
-			BufferAccessStrategy bstrategy)
+			const VacuumParams *params, List *va_cols, bool in_outer_xact)
 {
 	Relation	onerel;
 	int			elevel;
@@ -124,7 +122,6 @@ analyze_rel(Oid relid, RangeVar *relation,
 		elevel = DEBUG2;
 
 	/* Set up static variables */
-	vac_strategy = bstrategy;
 
 	/*
 	 * Check for user-requested abort.
@@ -730,7 +727,6 @@ do_analyze_rel(Relation onerel, const VacuumParams *params,
 			ivinfo.estimated_count = true;
 			ivinfo.message_level = elevel;
 			ivinfo.num_heap_tuples = onerel->rd_rel->reltuples;
-			ivinfo.strategy = vac_strategy;
 
 			stats = index_vacuum_cleanup(&ivinfo, NULL);
 
@@ -1302,7 +1298,6 @@ acquire_sample_rows(Relation onerel, int elevel,
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_USE_BATCHING,
-										vac_strategy,
 										scan->rs_rd,
 										MAIN_FORKNUM,
 										block_sampling_read_stream_next,
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index f0819d15ab7..6f65d48b6bd 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -262,7 +262,6 @@ ScanSourceDatabasePgClass(Oid tbid, Oid dbid, char *srcpath)
 	LockRelId	relid;
 	Snapshot	snapshot;
 	SMgrRelation smgr;
-	BufferAccessStrategy bstrategy;
 
 	/* Get pg_class relfilenumber. */
 	relfilenumber = RelationMapOidToFilenumberForDatabase(srcpath,
@@ -282,9 +281,6 @@ ScanSourceDatabasePgClass(Oid tbid, Oid dbid, char *srcpath)
 	nblocks = smgrnblocks(smgr, MAIN_FORKNUM);
 	smgrclose(smgr);
 
-	/* Use a buffer access strategy since this is a bulk read operation. */
-	bstrategy = GetAccessStrategy(BAS_BULKREAD);
-
 	/*
 	 * As explained in the function header comments, we need a snapshot that
 	 * will see all committed transactions as committed, and our transaction
@@ -299,7 +295,7 @@ ScanSourceDatabasePgClass(Oid tbid, Oid dbid, char *srcpath)
 		CHECK_FOR_INTERRUPTS();
 
 		buf = ReadBufferWithoutRelcache(rlocator, MAIN_FORKNUM, blkno,
-										RBM_NORMAL, bstrategy, true);
+										RBM_NORMAL, true);
 
 		LockBuffer(buf, BUFFER_LOCK_SHARE);
 		page = BufferGetPage(buf);
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 02883fe34a4..d8c15a8b1bc 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -2447,7 +2447,7 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 			if (params->options & CLUOPT_VERBOSE)
 				vac_params.options |= VACOPT_VERBOSE;
 			analyze_rel(tableOid, NULL, &vac_params,
-						stmt->relation->va_cols, true, NULL);
+						stmt->relation->va_cols, true);
 			PopActiveSnapshot();
 			CommandCounterIncrement();
 		}
diff --git a/src/backend/commands/sequence.c b/src/backend/commands/sequence.c
index 551667650ba..ac1ab948211 100644
--- a/src/backend/commands/sequence.c
+++ b/src/backend/commands/sequence.c
@@ -358,7 +358,7 @@ fill_seq_fork_with_data(Relation rel, HeapTuple tuple, ForkNumber forkNum)
 
 	/* Initialize first page of relation with special magic number */
 
-	buf = ExtendBufferedRel(BMR_REL(rel), forkNum, NULL,
+	buf = ExtendBufferedRel(BMR_REL(rel), forkNum,
 							EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 	Assert(BufferGetBlockNumber(buf) == 0);
 
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 38539a6fd3d..66acd27bd48 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -127,32 +127,11 @@ static void vac_truncate_clog(TransactionId frozenXID,
 							  TransactionId lastSaneFrozenXid,
 							  MultiXactId lastSaneMinMulti);
 static bool vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
-					   BufferAccessStrategy bstrategy, bool isTopLevel);
+					   bool isTopLevel);
 static double compute_parallel_delay(void);
 static VacOptValue get_vacoptval_from_boolean(DefElem *def);
 static bool vac_tid_reaped(ItemPointer itemptr, void *state);
 
-/*
- * GUC check function to ensure GUC value specified is within the allowable
- * range.
- */
-bool
-check_vacuum_buffer_usage_limit(int *newval, void **extra,
-								GucSource source)
-{
-	/* Value upper and lower hard limits are inclusive */
-	if (*newval == 0 || (*newval >= MIN_BAS_VAC_RING_SIZE_KB &&
-						 *newval <= MAX_BAS_VAC_RING_SIZE_KB))
-		return true;
-
-	/* Value does not fall within any allowable range */
-	GUC_check_errdetail("\"%s\" must be 0 or between %d kB and %d kB.",
-						"vacuum_buffer_usage_limit",
-						MIN_BAS_VAC_RING_SIZE_KB, MAX_BAS_VAC_RING_SIZE_KB);
-
-	return false;
-}
-
 /*
  * Primary entry point for manual VACUUM and ANALYZE commands
  *
@@ -163,7 +142,6 @@ void
 ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 {
 	VacuumParams params;
-	BufferAccessStrategy bstrategy = NULL;
 	bool		verbose = false;
 	bool		skip_locked = false;
 	bool		analyze = false;
@@ -172,7 +150,6 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 	bool		disable_page_skipping = false;
 	bool		process_main = true;
 	bool		process_toast = true;
-	int			ring_size;
 	bool		skip_database_stats = false;
 	bool		only_database_stats = false;
 	MemoryContext vac_context;
@@ -188,12 +165,6 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 	/* Will be set later if we recurse to a TOAST table. */
 	params.toast_parent = InvalidOid;
 
-	/*
-	 * Set this to an invalid value so it is clear whether or not a
-	 * BUFFER_USAGE_LIMIT was specified when making the access strategy.
-	 */
-	ring_size = -1;
-
 	/* Parse options list */
 	foreach(lc, vacstmt->options)
 	{
@@ -204,32 +175,6 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 			verbose = defGetBoolean(opt);
 		else if (strcmp(opt->defname, "skip_locked") == 0)
 			skip_locked = defGetBoolean(opt);
-		else if (strcmp(opt->defname, "buffer_usage_limit") == 0)
-		{
-			const char *hintmsg;
-			int			result;
-			char	   *vac_buffer_size;
-
-			vac_buffer_size = defGetString(opt);
-
-			/*
-			 * Check that the specified value is valid and the size falls
-			 * within the hard upper and lower limits if it is not 0.
-			 */
-			if (!parse_int(vac_buffer_size, &result, GUC_UNIT_KB, &hintmsg) ||
-				(result != 0 &&
-				 (result < MIN_BAS_VAC_RING_SIZE_KB || result > MAX_BAS_VAC_RING_SIZE_KB)))
-			{
-				ereport(ERROR,
-						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-						 errmsg("%s option must be 0 or between %d kB and %d kB",
-								"BUFFER_USAGE_LIMIT",
-								MIN_BAS_VAC_RING_SIZE_KB, MAX_BAS_VAC_RING_SIZE_KB),
-						 hintmsg ? errhint_internal("%s", _(hintmsg)) : 0));
-			}
-
-			ring_size = result;
-		}
 		else if (!vacstmt->is_vacuumcmd)
 			ereport(ERROR,
 					(errcode(ERRCODE_SYNTAX_ERROR),
@@ -325,17 +270,6 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("VACUUM FULL cannot be performed in parallel")));
 
-	/*
-	 * BUFFER_USAGE_LIMIT does nothing for VACUUM (FULL) so just raise an
-	 * ERROR for that case.  VACUUM (FULL, ANALYZE) does make use of it, so
-	 * we'll permit that.
-	 */
-	if (ring_size != -1 && (params.options & VACOPT_FULL) &&
-		!(params.options & VACOPT_ANALYZE))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("BUFFER_USAGE_LIMIT cannot be specified for VACUUM FULL")));
-
 	/*
 	 * Make sure VACOPT_ANALYZE is specified if any column lists are present.
 	 */
@@ -431,38 +365,8 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 										"Vacuum",
 										ALLOCSET_DEFAULT_SIZES);
 
-	/*
-	 * Make a buffer strategy object in the cross-transaction memory context.
-	 * We needn't bother making this for VACUUM (FULL) or VACUUM
-	 * (ONLY_DATABASE_STATS) as they'll not make use of it.  VACUUM (FULL,
-	 * ANALYZE) is possible, so we'd better ensure that we make a strategy
-	 * when we see ANALYZE.
-	 */
-	if ((params.options & (VACOPT_ONLY_DATABASE_STATS |
-						   VACOPT_FULL)) == 0 ||
-		(params.options & VACOPT_ANALYZE) != 0)
-	{
-
-		MemoryContext old_context = MemoryContextSwitchTo(vac_context);
-
-		Assert(ring_size >= -1);
-
-		/*
-		 * If BUFFER_USAGE_LIMIT was specified by the VACUUM or ANALYZE
-		 * command, it overrides the value of VacuumBufferUsageLimit.  Either
-		 * value may be 0, in which case GetAccessStrategyWithSize() will
-		 * return NULL, effectively allowing full use of shared buffers.
-		 */
-		if (ring_size == -1)
-			ring_size = VacuumBufferUsageLimit;
-
-		bstrategy = GetAccessStrategyWithSize(BAS_VACUUM, ring_size);
-
-		MemoryContextSwitchTo(old_context);
-	}
-
 	/* Now go through the common routine */
-	vacuum(vacstmt->rels, &params, bstrategy, vac_context, isTopLevel);
+	vacuum(vacstmt->rels, &params, vac_context, isTopLevel);
 
 	/* Finally, clean up the vacuum memory context */
 	MemoryContextDelete(vac_context);
@@ -479,19 +383,13 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
  * params contains a set of parameters that can be used to customize the
  * behavior.
  *
- * bstrategy may be passed in as NULL when the caller does not want to
- * restrict the number of shared_buffers that VACUUM / ANALYZE can use,
- * otherwise, the caller must build a BufferAccessStrategy with the number of
- * shared_buffers that VACUUM / ANALYZE should try to limit themselves to
- * using.
- *
  * isTopLevel should be passed down from ProcessUtility.
  *
  * It is the caller's responsibility that all parameters are allocated in a
  * memory context that will not disappear at transaction commit.
  */
 void
-vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrategy,
+vacuum(List *relations, const VacuumParams *params,
 	   MemoryContext vac_context, bool isTopLevel)
 {
 	static bool in_vacuum = false;
@@ -630,7 +528,7 @@ vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrate
 
 			if (params->options & VACOPT_VACUUM)
 			{
-				if (!vacuum_rel(vrel->oid, vrel->relation, *params, bstrategy,
+				if (!vacuum_rel(vrel->oid, vrel->relation, *params,
 								isTopLevel))
 					continue;
 			}
@@ -649,7 +547,7 @@ vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrate
 				}
 
 				analyze_rel(vrel->oid, vrel->relation, params,
-							vrel->va_cols, in_outer_xact, bstrategy);
+							vrel->va_cols, in_outer_xact);
 
 				if (use_own_xacts)
 				{
@@ -2010,7 +1908,7 @@ vac_truncate_clog(TransactionId frozenXID,
  */
 static bool
 vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
-		   BufferAccessStrategy bstrategy, bool isTopLevel)
+		   bool isTopLevel)
 {
 	LOCKMODE	lmode;
 	Relation	rel;
@@ -2307,7 +2205,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 			rel = NULL;
 		}
 		else
-			table_relation_vacuum(rel, &params, bstrategy);
+			table_relation_vacuum(rel, &params);
 	}
 
 	/* Roll back any GUC changes executed by index functions */
@@ -2344,7 +2242,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 		toast_vacuum_params.options |= VACOPT_PROCESS_MAIN;
 		toast_vacuum_params.toast_parent = relid;
 
-		vacuum_rel(toast_relid, NULL, toast_vacuum_params, bstrategy,
+		vacuum_rel(toast_relid, NULL, toast_vacuum_params,
 				   isTopLevel);
 	}
 
diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c
index 41cefcfde54..8b4b835db9b 100644
--- a/src/backend/commands/vacuumparallel.c
+++ b/src/backend/commands/vacuumparallel.c
@@ -125,12 +125,6 @@ typedef struct PVShared
 	 */
 	int			maintenance_work_mem_worker;
 
-	/*
-	 * The number of buffers each worker's Buffer Access Strategy ring should
-	 * contain.
-	 */
-	int			ring_nbuffers;
-
 	/*
 	 * Shared vacuum cost balance.  During parallel vacuum,
 	 * VacuumSharedCostBalance points to this value and it accumulates the
@@ -257,9 +251,6 @@ struct ParallelVacuumState
 	int			nindexes_parallel_cleanup;
 	int			nindexes_parallel_condcleanup;
 
-	/* Buffer access strategy used by leader process */
-	BufferAccessStrategy bstrategy;
-
 	/*
 	 * Error reporting state.  The error callback is set only for workers
 	 * processes during parallel index vacuum.
@@ -304,7 +295,7 @@ static void parallel_vacuum_dsm_detach(dsm_segment *seg, Datum arg);
 ParallelVacuumState *
 parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes,
 					 int nrequested_workers, int vac_work_mem,
-					 int elevel, BufferAccessStrategy bstrategy)
+					 int elevel)
 {
 	ParallelVacuumState *pvs;
 	ParallelContext *pcxt;
@@ -345,7 +336,6 @@ parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes,
 	pvs->indrels = indrels;
 	pvs->nindexes = nindexes;
 	pvs->will_parallel_vacuum = will_parallel_vacuum;
-	pvs->bstrategy = bstrategy;
 	pvs->heaprel = rel;
 
 	EnterParallelMode();
@@ -447,9 +437,6 @@ parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes,
 	shared->dead_items_handle = TidStoreGetHandle(dead_items);
 	shared->dead_items_dsa_handle = dsa_get_handle(TidStoreGetDSA(dead_items));
 
-	/* Use the same buffer size for all workers */
-	shared->ring_nbuffers = GetAccessStrategyBufferCount(bstrategy);
-
 	pg_atomic_init_u32(&(shared->cost_balance), 0);
 	pg_atomic_init_u32(&(shared->active_nworkers), 0);
 	pg_atomic_init_u32(&(shared->idx), 0);
@@ -1091,7 +1078,6 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
 	ivinfo.message_level = DEBUG2;
 	ivinfo.estimated_count = pvs->shared->estimated_count;
 	ivinfo.num_heap_tuples = pvs->shared->reltuples;
-	ivinfo.strategy = pvs->bstrategy;
 
 	/* Update error traceback information */
 	pvs->indname = pstrdup(RelationGetRelationName(indrel));
@@ -1294,10 +1280,6 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
 	pvs.indname = NULL;
 	pvs.status = PARALLEL_INDVAC_STATUS_INITIAL;
 
-	/* Each parallel VACUUM worker gets its own access strategy. */
-	pvs.bstrategy = GetAccessStrategyWithSize(BAS_VACUUM,
-											  shared->ring_nbuffers * (BLCKSZ / 1024));
-
 	/* Setup error traceback support for ereport() */
 	errcallback.callback = parallel_vacuum_error_callback;
 	errcallback.arg = &pvs;
@@ -1328,7 +1310,6 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
 
 	vac_close_indexes(nindexes, indrels, RowExclusiveLock);
 	table_close(rel, ShareUpdateExclusiveLock);
-	FreeAccessStrategy(pvs.bstrategy);
 
 	if (shared->is_autovacuum)
 		pv_shared_cost_params = NULL;
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 45abf48768a..e021e798805 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -386,8 +386,7 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
 									  bool *dovacuum, bool *doanalyze, bool *wraparound,
 									  AutoVacuumScores *scores);
 
-static void autovacuum_do_vac_analyze(autovac_table *tab,
-									  BufferAccessStrategy bstrategy);
+static void autovacuum_do_vac_analyze(autovac_table *tab);
 static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
 										 TupleDesc pg_class_desc);
 static void perform_work_item(AutoVacuumWorkItem *workitem);
@@ -1936,7 +1935,6 @@ do_autovacuum(void)
 	HASHCTL		ctl;
 	HTAB	   *table_toast_map;
 	ListCell   *volatile cell;
-	BufferAccessStrategy bstrategy;
 	ScanKeyData key;
 	TupleDesc	pg_class_desc;
 	int			effective_multixact_freeze_max_age;
@@ -2322,23 +2320,6 @@ do_autovacuum(void)
 		autovacuum_analyze_score_weight != 0.0)
 		list_sort(tables_to_process, TableToProcessComparator);
 
-	/*
-	 * Optionally, create a buffer access strategy object for VACUUM to use.
-	 * We use the same BufferAccessStrategy object for all tables VACUUMed by
-	 * this worker to prevent autovacuum from blowing out shared buffers.
-	 *
-	 * VacuumBufferUsageLimit being set to 0 results in
-	 * GetAccessStrategyWithSize returning NULL, effectively meaning we can
-	 * use up to all of shared buffers.
-	 *
-	 * If we later enter failsafe mode on any of the tables being vacuumed, we
-	 * will cease use of the BufferAccessStrategy only for that table.
-	 *
-	 * XXX should we consider adding code to adjust the size of this if
-	 * VacuumBufferUsageLimit changes?
-	 */
-	bstrategy = GetAccessStrategyWithSize(BAS_VACUUM, VacuumBufferUsageLimit);
-
 	/*
 	 * create a memory context to act as fake PortalContext, so that the
 	 * contexts created in the vacuum code are cleaned up for each table.
@@ -2516,7 +2497,7 @@ do_autovacuum(void)
 			MemoryContextSwitchTo(PortalContext);
 
 			/* have at it */
-			autovacuum_do_vac_analyze(tab, bstrategy);
+			autovacuum_do_vac_analyze(tab);
 
 			/*
 			 * Clear a possible query-cancel signal, to avoid a late reaction
@@ -2636,8 +2617,6 @@ deleted:
 #ifdef USE_VALGRIND
 	hash_destroy(table_toast_map);
 	FreeTupleDesc(pg_class_desc);
-	if (bstrategy)
-		pfree(bstrategy);
 #endif
 
 	/* Run the rest in xact context, mainly to avoid Valgrind leak warnings */
@@ -3348,7 +3327,7 @@ relation_needs_vacanalyze(Oid relid,
  * disappear at transaction commit.
  */
 static void
-autovacuum_do_vac_analyze(autovac_table *tab, BufferAccessStrategy bstrategy)
+autovacuum_do_vac_analyze(autovac_table *tab)
 {
 	RangeVar   *rangevar;
 	VacuumRelation *rel;
@@ -3371,7 +3350,7 @@ autovacuum_do_vac_analyze(autovac_table *tab, BufferAccessStrategy bstrategy)
 	rel_list = list_make1(rel);
 	MemoryContextSwitchTo(old_context);
 
-	vacuum(rel_list, &tab->at_params, bstrategy, vac_context, true);
+	vacuum(rel_list, &tab->at_params, vac_context, true);
 
 	MemoryContextDelete(vac_context);
 }
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 73dc539836b..b6612f1e517 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -390,7 +390,7 @@ static List *BuildRelationList(bool temp_relations, bool include_shared);
 static void FreeDatabaseList(List *dblist);
 static DataChecksumsWorkerResult ProcessDatabase(DataChecksumsWorkerDatabase *db);
 static bool ProcessAllDatabases(void);
-static bool ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy);
+static bool ProcessSingleRelationFork(Relation reln, ForkNumber forkNum);
 static void ResetDataChecksumsProgressCounters(void);
 static void launcher_cancel_handler(SIGNAL_ARGS);
 static void WaitForAllTransactionsToFinish(void);
@@ -687,7 +687,7 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
  * error is raised in the lower levels.
  */
 static bool
-ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy)
+ProcessSingleRelationFork(Relation reln, ForkNumber forkNum)
 {
 	BlockNumber numblocks = RelationGetNumberOfBlocksInFork(reln, forkNum);
 	char		activity[NAMEDATALEN * 2 + 128];
@@ -722,7 +722,7 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg
 	 */
 	for (BlockNumber blknum = 0; blknum < numblocks; blknum++)
 	{
-		Buffer		buf = ReadBufferExtended(reln, forkNum, blknum, RBM_NORMAL, strategy);
+		Buffer		buf = ReadBufferExtended(reln, forkNum, blknum, RBM_NORMAL);
 
 		/* Need to get an exclusive lock to mark the buffer as dirty */
 		LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
@@ -808,7 +808,7 @@ ResetDataChecksumsProgressCounters(void)
  * error is raised in the lower levels.
  */
 static bool
-ProcessSingleRelationByOid(Oid relationId, BufferAccessStrategy strategy)
+ProcessSingleRelationByOid(Oid relationId)
 {
 	Relation	rel;
 	bool		aborted = false;
@@ -835,7 +835,7 @@ ProcessSingleRelationByOid(Oid relationId, BufferAccessStrategy strategy)
 	{
 		if (smgrexists(rel->rd_smgr, fnum))
 		{
-			if (!ProcessSingleRelationFork(rel, fnum, strategy))
+			if (!ProcessSingleRelationFork(rel, fnum))
 			{
 				aborted = true;
 				break;
@@ -1582,7 +1582,6 @@ DataChecksumsWorkerMain(Datum arg)
 	Oid			dboid;
 	List	   *RelationList = NIL;
 	List	   *InitialTempTableList = NIL;
-	BufferAccessStrategy strategy;
 	bool		aborted = false;
 	int64		rels_done;
 	bool		process_shared;
@@ -1650,11 +1649,6 @@ DataChecksumsWorkerMain(Datum arg)
 	VacuumUpdateCosts();
 	VacuumCostBalance = 0;
 
-	/*
-	 * Create and set the vacuum strategy as our buffer strategy.
-	 */
-	strategy = GetAccessStrategy(BAS_VACUUM);
-
 	RelationList = BuildRelationList(false, process_shared);
 
 	/* Update the total number of relations to be processed in this DB. */
@@ -1676,9 +1670,7 @@ DataChecksumsWorkerMain(Datum arg)
 	rels_done = 0;
 	foreach_oid(reloid, RelationList)
 	{
-		bool		costs_updated = false;
-
-		if (!ProcessSingleRelationByOid(reloid, strategy))
+		if (!ProcessSingleRelationByOid(reloid))
 		{
 			aborted = true;
 			break;
@@ -1694,8 +1686,7 @@ DataChecksumsWorkerMain(Datum arg)
 
 		/*
 		 * Check if the cost settings changed during runtime and if so, update
-		 * to reflect the new values and signal that the access strategy needs
-		 * to be refreshed.
+		 * to reflect the new values.
 		 */
 		LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
 		if (DataChecksumState->worker_invocation != worker_invocation)
@@ -1706,7 +1697,6 @@ DataChecksumsWorkerMain(Datum arg)
 		if ((DataChecksumState->launch_cost_delay != DataChecksumState->cost_delay)
 			|| (DataChecksumState->launch_cost_limit != DataChecksumState->cost_limit))
 		{
-			costs_updated = true;
 			VacuumCostDelay = DataChecksumState->launch_cost_delay;
 			VacuumCostLimit = DataChecksumState->launch_cost_limit;
 			VacuumUpdateCosts();
@@ -1714,19 +1704,10 @@ DataChecksumsWorkerMain(Datum arg)
 			DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
 			DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
 		}
-		else
-			costs_updated = false;
 		LWLockRelease(DataChecksumsWorkerLock);
-
-		if (costs_updated)
-		{
-			FreeAccessStrategy(strategy);
-			strategy = GetAccessStrategy(BAS_VACUUM);
-		}
 	}
 
 	list_free(RelationList);
-	FreeAccessStrategy(strategy);
 
 	if (aborted || abort_requested)
 	{
diff --git a/src/backend/storage/aio/read_stream.c b/src/backend/storage/aio/read_stream.c
index a318539e56c..08d344f063d 100644
--- a/src/backend/storage/aio/read_stream.c
+++ b/src/backend/storage/aio/read_stream.c
@@ -757,7 +757,6 @@ read_stream_look_ahead(ReadStream *stream)
  */
 static ReadStream *
 read_stream_begin_impl(int flags,
-					   BufferAccessStrategy strategy,
 					   Relation rel,
 					   SMgrRelation smgr,
 					   char persistence,
@@ -771,7 +770,6 @@ read_stream_begin_impl(int flags,
 	int16		queue_size;
 	int16		queue_overflow;
 	int			max_ios;
-	int			strategy_pin_limit;
 	uint32		max_pinned_buffers;
 	uint32		max_possible_buffer_limit;
 	Oid			tablespace_id;
@@ -835,10 +833,6 @@ read_stream_begin_impl(int flags,
 	max_pinned_buffers = Min(max_pinned_buffers,
 							 PG_INT16_MAX - queue_overflow - 1);
 
-	/* Give the strategy a chance to limit the number of buffers we pin. */
-	strategy_pin_limit = GetAccessStrategyPinLimit(strategy);
-	max_pinned_buffers = Min(strategy_pin_limit, max_pinned_buffers);
-
 	/*
 	 * Also limit our queue to the maximum number of pins we could ever be
 	 * allowed to acquire according to the buffer manager.  We may not really
@@ -962,7 +956,6 @@ read_stream_begin_impl(int flags,
 		stream->ios[i].op.smgr = smgr;
 		stream->ios[i].op.persistence = persistence;
 		stream->ios[i].op.forknum = forknum;
-		stream->ios[i].op.strategy = strategy;
 	}
 
 	return stream;
@@ -974,7 +967,6 @@ read_stream_begin_impl(int flags,
  */
 ReadStream *
 read_stream_begin_relation(int flags,
-						   BufferAccessStrategy strategy,
 						   Relation rel,
 						   ForkNumber forknum,
 						   ReadStreamBlockNumberCB callback,
@@ -982,7 +974,6 @@ read_stream_begin_relation(int flags,
 						   size_t per_buffer_data_size)
 {
 	return read_stream_begin_impl(flags,
-								  strategy,
 								  rel,
 								  RelationGetSmgr(rel),
 								  rel->rd_rel->relpersistence,
@@ -998,7 +989,6 @@ read_stream_begin_relation(int flags,
  */
 ReadStream *
 read_stream_begin_smgr_relation(int flags,
-								BufferAccessStrategy strategy,
 								SMgrRelation smgr,
 								char smgr_persistence,
 								ForkNumber forknum,
@@ -1007,7 +997,6 @@ read_stream_begin_smgr_relation(int flags,
 								size_t per_buffer_data_size)
 {
 	return read_stream_begin_impl(flags,
-								  strategy,
 								  NULL,
 								  smgr,
 								  smgr_persistence,
@@ -1370,13 +1359,11 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data)
  * Transitional support for code that would like to perform or skip reads
  * itself, without using the stream.  Returns, and consumes, the next block
  * number that would be read by the stream's look-ahead algorithm, or
- * InvalidBlockNumber if the end of the stream is reached.  Also reports the
- * strategy that would be used to read it.
+ * InvalidBlockNumber if the end of the stream is reached.
  */
 BlockNumber
-read_stream_next_block(ReadStream *stream, BufferAccessStrategy *strategy)
+read_stream_next_block(ReadStream *stream)
 {
-	*strategy = stream->ios[0].op.strategy;
 	return read_stream_get_block(stream, NULL);
 }
 
diff --git a/src/backend/storage/buffer/README b/src/backend/storage/buffer/README
index b332e002ba1..58df6bd011a 100644
--- a/src/backend/storage/buffer/README
+++ b/src/backend/storage/buffer/README
@@ -203,50 +203,6 @@ have to give up and try another buffer.  This however is not a concern
 of the basic select-a-victim-buffer algorithm.)
 
 
-Buffer Ring Replacement Strategy
----------------------------------
-
-When running a query that needs to access a large number of pages just once,
-such as VACUUM or a large sequential scan, a different strategy is used.
-A page that has been touched only by such a scan is unlikely to be needed
-again soon, so instead of running the normal clock-sweep algorithm and
-blowing out the entire buffer cache, a small ring of buffers is allocated
-using the normal clock-sweep algorithm and those buffers are reused for the
-whole scan.  This also implies that much of the write traffic caused by such
-a statement will be done by the backend itself and not pushed off onto other
-processes.
-
-For sequential scans, a 256KB ring is used. That's small enough to fit in L2
-cache, which makes transferring pages from OS cache to shared buffer cache
-efficient.  Even less would often be enough, but the ring must be big enough
-to accommodate all pages in the scan that are pinned concurrently.  256KB
-should also be enough to leave a small cache trail for other backends to
-join in a synchronized seq scan.  If a ring buffer is dirtied and its LSN
-updated, we would normally have to write and flush WAL before we could
-re-use the buffer; in this case we instead discard the buffer from the ring
-and (later) choose a replacement using the normal clock-sweep algorithm.
-Hence this strategy works best for scans that are read-only (or at worst
-update hint bits).  In a scan that modifies every page in the scan, like a
-bulk UPDATE or DELETE, the buffers in the ring will always be dirtied and
-the ring strategy effectively degrades to the normal strategy.
-
-VACUUM uses a ring like sequential scans, however, the size of this ring is
-controlled by the vacuum_buffer_usage_limit GUC.  Dirty pages are not removed
-from the ring.  Instead, the WAL is flushed if needed to allow reuse of the
-buffers.  Before introducing the buffer ring strategy in 8.3, VACUUM's buffers
-were sent to the freelist, which was effectively a buffer ring of 1 buffer,
-resulting in excessive WAL flushing.
-
-Bulk writes work similarly to VACUUM.  Currently this applies only to
-COPY IN and CREATE TABLE AS SELECT.  (Might it be interesting to make
-seqscan UPDATE and DELETE use the bulkwrite strategy?)  For bulk writes
-we use a ring size of 16MB (but not more than 1/8th of shared_buffers).
-Smaller sizes have been shown to result in the COPY blocking too often
-for WAL flushes.  While it's okay for a background vacuum to be slowed by
-doing its own WAL flushing, we'd prefer that COPY not be subject to that,
-so we let it use up a bit more of the buffer arena.
-
-
 Background Writer's Processing
 ------------------------------
 
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 1173b50b7c1..30f2443e577 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -612,10 +612,9 @@ ForgetPrivateRefCountEntry(PrivateRefCountEntry *ref)
 static Buffer ReadBuffer_common(Relation rel,
 								SMgrRelation smgr, char smgr_persistence,
 								ForkNumber forkNum, BlockNumber blockNum,
-								ReadBufferMode mode, BufferAccessStrategy strategy);
+								ReadBufferMode mode);
 static BlockNumber ExtendBufferedRelCommon(BufferManagerRelation bmr,
 										   ForkNumber fork,
-										   BufferAccessStrategy strategy,
 										   uint32 flags,
 										   uint32 extend_by,
 										   BlockNumber extend_upto,
@@ -623,13 +622,12 @@ static BlockNumber ExtendBufferedRelCommon(BufferManagerRelation bmr,
 										   uint32 *extended_by);
 static BlockNumber ExtendBufferedRelShared(BufferManagerRelation bmr,
 										   ForkNumber fork,
-										   BufferAccessStrategy strategy,
 										   uint32 flags,
 										   uint32 extend_by,
 										   BlockNumber extend_upto,
 										   Buffer *buffers,
 										   uint32 *extended_by);
-static bool PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy,
+static bool PinBuffer(BufferDesc *buf,
 					  bool skip_if_not_valid);
 static void PinBuffer_Locked(BufferDesc *buf);
 static void UnpinBuffer(BufferDesc *buf);
@@ -645,7 +643,6 @@ static inline BufferDesc *BufferAlloc(SMgrRelation smgr,
 									  char relpersistence,
 									  ForkNumber forkNum,
 									  BlockNumber blockNum,
-									  BufferAccessStrategy strategy,
 									  bool *foundPtr, IOContext io_context);
 static bool AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress);
 static void CheckReadBuffersOperation(ReadBuffersOperation *operation, bool is_complete);
@@ -654,7 +651,7 @@ static pg_attribute_always_inline void TrackBufferHit(IOObject io_object,
 													  IOContext io_context,
 													  Relation rel, char persistence, SMgrRelation smgr,
 													  ForkNumber forknum, BlockNumber blocknum);
-static Buffer GetVictimBuffer(BufferAccessStrategy strategy, IOContext io_context);
+static Buffer GetVictimBuffer(IOContext io_context);
 static void FlushUnlockedBuffer(BufferDesc *buf, SMgrRelation reln,
 								IOObject io_object, IOContext io_context);
 static void FlushBuffer(BufferDesc *buf, SMgrRelation reln,
@@ -858,7 +855,7 @@ ReadRecentBuffer(RelFileLocator rlocator, ForkNumber forkNum, BlockNumber blockN
 		 * pin.
 		 */
 		if (BufferTagsEqual(&tag, &bufHdr->tag) &&
-			PinBuffer(bufHdr, NULL, true))
+			PinBuffer(bufHdr, true))
 		{
 			if (BufferTagsEqual(&tag, &bufHdr->tag))
 			{
@@ -874,12 +871,12 @@ ReadRecentBuffer(RelFileLocator rlocator, ForkNumber forkNum, BlockNumber blockN
 
 /*
  * ReadBuffer -- a shorthand for ReadBufferExtended, for reading from main
- *		fork with RBM_NORMAL mode and default strategy.
+ *		fork with RBM_NORMAL mode.
  */
 Buffer
 ReadBuffer(Relation reln, BlockNumber blockNum)
 {
-	return ReadBufferExtended(reln, MAIN_FORKNUM, blockNum, RBM_NORMAL, NULL);
+	return ReadBufferExtended(reln, MAIN_FORKNUM, blockNum, RBM_NORMAL);
 }
 
 /*
@@ -919,13 +916,10 @@ ReadBuffer(Relation reln, BlockNumber blockNum)
  * a cleanup-strength lock on the page.
  *
  * RBM_NORMAL_NO_LOG mode is treated the same as RBM_NORMAL here.
- *
- * If strategy is not NULL, a nondefault buffer access strategy is used.
- * See buffer/README for details.
  */
 inline Buffer
 ReadBufferExtended(Relation reln, ForkNumber forkNum, BlockNumber blockNum,
-				   ReadBufferMode mode, BufferAccessStrategy strategy)
+				   ReadBufferMode mode)
 {
 	Buffer		buf;
 
@@ -935,7 +929,7 @@ ReadBufferExtended(Relation reln, ForkNumber forkNum, BlockNumber blockNum,
 	 * ReadBuffer_common().
 	 */
 	buf = ReadBuffer_common(reln, RelationGetSmgr(reln), 0,
-							forkNum, blockNum, mode, strategy);
+							forkNum, blockNum, mode);
 
 	return buf;
 }
@@ -954,14 +948,14 @@ ReadBufferExtended(Relation reln, ForkNumber forkNum, BlockNumber blockNum,
 Buffer
 ReadBufferWithoutRelcache(RelFileLocator rlocator, ForkNumber forkNum,
 						  BlockNumber blockNum, ReadBufferMode mode,
-						  BufferAccessStrategy strategy, bool permanent)
+						  bool permanent)
 {
 	SMgrRelation smgr = smgropen(rlocator, INVALID_PROC_NUMBER);
 
 	return ReadBuffer_common(NULL, smgr,
 							 permanent ? RELPERSISTENCE_PERMANENT : RELPERSISTENCE_UNLOGGED,
 							 forkNum, blockNum,
-							 mode, strategy);
+							 mode);
 }
 
 /*
@@ -970,13 +964,12 @@ ReadBufferWithoutRelcache(RelFileLocator rlocator, ForkNumber forkNum,
 Buffer
 ExtendBufferedRel(BufferManagerRelation bmr,
 				  ForkNumber forkNum,
-				  BufferAccessStrategy strategy,
 				  uint32 flags)
 {
 	Buffer		buf;
 	uint32		extend_by = 1;
 
-	ExtendBufferedRelBy(bmr, forkNum, strategy, flags, extend_by,
+	ExtendBufferedRelBy(bmr, forkNum, flags, extend_by,
 						&buf, &extend_by);
 
 	return buf;
@@ -1002,7 +995,6 @@ ExtendBufferedRel(BufferManagerRelation bmr,
 BlockNumber
 ExtendBufferedRelBy(BufferManagerRelation bmr,
 					ForkNumber fork,
-					BufferAccessStrategy strategy,
 					uint32 flags,
 					uint32 extend_by,
 					Buffer *buffers,
@@ -1015,7 +1007,7 @@ ExtendBufferedRelBy(BufferManagerRelation bmr,
 	if (bmr.relpersistence == '\0')
 		bmr.relpersistence = bmr.rel->rd_rel->relpersistence;
 
-	return ExtendBufferedRelCommon(bmr, fork, strategy, flags,
+	return ExtendBufferedRelCommon(bmr, fork, flags,
 								   extend_by, InvalidBlockNumber,
 								   buffers, extended_by);
 }
@@ -1031,7 +1023,6 @@ ExtendBufferedRelBy(BufferManagerRelation bmr,
 Buffer
 ExtendBufferedRelTo(BufferManagerRelation bmr,
 					ForkNumber fork,
-					BufferAccessStrategy strategy,
 					uint32 flags,
 					BlockNumber extend_to,
 					ReadBufferMode mode)
@@ -1097,7 +1088,7 @@ ExtendBufferedRelTo(BufferManagerRelation bmr,
 		if ((uint64) current_size + num_pages > extend_to)
 			num_pages = extend_to - current_size;
 
-		first_block = ExtendBufferedRelCommon(bmr, fork, strategy, flags,
+		first_block = ExtendBufferedRelCommon(bmr, fork, flags,
 											  num_pages, extend_to,
 											  buffers, &extended_by);
 
@@ -1123,7 +1114,7 @@ ExtendBufferedRelTo(BufferManagerRelation bmr,
 	{
 		Assert(extended_by == 0);
 		buffer = ReadBuffer_common(bmr.rel, BMR_GET_SMGR(bmr), bmr.relpersistence,
-								   fork, extend_to - 1, mode, strategy);
+								   fork, extend_to - 1, mode);
 	}
 
 	return buffer;
@@ -1226,7 +1217,6 @@ PinBufferForBlock(Relation rel,
 				  char persistence,
 				  ForkNumber forkNum,
 				  BlockNumber blockNum,
-				  BufferAccessStrategy strategy,
 				  IOObject io_object,
 				  IOContext io_context,
 				  bool *foundPtr)
@@ -1250,7 +1240,7 @@ PinBufferForBlock(Relation rel,
 		bufHdr = LocalBufferAlloc(smgr, forkNum, blockNum, foundPtr);
 	else
 		bufHdr = BufferAlloc(smgr, persistence, forkNum, blockNum,
-							 strategy, foundPtr, io_context);
+							 foundPtr, io_context);
 
 	if (*foundPtr)
 		TrackBufferHit(io_object, io_context, rel, persistence, smgr, forkNum, blockNum);
@@ -1276,8 +1266,7 @@ PinBufferForBlock(Relation rel,
 static pg_attribute_always_inline Buffer
 ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
 				  ForkNumber forkNum,
-				  BlockNumber blockNum, ReadBufferMode mode,
-				  BufferAccessStrategy strategy)
+				  BlockNumber blockNum, ReadBufferMode mode)
 {
 	ReadBuffersOperation operation;
 	Buffer		buffer;
@@ -1313,7 +1302,7 @@ ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
 		if (mode == RBM_ZERO_AND_LOCK || mode == RBM_ZERO_AND_CLEANUP_LOCK)
 			flags |= EB_LOCK_FIRST;
 
-		return ExtendBufferedRel(BMR_REL(rel), forkNum, strategy, flags);
+		return ExtendBufferedRel(BMR_REL(rel), forkNum, flags);
 	}
 
 	if (rel)
@@ -1335,12 +1324,12 @@ ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
 		}
 		else
 		{
-			io_context = IOContextForStrategy(strategy);
+			io_context = IOCONTEXT_NORMAL;
 			io_object = IOOBJECT_RELATION;
 		}
 
 		buffer = PinBufferForBlock(rel, smgr, persistence,
-								   forkNum, blockNum, strategy,
+								   forkNum, blockNum,
 								   io_object, io_context, &found);
 		ZeroAndLockBuffer(buffer, mode, found);
 		return buffer;
@@ -1358,7 +1347,6 @@ ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
 	operation.rel = rel;
 	operation.persistence = persistence;
 	operation.forknum = forkNum;
-	operation.strategy = strategy;
 	if (StartReadBuffer(&operation,
 						&buffer,
 						blockNum,
@@ -1399,7 +1387,7 @@ StartReadBuffersImpl(ReadBuffersOperation *operation,
 	}
 	else
 	{
-		io_context = IOContextForStrategy(operation->strategy);
+		io_context = IOCONTEXT_NORMAL;
 		io_object = IOOBJECT_RELATION;
 	}
 
@@ -1450,7 +1438,6 @@ StartReadBuffersImpl(ReadBuffersOperation *operation,
 										   operation->persistence,
 										   operation->forknum,
 										   blockNum + i,
-										   operation->strategy,
 										   io_object, io_context,
 										   &found);
 		}
@@ -1771,7 +1758,7 @@ WaitReadBuffers(ReadBuffersOperation *operation)
 	}
 	else
 	{
-		io_context = IOContextForStrategy(operation->strategy);
+		io_context = IOCONTEXT_NORMAL;
 		io_object = IOOBJECT_RELATION;
 	}
 
@@ -1961,7 +1948,7 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress)
 	}
 	else
 	{
-		io_context = IOContextForStrategy(operation->strategy);
+		io_context = IOCONTEXT_NORMAL;
 		io_object = IOOBJECT_RELATION;
 	}
 
@@ -2180,24 +2167,18 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress)
  *		buffer.  If no buffer exists already, selects a replacement victim and
  *		evicts the old page, but does NOT read in new page.
  *
- * "strategy" can be a buffer replacement strategy object, or NULL for
- * the default strategy.  The selected buffer's usage_count is advanced when
- * using the default strategy, but otherwise possibly not (see PinBuffer).
- *
  * The returned buffer is pinned and is already marked as holding the
  * desired page.  If it already did have the desired page, *foundPtr is
  * set true.  Otherwise, *foundPtr is set false.
  *
- * io_context is passed as an output parameter to avoid calling
- * IOContextForStrategy() when there is a shared buffers hit and no IO
- * statistics need be captured.
+ * io_context is passed as an output parameter to avoid capturing IO
+ * statistics when there is a shared buffers hit and no IO occurs.
  *
  * No locks are held either at entry or exit.
  */
 static pg_attribute_always_inline BufferDesc *
 BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 			BlockNumber blockNum,
-			BufferAccessStrategy strategy,
 			bool *foundPtr, IOContext io_context)
 {
 	BufferTag	newTag;			/* identity of requested block */
@@ -2235,7 +2216,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 		 */
 		buf = GetBufferDescriptor(existing_buf_id);
 
-		valid = PinBuffer(buf, strategy, false);
+		valid = PinBuffer(buf, false);
 
 		/* Can release the mapping lock as soon as we've pinned it */
 		LWLockRelease(newPartitionLock);
@@ -2266,7 +2247,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	 * don't hold any conflicting locks. If so we'll have to undo our work
 	 * later.
 	 */
-	victim_buffer = GetVictimBuffer(strategy, io_context);
+	victim_buffer = GetVictimBuffer(io_context);
 	victim_buf_hdr = GetBufferDescriptor(victim_buffer - 1);
 
 	/*
@@ -2297,7 +2278,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 
 		existing_buf_hdr = GetBufferDescriptor(existing_buf_id);
 
-		valid = PinBuffer(existing_buf_hdr, strategy, false);
+		valid = PinBuffer(existing_buf_hdr, false);
 
 		/* Can release the mapping lock as soon as we've pinned it */
 		LWLockRelease(newPartitionLock);
@@ -2335,9 +2316,12 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	 * just like permanent relations.
 	 */
 	set_bits |= BM_TAG_VALID;
-	/* Admit the newly loaded page COOL (probation); a second access via
+
+	/*
+	 * Admit the newly loaded page COOL (probation); a second access via
 	 * PinBuffer promotes it to HOT.  This is what makes a one-touch scan
-	 * self-evicting -- see the cooling-state notes in buf_internals.h. */
+	 * self-evicting -- see the cooling-state notes in buf_internals.h.
+	 */
 	if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM)
 		set_bits |= BM_PERMANENT;
 
@@ -2549,12 +2533,11 @@ InvalidateVictimBuffer(BufferDesc *buf_hdr)
 }
 
 static Buffer
-GetVictimBuffer(BufferAccessStrategy strategy, IOContext io_context)
+GetVictimBuffer(IOContext io_context)
 {
 	BufferDesc *buf_hdr;
 	Buffer		buf;
 	uint64		buf_state;
-	bool		from_ring;
 
 	/*
 	 * Ensure, before we pin a victim buffer, that there's a free refcount
@@ -2570,7 +2553,7 @@ again:
 	 * Select a victim buffer.  The buffer is returned pinned and owned by
 	 * this backend.
 	 */
-	buf_hdr = StrategyGetBuffer(strategy, &buf_state, &from_ring);
+	buf_hdr = StrategyGetBuffer(&buf_state);
 	buf = BufferDescriptorGetBuffer(buf_hdr);
 
 	/*
@@ -2614,26 +2597,6 @@ again:
 			goto again;
 		}
 
-		/*
-		 * If using a nondefault strategy, and this victim came from the
-		 * strategy ring, let the strategy decide whether to reject it when
-		 * reusing it would require a WAL flush.  This only applies to
-		 * permanent buffers; unlogged buffers can have fake LSNs, so
-		 * XLogNeedsFlush() is not meaningful for them.
-		 *
-		 * We need to hold the content lock in at least share-exclusive mode
-		 * to safely inspect the page LSN, so this couldn't have been done
-		 * inside StrategyGetBuffer().
-		 */
-		if (strategy && from_ring &&
-			buf_state & BM_PERMANENT &&
-			XLogNeedsFlush(BufferGetLSN(buf_hdr)) &&
-			StrategyRejectBuffer(strategy, buf_hdr, from_ring))
-		{
-			UnlockReleaseBuffer(buf);
-			goto again;
-		}
-
 		/* OK, do the I/O */
 		FlushBuffer(buf_hdr, NULL, IOOBJECT_RELATION, io_context);
 		LockBuffer(buf, BUFFER_LOCK_UNLOCK);
@@ -2646,23 +2609,15 @@ again:
 	if (buf_state & BM_VALID)
 	{
 		/*
-		 * When a BufferAccessStrategy is in use, blocks evicted from shared
-		 * buffers are counted as IOOP_EVICT in the corresponding context
-		 * (e.g. IOCONTEXT_BULKWRITE). Shared buffers are evicted by a
-		 * strategy in two cases: 1) while initially claiming buffers for the
-		 * strategy ring 2) to replace an existing strategy ring buffer
-		 * because it is pinned or in use and cannot be reused.
+		 * Blocks evicted from shared buffers are counted as IOOP_EVICT.
 		 *
-		 * Blocks evicted from buffers already in the strategy ring are
-		 * counted as IOOP_REUSE in the corresponding strategy context.
-		 *
-		 * At this point, we can accurately count evictions and reuses,
-		 * because we have successfully claimed the valid buffer. Previously,
-		 * we may have been forced to release the buffer due to concurrent
-		 * pinners or erroring out.
+		 * At this point, we can accurately count evictions, because we have
+		 * successfully claimed the valid buffer. Previously, we may have been
+		 * forced to release the buffer due to concurrent pinners or erroring
+		 * out.
 		 */
 		pgstat_count_io_op(IOOBJECT_RELATION, io_context,
-						   from_ring ? IOOP_REUSE : IOOP_EVICT, 1, 0);
+						   IOOP_EVICT, 1, 0);
 	}
 
 	/*
@@ -2754,7 +2709,6 @@ LimitAdditionalPins(uint32 *additional_pins)
 static BlockNumber
 ExtendBufferedRelCommon(BufferManagerRelation bmr,
 						ForkNumber fork,
-						BufferAccessStrategy strategy,
 						uint32 flags,
 						uint32 extend_by,
 						BlockNumber extend_upto,
@@ -2789,7 +2743,7 @@ ExtendBufferedRelCommon(BufferManagerRelation bmr,
 											 buffers, &extend_by);
 	}
 	else
-		first_block = ExtendBufferedRelShared(bmr, fork, strategy, flags,
+		first_block = ExtendBufferedRelShared(bmr, fork, flags,
 											  extend_by, extend_upto,
 											  buffers, &extend_by);
 	*extended_by = extend_by;
@@ -2812,7 +2766,6 @@ ExtendBufferedRelCommon(BufferManagerRelation bmr,
 static BlockNumber
 ExtendBufferedRelShared(BufferManagerRelation bmr,
 						ForkNumber fork,
-						BufferAccessStrategy strategy,
 						uint32 flags,
 						uint32 extend_by,
 						BlockNumber extend_upto,
@@ -2820,7 +2773,7 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 						uint32 *extended_by)
 {
 	BlockNumber first_block;
-	IOContext	io_context = IOContextForStrategy(strategy);
+	IOContext	io_context = IOCONTEXT_NORMAL;
 	instr_time	io_start;
 
 	LimitAdditionalPins(&extend_by);
@@ -2839,7 +2792,7 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 	{
 		Block		buf_block;
 
-		buffers[i] = GetVictimBuffer(strategy, io_context);
+		buffers[i] = GetVictimBuffer(io_context);
 		buf_block = BufHdrGetBlock(GetBufferDescriptor(buffers[i] - 1));
 
 		/* new buffers are zero-filled */
@@ -2957,7 +2910,7 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 			 * Pin the existing buffer before releasing the partition lock,
 			 * preventing it from being evicted.
 			 */
-			valid = PinBuffer(existing_hdr, strategy, false);
+			valid = PinBuffer(existing_hdr, false);
 
 			LWLockRelease(partition_lock);
 			UnpinBuffer(victim_buf_hdr);
@@ -3007,8 +2960,11 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 			victim_buf_hdr->tag = tag;
 
 			set_bits |= BM_TAG_VALID;
-			/* Admit COOL (probation); see the comment at the other admission
-			 * site and the cooling-state notes in buf_internals.h. */
+
+			/*
+			 * Admit COOL (probation); see the comment at the other admission
+			 * site and the cooling-state notes in buf_internals.h.
+			 */
 			if (bmr.relpersistence == RELPERSISTENCE_PERMANENT || fork == INIT_FORKNUM)
 				set_bits |= BM_PERMANENT;
 
@@ -3275,13 +3231,8 @@ ReleaseAndReadBuffer(Buffer buffer,
 /*
  * PinBuffer -- make buffer unavailable for replacement.
  *
- * For the default access strategy, the buffer's usage_count is incremented
- * when we first pin it; for other strategies we just make sure the usage_count
- * isn't zero.  (The idea of the latter is that we don't want synchronized
- * heap scans to inflate the count, but we need it to not be zero to discourage
- * other backends from stealing buffers from our ring.  As long as we cycle
- * through the ring faster than the global clock-sweep cycles, buffers in
- * our ring won't be chosen as victims for replacement by other backends.)
+ * The buffer's cooling state is promoted to HOT (the 2Q rescue) when we pin
+ * it; see the cooling-state notes in buf_internals.h.
  *
  * This should be applied only to shared buffers, never local ones.
  *
@@ -3298,7 +3249,7 @@ ReleaseAndReadBuffer(Buffer buffer,
  * (recently) invalid and has not been pinned.
  */
 static bool
-PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy,
+PinBuffer(BufferDesc *buf,
 		  bool skip_if_not_valid)
 {
 	Buffer		b = BufferDescriptorGetBuffer(buf);
@@ -3396,7 +3347,7 @@ PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy,
  * ResourceOwnerEnlarge(CurrentResourceOwner);
  *
  * Currently, no callers of this function want to modify the buffer's
- * usage_count at all, so there's no need for a strategy parameter.
+ * cooling state at all.
  * Also we don't bother with a BM_VALID test (the caller could check that for
  * itself).
  *
@@ -4659,22 +4610,8 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object,
 			  false);
 
 	/*
-	 * When a strategy is in use, only flushes of dirty buffers already in the
-	 * strategy ring are counted as strategy writes (IOCONTEXT
-	 * [BULKREAD|BULKWRITE|VACUUM] IOOP_WRITE) for the purpose of IO
-	 * statistics tracking.
-	 *
-	 * If a shared buffer initially added to the ring must be flushed before
-	 * being used, this is counted as an IOCONTEXT_NORMAL IOOP_WRITE.
-	 *
-	 * If a shared buffer which was added to the ring later because the
-	 * current strategy buffer is pinned or in use or because all strategy
-	 * buffers were dirty and rejected (for BAS_BULKREAD operations only)
-	 * requires flushing, this is counted as an IOCONTEXT_NORMAL IOOP_WRITE
-	 * (from_ring will be false).
-	 *
-	 * When a strategy is not in use, the write can only be a "regular" write
-	 * of a dirty shared buffer (IOCONTEXT_NORMAL IOOP_WRITE).
+	 * All writes of a dirty shared buffer are counted as an IOCONTEXT_NORMAL
+	 * IOOP_WRITE for the purpose of IO statistics tracking.
 	 */
 	pgstat_count_io_op_time(io_object, io_context,
 							IOOP_WRITE, io_start, 1, BLCKSZ);
@@ -5435,8 +5372,6 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 	BlockNumber nblocks;
 	BlockNumber blkno;
 	PGIOAlignedBlock buf;
-	BufferAccessStrategy bstrategy_src;
-	BufferAccessStrategy bstrategy_dst;
 	BlockRangeReadStreamPrivate p;
 	ReadStream *src_stream;
 	SMgrRelation src_smgr;
@@ -5464,10 +5399,6 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 	smgrextend(smgropen(dstlocator, INVALID_PROC_NUMBER), forkNum, nblocks - 1,
 			   buf.data, true);
 
-	/* This is a bulk operation, so use buffer access strategies. */
-	bstrategy_src = GetAccessStrategy(BAS_BULKREAD);
-	bstrategy_dst = GetAccessStrategy(BAS_BULKWRITE);
-
 	/* Initialize streaming read */
 	p.current_blocknum = 0;
 	p.last_exclusive = nblocks;
@@ -5479,7 +5410,6 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 	 */
 	src_stream = read_stream_begin_smgr_relation(READ_STREAM_FULL |
 												 READ_STREAM_USE_BATCHING,
-												 bstrategy_src,
 												 src_smgr,
 												 permanent ? RELPERSISTENCE_PERMANENT : RELPERSISTENCE_UNLOGGED,
 												 forkNum,
@@ -5499,7 +5429,7 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 
 		dstBuf = ReadBufferWithoutRelcache(dstlocator, forkNum,
 										   BufferGetBlockNumber(srcBuf),
-										   RBM_ZERO_AND_LOCK, bstrategy_dst,
+										   RBM_ZERO_AND_LOCK,
 										   permanent);
 		dstPage = BufferGetPage(dstBuf);
 
@@ -5520,9 +5450,6 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 	}
 	Assert(read_stream_next_buffer(src_stream, NULL) == InvalidBuffer);
 	read_stream_end(src_stream);
-
-	FreeAccessStrategy(bstrategy_src);
-	FreeAccessStrategy(bstrategy_dst);
 }
 
 /* ---------------------------------------------------------------------
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index cc77144224c..63b2aa9b607 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -77,40 +77,6 @@ const ShmemCallbacks StrategyCtlShmemCallbacks = {
 	.init_fn = StrategyCtlShmemInit,
 };
 
-/*
- * Private (non-shared) state for managing a ring of shared buffers to re-use.
- * This is currently the only kind of BufferAccessStrategy object, but someday
- * we might have more kinds.
- */
-typedef struct BufferAccessStrategyData
-{
-	/* Overall strategy type */
-	BufferAccessStrategyType btype;
-	/* Number of elements in buffers[] array */
-	int			nbuffers;
-
-	/*
-	 * Index of the "current" slot in the ring, ie, the one most recently
-	 * returned by GetBufferFromRing.
-	 */
-	int			current;
-
-	/*
-	 * Array of buffer numbers.  InvalidBuffer (that is, zero) indicates we
-	 * have not yet selected a buffer for this ring slot.  For allocation
-	 * simplicity this is palloc'd together with the fixed fields of the
-	 * struct.
-	 */
-	Buffer		buffers[FLEXIBLE_ARRAY_MEMBER];
-}			BufferAccessStrategyData;
-
-
-/* Prototypes for internal functions */
-static BufferDesc *GetBufferFromRing(BufferAccessStrategy strategy,
-									 uint64 *buf_state);
-static void AddBufferToRing(BufferAccessStrategy strategy,
-							BufferDesc *buf);
-
 /*
  * Per-backend state for the batched clock sweep.  Each backend claims a run
  * of consecutive clock-hand values with a single atomic fetch-add and then
@@ -157,8 +123,8 @@ ClockSweepTick(void)
 			 * must hold the spinlock so StrategySyncStart() can read
 			 * nextVictimBuffer and completePasses consistently.
 			 *
-			 * With batching, multiple backends may each land a fetch-add
-			 * that returns a value past NBuffers in the same pass.  After
+			 * With batching, multiple backends may each land a fetch-add that
+			 * returns a value past NBuffers in the same pass.  After
 			 * acquiring the spinlock we re-read the counter: if another
 			 * backend already wrapped it below NBuffers we are done.
 			 */
@@ -196,8 +162,6 @@ ClockSweepTick(void)
  *	GetVictimBuffer(). The only hard requirement GetVictimBuffer() has is that
  *	the selected buffer must not currently be pinned by anyone.
  *
- *	strategy is a BufferAccessStrategy object, or NULL for default strategy.
- *
  *	It is the callers responsibility to ensure the buffer ownership can be
  *	tracked via TrackNewBufferPin().
  *
@@ -205,29 +169,13 @@ ClockSweepTick(void)
  *	before returning.
  */
 BufferDesc *
-StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_ring)
+StrategyGetBuffer(uint64 *buf_state)
 {
 	BufferDesc *buf;
 	int			bgwprocno;
 	int			trycounter;
 	bool		force_cool;
 
-	*from_ring = false;
-
-	/*
-	 * If given a strategy object, see whether it can select a buffer. We
-	 * assume strategy objects don't need buffer_strategy_lock.
-	 */
-	if (strategy != NULL)
-	{
-		buf = GetBufferFromRing(strategy, buf_state);
-		if (buf != NULL)
-		{
-			*from_ring = true;
-			return buf;
-		}
-	}
-
 	/*
 	 * If asked, we need to waken the bgwriter. Since we don't want to rely on
 	 * a spinlock for this we force a read from shared memory once, and then
@@ -256,8 +204,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 
 	/*
 	 * We count buffer allocation requests so that the bgwriter can estimate
-	 * the rate of buffer consumption.  Note that buffers recycled by a
-	 * strategy object are intentionally not counted here.
+	 * the rate of buffer consumption.
 	 */
 	pg_atomic_fetch_add_u32(&StrategyControl->numBufferAllocs, 1);
 
@@ -323,7 +270,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 				if (!force_cool)
 				{
 					no_progress = true;
-					break;			/* advance the hand, look for COOL */
+					break;		/* advance the hand, look for COOL */
 				}
 
 				local_buf_state &= ~BUF_USAGECOUNT_MASK;	/* HOT -> COOL, clear ref bit */
@@ -356,8 +303,6 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 												   local_buf_state))
 				{
 					/* Found a usable buffer */
-					if (strategy != NULL)
-						AddBufferToRing(strategy, buf);
 					*buf_state = local_buf_state;
 
 					TrackNewBufferPin(BufferDescriptorGetBuffer(buf));
@@ -368,11 +313,12 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 		}
 
 		/*
-		 * A tick that made no progress toward a victim counts down trycounter.
-		 * A full unproductive pass escalates to force_cool (cool HOT buffers
-		 * into victims); a second unproductive full pass means everything is
-		 * pinned, so fail rather than spin forever.  (A failed CAS above is
-		 * neither progress nor a full miss: we simply retry the same buffer.)
+		 * A tick that made no progress toward a victim counts down
+		 * trycounter. A full unproductive pass escalates to force_cool (cool
+		 * HOT buffers into victims); a second unproductive full pass means
+		 * everything is pinned, so fail rather than spin forever.  (A failed
+		 * CAS above is neither progress nor a full miss: we simply retry the
+		 * same buffer.)
 		 */
 		if (no_progress && --trycounter == 0)
 		{
@@ -500,361 +446,3 @@ StrategyCtlShmemInit(void *arg)
 	else
 		StrategyControl->batchSize = 1;
 }
-
-
-/* ----------------------------------------------------------------
- *				Backend-private buffer ring management
- * ----------------------------------------------------------------
- */
-
-
-/*
- * GetAccessStrategy -- create a BufferAccessStrategy object
- *
- * The object is allocated in the current memory context.
- */
-BufferAccessStrategy
-GetAccessStrategy(BufferAccessStrategyType btype)
-{
-	int			ring_size_kb;
-
-	/*
-	 * Select ring size to use.  See buffer/README for rationales.
-	 *
-	 * Note: if you change the ring size for BAS_BULKREAD, see also
-	 * SYNC_SCAN_REPORT_INTERVAL in access/heap/syncscan.c.
-	 */
-	switch (btype)
-	{
-		case BAS_NORMAL:
-			/* if someone asks for NORMAL, just give 'em a "default" object */
-			return NULL;
-
-		case BAS_BULKREAD:
-			{
-				int			ring_max_kb;
-
-				/*
-				 * The ring always needs to be large enough to allow some
-				 * separation in time between providing a buffer to the user
-				 * of the strategy and that buffer being reused. Otherwise the
-				 * user's pin will prevent reuse of the buffer, even without
-				 * concurrent activity.
-				 *
-				 * We also need to ensure the ring always is large enough for
-				 * SYNC_SCAN_REPORT_INTERVAL, as noted above.
-				 *
-				 * Thus we start out a minimal size and increase the size
-				 * further if appropriate.
-				 */
-				ring_size_kb = 256;
-
-				/*
-				 * There's no point in a larger ring if we won't be allowed to
-				 * pin sufficiently many buffers.  But we never limit to less
-				 * than the minimal size above.
-				 */
-				ring_max_kb = GetPinLimit() * (BLCKSZ / 1024);
-				ring_max_kb = Max(ring_size_kb, ring_max_kb);
-
-				/*
-				 * We would like the ring to additionally have space for the
-				 * configured degree of IO concurrency. While being read in,
-				 * buffers can obviously not yet be reused.
-				 *
-				 * Each IO can be up to io_combine_limit blocks large, and we
-				 * want to start up to effective_io_concurrency IOs.
-				 *
-				 * Note that effective_io_concurrency may be 0, which disables
-				 * AIO.
-				 */
-				ring_size_kb += (BLCKSZ / 1024) *
-					io_combine_limit * effective_io_concurrency;
-
-				if (ring_size_kb > ring_max_kb)
-					ring_size_kb = ring_max_kb;
-				break;
-			}
-		case BAS_BULKWRITE:
-			ring_size_kb = 16 * 1024;
-			break;
-		case BAS_VACUUM:
-			ring_size_kb = 2048;
-			break;
-
-		default:
-			elog(ERROR, "unrecognized buffer access strategy: %d",
-				 (int) btype);
-			return NULL;		/* keep compiler quiet */
-	}
-
-	return GetAccessStrategyWithSize(btype, ring_size_kb);
-}
-
-/*
- * GetAccessStrategyWithSize -- create a BufferAccessStrategy object with a
- *		number of buffers equivalent to the passed in size.
- *
- * If the given ring size is 0, no BufferAccessStrategy will be created and
- * the function will return NULL.  ring_size_kb must not be negative.
- */
-BufferAccessStrategy
-GetAccessStrategyWithSize(BufferAccessStrategyType btype, int ring_size_kb)
-{
-	int			ring_buffers;
-	BufferAccessStrategy strategy;
-
-	Assert(ring_size_kb >= 0);
-
-	/* Figure out how many buffers ring_size_kb is */
-	ring_buffers = ring_size_kb / (BLCKSZ / 1024);
-
-	/* 0 means unlimited, so no BufferAccessStrategy required */
-	if (ring_buffers == 0)
-		return NULL;
-
-	/* Cap to 1/8th of shared_buffers */
-	ring_buffers = Min(NBuffers / 8, ring_buffers);
-
-	/* NBuffers should never be less than 16, so this shouldn't happen */
-	Assert(ring_buffers > 0);
-
-	/* Allocate the object and initialize all elements to zeroes */
-	strategy = (BufferAccessStrategy)
-		palloc0(offsetof(BufferAccessStrategyData, buffers) +
-				ring_buffers * sizeof(Buffer));
-
-	/* Set fields that don't start out zero */
-	strategy->btype = btype;
-	strategy->nbuffers = ring_buffers;
-
-	return strategy;
-}
-
-/*
- * GetAccessStrategyBufferCount -- an accessor for the number of buffers in
- *		the ring
- *
- * Returns 0 on NULL input to match behavior of GetAccessStrategyWithSize()
- * returning NULL with 0 size.
- */
-int
-GetAccessStrategyBufferCount(BufferAccessStrategy strategy)
-{
-	if (strategy == NULL)
-		return 0;
-
-	return strategy->nbuffers;
-}
-
-/*
- * GetAccessStrategyPinLimit -- get cap of number of buffers that should be pinned
- *
- * When pinning extra buffers to look ahead, users of a ring-based strategy are
- * in danger of pinning too much of the ring at once while performing look-ahead.
- * For some strategies, that means "escaping" from the ring, and in others it
- * means forcing dirty data to disk very frequently with associated WAL
- * flushing.  Since external code has no insight into any of that, allow
- * individual strategy types to expose a clamp that should be applied when
- * deciding on a maximum number of buffers to pin at once.
- *
- * Callers should combine this number with other relevant limits and take the
- * minimum.
- */
-int
-GetAccessStrategyPinLimit(BufferAccessStrategy strategy)
-{
-	if (strategy == NULL)
-		return NBuffers;
-
-	switch (strategy->btype)
-	{
-		case BAS_BULKREAD:
-
-			/*
-			 * Since BAS_BULKREAD uses StrategyRejectBuffer(), dirty buffers
-			 * shouldn't be a problem and the caller is free to pin up to the
-			 * entire ring at once.
-			 */
-			return strategy->nbuffers;
-
-		default:
-
-			/*
-			 * Tell caller not to pin more than half the buffers in the ring.
-			 * This is a trade-off between look ahead distance and deferring
-			 * writeback and associated WAL traffic.
-			 */
-			return strategy->nbuffers / 2;
-	}
-}
-
-/*
- * FreeAccessStrategy -- release a BufferAccessStrategy object
- *
- * A simple pfree would do at the moment, but we would prefer that callers
- * don't assume that much about the representation of BufferAccessStrategy.
- */
-void
-FreeAccessStrategy(BufferAccessStrategy strategy)
-{
-	/* don't crash if called on a "default" strategy */
-	if (strategy != NULL)
-		pfree(strategy);
-}
-
-/*
- * GetBufferFromRing -- returns a buffer from the ring, or NULL if the
- *		ring is empty / not usable.
- *
- * The buffer is pinned and marked as owned, using TrackNewBufferPin(), before
- * returning.
- */
-static BufferDesc *
-GetBufferFromRing(BufferAccessStrategy strategy, uint64 *buf_state)
-{
-	BufferDesc *buf;
-	Buffer		bufnum;
-	uint64		old_buf_state;
-	uint64		local_buf_state;	/* to avoid repeated (de-)referencing */
-
-
-	/* Advance to next ring slot */
-	if (++strategy->current >= strategy->nbuffers)
-		strategy->current = 0;
-
-	/*
-	 * If the slot hasn't been filled yet, tell the caller to allocate a new
-	 * buffer with the normal allocation strategy.  He will then fill this
-	 * slot by calling AddBufferToRing with the new buffer.
-	 */
-	bufnum = strategy->buffers[strategy->current];
-	if (bufnum == InvalidBuffer)
-		return NULL;
-
-	buf = GetBufferDescriptor(bufnum - 1);
-
-	/*
-	 * Check whether the buffer can be used and pin it if so. Do this using a
-	 * CAS loop, to avoid having to lock the buffer header.
-	 */
-	old_buf_state = pg_atomic_read_u64(&buf->state);
-	for (;;)
-	{
-		local_buf_state = old_buf_state;
-
-		/*
-		 * If the buffer is pinned we cannot use it under any circumstances.
-		 *
-		 * With the cooling-state replacement the field holds only COOL or HOT,
-		 * so the stock "usage_count > 1 means another backend touched it"
-		 * heuristic no longer applies: a ring element is reusable whenever it
-		 * is unpinned.  (The whole ring mechanism is removed in a later patch;
-		 * scan resistance is now intrinsic to the sweep.)
-		 */
-		if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0)
-			break;
-
-		/* See equivalent code in PinBuffer() */
-		if (unlikely(local_buf_state & BM_LOCKED))
-		{
-			old_buf_state = WaitBufHdrUnlocked(buf);
-			continue;
-		}
-
-		/* pin the buffer if the CAS succeeds */
-		local_buf_state += BUF_REFCOUNT_ONE;
-
-		if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
-										   local_buf_state))
-		{
-			*buf_state = local_buf_state;
-
-			TrackNewBufferPin(BufferDescriptorGetBuffer(buf));
-			return buf;
-		}
-	}
-
-	/*
-	 * Tell caller to allocate a new buffer with the normal allocation
-	 * strategy.  He'll then replace this ring element via AddBufferToRing.
-	 */
-	return NULL;
-}
-
-/*
- * AddBufferToRing -- add a buffer to the buffer ring
- *
- * Caller must hold the buffer header spinlock on the buffer.  Since this
- * is called with the spinlock held, it had better be quite cheap.
- */
-static void
-AddBufferToRing(BufferAccessStrategy strategy, BufferDesc *buf)
-{
-	strategy->buffers[strategy->current] = BufferDescriptorGetBuffer(buf);
-}
-
-/*
- * Utility function returning the IOContext of a given BufferAccessStrategy's
- * strategy ring.
- */
-IOContext
-IOContextForStrategy(BufferAccessStrategy strategy)
-{
-	if (!strategy)
-		return IOCONTEXT_NORMAL;
-
-	switch (strategy->btype)
-	{
-		case BAS_NORMAL:
-
-			/*
-			 * Currently, GetAccessStrategy() returns NULL for
-			 * BufferAccessStrategyType BAS_NORMAL, so this case is
-			 * unreachable.
-			 */
-			pg_unreachable();
-			return IOCONTEXT_NORMAL;
-		case BAS_BULKREAD:
-			return IOCONTEXT_BULKREAD;
-		case BAS_BULKWRITE:
-			return IOCONTEXT_BULKWRITE;
-		case BAS_VACUUM:
-			return IOCONTEXT_VACUUM;
-	}
-
-	elog(ERROR, "unrecognized BufferAccessStrategyType: %d", strategy->btype);
-	pg_unreachable();
-}
-
-/*
- * StrategyRejectBuffer -- consider rejecting a dirty buffer
- *
- * When a nondefault strategy is used, the buffer manager calls this function
- * when it turns out that the buffer selected by StrategyGetBuffer needs to
- * be written out and doing so would require flushing WAL too.  This gives us
- * a chance to choose a different victim.
- *
- * Returns true if buffer manager should ask for a new victim, and false
- * if this buffer should be written and re-used.
- */
-bool
-StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf, bool from_ring)
-{
-	/* We only do this in bulkread mode */
-	if (strategy->btype != BAS_BULKREAD)
-		return false;
-
-	/* Don't muck with behavior of normal buffer-replacement strategy */
-	if (!from_ring ||
-		strategy->buffers[strategy->current] != BufferDescriptorGetBuffer(buf))
-		return false;
-
-	/*
-	 * Remove the dirty buffer from the ring; necessary to prevent infinite
-	 * loop if all ring members are dirty.
-	 */
-	strategy->buffers[strategy->current] = InvalidBuffer;
-
-	return true;
-}
diff --git a/src/backend/storage/freespace/freespace.c b/src/backend/storage/freespace/freespace.c
index 006edab9d77..29cb2d70f66 100644
--- a/src/backend/storage/freespace/freespace.c
+++ b/src/backend/storage/freespace/freespace.c
@@ -603,7 +603,7 @@ fsm_readbuf(Relation rel, FSMAddress addr, bool extend)
 			return InvalidBuffer;
 	}
 	else
-		buf = ReadBufferExtended(rel, FSM_FORKNUM, blkno, RBM_ZERO_ON_ERROR, NULL);
+		buf = ReadBufferExtended(rel, FSM_FORKNUM, blkno, RBM_ZERO_ON_ERROR);
 
 	/*
 	 * Initializing the page when needed is trickier than it looks, because of
@@ -638,7 +638,7 @@ fsm_readbuf(Relation rel, FSMAddress addr, bool extend)
 static Buffer
 fsm_extend(Relation rel, BlockNumber fsm_nblocks)
 {
-	return ExtendBufferedRelTo(BMR_REL(rel), FSM_FORKNUM, NULL,
+	return ExtendBufferedRelTo(BMR_REL(rel), FSM_FORKNUM,
 							   EB_CREATE_FORK_IF_NEEDED |
 							   EB_CLEAR_SIZE_CACHE,
 							   fsm_nblocks,
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 718c1cfc0f9..3c3e35ab8a8 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -1540,15 +1540,8 @@ register_dirty_segment(SMgrRelation reln, ForkNumber forknum, MdfdVec *seg)
 							FilePathName(seg->mdfd_vfd))));
 
 		/*
-		 * We have no way of knowing if the current IOContext is
-		 * IOCONTEXT_NORMAL or IOCONTEXT_[BULKREAD, BULKWRITE, VACUUM] at this
-		 * point, so count the fsync as being in the IOCONTEXT_NORMAL
-		 * IOContext. This is probably okay, because the number of backend
-		 * fsyncs doesn't say anything about the efficacy of the
-		 * BufferAccessStrategy. And counting both fsyncs done in
-		 * IOCONTEXT_NORMAL and IOCONTEXT_[BULKREAD, BULKWRITE, VACUUM] under
-		 * IOCONTEXT_NORMAL is likely clearer when investigating the number of
-		 * backend fsyncs.
+		 * Count the fsync in the IOCONTEXT_NORMAL IOContext, the only
+		 * IOContext relations are read/written under.
 		 */
 		pgstat_count_io_op_time(IOOBJECT_RELATION, IOCONTEXT_NORMAL,
 								IOOP_FSYNC, io_start, 1, 0);
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 38bae7b15d2..d66a23749d5 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -241,16 +241,10 @@ pgstat_get_io_context_name(IOContext io_context)
 {
 	switch (io_context)
 	{
-		case IOCONTEXT_BULKREAD:
-			return "bulkread";
-		case IOCONTEXT_BULKWRITE:
-			return "bulkwrite";
 		case IOCONTEXT_INIT:
 			return "init";
 		case IOCONTEXT_NORMAL:
 			return "normal";
-		case IOCONTEXT_VACUUM:
-			return "vacuum";
 	}
 
 	elog(ERROR, "unrecognized IOContext value: %d", io_context);
@@ -451,18 +445,6 @@ pgstat_tracks_io_object(BackendType bktype, IOObject io_object,
 	 * IOContexts, and, while it may not be inherently incorrect for them to
 	 * do so, excluding those rows from the view makes the view easier to use.
 	 */
-	if ((bktype == B_CHECKPOINTER || bktype == B_BG_WRITER) &&
-		(io_context == IOCONTEXT_BULKREAD ||
-		 io_context == IOCONTEXT_BULKWRITE ||
-		 io_context == IOCONTEXT_VACUUM))
-		return false;
-
-	if (bktype == B_AUTOVAC_LAUNCHER && io_context == IOCONTEXT_VACUUM)
-		return false;
-
-	if ((bktype == B_AUTOVAC_WORKER || bktype == B_AUTOVAC_LAUNCHER) &&
-		io_context == IOCONTEXT_BULKWRITE)
-		return false;
 
 	return true;
 }
@@ -479,8 +461,6 @@ bool
 pgstat_tracks_io_op(BackendType bktype, IOObject io_object,
 					IOContext io_context, IOOp io_op)
 {
-	bool		strategy_io_context;
-
 	/* if (io_context, io_object) will never collect stats, we're done */
 	if (!pgstat_tracks_io_object(bktype, io_object, io_context))
 		return false;
@@ -521,17 +501,13 @@ pgstat_tracks_io_op(BackendType bktype, IOObject io_object,
 	/*
 	 * Some IOOps are not valid in certain IOContexts and some IOOps are only
 	 * valid in certain contexts.
+	 *
+	 * IOOP_REUSE was only relevant when a BufferAccessStrategy was in use.
+	 * Buffer access strategies (ring buffers) have been removed -- scan
+	 * resistance is now intrinsic to the cooling-stage clock sweep -- so
+	 * IOOP_REUSE never occurs.
 	 */
-	if (io_context == IOCONTEXT_BULKREAD && io_op == IOOP_EXTEND)
-		return false;
-
-	strategy_io_context = io_context == IOCONTEXT_BULKREAD ||
-		io_context == IOCONTEXT_BULKWRITE || io_context == IOCONTEXT_VACUUM;
-
-	/*
-	 * IOOP_REUSE is only relevant when a BufferAccessStrategy is in use.
-	 */
-	if (!strategy_io_context && io_op == IOOP_REUSE)
+	if (io_op == IOOP_REUSE)
 		return false;
 
 	/*
@@ -545,14 +521,5 @@ pgstat_tracks_io_op(BackendType bktype, IOObject io_object,
 		!(io_op == IOOP_WRITE || io_op == IOOP_READ || io_op == IOOP_FSYNC))
 		return false;
 
-	/*
-	 * IOOP_FSYNC IOOps done by a backend using a BufferAccessStrategy are
-	 * counted in the IOCONTEXT_NORMAL IOContext. See comment in
-	 * register_dirty_segment() for more details.
-	 */
-	if (strategy_io_context && io_op == IOOP_FSYNC)
-		return false;
-
-
 	return true;
 }
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index bbd28d14d99..555c04272f5 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -149,7 +149,6 @@ int			autovacuum_max_parallel_workers = 0;
 int			MaxBackends = 0;
 
 /* GUC parameters for vacuum */
-int			VacuumBufferUsageLimit = 2048;
 
 int			VacuumCostPageHit = 1;
 int			VacuumCostPageMiss = 2;
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index d421cdbde76..b1ea81f536c 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -3326,16 +3326,6 @@
   boot_val => 'DEFAULT_UPDATE_PROCESS_TITLE',
 },
 
-{ name => 'vacuum_buffer_usage_limit', type => 'int', context => 'PGC_USERSET', group => 'RESOURCES_MEM',
-  short_desc => 'Sets the buffer pool size for VACUUM, ANALYZE, and autovacuum.',
-  flags => 'GUC_UNIT_KB',
-  variable => 'VacuumBufferUsageLimit',
-  boot_val => '2048',
-  min => '0',
-  max => 'MAX_BAS_VAC_RING_SIZE_KB',
-  check_hook => 'check_vacuum_buffer_usage_limit',
-},
-
 { name => 'vacuum_cost_delay', type => 'real', context => 'PGC_USERSET', group => 'VACUUM_COST_DELAY',
   short_desc => 'Vacuum cost delay in milliseconds.',
   flags => 'GUC_UNIT_MS',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 7958653077b..cb1eb8ef3ff 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -164,9 +164,6 @@
                                         #   mmap
                                         # (change requires restart)
 #min_dynamic_shared_memory = 0MB        # (change requires restart)
-#vacuum_buffer_usage_limit = 2MB        # size of vacuum and analyze buffer access strategy ring;
-                                        # 0 to disable vacuum buffer access strategy;
-                                        # range 128kB to 16GB
 
 # SLRU buffers (change requires restart)
 #commit_timestamp_buffers = 0           # memory for pg_commit_ts (0 = auto)
diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index f8158ca6a78..f78e57ff2c3 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -57,7 +57,6 @@ main(int argc, char *argv[])
 		{"no-truncate", no_argument, NULL, 10},
 		{"no-process-toast", no_argument, NULL, 11},
 		{"no-process-main", no_argument, NULL, 12},
-		{"buffer-usage-limit", required_argument, NULL, 13},
 		{"missing-stats-only", no_argument, NULL, 14},
 		{"dry-run", no_argument, NULL, 15},
 		{NULL, 0, NULL, 0}
@@ -202,9 +201,6 @@ main(int argc, char *argv[])
 			case 12:
 				vacopts.process_main = false;
 				break;
-			case 13:
-				vacopts.buffer_usage_limit = escape_quotes(optarg);
-				break;
 			case 14:
 				vacopts.missing_stats_only = true;
 				break;
@@ -290,14 +286,6 @@ main(int argc, char *argv[])
 		pg_fatal("cannot use the \"%s\" option with the \"%s\" option",
 				 "no-index-cleanup", "force-index-cleanup");
 
-	/*
-	 * buffer-usage-limit is not allowed with VACUUM FULL unless ANALYZE is
-	 * included too.
-	 */
-	if (vacopts.buffer_usage_limit && vacopts.full && !vacopts.and_analyze)
-		pg_fatal("cannot use the \"%s\" option with the \"%s\" option",
-				 "buffer-usage-limit", "full");
-
 	/*
 	 * Prohibit --missing-stats-only without --analyze-only or
 	 * --analyze-in-stages.
@@ -352,7 +340,6 @@ help(const char *progname)
 	printf(_("  %s [OPTION]... [DBNAME]\n"), progname);
 	printf(_("\nOptions:\n"));
 	printf(_("  -a, --all                       vacuum all databases\n"));
-	printf(_("      --buffer-usage-limit=SIZE   size of ring buffer used for vacuum\n"));
 	printf(_("  -d, --dbname=DBNAME             database to vacuum\n"));
 	printf(_("      --disable-page-skipping     disable all page-skipping behavior\n"));
 	printf(_("      --dry-run                   show the commands that would be sent to the server\n"));
diff --git a/src/bin/scripts/vacuuming.c b/src/bin/scripts/vacuuming.c
index 855a5754c98..078a3bbb516 100644
--- a/src/bin/scripts/vacuuming.c
+++ b/src/bin/scripts/vacuuming.c
@@ -264,13 +264,6 @@ vacuum_one_database(ConnParams *cparams,
 				 "--parallel", "13");
 	}
 
-	if (vacopts->buffer_usage_limit && PQserverVersion(conn) < 160000)
-	{
-		PQfinish(conn);
-		pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s",
-				 "--buffer-usage-limit", "16");
-	}
-
 	if (vacopts->missing_stats_only && PQserverVersion(conn) < 150000)
 	{
 		PQfinish(conn);
@@ -865,13 +858,6 @@ prepare_vacuum_command(PGconn *conn, PQExpBuffer sql,
 				appendPQExpBuffer(sql, "%sVERBOSE", sep);
 				sep = comma;
 			}
-			if (vacopts->buffer_usage_limit)
-			{
-				Assert(serverVersion >= 160000);
-				appendPQExpBuffer(sql, "%sBUFFER_USAGE_LIMIT '%s'", sep,
-								  vacopts->buffer_usage_limit);
-				sep = comma;
-			}
 			if (sep != paren)
 				appendPQExpBufferChar(sql, ')');
 		}
@@ -974,13 +960,6 @@ prepare_vacuum_command(PGconn *conn, PQExpBuffer sql,
 								  vacopts->parallel_workers);
 				sep = comma;
 			}
-			if (vacopts->buffer_usage_limit)
-			{
-				Assert(serverVersion >= 160000);
-				appendPQExpBuffer(sql, "%sBUFFER_USAGE_LIMIT '%s'", sep,
-								  vacopts->buffer_usage_limit);
-				sep = comma;
-			}
 			if (sep != paren)
 				appendPQExpBufferChar(sql, ')');
 		}
diff --git a/src/bin/scripts/vacuuming.h b/src/bin/scripts/vacuuming.h
index 5a491db2526..83a2469a6aa 100644
--- a/src/bin/scripts/vacuuming.h
+++ b/src/bin/scripts/vacuuming.h
@@ -49,7 +49,6 @@ typedef struct vacuumingOptions
 	bool		process_main;
 	bool		process_toast;
 	bool		skip_database_stats;
-	char	   *buffer_usage_limit;
 	bool		missing_stats_only;
 	bool		echo;
 	bool		quiet;
diff --git a/src/include/access/genam.h b/src/include/access/genam.h
index 68bfe405db3..bc87f6f7cc0 100644
--- a/src/include/access/genam.h
+++ b/src/include/access/genam.h
@@ -58,7 +58,6 @@ typedef struct IndexVacuumInfo
 	bool		estimated_count;	/* num_heap_tuples is an estimate */
 	int			message_level;	/* ereport level for progress messages */
 	double		num_heap_tuples;	/* tuples remaining in heap */
-	BufferAccessStrategy strategy;	/* access strategy for reads */
 } IndexVacuumInfo;
 
 /*
diff --git a/src/include/access/hash.h b/src/include/access/hash.h
index a8702f0e5ea..7c9268fe953 100644
--- a/src/include/access/hash.h
+++ b/src/include/access/hash.h
@@ -405,12 +405,11 @@ extern void _hash_pgaddmultitup(Relation rel, Buffer buf, IndexTuple *itups,
 extern Buffer _hash_addovflpage(Relation rel, Buffer metabuf, Buffer buf, bool retain_pin);
 extern BlockNumber _hash_freeovflpage(Relation rel, Buffer bucketbuf, Buffer ovflbuf,
 									  Buffer wbuf, IndexTuple *itups, OffsetNumber *itup_offsets,
-									  Size *tups_size, uint16 nitups, BufferAccessStrategy bstrategy);
+									  Size *tups_size, uint16 nitups);
 extern void _hash_initbitmapbuffer(Buffer buf, uint16 bmsize, bool initpage);
 extern void _hash_squeezebucket(Relation rel,
 								Bucket bucket, BlockNumber bucket_blkno,
-								Buffer bucket_buf,
-								BufferAccessStrategy bstrategy);
+								Buffer bucket_buf);
 extern uint32 _hash_ovflblkno_to_bitno(HashMetaPage metap, BlockNumber ovflblkno);
 
 /* hashpage.c */
@@ -428,9 +427,6 @@ extern void _hash_initbuf(Buffer buf, uint32 max_bucket, uint32 num_bucket,
 						  uint32 flag, bool initpage);
 extern Buffer _hash_getnewbuf(Relation rel, BlockNumber blkno,
 							  ForkNumber forkNum);
-extern Buffer _hash_getbuf_with_strategy(Relation rel, BlockNumber blkno,
-										 int access, int flags,
-										 BufferAccessStrategy bstrategy);
 extern void _hash_relbuf(Relation rel, Buffer buf);
 extern void _hash_dropbuf(Relation rel, Buffer buf);
 extern void _hash_dropscanbuf(Relation rel, HashScanOpaque so);
@@ -481,7 +477,6 @@ extern void _hash_kill_items(IndexScanDesc scan);
 /* hash.c */
 extern void hashbucketcleanup(Relation rel, Bucket cur_bucket,
 							  Buffer bucket_buf, BlockNumber bucket_blkno,
-							  BufferAccessStrategy bstrategy,
 							  uint32 maxbucket, uint32 highmask, uint32 lowmask,
 							  double *tuples_removed, double *num_index_tuples,
 							  bool split_cleanup,
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 5176478c295..0967b659794 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -72,8 +72,6 @@ typedef struct HeapScanDescData
 	Buffer		rs_cbuf;		/* current buffer in scan, if any */
 	/* NB: if rs_cbuf is not InvalidBuffer, we hold a pin on that buffer */
 
-	BufferAccessStrategy rs_strategy;	/* access strategy for reads */
-
 	HeapTupleData rs_ctup;		/* current tuple in scan, if any */
 
 	/* For scans that stream reads */
@@ -466,7 +464,7 @@ extern void log_heap_prune_and_freeze(Relation relation, Buffer buffer,
 
 /* in heap/vacuumlazy.c */
 extern void heap_vacuum_rel(Relation rel,
-							const VacuumParams *params, BufferAccessStrategy bstrategy);
+							const VacuumParams *params);
 #ifdef USE_ASSERT_CHECKING
 extern bool heap_page_is_all_visible(Relation rel, Buffer buf,
 									 GlobalVisState *vistest,
diff --git a/src/include/access/hio.h b/src/include/access/hio.h
index 60cfc375fd5..63bfcc884ee 100644
--- a/src/include/access/hio.h
+++ b/src/include/access/hio.h
@@ -28,7 +28,6 @@
  */
 typedef struct BulkInsertStateData
 {
-	BufferAccessStrategy strategy;	/* our BULKWRITE strategy object */
 	Buffer		current_buf;	/* current insertion target page */
 
 	/*
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index f2c36696bca..83ba0f3ca73 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -689,8 +689,7 @@ typedef struct TableAmRoutine
 	 * integrate with autovacuum's scheduling.
 	 */
 	void		(*relation_vacuum) (Relation rel,
-									const VacuumParams *params,
-									BufferAccessStrategy bstrategy);
+									const VacuumParams *params);
 
 	/*
 	 * Prepare to analyze block `blockno` of `scan`. The scan has been started
@@ -1774,10 +1773,9 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
  * routine, even if (for ANALYZE) it is part of the same VACUUM command.
  */
 static inline void
-table_relation_vacuum(Relation rel, const VacuumParams *params,
-					  BufferAccessStrategy bstrategy)
+table_relation_vacuum(Relation rel, const VacuumParams *params)
 {
-	rel->rd_tableam->relation_vacuum(rel, params, bstrategy);
+	rel->rd_tableam->relation_vacuum(rel, params);
 }
 
 /*
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 956d9cea36d..14a9b530bcf 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -363,7 +363,7 @@ extern PGDLLIMPORT int64 parallel_vacuum_worker_delay_ns;
 /* in commands/vacuum.c */
 extern void ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel);
 extern void vacuum(List *relations, const VacuumParams *params,
-				   BufferAccessStrategy bstrategy, MemoryContext vac_context,
+				   MemoryContext vac_context,
 				   bool isTopLevel);
 extern void vac_open_indexes(Relation relation, LOCKMODE lockmode,
 							 int *nindexes, Relation **Irel);
@@ -407,8 +407,7 @@ extern void VacuumUpdateCosts(void);
 /* in commands/vacuumparallel.c */
 extern ParallelVacuumState *parallel_vacuum_init(Relation rel, Relation *indrels,
 												 int nindexes, int nrequested_workers,
-												 int vac_work_mem, int elevel,
-												 BufferAccessStrategy bstrategy);
+												 int vac_work_mem, int elevel);
 extern void parallel_vacuum_end(ParallelVacuumState *pvs, IndexBulkDeleteResult **istats);
 extern TidStore *parallel_vacuum_get_dead_items(ParallelVacuumState *pvs,
 												VacDeadItemsInfo **dead_items_info_p);
@@ -428,8 +427,7 @@ extern void parallel_vacuum_main(dsm_segment *seg, shm_toc *toc);
 
 /* in commands/analyze.c */
 extern void analyze_rel(Oid relid, RangeVar *relation,
-						const VacuumParams *params, List *va_cols, bool in_outer_xact,
-						BufferAccessStrategy bstrategy);
+						const VacuumParams *params, List *va_cols, bool in_outer_xact);
 extern bool attribute_is_analyzable(Relation onerel, int attnum, Form_pg_attribute attr,
 									int *p_attstattarget);
 extern bool std_typanalyze(VacAttrStats *stats);
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 7170a4bff98..c5d582b152b 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -273,15 +273,6 @@ extern PGDLLIMPORT double hash_mem_multiplier;
 extern PGDLLIMPORT int maintenance_work_mem;
 extern PGDLLIMPORT int max_parallel_maintenance_workers;
 
-/*
- * Upper and lower hard limits for the buffer access strategy ring size
- * specified by the VacuumBufferUsageLimit GUC and BUFFER_USAGE_LIMIT option
- * to VACUUM and ANALYZE.
- */
-#define MIN_BAS_VAC_RING_SIZE_KB 128
-#define MAX_BAS_VAC_RING_SIZE_KB (16 * 1024 * 1024)
-
-extern PGDLLIMPORT int VacuumBufferUsageLimit;
 extern PGDLLIMPORT int VacuumCostPageHit;
 extern PGDLLIMPORT int VacuumCostPageMiss;
 extern PGDLLIMPORT int VacuumCostPageDirty;
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..68b4e5093a2 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -287,14 +287,11 @@ typedef enum IOObject
 
 typedef enum IOContext
 {
-	IOCONTEXT_BULKREAD,
-	IOCONTEXT_BULKWRITE,
 	IOCONTEXT_INIT,
 	IOCONTEXT_NORMAL,
-	IOCONTEXT_VACUUM,
 } IOContext;
 
-#define IOCONTEXT_NUM_TYPES (IOCONTEXT_VACUUM + 1)
+#define IOCONTEXT_NUM_TYPES (IOCONTEXT_NORMAL + 1)
 
 /*
  * Enumeration of IO operations.
diff --git a/src/include/storage/buf.h b/src/include/storage/buf.h
index b21445522b1..fa942a64f05 100644
--- a/src/include/storage/buf.h
+++ b/src/include/storage/buf.h
@@ -36,11 +36,4 @@ typedef int Buffer;
  */
 #define BufferIsLocal(buffer)	((buffer) < 0)
 
-/*
- * Buffer access strategy objects.
- *
- * BufferAccessStrategyData is private to freelist.c
- */
-typedef struct BufferAccessStrategyData *BufferAccessStrategy;
-
 #endif							/* BUF_H */
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index 131df126cda..3b44ad3e0b6 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -632,11 +632,7 @@ extern void TerminateBufferIO(BufferDesc *buf, bool clear_dirty, uint64 set_flag
 
 
 /* freelist.c */
-extern IOContext IOContextForStrategy(BufferAccessStrategy strategy);
-extern BufferDesc *StrategyGetBuffer(BufferAccessStrategy strategy,
-									 uint64 *buf_state, bool *from_ring);
-extern bool StrategyRejectBuffer(BufferAccessStrategy strategy,
-								 BufferDesc *buf, bool from_ring);
+extern BufferDesc *StrategyGetBuffer(uint64 *buf_state);
 
 extern int	StrategySyncStart(uint32 *complete_passes, uint32 *num_buf_alloc);
 extern void StrategyNotifyBgWriter(int bgwprocno);
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 6837b35fc6d..36023792f36 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -25,21 +25,6 @@
 
 typedef void *Block;
 
-/*
- * Possible arguments for GetAccessStrategy().
- *
- * If adding a new BufferAccessStrategyType, also add a new IOContext so
- * IO statistics using this strategy are tracked.
- */
-typedef enum BufferAccessStrategyType
-{
-	BAS_NORMAL,					/* Normal random access */
-	BAS_BULKREAD,				/* Large read-only scan (hint bit updates are
-								 * ok) */
-	BAS_BULKWRITE,				/* Large multi-block write (e.g. COPY IN) */
-	BAS_VACUUM,					/* VACUUM */
-} BufferAccessStrategyType;
-
 /* Possible modes for ReadBufferExtended() */
 typedef enum
 {
@@ -135,7 +120,6 @@ struct ReadBuffersOperation
 	SMgrRelation smgr;
 	char		persistence;
 	ForkNumber	forknum;
-	BufferAccessStrategy strategy;
 
 	/*
 	 * The following private members are private state for communication
@@ -235,11 +219,10 @@ extern bool ReadRecentBuffer(RelFileLocator rlocator, ForkNumber forkNum,
 							 BlockNumber blockNum, Buffer recent_buffer);
 extern Buffer ReadBuffer(Relation reln, BlockNumber blockNum);
 extern Buffer ReadBufferExtended(Relation reln, ForkNumber forkNum,
-								 BlockNumber blockNum, ReadBufferMode mode,
-								 BufferAccessStrategy strategy);
+								 BlockNumber blockNum, ReadBufferMode mode);
 extern Buffer ReadBufferWithoutRelcache(RelFileLocator rlocator,
 										ForkNumber forkNum, BlockNumber blockNum,
-										ReadBufferMode mode, BufferAccessStrategy strategy,
+										ReadBufferMode mode,
 										bool permanent);
 
 extern bool StartReadBuffer(ReadBuffersOperation *operation,
@@ -266,18 +249,15 @@ extern Buffer ReleaseAndReadBuffer(Buffer buffer, Relation relation,
 
 extern Buffer ExtendBufferedRel(BufferManagerRelation bmr,
 								ForkNumber forkNum,
-								BufferAccessStrategy strategy,
 								uint32 flags);
 extern BlockNumber ExtendBufferedRelBy(BufferManagerRelation bmr,
 									   ForkNumber fork,
-									   BufferAccessStrategy strategy,
 									   uint32 flags,
 									   uint32 extend_by,
 									   Buffer *buffers,
 									   uint32 *extended_by);
 extern Buffer ExtendBufferedRelTo(BufferManagerRelation bmr,
 								  ForkNumber fork,
-								  BufferAccessStrategy strategy,
 								  uint32 flags,
 								  BlockNumber extend_to,
 								  ReadBufferMode mode);
@@ -376,14 +356,6 @@ extern void AtProcExit_LocalBuffers(void);
 
 /* in freelist.c */
 
-extern BufferAccessStrategy GetAccessStrategy(BufferAccessStrategyType btype);
-extern BufferAccessStrategy GetAccessStrategyWithSize(BufferAccessStrategyType btype,
-													  int ring_size_kb);
-extern int	GetAccessStrategyBufferCount(BufferAccessStrategy strategy);
-extern int	GetAccessStrategyPinLimit(BufferAccessStrategy strategy);
-
-extern void FreeAccessStrategy(BufferAccessStrategy strategy);
-
 
 /* inline functions */
 
diff --git a/src/include/storage/read_stream.h b/src/include/storage/read_stream.h
index 48995c6d534..9e5fbbed9e8 100644
--- a/src/include/storage/read_stream.h
+++ b/src/include/storage/read_stream.h
@@ -83,17 +83,14 @@ extern BlockNumber block_range_read_stream_cb(ReadStream *stream,
 											  void *callback_private_data,
 											  void *per_buffer_data);
 extern ReadStream *read_stream_begin_relation(int flags,
-											  BufferAccessStrategy strategy,
 											  Relation rel,
 											  ForkNumber forknum,
 											  ReadStreamBlockNumberCB callback,
 											  void *callback_private_data,
 											  size_t per_buffer_data_size);
 extern Buffer read_stream_next_buffer(ReadStream *stream, void **per_buffer_data);
-extern BlockNumber read_stream_next_block(ReadStream *stream,
-										  BufferAccessStrategy *strategy);
+extern BlockNumber read_stream_next_block(ReadStream *stream);
 extern ReadStream *read_stream_begin_smgr_relation(int flags,
-												   BufferAccessStrategy strategy,
 												   SMgrRelation smgr,
 												   char smgr_persistence,
 												   ForkNumber forknum,
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 307f4fbaefe..3f28558d47c 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -31,8 +31,6 @@ extern void assign_application_name(const char *newval, void *extra);
 extern const char *show_archive_command(void);
 extern bool check_autovacuum_work_mem(int *newval, void **extra,
 									  GucSource source);
-extern bool check_vacuum_buffer_usage_limit(int *newval, void **extra,
-											GucSource source);
 extern bool check_backtrace_functions(char **newval, void **extra,
 									  GucSource source);
 extern void assign_backtrace_functions(const char *newval, void *extra);
diff --git a/src/test/modules/test_aio/test_aio.c b/src/test/modules/test_aio/test_aio.c
index 6270775af7c..8d7b2a728d6 100644
--- a/src/test/modules/test_aio/test_aio.c
+++ b/src/test/modules/test_aio/test_aio.c
@@ -194,7 +194,6 @@ grow_rel(PG_FUNCTION_ARGS)
 
 		ExtendBufferedRelBy(BMR_REL(rel),
 							MAIN_FORKNUM,
-							NULL,
 							0,
 							extend_by_pages,
 							victim_buffers,
@@ -231,7 +230,7 @@ modify_rel_block(PG_FUNCTION_ARGS)
 	rel = relation_open(relid, AccessExclusiveLock);
 
 	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno,
-							 RBM_ZERO_ON_ERROR, NULL);
+							 RBM_ZERO_ON_ERROR);
 
 	LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
 
@@ -331,7 +330,7 @@ create_toy_buffer(Relation rel, BlockNumber blkno)
 	uint64		unset_bits = 0;
 
 	/* place buffer in shared buffers without erroring out */
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_ZERO_AND_LOCK, NULL);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_ZERO_AND_LOCK);
 	LockBuffer(buf, BUFFER_LOCK_UNLOCK);
 
 	if (RelationUsesLocalBuffers(rel))
@@ -737,7 +736,6 @@ read_buffers(PG_FUNCTION_ARGS)
 		operation->rel = rel;
 		operation->smgr = smgr;
 		operation->persistence = rel->rd_rel->relpersistence;
-		operation->strategy = NULL;
 		operation->forknum = MAIN_FORKNUM;
 
 		io_reqds[nios] = StartReadBuffers(operation,
@@ -879,7 +877,6 @@ read_stream_for_blocks(PG_FUNCTION_ARGS)
 	rel = relation_open(relid, AccessShareLock);
 
 	stream = read_stream_begin_relation(READ_STREAM_FULL,
-										NULL,
 										rel,
 										MAIN_FORKNUM,
 										read_stream_for_blocks_cb,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index 03cbc1cdef5..e4b9de0b4a8 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -16,22 +16,16 @@ SHOW track_counts;  -- must be on
 SELECT backend_type, object, context FROM pg_stat_io
   ORDER BY backend_type COLLATE "C", object COLLATE "C", context COLLATE "C";
 backend_type|object|context
-autovacuum launcher|relation|bulkread
 autovacuum launcher|relation|init
 autovacuum launcher|relation|normal
 autovacuum launcher|wal|init
 autovacuum launcher|wal|normal
-autovacuum worker|relation|bulkread
 autovacuum worker|relation|init
 autovacuum worker|relation|normal
-autovacuum worker|relation|vacuum
 autovacuum worker|wal|init
 autovacuum worker|wal|normal
-background worker|relation|bulkread
-background worker|relation|bulkwrite
 background worker|relation|init
 background worker|relation|normal
-background worker|relation|vacuum
 background worker|temp relation|normal
 background worker|wal|init
 background worker|wal|normal
@@ -43,67 +37,43 @@ checkpointer|relation|init
 checkpointer|relation|normal
 checkpointer|wal|init
 checkpointer|wal|normal
-client backend|relation|bulkread
-client backend|relation|bulkwrite
 client backend|relation|init
 client backend|relation|normal
-client backend|relation|vacuum
 client backend|temp relation|normal
 client backend|wal|init
 client backend|wal|normal
-datachecksums launcher|relation|bulkread
-datachecksums launcher|relation|bulkwrite
 datachecksums launcher|relation|init
 datachecksums launcher|relation|normal
-datachecksums launcher|relation|vacuum
 datachecksums launcher|temp relation|normal
 datachecksums launcher|wal|init
 datachecksums launcher|wal|normal
-datachecksums worker|relation|bulkread
-datachecksums worker|relation|bulkwrite
 datachecksums worker|relation|init
 datachecksums worker|relation|normal
-datachecksums worker|relation|vacuum
 datachecksums worker|temp relation|normal
 datachecksums worker|wal|init
 datachecksums worker|wal|normal
-io worker|relation|bulkread
-io worker|relation|bulkwrite
 io worker|relation|init
 io worker|relation|normal
-io worker|relation|vacuum
 io worker|temp relation|normal
 io worker|wal|init
 io worker|wal|normal
-slotsync worker|relation|bulkread
-slotsync worker|relation|bulkwrite
 slotsync worker|relation|init
 slotsync worker|relation|normal
-slotsync worker|relation|vacuum
 slotsync worker|temp relation|normal
 slotsync worker|wal|init
 slotsync worker|wal|normal
-standalone backend|relation|bulkread
-standalone backend|relation|bulkwrite
 standalone backend|relation|init
 standalone backend|relation|normal
-standalone backend|relation|vacuum
 standalone backend|wal|init
 standalone backend|wal|normal
-startup|relation|bulkread
-startup|relation|bulkwrite
 startup|relation|init
 startup|relation|normal
-startup|relation|vacuum
 startup|wal|init
 startup|wal|normal
 walreceiver|wal|init
 walreceiver|wal|normal
-walsender|relation|bulkread
-walsender|relation|bulkwrite
 walsender|relation|init
 walsender|relation|normal
-walsender|relation|vacuum
 walsender|temp relation|normal
 walsender|wal|init
 walsender|wal|normal
@@ -111,7 +81,7 @@ walsummarizer|wal|init
 walsummarizer|wal|normal
 walwriter|wal|init
 walwriter|wal|normal
-(95 rows)
+(65 rows)
 \a
 -- List of registered statistics kinds.
 SELECT id, name, fixed_amount,
@@ -1757,70 +1727,6 @@ SELECT :io_sum_local_new_tblspc_writes > :io_sum_local_after_writes;
 (1 row)
 
 RESET temp_buffers;
--- Test that reuse of strategy buffers and reads of blocks into these reused
--- buffers while VACUUMing are tracked in pg_stat_io. If there is sufficient
--- demand for shared buffers from concurrent queries, some buffers may be
--- pinned by other backends before they can be reused. In such cases, the
--- backend will evict a buffer from outside the ring and add it to the
--- ring. This is considered an eviction and not a reuse.
--- Set wal_skip_threshold smaller than the expected size of
--- test_io_vac_strategy so that, even if wal_level is minimal, VACUUM FULL will
--- fsync the newly rewritten test_io_vac_strategy instead of writing it to WAL.
--- Writing it to WAL will result in the newly written relation pages being in
--- shared buffers -- preventing us from testing BAS_VACUUM BufferAccessStrategy
--- reads.
-SET wal_skip_threshold = '1 kB';
-SELECT sum(reuses) AS reuses, sum(reads) AS reads, sum(evictions) AS evictions
-  FROM pg_stat_io WHERE context = 'vacuum' \gset io_sum_vac_strategy_before_
-CREATE TABLE test_io_vac_strategy(a int, b int) WITH (autovacuum_enabled = 'false');
-INSERT INTO test_io_vac_strategy SELECT i, i from generate_series(1, 4500)i;
--- Ensure that the next VACUUM will need to perform IO by rewriting the table
--- first with VACUUM (FULL).
-VACUUM (FULL) test_io_vac_strategy;
--- Use the minimum BUFFER_USAGE_LIMIT to cause reuses or evictions with the
--- smallest table possible.
-VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) test_io_vac_strategy;
-SELECT pg_stat_force_next_flush();
- pg_stat_force_next_flush 
---------------------------
- 
-(1 row)
-
-SELECT sum(reuses) AS reuses, sum(reads) AS reads, sum(evictions) AS evictions
-  FROM pg_stat_io WHERE context = 'vacuum' \gset io_sum_vac_strategy_after_
-SELECT :io_sum_vac_strategy_after_reads > :io_sum_vac_strategy_before_reads;
- ?column? 
-----------
- t
-(1 row)
-
-SELECT (:io_sum_vac_strategy_after_reuses + :io_sum_vac_strategy_after_evictions) >
-  (:io_sum_vac_strategy_before_reuses + :io_sum_vac_strategy_before_evictions);
- ?column? 
-----------
- t
-(1 row)
-
-RESET wal_skip_threshold;
--- Test that extends done by a CTAS, which uses a BAS_BULKWRITE
--- BufferAccessStrategy, are tracked in pg_stat_io.
-SELECT sum(extends) AS io_sum_bulkwrite_strategy_extends_before
-  FROM pg_stat_io WHERE context = 'bulkwrite' \gset
-CREATE TABLE test_io_bulkwrite_strategy AS SELECT i FROM generate_series(1,100)i;
-SELECT pg_stat_force_next_flush();
- pg_stat_force_next_flush 
---------------------------
- 
-(1 row)
-
-SELECT sum(extends) AS io_sum_bulkwrite_strategy_extends_after
-  FROM pg_stat_io WHERE context = 'bulkwrite' \gset
-SELECT :io_sum_bulkwrite_strategy_extends_after > :io_sum_bulkwrite_strategy_extends_before;
- ?column? 
-----------
- t
-(1 row)
-
 -- Test IO stats reset
 SELECT pg_stat_have_stats('io', 0, 0);
  pg_stat_have_stats 
@@ -1841,20 +1747,16 @@ SELECT pg_stat_reset_shared('io');
 SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS io_stats_post_reset
   FROM pg_stat_io \gset
 SELECT :io_stats_post_reset < :io_stats_pre_reset;
- ?column? 
-----------
- t
-(1 row)
-
+ERROR:  syntax error at or near ":"
+LINE 1: SELECT :io_stats_post_reset < :io_stats_pre_reset;
+               ^
 SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_post_reset
   FROM pg_stat_get_backend_io(pg_backend_pid()) \gset
 -- pg_stat_reset_shared() did not reset backend IO stats
 SELECT :my_io_stats_pre_reset <= :my_io_stats_post_reset;
- ?column? 
-----------
- t
-(1 row)
-
+ERROR:  syntax error at or near ":"
+LINE 1: SELECT :my_io_stats_pre_reset <= :my_io_stats_post_reset;
+               ^
 -- but pg_stat_reset_backend_stats() does
 SELECT pg_stat_reset_backend_stats(pg_backend_pid());
  pg_stat_reset_backend_stats 
@@ -1865,11 +1767,9 @@ SELECT pg_stat_reset_backend_stats(pg_backend_pid());
 SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_post_backend_reset
   FROM pg_stat_get_backend_io(pg_backend_pid()) \gset
 SELECT :my_io_stats_pre_reset > :my_io_stats_post_backend_reset;
- ?column? 
-----------
- t
-(1 row)
-
+ERROR:  syntax error at or near ":"
+LINE 1: SELECT :my_io_stats_pre_reset > :my_io_stats_post_backend_re...
+               ^
 -- Check invalid input for pg_stat_get_backend_io()
 SELECT pg_stat_get_backend_io(NULL);
  pg_stat_get_backend_io 
diff --git a/src/test/regress/expected/vacuum.out b/src/test/regress/expected/vacuum.out
index d4696bc3325..85d06907cec 100644
--- a/src/test/regress/expected/vacuum.out
+++ b/src/test/regress/expected/vacuum.out
@@ -526,25 +526,6 @@ SELECT t.relfilenode = :toast_filenode AS is_same_toast_filenode
  f
 (1 row)
 
--- BUFFER_USAGE_LIMIT option
-VACUUM (BUFFER_USAGE_LIMIT '512 kB') vac_option_tab;
-ANALYZE (BUFFER_USAGE_LIMIT '512 kB') vac_option_tab;
--- try disabling the buffer usage limit
-VACUUM (BUFFER_USAGE_LIMIT 0) vac_option_tab;
-ANALYZE (BUFFER_USAGE_LIMIT 0) vac_option_tab;
--- value exceeds max size error
-VACUUM (BUFFER_USAGE_LIMIT 16777220) vac_option_tab;
-ERROR:  BUFFER_USAGE_LIMIT option must be 0 or between 128 kB and 16777216 kB
--- value is less than min size error
-VACUUM (BUFFER_USAGE_LIMIT 120) vac_option_tab;
-ERROR:  BUFFER_USAGE_LIMIT option must be 0 or between 128 kB and 16777216 kB
--- integer overflow error
-VACUUM (BUFFER_USAGE_LIMIT 10000000000) vac_option_tab;
-ERROR:  BUFFER_USAGE_LIMIT option must be 0 or between 128 kB and 16777216 kB
-HINT:  Value exceeds integer range.
--- incompatible with VACUUM FULL error
-VACUUM (BUFFER_USAGE_LIMIT '512 kB', FULL) vac_option_tab;
-ERROR:  BUFFER_USAGE_LIMIT cannot be specified for VACUUM FULL
 -- SKIP_DATABASE_STATS option
 VACUUM (SKIP_DATABASE_STATS) vactst;
 -- ONLY_DATABASE_STATS option
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index 4c265d1245c..7a049d88a77 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -814,48 +814,6 @@ SELECT sum(writes) AS io_sum_local_new_tblspc_writes
 SELECT :io_sum_local_new_tblspc_writes > :io_sum_local_after_writes;
 RESET temp_buffers;
 
--- Test that reuse of strategy buffers and reads of blocks into these reused
--- buffers while VACUUMing are tracked in pg_stat_io. If there is sufficient
--- demand for shared buffers from concurrent queries, some buffers may be
--- pinned by other backends before they can be reused. In such cases, the
--- backend will evict a buffer from outside the ring and add it to the
--- ring. This is considered an eviction and not a reuse.
-
--- Set wal_skip_threshold smaller than the expected size of
--- test_io_vac_strategy so that, even if wal_level is minimal, VACUUM FULL will
--- fsync the newly rewritten test_io_vac_strategy instead of writing it to WAL.
--- Writing it to WAL will result in the newly written relation pages being in
--- shared buffers -- preventing us from testing BAS_VACUUM BufferAccessStrategy
--- reads.
-SET wal_skip_threshold = '1 kB';
-SELECT sum(reuses) AS reuses, sum(reads) AS reads, sum(evictions) AS evictions
-  FROM pg_stat_io WHERE context = 'vacuum' \gset io_sum_vac_strategy_before_
-CREATE TABLE test_io_vac_strategy(a int, b int) WITH (autovacuum_enabled = 'false');
-INSERT INTO test_io_vac_strategy SELECT i, i from generate_series(1, 4500)i;
--- Ensure that the next VACUUM will need to perform IO by rewriting the table
--- first with VACUUM (FULL).
-VACUUM (FULL) test_io_vac_strategy;
--- Use the minimum BUFFER_USAGE_LIMIT to cause reuses or evictions with the
--- smallest table possible.
-VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) test_io_vac_strategy;
-SELECT pg_stat_force_next_flush();
-SELECT sum(reuses) AS reuses, sum(reads) AS reads, sum(evictions) AS evictions
-  FROM pg_stat_io WHERE context = 'vacuum' \gset io_sum_vac_strategy_after_
-SELECT :io_sum_vac_strategy_after_reads > :io_sum_vac_strategy_before_reads;
-SELECT (:io_sum_vac_strategy_after_reuses + :io_sum_vac_strategy_after_evictions) >
-  (:io_sum_vac_strategy_before_reuses + :io_sum_vac_strategy_before_evictions);
-RESET wal_skip_threshold;
-
--- Test that extends done by a CTAS, which uses a BAS_BULKWRITE
--- BufferAccessStrategy, are tracked in pg_stat_io.
-SELECT sum(extends) AS io_sum_bulkwrite_strategy_extends_before
-  FROM pg_stat_io WHERE context = 'bulkwrite' \gset
-CREATE TABLE test_io_bulkwrite_strategy AS SELECT i FROM generate_series(1,100)i;
-SELECT pg_stat_force_next_flush();
-SELECT sum(extends) AS io_sum_bulkwrite_strategy_extends_after
-  FROM pg_stat_io WHERE context = 'bulkwrite' \gset
-SELECT :io_sum_bulkwrite_strategy_extends_after > :io_sum_bulkwrite_strategy_extends_before;
-
 -- Test IO stats reset
 SELECT pg_stat_have_stats('io', 0, 0);
 SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS io_stats_pre_reset
diff --git a/src/test/regress/sql/vacuum.sql b/src/test/regress/sql/vacuum.sql
index 247b8e23b23..506c4389765 100644
--- a/src/test/regress/sql/vacuum.sql
+++ b/src/test/regress/sql/vacuum.sql
@@ -390,21 +390,6 @@ SELECT t.relfilenode = :toast_filenode AS is_same_toast_filenode
   FROM pg_class c, pg_class t
   WHERE c.reltoastrelid = t.oid AND c.relname = 'vac_option_tab';
 
--- BUFFER_USAGE_LIMIT option
-VACUUM (BUFFER_USAGE_LIMIT '512 kB') vac_option_tab;
-ANALYZE (BUFFER_USAGE_LIMIT '512 kB') vac_option_tab;
--- try disabling the buffer usage limit
-VACUUM (BUFFER_USAGE_LIMIT 0) vac_option_tab;
-ANALYZE (BUFFER_USAGE_LIMIT 0) vac_option_tab;
--- value exceeds max size error
-VACUUM (BUFFER_USAGE_LIMIT 16777220) vac_option_tab;
--- value is less than min size error
-VACUUM (BUFFER_USAGE_LIMIT 120) vac_option_tab;
--- integer overflow error
-VACUUM (BUFFER_USAGE_LIMIT 10000000000) vac_option_tab;
--- incompatible with VACUUM FULL error
-VACUUM (BUFFER_USAGE_LIMIT '512 kB', FULL) vac_option_tab;
-
 -- SKIP_DATABASE_STATS option
 VACUUM (SKIP_DATABASE_STATS) vactst;
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 56c1f997f88..064a9878e72 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -355,8 +355,6 @@ BtreeLevel
 Bucket
 BufFile
 Buffer
-BufferAccessStrategy
-BufferAccessStrategyType
 BufferCacheOsPagesContext
 BufferCacheOsPagesRec
 BufferDesc
-- 
2.51.2



^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention
  2026-04-25 20:08 [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-04-27 14:15 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-04-28 14:30   ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 14:00     ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 18:07       ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-11 15:54         ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
@ 2026-07-14 17:53           ` Greg Burd <greg@burd.me>
  2026-07-20 15:36             ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-22 14:52             ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Ants Aasma <ants.aasma@cybertec.at>
  2026-07-22 15:10             ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  0 siblings, 3 replies; 21+ messages in thread

From: Greg Burd @ 2026-07-14 17:53 UTC (permalink / raw)
  To: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; +Cc: Tomas Vondra <tomas@vondra.me>; Nathan Bossart <nathandbossart@gmail.com>; Andres Freund <andres@anarazel.de>

Hi,

v6 is attached, rebased on master (d7cd5d6).

  0001  Batch the clock sweep to reduce nextVictimBuffer atomic contention
  0002  Replace the usage_count clock sweep with a cooling-stage evictor
  0003  Remove BufferAccessStrategy; scan resistance is now intrinsic

Two kinds of change since v5: three correctness fixes, and one bgwriter
tweak (folded into 0002) that deals with a regression I found while chasing
down whether removing StrategyRejectBuffer would hurt.  Most of this mail is
that second thing, with numbers, because it is the part I most want picked
apart.

The three fixes first, briefly:

stats.out was a real bug.  Once the rings are gone in 0003, pg_stat_io's
"reuses" column is NULL for every row, so the reset test's sum(reuses) came
back NULL, the \gset made an empty variable, and the next line died with
"syntax error at or near ':'".  I dropped the now-meaningless reuses term
from the reset-test sums and regenerated the expected output.

force_cool now honours the reference bit.  When the sweep has found no COOL
victim in a full pass and starts demoting HOT buffers, it applies the same
single second-chance ref-bit rule the bgwriter uses instead of demoting
unconditionally, so a buffer touched mid-pass isn't demoted a tick later.
And completePasses now counts an intra-batch wrap: with 0001's batched claim
a batch can straddle the NBuffers wrap, and that wasn't being reflected, so
the bgwriter's pass accounting could drift by up to a batch.


Let's talk regression...

The buffer cache ring strategy didn't only bound cache pollution, it also
deferred writeback. A backend running a big COPY, bulk UPDATE or VACUUM
reused a small ring and, via StrategyRejectBuffer, pushed dirty victims back
for the bgwriter and checkpointer to write instead of writing (and
WAL-flushing) them inline on its own allocation path.  Pull the rings (as in
0003) and, done naively, that deferral goes too.

I wrote a small benchmark to see how much that costs.  It's on an AWS
m6id.metal (Sapphire Rapids, 128 vCPU, 512 GB), with the data and WAL on the
instance-local NVMe (mdraid-0), not EBS.  Both builds are meson
debugoptimized, cassert off, libnuma on; stock is master at d15a6bc2e16,
patched is the series on the same base.

Server pinned with "numactl --cpunodebind=0 --interleave=all",
non-default GUCs, same for both:

    shared_buffers = 8GB            # small enough that a bulk op spills
    huge_pages = try
    maintenance_work_mem = 1GB
    max_wal_size = 64GB
    min_wal_size = 8GB
    checkpoint_timeout = 30min
    checkpoint_completion_target = 0.9
    wal_level = replica
    fsync = on
    synchronous_commit = on
    track_io_timing = on

The table is ~4GB (20M rows, 180-byte payload, fillfactor 90), half of
shared_buffers, so a whole-table dirtying op has to evict dirty victims.
Each of the three statements runs on its own fresh cluster, three times,
median reported; IO stats are reset right before each timed statement so
the write count is per-statement, not cumulative:

    CREATE TABLE bulk (id int, pad text) WITH (fillfactor=90);
    INSERT INTO bulk SELECT g, repeat('x',180) FROM generate_series(1,2e7) g;
    VACUUM (FREEZE) bulk; CHECKPOINT;

    -- (1) UPDATE, dirties every page
    UPDATE bulk SET id = id + 1;

    -- (2) bulk INSERT into an empty table
    CREATE TABLE ins (id int, pad text);
    INSERT INTO ins SELECT g, repeat('y',180) FROM generate_series(1,2e7) g;

    -- (3) VACUUM after dirtying half the pages
    UPDATE bulk SET pad = repeat('z',180) WHERE id % 2 = 0; CHECKPOINT;
    VACUUM bulk;

One caveat up front: (2) is INSERT...SELECT, not the COPY command.  It hits
the same relation-extend + WAL-logged-write path BAS_BULKWRITE served, which
is what I wanted to stress, but if that distinction matters to anyone's
conclusion I'll re-run with COPY FROM.

For each statement I record time_s (wall seconds), bwrites (buffers the
backend wrote itself -- sum(writes) from pg_stat_io where object='relation'
after a forced flush, i.e. the writes the rings used to hand off), and
wal_gb (LSN delta, just a check the two builds did the same work).  Full
series, Option B included:

    workload      metric     stock       patched     impact of patches
    ------------  ---------  ----------  ----------  ------------------
    bulk UPDATE   time_s         26.0        27.3     5% slower
                  bwrites       90754      158165     74% more
                  wal_gb          5.83        5.90    +1%
    bulk INSERT   time_s         14.2        15.6     10% slower
                  bwrites           0           0     none; extend path
                  wal_gb          5.00        5.11    +2%
    VACUUM        time_s         11.4         9.3     19% faster
                  bwrites      996255      390945     61% fewer
                  wal_gb          6.80        6.77    -1%

Per-run time_s so you can see the spread (stock / patched):
  UPDATE  29.0 25.9 26.0  /  29.4 27.2 27.3
  INSERT  15.1 14.2 13.4  /  15.5 15.6 15.7
  VACUUM  11.5 11.4 11.4  /   9.2  9.3  9.4

The 5% and 10% are only a couple of run-to-run wobbles apart, but the
direction is steady across runs; the VACUUM win and the bwrites jump are
well clear of the noise.

That +74% bwrites on UPDATE is the crux: the backend is doing the writeback
the rings used to defer.  Before I added the bgwriter tweak it was worse --
8% slower, 77% more writes -- and VACUUM was only 8% faster rather than 19%.
Two things I tried.  First, having the sweep prefer a clean COOL victim and
skip dirty ones (bounded, so it can't turn into a full-pool rescan).  That
made UPDATE *slower still*, 14% over stock, because under bulk dirtying the
clean victims simply aren't there and the skipping is wasted work.  I threw
that out.

What stuck instead was letting the bgwriter keep up.  Its per-cycle
clean-write cap is bgwriter_lru_maxpages, default 100; under bulk dirtying
the pool fills with dirty COOL buffers faster than 100/cycle can clean, the
bgwriter stalls at the cap, and the backend is left flushing victims itself.
So when the bgwriter's own predicted demand for the next cycle already
exceeds the cap, I let the cap follow demand:

        write_limit = bgwriter_lru_maxpages;
        if (upcoming_alloc_est > write_limit)
            write_limit = upcoming_alloc_est;

It's still bounded -- by demand and by lapping the strategy point -- so it
can't run away, and anything whose demand sits below the cap
(i.e. everything that isn't bulk-dirtying) is untouched. A small change in
BgBufferSync, folded into 0002 in v6.

That halves the UPDATE slowdown and flips VACUUM into a clear win, but it
doesn't erase the residual, so I ran one more comparison to see where the
residual actually lives: patches 0001+0002 (rings still in place, 0003 not
applied).  Same instance type, same three runs:

    workload      metric     stock       0001+0002   impact
    ------------  ---------  ----------  ----------  ------------------
    bulk UPDATE   time_s         25.8        25.4     1% faster
                  bwrites       90797      122416     35% more
    bulk INSERT   time_s         14.0        15.2     8% slower
                  bwrites           0        ~0       extend path
    VACUUM        time_s         11.5         9.7     15% faster
                  bwrites      986865      500190     49% fewer

So with the rings still doing their deferral, the new evictor on its own is
a touch faster on UPDATE and already a solid win on VACUUM.  The 5% UPDATE
slowdown in the full series is therefore the price of removing the rings
(0003) specifically -- not the evictor -- and now I can say that against a
rings-kept baseline rather than just asserting it.  The bgwriter tweak moves
those writes off the critical path (why the time recovers to 5%) but doesn't
reduce their number; some of that is just the cost of not having a private
ring to write-behind from.

Bulk INSERT is the more interesting one, because it's 8% slower even with
the rings kept -- so it's not about ring removal at all, it's something in
0001+0002 on the extend/write path (the pages are extended, not evicted:
bwrites is ~0, and it's WAL-bound at +2%).  0003 adds only the last two
points.  I haven't fully root-caused it; my guess is lost write coalescing
on the extend path that BULKWRITE gave for free.  It's small and contained,
and since it shows up without 0003 I expect it's fixable without bringing
the strategy machinery back.  I'd rather understand it than wave at it, so
I'll follow up on that specifically.

The harness is just three psql scripts and the pg_stat_io / pg_stat_wal
deltas above; happy to post them if anyone wants to reproduce.

One loose end: 0003 drops the vacuum_buffer_usage_limit GUC, which only ever
sized the BAS_VACUUM ring.  With no ring to size it does nothing, so I
removed it.


So the questions I'd most like opinions on:

Is a 5%-slower bulk UPDATE (the cost of retiring the rings) and an 8-10%
slower bulk INSERT (an extend-path effect independent of 0003) an acceptable
price for making scan resistance an algorithm property and dropping the
rings?  Or would you rather the rings stayed and 0002 rode alongside them --
which the isolation run shows is a real option, since 0001+0002 with the
rings kept is neutral-to-better on the eviction-bound workloads?  My hope
was to offer both a better/simpler algorithm for clock-sweep as well as
remove a lot of code.

Separately: is admitting demand-loaded pages COOL (probationary, promoted on
the second touch) an acceptable basis for scan resistance in core at all?
Where is my in-cache testing flattering the sweep, and what would you want
measured instead?  And is reinterpreting the usage_count field as
{HOT/COOL, ref} bits, plus collapsing pg_stat_io's contexts, something you'd
accept, or is there a representation you'd want to see first?

This idea started with the question of, "I wonder if we really need a 5
count on each buffer in the clock-sweep eviction path or if that is simply
overhead and carries no signal at all for eviction or information useful to
the bgwriter?"  FWIW, I feel that the COOL/HOT clock is simplier and that
tests show it is better than the 0..5 clock.  I'm not thrilled by *any*
regression, especially not one that induces more WAL traffic, so I'm open to
thoughts on this.


That's all for now.

-greg

Attachments:

  [text/x-patch] v6-0001-Batch-the-clock-sweep-to-reduce-nextVictimBuffer-.patch (9.8K, ../../96dc1db7-58a2-41ba-ae2e-bea86fa2fb3a@app.fastmail.com/2-v6-0001-Batch-the-clock-sweep-to-reduce-nextVictimBuffer-.patch)
  download | inline diff:
From 0d8492a6b59fd0acfc2ebc75ebfbd8e8842ee3ac Mon Sep 17 00:00:00 2001
From: Greg Burd <greg@burd.me>
Date: Mon, 6 Jul 2026 17:25:00 -0400
Subject: [PATCH v6 1/3] Batch the clock sweep to reduce nextVictimBuffer
 atomic contention

StrategyGetBuffer() advances the shared clock hand, nextVictimBuffer, with a
pg_atomic_fetch_add_u32(..., 1) on every tick.  On a multi-socket system the
cache line holding that counter has to travel over the interconnect on each
operation, pushing a sweep tick from ~20ns (same-socket, line warm in L1/L2)
into the ~100-200ns range.  Under eviction pressure with hundreds of backends
in StrategyGetBuffer() concurrently, that single cache line becomes the
dominant cost of the sweep, visible as elevated bus-cycles and cache-misses in
a perf profile.

Have each backend claim a run of consecutive buffer IDs from the shared hand
with a single fetch-add and then iterate through them privately.  The sweep
still advances through the pool in order, each buffer is still visited exactly
once per complete pass, and the meaning of the clock state is unchanged; only
the temporal ordering of visits within a pass changes, which the algorithm
does not depend on.  The contended atomic now fires roughly once per batch
rather than once per buffer.

The batch is one cache line's worth of clock-hand values --
PG_CACHE_LINE_SIZE / sizeof(uint32) -- capped at NBuffers so a claim can never
wrap the pool more than once.  Batching only helps when the counter's cache
line actually bounces between sockets, so it is enabled only on multi-node
NUMA hardware (pg_numa_get_max_node() >= 1); on a single socket, or where
libnuma is unavailable, the batch size stays 1 and the code path is
byte-identical to the stock clock sweep.

Wraparound handling is adjusted: with batching, several backends can each see
a fetch-add return a value past NBuffers within the same pass.  Any such
backend takes buffer_strategy_lock, re-reads the counter, and if it is still
out of range wraps it with a single CAS and increments completePasses.
StrategySyncStart() continues to see a consistent (nextVictimBuffer,
completePasses) pair.

This is the batched-clock-sweep idea from Jim Mlodgenski's pgsql-hackers
thread, adapted to derive the batch size from the platform cache-line size.

Co-authored-by: Jim Mlodgenski <mlodj@amazon.com>
---
 src/backend/storage/buffer/freelist.c | 152 ++++++++++++++++++--------
 1 file changed, 109 insertions(+), 43 deletions(-)

diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index fdb5bad7910..46104b3341d 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -22,6 +22,7 @@
 #include "storage/proc.h"
 #include "storage/shmem.h"
 #include "storage/subsystems.h"
+#include "port/pg_numa.h"
 
 #define INT_ACCESS_ONCE(var)	((int)(*((volatile int *)&(var))))
 
@@ -100,68 +101,110 @@ static BufferDesc *GetBufferFromRing(BufferAccessStrategy strategy,
 static void AddBufferToRing(BufferAccessStrategy strategy,
 							BufferDesc *buf);
 
+/*
+ * Per-backend state for the batched clock sweep.  Each backend claims a run
+ * of consecutive clock-hand values with a single atomic fetch-add and then
+ * iterates through them privately, so the contended nextVictimBuffer cache
+ * line is touched roughly 1/batch as often.  MyBatchPos is the next hand
+ * value to hand out; MyBatchEnd is one past the end of the claimed run.  Both
+ * are absolute (monotonically increasing) hand values; the buffer id is the
+ * value modulo NBuffers.
+ */
+static uint32 MyBatchPos = 0;
+static uint32 MyBatchEnd = 0;
+
+/*
+ * Number of clock-hand values a backend claims per atomic fetch-add,
+ * computed once at startup (see StrategyCtlShmemInit).  When batching is
+ * enabled it is one cache line's worth of hand advance, so concurrent
+ * backends sweep non-overlapping, cache-line-sized runs of the pool; the
+ * global sweep order is preserved (each buffer is still visited exactly once
+ * per pass).  Batching is enabled only on multi-node NUMA hardware; otherwise
+ * this stays 1 and the sweep is byte-identical to the stock clock.
+ */
+static uint32 ClockSweepBatchSize = 1;
+
 /*
  * ClockSweepTick - Helper routine for StrategyGetBuffer()
  *
- * Move the clock hand one buffer ahead of its current position and return the
- * id of the buffer now under the hand.
+ * Return the next buffer to consider for eviction.  Backends claim batches of
+ * consecutive buffer IDs from the shared clock hand, then iterate through
+ * them locally without further atomic operations.  This preserves the global
+ * sweep order while reducing contention on the shared counter.
  */
 static inline uint32
 ClockSweepTick(void)
 {
 	uint32		victim;
 
-	/*
-	 * Atomically move hand ahead one buffer - if there's several processes
-	 * doing this, this can lead to buffers being returned slightly out of
-	 * apparent order.
-	 */
-	victim =
-		pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer, 1);
-
-	if (victim >= NBuffers)
+	if (MyBatchPos >= MyBatchEnd)
 	{
-		uint32		originalVictim = victim;
-
-		/* always wrap what we look up in BufferDescriptors */
-		victim = victim % NBuffers;
-
 		/*
-		 * If we're the one that just caused a wraparound, force
-		 * completePasses to be incremented while holding the spinlock. We
-		 * need the spinlock so StrategySyncStart() can return a consistent
-		 * value consisting of nextVictimBuffer and completePasses.
+		 * Claim a fresh batch from the shared clock hand.  This is the only
+		 * atomic operation per batch, reducing contention by the batch size.
 		 */
-		if (victim == 0)
-		{
-			uint32		expected;
-			uint32		wrapped;
-			bool		success = false;
+		uint32		start;
+		uint32		batch_size = ClockSweepBatchSize;
 
-			expected = originalVictim + 1;
+		start = pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer,
+										batch_size);
 
-			while (!success)
-			{
-				/*
-				 * Acquire the spinlock while increasing completePasses. That
-				 * allows other readers to read nextVictimBuffer and
-				 * completePasses in a consistent manner which is required for
-				 * StrategySyncStart().  In theory delaying the increment
-				 * could lead to an overflow of nextVictimBuffers, but that's
-				 * highly unlikely and wouldn't be particularly harmful.
-				 */
-				SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
+		if (start >= (uint32) NBuffers)
+		{
+			start = start % NBuffers;
 
-				wrapped = expected % NBuffers;
+			/*
+			 * The counter has grown past NBuffers; try to wrap it back.  We
+			 * must hold the spinlock so StrategySyncStart() can read
+			 * nextVictimBuffer and completePasses consistently.
+			 *
+			 * With batching, multiple backends may each land a fetch-add
+			 * that returns a value past NBuffers in the same pass.  After
+			 * acquiring the spinlock we re-read the counter: if another
+			 * backend already wrapped it below NBuffers we are done.
+			 */
+			SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
+			{
+				uint32		current;
+				uint32		wrapped;
 
-				success = pg_atomic_compare_exchange_u32(&StrategyControl->nextVictimBuffer,
-														 &expected, wrapped);
-				if (success)
-					StrategyControl->completePasses++;
-				SpinLockRelease(&StrategyControl->buffer_strategy_lock);
+				current = pg_atomic_read_u32(&StrategyControl->nextVictimBuffer);
+				if (current >= (uint32) NBuffers)
+				{
+					wrapped = current % NBuffers;
+					if (pg_atomic_compare_exchange_u32(&StrategyControl->nextVictimBuffer,
+													   &current, wrapped))
+						StrategyControl->completePasses++;
+				}
 			}
+			SpinLockRelease(&StrategyControl->buffer_strategy_lock);
+		}
+		else if (start + batch_size > (uint32) NBuffers)
+		{
+			/*
+			 * The fetch-add returned a value below NBuffers, but this batch
+			 * spans the wrap point (start .. NBuffers-1 then 0 .. k are
+			 * iterated locally via "% NBuffers").  That crossing is a
+			 * complete pass too, but the wrap-and-increment path above only
+			 * fires when the fetch-add return is itself >= NBuffers, so
+			 * without this it would go uncounted and completePasses would
+			 * drift low (it feeds bgwriter pacing / StrategySyncStart, not
+			 * correctness).  The batch is capped at NBuffers, so a batch
+			 * spans the wrap at most once; count it under the spinlock for a
+			 * consistent (nextVictimBuffer, completePasses) pair.
+			 */
+			SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
+			StrategyControl->completePasses++;
+			SpinLockRelease(&StrategyControl->buffer_strategy_lock);
 		}
+
+		MyBatchPos = start;
+		MyBatchEnd = start + batch_size;
 	}
+
+	victim = MyBatchPos % NBuffers;
+	MyBatchPos++;
+
 	return victim;
 }
 
@@ -408,6 +451,29 @@ StrategyCtlShmemInit(void *arg)
 
 	/* No pending notification */
 	StrategyControl->bgwprocno = -1;
+
+	/*
+	 * Decide whether to batch the clock sweep.
+	 *
+	 * Batching claims a run of consecutive buffer IDs per atomic fetch-add so
+	 * concurrent backends touch the shared nextVictimBuffer cache line ~1/batch
+	 * as often -- a win only when that line actually bounces across sockets,
+	 * i.e. on multi-node NUMA hardware.  On a single socket the atomic is
+	 * already node-local and batching would only make backends skip ahead for
+	 * no benefit, so we fall back to batch size 1 there (byte-identical to the
+	 * stock one-buffer-at-a-time clock sweep).
+	 *
+	 * Batch (> 1) only when libnuma reports more than one node
+	 * (pg_numa_get_max_node() >= 1); pg_numa_init() returns -1 when NUMA is
+	 * unavailable.  (Benchmarking showed the win holds with or without huge
+	 * pages, so huge-page availability is intentionally not part of the gate.)
+	 */
+	if (pg_numa_init() != -1 &&
+		pg_numa_get_max_node() >= 1)
+		ClockSweepBatchSize = Min(PG_CACHE_LINE_SIZE / (uint32) sizeof(uint32),
+								  (uint32) NBuffers);
+	else
+		ClockSweepBatchSize = 1;
 }
 
 
-- 
2.51.2



  [text/x-patch] v6-0002-Replace-the-usage_count-clock-sweep-with-a-coolin.patch (30.4K, ../../96dc1db7-58a2-41ba-ae2e-bea86fa2fb3a@app.fastmail.com/3-v6-0002-Replace-the-usage_count-clock-sweep-with-a-coolin.patch)
  download | inline diff:
From 826edb64e97e3ba2d1e2d1aedd5e76dec98fff2d Mon Sep 17 00:00:00 2001
From: Greg Burd <greg@burd.me>
Date: Mon, 6 Jul 2026 19:22:45 -0400
Subject: [PATCH v6 2/3] Replace the usage_count clock sweep with a
 cooling-stage evictor

Replace the 0..5 usage_count buffer-replacement policy with a cooling-stage
clock (the LeanStore / 2Q-A1 model): a buffer is simply HOT (recently used)
or COOL (an eviction candidate), with "pinned" being the existing refcount.
There is no per-buffer access counter.

  - A demand-loaded page is admitted COOL (probationary), not HOT.  A second
    access via PinBuffer promotes it COOL -> HOT (the rescue).  So a page
    touched once -- a sequential scan -- fills and drains the COOL stage and
    is evicted from it without ever displacing the HOT working set.  Scan
    resistance is intrinsic to the replacement algorithm, which is what lets a
    later commit remove the BufferAccessStrategy ring buffers entirely.

  - The foreground sweep in StrategyGetBuffer() reclaims an already-COOL,
    unpinned buffer, pinning it with a CAS so a racing PinBuffer always wins.
    Promotion (COOL -> HOT) and demotion (HOT -> COOL) are single-bit
    transitions; only the eviction claim is a CAS.

  - The background writer maintains the supply of COOL victims.  As its LRU
    scan runs ahead of the clock hand it demotes HOT buffers to COOL, so the
    foreground finds a victim in a single pass rather than having to cool
    buffers itself.  The demotion is demand-driven -- bounded by the predicted
    allocation for the next cycle -- so it stages just enough COOL buffers
    without cooling the whole pool, and it is done under the buffer header
    lock the scan already holds.  A single second-chance reference bit
    (set by PinBuffer, cleared on the bgwriter's first pass over a HOT buffer)
    spares a recently-accessed buffer one cooling pass, keeping the genuinely
    hot set out of the COOL stage under scan pressure.  BgBufferSync also
    tracks the reusable-buffer density it directly observes on a shorter
    smoothing window, so a burst of probationary/scan COOL pages is followed
    promptly rather than averaged away.  Under a bulk-dirtying workload the
    per-cycle clean-write cap is raised from bgwriter_lru_maxpages to predicted
    demand so the bgwriter keeps supplying clean victims, rather than the
    foreground sweep having to flush dirty victims inline; normal workloads,
    where demand is below the cap, are unaffected.

The 4-bit usage_count field is reinterpreted in place: bit 0 is the HOT/COOL
state (BUF_COOLSTATE_ONE), bit 1 the reference bit (BUF_REFBIT).  The 64-bit
buffer-state layout -- refcount, flag and lock offsets and their StaticAsserts
-- is unchanged; only the meaning of the field and the instructions that touch
it change.  A StaticAssert requires the field to be at least 2 bits wide so a
future width change cannot push the reference bit into the flag bits.
BM_MAX_USAGE_COUNT becomes BUF_COOLSTATE_HOT (1), so the pin fast path
saturates at HOT.  Local (temp-table) buffers get the same two-state
treatment; being single-backend they need no background cooler.

Because the reference bit shares the field, the full 4-bit value can be 0..3;
readers that mean "the cooling state" must use BUF_STATE_GET_COOLSTATE(), which
masks to bit 0 and returns only 0 (COOL) or 1 (HOT), never the raw field.
contrib/pg_buffercache is updated accordingly: its usagecount column and the
pg_buffercache_summary average report the cooling state (0 = COOL, 1 = HOT),
and pg_buffercache_usage_counts() buckets on it.  Using the raw 4-bit getter
there would index its BM_MAX_USAGE_COUNT+1 = 2-element arrays with values up
to 3 and overrun the stack; masking to the cooling bit keeps the index in
range.  The reference bit is deliberately not exposed as usagecount.

Depends on the batched clock sweep from the previous commit.
---
 contrib/pg_buffercache/pg_buffercache_pages.c |   6 +-
 src/backend/storage/buffer/bufmgr.c           | 124 +++++++++++----
 src/backend/storage/buffer/freelist.c         | 141 ++++++++++++------
 src/backend/storage/buffer/localbuf.c         |  13 +-
 src/include/storage/buf_internals.h           |  63 +++++++-
 5 files changed, 260 insertions(+), 87 deletions(-)

diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index 510455998aa..b94e6f6fda8 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -161,7 +161,7 @@ pg_buffercache_pages(PG_FUNCTION_ARGS)
 		reldatabase = bufHdr->tag.dbOid;
 		forknum = BufTagGetForkNum(&bufHdr->tag);
 		blocknum = bufHdr->tag.blockNum;
-		usagecount = BUF_STATE_GET_USAGECOUNT(buf_state);
+		usagecount = BUF_STATE_GET_COOLSTATE(buf_state);
 		pinning_backends = BUF_STATE_GET_REFCOUNT(buf_state);
 
 		if (buf_state & BM_DIRTY)
@@ -605,7 +605,7 @@ pg_buffercache_summary(PG_FUNCTION_ARGS)
 		if (buf_state & BM_VALID)
 		{
 			buffers_used++;
-			usagecount_total += BUF_STATE_GET_USAGECOUNT(buf_state);
+			usagecount_total += BUF_STATE_GET_COOLSTATE(buf_state);
 
 			if (buf_state & BM_DIRTY)
 				buffers_dirty++;
@@ -655,7 +655,7 @@ pg_buffercache_usage_counts(PG_FUNCTION_ARGS)
 
 		CHECK_FOR_INTERRUPTS();
 
-		usage_count = BUF_STATE_GET_USAGECOUNT(buf_state);
+		usage_count = BUF_STATE_GET_COOLSTATE(buf_state);
 		usage_counts[usage_count]++;
 
 		if (buf_state & BM_DIRTY)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 3908529872a..1f5a00849e6 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -83,6 +83,7 @@
 /* Bits in SyncOneBuffer's return value */
 #define BUF_WRITTEN				0x01
 #define BUF_REUSABLE			0x02
+#define BUF_COOLED				0x04
 
 #define RELS_BSEARCH_THRESHOLD		20
 
@@ -634,7 +635,7 @@ static void PinBuffer_Locked(BufferDesc *buf);
 static void UnpinBuffer(BufferDesc *buf);
 static void UnpinBufferNoOwner(BufferDesc *buf);
 static void BufferSync(int flags);
-static int	SyncOneBuffer(int buf_id, bool skip_recently_used,
+static int	SyncOneBuffer(int buf_id, bool skip_recently_used, bool cool_if_hot,
 						  WritebackContext *wb_context);
 static void WaitIO(BufferDesc *buf);
 static void AbortBufferIO(Buffer buffer);
@@ -2333,7 +2334,10 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	 * checkpoints, except for their "init" forks, which need to be treated
 	 * just like permanent relations.
 	 */
-	set_bits |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
+	set_bits |= BM_TAG_VALID;
+	/* Admit the newly loaded page COOL (probation); a second access via
+	 * PinBuffer promotes it to HOT.  This is what makes a one-touch scan
+	 * self-evicting -- see the cooling-state notes in buf_internals.h. */
 	if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM)
 		set_bits |= BM_PERMANENT;
 
@@ -3002,7 +3006,9 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 
 			victim_buf_hdr->tag = tag;
 
-			set_bits |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
+			set_bits |= BM_TAG_VALID;
+			/* Admit COOL (probation); see the comment at the other admission
+			 * site and the cooling-state notes in buf_internals.h. */
 			if (bmr.relpersistence == RELPERSISTENCE_PERMANENT || fork == INIT_FORKNUM)
 				set_bits |= BM_PERMANENT;
 
@@ -3332,21 +3338,17 @@ PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy,
 			/* increase refcount */
 			buf_state += BUF_REFCOUNT_ONE;
 
-			if (strategy == NULL)
-			{
-				/* Default case: increase usagecount unless already max. */
-				if (BUF_STATE_GET_USAGECOUNT(buf_state) < BM_MAX_USAGE_COUNT)
-					buf_state += BUF_USAGECOUNT_ONE;
-			}
-			else
-			{
-				/*
-				 * Ring buffers shouldn't evict others from pool.  Thus we
-				 * don't make usagecount more than 1.
-				 */
-				if (BUF_STATE_GET_USAGECOUNT(buf_state) == 0)
-					buf_state += BUF_USAGECOUNT_ONE;
-			}
+			/*
+			 * Accessing a resident buffer promotes it to HOT (the 2Q rescue):
+			 * a page loaded COOL on probation becomes part of the hot working
+			 * set on its second touch.  BM_MAX_USAGE_COUNT is
+			 * BUF_COOLSTATE_HOT (1), so this saturates at HOT and never
+			 * overflows the field.  We also set the second-chance ref bit so
+			 * the bgwriter's next cooling pass spares this recently-used buffer.
+			 */
+			if (BUF_STATE_GET_COOLSTATE(buf_state) < BUF_COOLSTATE_HOT)
+				buf_state += BUF_COOLSTATE_ONE;
+			buf_state |= BUF_REFBIT;
 
 			if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
 											   buf_state))
@@ -3785,7 +3787,7 @@ BufferSync(int flags)
 		 */
 		if (pg_atomic_read_u64(&bufHdr->state) & BM_CHECKPOINT_NEEDED)
 		{
-			if (SyncOneBuffer(buf_id, false, &wb_context) & BUF_WRITTEN)
+			if (SyncOneBuffer(buf_id, false, false, &wb_context) & BUF_WRITTEN)
 			{
 				TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
 				PendingCheckpointerStats.buffers_written++;
@@ -3876,6 +3878,19 @@ BgBufferSync(WritebackContext *wb_context)
 	float		smoothing_samples = 16;
 	float		scan_whole_pool_milliseconds = 120000.0;
 
+	/*
+	 * The cleaner scan directly observes the reusable (COOL, unpinned) buffer
+	 * density over the region it walks, which -- with the cooling-stage
+	 * evictor -- is exactly the sweep's victim predicate.  That observation is
+	 * ground truth for the buffers about to be reused, whereas the strategy
+	 * scan's positional proxy (strategy_delta/recent_alloc) blurs a pool whose
+	 * COOL population is spatially clustered (a scan burst leaves whole regions
+	 * COOL, hot OLTP regions not).  So we let the cleaner's own sample adapt on
+	 * a shorter window than the strategy proxy, tracking a burst of
+	 * probationary/scan COOL pages within a cycle or two instead of lagging it.
+	 */
+	float		cleaner_smoothing_samples = 4;
+
 	/* Used to compute how far we scan ahead */
 	long		strategy_delta;
 	int			bufs_to_lap;
@@ -3889,6 +3904,7 @@ BgBufferSync(WritebackContext *wb_context)
 	int			num_to_scan;
 	int			num_written;
 	int			reusable_buffers;
+	int			write_limit;
 
 	/* Variables for final smoothed_density update */
 	long		new_strategy_delta;
@@ -4063,8 +4079,22 @@ BgBufferSync(WritebackContext *wb_context)
 	 * Now write out dirty reusable buffers, working forward from the
 	 * next_to_clean point, until we have lapped the strategy scan, or cleaned
 	 * enough buffers to match our estimate of the next cycle's allocation
-	 * requirements, or hit the bgwriter_lru_maxpages limit.
-	 */
+	 * requirements, or hit the write limit.
+	 *
+	 * The per-cycle write cap is normally bgwriter_lru_maxpages.  But under a
+	 * bulk-dirtying workload (COPY, bulk UPDATE, VACUUM) the pool fills with
+	 * dirty COOL buffers faster than that fixed cap can clean, so the
+	 * foreground clock sweep is forced to flush dirty victims inline -- the
+	 * very cost the cooling-stage evictor is meant to keep off the critical
+	 * path.  When predicted demand (upcoming_alloc_est) exceeds the fixed cap,
+	 * raise the limit to meet demand so the bgwriter stays ahead and supplies
+	 * clean victims.  This stays bounded (by demand and by lapping the
+	 * strategy point), so it cannot run away, and normal workloads are
+	 * unaffected because there upcoming_alloc_est <= bgwriter_lru_maxpages.
+	 */
+	write_limit = bgwriter_lru_maxpages;
+	if (upcoming_alloc_est > write_limit)
+		write_limit = upcoming_alloc_est;
 
 	num_to_scan = bufs_to_lap;
 	num_written = 0;
@@ -4073,7 +4103,7 @@ BgBufferSync(WritebackContext *wb_context)
 	/* Execute the LRU scan */
 	while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
 	{
-		int			sync_state = SyncOneBuffer(next_to_clean, true,
+		int			sync_state = SyncOneBuffer(next_to_clean, true, true,
 											   wb_context);
 
 		if (++next_to_clean >= NBuffers)
@@ -4086,7 +4116,7 @@ BgBufferSync(WritebackContext *wb_context)
 		if (sync_state & BUF_WRITTEN)
 		{
 			reusable_buffers++;
-			if (++num_written >= bgwriter_lru_maxpages)
+			if (++num_written >= write_limit)
 			{
 				PendingBgWriterStats.maxwritten_clean++;
 				break;
@@ -4121,7 +4151,7 @@ BgBufferSync(WritebackContext *wb_context)
 	{
 		scans_per_alloc = (float) new_strategy_delta / (float) new_recent_alloc;
 		smoothed_density += (scans_per_alloc - smoothed_density) /
-			smoothing_samples;
+			cleaner_smoothing_samples;
 
 #ifdef BGW_DEBUG
 		elog(DEBUG2, "bgwriter: cleaner density alloc=%u scan=%ld density=%.2f new smoothed=%.2f",
@@ -4140,16 +4170,26 @@ BgBufferSync(WritebackContext *wb_context)
  * If skip_recently_used is true, we don't write currently-pinned buffers, nor
  * buffers marked recently used, as these are not replacement candidates.
  *
+ * If cool_if_hot is true (the bgwriter's LRU scan), an unpinned HOT buffer is
+ * demoted HOT -> COOL as we pass it, pre-staging eviction candidates so the
+ * foreground clock sweep finds a COOL victim in a single pass instead of
+ * having to cool buffers itself (force_cool).  The demotion is done under the
+ * buffer header lock we already hold, so it needs no CAS and cannot race a
+ * concurrent demotion.  A concurrent PinBuffer promotes it back to HOT, which
+ * is the intended 2Q behavior (a re-accessed buffer is rescued).
+ *
  * Returns a bitmask containing the following flag bits:
  *	BUF_WRITTEN: we wrote the buffer.
  *	BUF_REUSABLE: buffer is available for replacement, ie, it has
- *		pin count 0 and usage count 0.
+ *		pin count 0 and is COOL (an eviction candidate).
+ *	BUF_COOLED: we demoted this buffer HOT -> COOL this call.
  *
  * (BUF_WRITTEN could be set in error if FlushBuffer finds the buffer clean
  * after locking it, but we don't care all that much.)
  */
 static int
-SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
+SyncOneBuffer(int buf_id, bool skip_recently_used, bool cool_if_hot,
+			  WritebackContext *wb_context)
 {
 	BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
 	int			result = 0;
@@ -4171,8 +4211,38 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 	 */
 	buf_state = LockBufHdr(bufHdr);
 
+	/*
+	 * Pre-cool with a second chance: if asked, act on an unpinned HOT buffer.
+	 * If its ref bit is set (accessed since our last pass), clear the ref bit
+	 * and leave it HOT -- a recently-used buffer earns one reprieve, keeping
+	 * the hot working set out of the COOL stage under scan pressure.  Only a
+	 * HOT buffer whose ref bit is already clear is demoted HOT -> COOL,
+	 * pre-staging it as an eviction candidate for the foreground sweep.  We
+	 * hold the header lock, so each transition is a plain masked store applied
+	 * atomically by UnlockBufHdrExt; we re-lock to continue the dirty-write
+	 * inspection below.
+	 */
+	if (cool_if_hot &&
+		BUF_STATE_GET_REFCOUNT(buf_state) == 0 &&
+		BUF_STATE_GET_COOLSTATE(buf_state) != BUF_COOLSTATE_COOL)
+	{
+		if (BUF_STATE_GET_REFBIT(buf_state))
+		{
+			/* second chance: consume the ref bit, stay HOT */
+			UnlockBufHdrExt(bufHdr, buf_state, 0, BUF_REFBIT, 0);
+			buf_state = LockBufHdr(bufHdr);
+		}
+		else
+		{
+			/* not re-accessed since last pass: demote to COOL */
+			UnlockBufHdrExt(bufHdr, buf_state, 0, BUF_USAGECOUNT_MASK, 0);
+			buf_state = LockBufHdr(bufHdr);
+			result |= BUF_COOLED;
+		}
+	}
+
 	if (BUF_STATE_GET_REFCOUNT(buf_state) == 0 &&
-		BUF_STATE_GET_USAGECOUNT(buf_state) == 0)
+		BUF_STATE_GET_COOLSTATE(buf_state) == BUF_COOLSTATE_COOL)
 	{
 		result |= BUF_REUSABLE;
 	}
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index 46104b3341d..e8cf13f3362 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -49,6 +49,16 @@ typedef struct
 	uint32		completePasses; /* Complete cycles of the clock-sweep */
 	pg_atomic_uint32 numBufferAllocs;	/* Buffers allocated since last reset */
 
+	/*
+	 * Number of clock-hand values a backend claims per atomic fetch-add.
+	 * Computed once at startup (see StrategyCtlShmemInit).  Kept in shared
+	 * memory rather than a backend-local static so that EXEC_BACKEND children
+	 * (which do not inherit the postmaster's statics) see the same value; a
+	 * backend-local copy would silently reset to 1 there, disabling batching
+	 * on Windows.
+	 */
+	uint32		batchSize;
+
 	/*
 	 * Bgworker process to be notified upon activity or -1 if none. See
 	 * StrategyNotifyBgWriter.
@@ -113,17 +123,6 @@ static void AddBufferToRing(BufferAccessStrategy strategy,
 static uint32 MyBatchPos = 0;
 static uint32 MyBatchEnd = 0;
 
-/*
- * Number of clock-hand values a backend claims per atomic fetch-add,
- * computed once at startup (see StrategyCtlShmemInit).  When batching is
- * enabled it is one cache line's worth of hand advance, so concurrent
- * backends sweep non-overlapping, cache-line-sized runs of the pool; the
- * global sweep order is preserved (each buffer is still visited exactly once
- * per pass).  Batching is enabled only on multi-node NUMA hardware; otherwise
- * this stays 1 and the sweep is byte-identical to the stock clock.
- */
-static uint32 ClockSweepBatchSize = 1;
-
 /*
  * ClockSweepTick - Helper routine for StrategyGetBuffer()
  *
@@ -144,7 +143,7 @@ ClockSweepTick(void)
 		 * atomic operation per batch, reducing contention by the batch size.
 		 */
 		uint32		start;
-		uint32		batch_size = ClockSweepBatchSize;
+		uint32		batch_size = StrategyControl->batchSize;
 
 		start = pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer,
 										batch_size);
@@ -229,6 +228,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 	BufferDesc *buf;
 	int			bgwprocno;
 	int			trycounter;
+	bool		force_cool;
 
 	*from_ring = false;
 
@@ -279,12 +279,32 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 	 */
 	pg_atomic_fetch_add_u32(&StrategyControl->numBufferAllocs, 1);
 
-	/* Use the "clock sweep" algorithm to find a free buffer */
+	/*
+	 * Use the cooling-stage clock sweep to find a victim.
+	 *
+	 * A buffer is HOT (recently used) or COOL (an eviction candidate).  We
+	 * prefer to reclaim an already-COOL buffer and demote a HOT buffer to
+	 * COOL only once a full sweep has found no COOL victim (force_cool) -- so
+	 * an abundant supply of COOL/probationary pages (e.g. a scan) is drained
+	 * before the hot working set is cooled.  Newly loaded pages are admitted
+	 * COOL (see BufferAlloc), so scan resistance falls out of the algorithm.
+	 *
+	 * trycounter bounds the search.  Any tick that does not produce a victim
+	 * and does not make progress -- a pinned buffer, or a HOT buffer skipped
+	 * on a prefer-COOL pass -- decrements it.  Cooling a HOT buffer (under
+	 * force_cool) is progress and resets it.  When a full pass (NBuffers)
+	 * makes no progress we escalate to force_cool so the next pass cools HOT
+	 * buffers into victims; if a force_cool pass ALSO makes no progress every
+	 * buffer is pinned and we fail, matching the stock "no unpinned buffers
+	 * available" contract.
+	 */
 	trycounter = NBuffers;
+	force_cool = false;
 	for (;;)
 	{
 		uint64		old_buf_state;
 		uint64		local_buf_state;
+		bool		no_progress = false;
 
 		buf = GetBufferDescriptor(ClockSweepTick());
 
@@ -297,25 +317,10 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 		{
 			local_buf_state = old_buf_state;
 
-			/*
-			 * If the buffer is pinned or has a nonzero usage_count, we cannot
-			 * use it; decrement the usage_count (unless pinned) and keep
-			 * scanning.
-			 */
-
+			/* If the buffer is pinned we cannot use it; keep scanning. */
 			if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0)
 			{
-				if (--trycounter == 0)
-				{
-					/*
-					 * We've scanned all the buffers without making any state
-					 * changes, so all the buffers are pinned (or were when we
-					 * looked at them). We could hope that someone will free
-					 * one eventually, but it's probably better to fail than
-					 * to risk getting stuck in an infinite loop.
-					 */
-					elog(ERROR, "no unpinned buffers available");
-				}
+				no_progress = true;
 				break;
 			}
 
@@ -326,20 +331,56 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 				continue;
 			}
 
-			if (BUF_STATE_GET_USAGECOUNT(local_buf_state) != 0)
+			if (BUF_STATE_GET_COOLSTATE(local_buf_state) != BUF_COOLSTATE_COOL)
 			{
-				local_buf_state -= BUF_USAGECOUNT_ONE;
+				/*
+				 * HOT buffer.  Prefer a COOL victim: on a normal pass just
+				 * advance the hand (no progress).  Under force_cool apply the
+				 * same second-chance rule the bgwriter's pre-cooling uses: a
+				 * HOT buffer whose ref bit is set (recently accessed) has the
+				 * ref bit cleared and is left HOT this pass; only a HOT buffer
+				 * whose ref bit is already clear is demoted HOT -> COOL.  This
+				 * keeps a just-touched buffer from being cooled the instant the
+				 * bgwriter falls behind and the foreground has to cool for
+				 * itself.  Either transition is progress toward a victim.
+				 */
+				if (!force_cool)
+				{
+					no_progress = true;
+					break;			/* advance the hand, look for COOL */
+				}
+
+				if (BUF_STATE_GET_REFBIT(local_buf_state))
+					local_buf_state &= ~BUF_REFBIT;			/* second chance: clear ref, stay HOT */
+				else
+					local_buf_state &= ~BUF_USAGECOUNT_MASK;	/* HOT -> COOL, clear ref bit */
 
 				if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
 												   local_buf_state))
 				{
+					/*
+					 * Making a cooling-state transition is progress toward a
+					 * victim, so reset the counter.  Stay in force_cool: we made
+					 * a transition but have not yet produced a reclaimable COOL
+					 * victim (a ref-clear leaves the buffer HOT; a demote leaves
+					 * it COOL for a later tick to claim), and dropping out here
+					 * would waste a full no-progress pass re-escalating.  An
+					 * all-HOT, all-recently-referenced pool thus takes up to ~3
+					 * full passes to yield a victim (discover no COOL, clear ref
+					 * bits, cool + reclaim) -- still within the stock clock's
+					 * worst case (up to BM_MAX_USAGE_COUNT+1 passes), and in
+					 * practice the bgwriter pre-cooling keeps a COOL victim
+					 * available so force_cool rarely fires at all.  force_cool
+					 * ends naturally once a COOL victim is found and returned
+					 * below.
+					 */
 					trycounter = NBuffers;
 					break;
 				}
 			}
 			else
 			{
-				/* pin the buffer if the CAS succeeds */
+				/* COOL and unpinned: claim it.  Pin if the CAS succeeds. */
 				local_buf_state += BUF_REFCOUNT_ONE;
 
 				if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
@@ -356,6 +397,21 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 				}
 			}
 		}
+
+		/*
+		 * A tick that made no progress toward a victim counts down trycounter.
+		 * A full unproductive pass escalates to force_cool (cool HOT buffers
+		 * into victims); a second unproductive full pass means everything is
+		 * pinned, so fail rather than spin forever.  (A failed CAS above is
+		 * neither progress nor a full miss: we simply retry the same buffer.)
+		 */
+		if (no_progress && --trycounter == 0)
+		{
+			if (force_cool)
+				elog(ERROR, "no unpinned buffers available");
+			force_cool = true;
+			trycounter = NBuffers;
+		}
 	}
 }
 
@@ -470,10 +526,10 @@ StrategyCtlShmemInit(void *arg)
 	 */
 	if (pg_numa_init() != -1 &&
 		pg_numa_get_max_node() >= 1)
-		ClockSweepBatchSize = Min(PG_CACHE_LINE_SIZE / (uint32) sizeof(uint32),
-								  (uint32) NBuffers);
+		StrategyControl->batchSize = Min(PG_CACHE_LINE_SIZE / (uint32) sizeof(uint32),
+										 (uint32) NBuffers);
 	else
-		ClockSweepBatchSize = 1;
+		StrategyControl->batchSize = 1;
 }
 
 
@@ -721,14 +777,13 @@ GetBufferFromRing(BufferAccessStrategy strategy, uint64 *buf_state)
 		/*
 		 * If the buffer is pinned we cannot use it under any circumstances.
 		 *
-		 * If usage_count is 0 or 1 then the buffer is fair game (we expect 1,
-		 * since our own previous usage of the ring element would have left it
-		 * there, but it might've been decremented by clock-sweep since then).
-		 * A higher usage_count indicates someone else has touched the buffer,
-		 * so we shouldn't re-use it.
+		 * With the cooling-state replacement the field holds only COOL or HOT,
+		 * so the stock "usage_count > 1 means another backend touched it"
+		 * heuristic no longer applies: a ring element is reusable whenever it
+		 * is unpinned.  (The whole ring mechanism is removed in a later patch;
+		 * scan resistance is now intrinsic to the sweep.)
 		 */
-		if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0
-			|| BUF_STATE_GET_USAGECOUNT(local_buf_state) > 1)
+		if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0)
 			break;
 
 		/* See equivalent code in PinBuffer() */
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 4870c8e13d0..21c08091c49 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -167,7 +167,7 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
 
 		buf_state = pg_atomic_read_u64(&bufHdr->state);
 		buf_state &= ~(BUF_FLAG_MASK | BUF_USAGECOUNT_MASK);
-		buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
+		buf_state |= BM_TAG_VALID;	/* admit COOL (probation) */
 		pg_atomic_unlocked_write_u64(&bufHdr->state, buf_state);
 
 		*foundPtr = false;
@@ -248,9 +248,10 @@ GetLocalVictimBuffer(void)
 		{
 			uint64		buf_state = pg_atomic_read_u64(&bufHdr->state);
 
-			if (BUF_STATE_GET_USAGECOUNT(buf_state) > 0)
+			if (BUF_STATE_GET_COOLSTATE(buf_state) != BUF_COOLSTATE_COOL)
 			{
-				buf_state -= BUF_USAGECOUNT_ONE;
+				/* HOT: give it a second chance, cool it and keep scanning. */
+				buf_state -= BUF_COOLSTATE_ONE;
 				pg_atomic_unlocked_write_u64(&bufHdr->state, buf_state);
 				trycounter = NLocBuffer;
 			}
@@ -454,7 +455,7 @@ ExtendBufferedRelLocal(BufferManagerRelation bmr,
 
 			victim_buf_hdr->tag = tag;
 
-			buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
+			buf_state |= BM_TAG_VALID;	/* admit COOL (probation) */
 
 			pg_atomic_unlocked_write_u64(&victim_buf_hdr->state, buf_state);
 
@@ -839,9 +840,9 @@ PinLocalBuffer(BufferDesc *buf_hdr, bool adjust_usagecount)
 		NLocalPinnedBuffers++;
 		buf_state += BUF_REFCOUNT_ONE;
 		if (adjust_usagecount &&
-			BUF_STATE_GET_USAGECOUNT(buf_state) < BM_MAX_USAGE_COUNT)
+			BUF_STATE_GET_COOLSTATE(buf_state) < BUF_COOLSTATE_HOT)
 		{
-			buf_state += BUF_USAGECOUNT_ONE;
+			buf_state += BUF_COOLSTATE_ONE;
 		}
 		pg_atomic_unlocked_write_u64(&buf_hdr->state, buf_state);
 
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index e4ff5619b79..131df126cda 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -67,6 +67,50 @@ StaticAssertDecl(BUF_REFCOUNT_BITS + BUF_USAGECOUNT_BITS + BUF_FLAG_BITS + BUF_L
 #define BUF_USAGECOUNT_ONE \
 	(UINT64CONST(1) << BUF_REFCOUNT_BITS)
 
+/*
+ * Cooling state (LeanStore / 2Q-A1 cooling-stage clock sweep).
+ *
+ * The field historically used for the 0..5 usage_count now holds a single
+ * cooling-state bit: HOT (recently accessed, not an eviction candidate) or
+ * COOL (an eviction candidate).  We reuse BUF_USAGECOUNT_ONE as the unit so
+ * the buffer-state bit geography -- refcount, flag, and lock offsets, and the
+ * 64-bit StaticAsserts -- is unchanged; only the meaning of the field and the
+ * instructions that touch it change.
+ *
+ * A demand-loaded page is admitted COOL (probation); a second access promotes
+ * it to HOT (the rescue).  The sweep prefers COOL victims and demotes HOT to
+ * COOL only when a full pass finds no COOL victim.  So a one-touch scan fills
+ * and drains the COOL stage without displacing the HOT working set -- scan
+ * resistance intrinsic to the replacement algorithm.
+ */
+#define BUF_COOLSTATE_COOL	0
+#define BUF_COOLSTATE_HOT	1
+#define BUF_COOLSTATE_ONE	BUF_USAGECOUNT_ONE
+
+/*
+ * Second-chance reference bit, bit 1 of the (former usagecount) field, one
+ * position above the cooling-state bit.  PinBuffer sets it on every access.
+ * The bgwriter's pre-cooling gives a HOT buffer a second chance: the first
+ * time it passes a HOT buffer whose ref bit is set, it clears the ref bit and
+ * leaves the buffer HOT; only a HOT buffer whose ref bit is already clear (not
+ * re-accessed since the previous bgwriter pass) is demoted to COOL.  This
+ * keeps genuinely-hot pages out of the COOL stage (protecting the working set
+ * from being cooled under scan pressure) while leaving the foreground sweep a
+ * single-pass search over the pre-staged COOL buffers.  A separate bit (not a
+ * count) so it stays a plain masked store under the header lock.
+ */
+#define BUF_REFBIT			(UINT64CONST(2) << BUF_REFCOUNT_BITS)
+#define BUF_STATE_GET_REFBIT(state)	(((state) & BUF_REFBIT) != 0)
+
+/*
+ * The cooling state occupies bit 0 and the reference bit occupies bit 1 of
+ * the (former usagecount) field, so the field must be at least 2 bits wide.
+ * Assert it here so a future change to BUF_USAGECOUNT_BITS cannot silently
+ * push BUF_REFBIT up into the flag bits.
+ */
+StaticAssertDecl(BUF_USAGECOUNT_BITS >= 2,
+				 "cooling state + reference bit need at least 2 bits in the usagecount field");
+
 /* flags related definitions */
 #define BUF_FLAG_SHIFT \
 	(BUF_REFCOUNT_BITS + BUF_USAGECOUNT_BITS)
@@ -92,6 +136,11 @@ StaticAssertDecl(BUF_REFCOUNT_BITS + BUF_USAGECOUNT_BITS + BUF_FLAG_BITS + BUF_L
 #define BUF_STATE_GET_USAGECOUNT(state) \
 	((uint32)(((state) & BUF_USAGECOUNT_MASK) >> BUF_USAGECOUNT_SHIFT))
 
+/* Cooling state (HOT/COOL) from buffer state -- bit 0 of the field only, so
+ * the second-chance ref bit (bit 1) does not perturb the HOT/COOL test. */
+#define BUF_STATE_GET_COOLSTATE(state) \
+	((uint32) (((state) >> BUF_USAGECOUNT_SHIFT) & 1))
+
 /*
  * Flags for buffer descriptors
  *
@@ -134,17 +183,15 @@ StaticAssertDecl(MAX_BACKENDS_BITS <= (BUF_LOCK_BITS - 2),
 
 
 /*
- * The maximum allowed value of usage_count represents a tradeoff between
- * accuracy and speed of the clock-sweep buffer management algorithm.  A
- * large value (comparable to NBuffers) would approximate LRU semantics.
- * But it can take as many as BM_MAX_USAGE_COUNT+1 complete cycles of the
- * clock-sweep hand to find a free buffer, so in practice we don't want the
- * value to be very large.
+ * The cooling state is a single bit (HOT/COOL); the maximum value stored in
+ * the field is therefore BUF_COOLSTATE_HOT.  Retained under the historical
+ * name BM_MAX_USAGE_COUNT so the pin fast path ("promote unless already at
+ * max") reads naturally.
  */
-#define BM_MAX_USAGE_COUNT	5
+#define BM_MAX_USAGE_COUNT	BUF_COOLSTATE_HOT
 
 StaticAssertDecl(BM_MAX_USAGE_COUNT < (UINT64CONST(1) << BUF_USAGECOUNT_BITS),
-				 "BM_MAX_USAGE_COUNT doesn't fit in BUF_USAGECOUNT_BITS bits");
+				 "cooling state doesn't fit in BUF_USAGECOUNT_BITS bits");
 
 /*
  * Buffer tag identifies which disk block the buffer contains.
-- 
2.51.2



  [text/x-patch] v6-0003-Remove-BufferAccessStrategy-scan-resistance-is-no.patch (180.2K, ../../96dc1db7-58a2-41ba-ae2e-bea86fa2fb3a@app.fastmail.com/4-v6-0003-Remove-BufferAccessStrategy-scan-resistance-is-no.patch)
  download | inline diff:
From 1f9e7eda9438c168a51a928f7d169fae9f84e96f Mon Sep 17 00:00:00 2001
From: Greg Burd <greg@burd.me>
Date: Tue, 7 Jul 2026 08:45:39 -0400
Subject: [PATCH v6 3/3] Remove BufferAccessStrategy; scan resistance is now
 intrinsic

The cooling-stage evictor admits demand-loaded pages COOL and promotes them to
HOT only on a second access, so a one-touch sequential scan fills and drains
the COOL stage without displacing the hot working set.  Scan resistance is
therefore a property of the replacement algorithm itself, and the
BufferAccessStrategy ring buffers that previously provided it are dead weight.
Remove them end to end.

Deleted:
  - the BufferAccessStrategy type and the BufferAccessStrategyType enum
    (BAS_NORMAL/BULKREAD/BULKWRITE/VACUUM);
  - the ring machinery in freelist.c (GetAccessStrategy[WithSize],
    GetAccessStrategyBufferCount, GetAccessStrategyPinLimit,
    FreeAccessStrategy, GetBufferFromRing, AddBufferToRing,
    StrategyRejectBuffer, IOContextForStrategy);
  - the strategy parameter from ReadBufferExtended, ReadBufferWithoutRelcache,
    the ExtendBufferedRel* family, StrategyGetBuffer, read_stream_begin_*,
    and every scan/vacuum/analyze/index-AM caller;
  - the strategy fields on HeapScanDescData, IndexScanDescData,
    BulkInsertStateData, ReadBuffersOperation, and ReadStream;
  - _hash_getbuf_with_strategy (identical to _hash_getbuf without a strategy).

pg_stat_io's per-strategy IO contexts collapse: IOCONTEXT_BULKREAD,
IOCONTEXT_BULKWRITE and IOCONTEXT_VACUUM are removed, leaving IOCONTEXT_INIT
and IOCONTEXT_NORMAL.  IOOP_REUSE only ever occurred while recycling a ring
buffer, so it is no longer tracked; GetVictimBuffer counts IOOP_EVICT only.

The vacuum_buffer_usage_limit GUC and the VACUUM/ANALYZE (BUFFER_USAGE_LIMIT
...) option are removed, along with the VacuumBufferUsageLimit global, the
ring-size plumbing through VacuumParams and parallel vacuum, and vacuumdb's
--buffer-usage-limit client option.  read_stream's per-backend pin budget is
now enforced solely by GetPinLimit()/GetLocalPinLimit(), which already applied
and is unchanged for the (formerly universal) no-strategy case.

Documentation and the stats/amcheck regression tests are updated to drop the
removed contexts and options.
---
 contrib/amcheck/expected/check_heap.out       |  16 +-
 contrib/amcheck/sql/check_heap.sql            |  16 +-
 contrib/amcheck/verify_gin.c                  |  18 +-
 contrib/amcheck/verify_heapam.c               |   3 -
 contrib/amcheck/verify_nbtree.c               |  11 +-
 contrib/bloom/blscan.c                        |   8 -
 contrib/bloom/blutils.c                       |   4 +-
 contrib/bloom/blvacuum.c                      |   2 -
 contrib/pageinspect/rawpage.c                 |   2 +-
 contrib/pg_prewarm/autoprewarm.c              |   1 -
 contrib/pg_prewarm/pg_prewarm.c               |   1 -
 contrib/pg_visibility/pg_visibility.c         |   7 +-
 contrib/pgstattuple/pgstatapprox.c            |   3 -
 contrib/pgstattuple/pgstatindex.c             |   9 +-
 contrib/pgstattuple/pgstattuple.c             |  37 +-
 doc/src/sgml/config.sgml                      |  30 --
 doc/src/sgml/monitoring.sgml                  |  38 +-
 doc/src/sgml/ref/analyze.sgml                 |  21 -
 doc/src/sgml/ref/vacuum.sgml                  |  25 -
 src/backend/access/brin/brin.c                |  11 +-
 src/backend/access/brin/brin_revmap.c         |   2 +-
 src/backend/access/gin/gininsert.c            |   4 +-
 src/backend/access/gin/ginutil.c              |   2 +-
 src/backend/access/gin/ginvacuum.c            |  17 +-
 src/backend/access/gist/gist.c                |   2 +-
 src/backend/access/gist/gistutil.c            |   2 +-
 src/backend/access/gist/gistvacuum.c          |  10 +-
 src/backend/access/hash/hash.c                |  13 +-
 src/backend/access/hash/hashovfl.c            |  55 +--
 src/backend/access/hash/hashpage.c            |  40 +-
 src/backend/access/heap/heapam.c              |  31 +-
 src/backend/access/heap/heapam_handler.c      |   4 +-
 src/backend/access/heap/hio.c                 |  13 +-
 src/backend/access/heap/vacuumlazy.c          |  22 +-
 src/backend/access/heap/visibilitymap.c       |   4 +-
 src/backend/access/nbtree/nbtpage.c           |   2 +-
 src/backend/access/nbtree/nbtree.c            |   4 +-
 src/backend/access/spgist/spgutils.c          |   2 +-
 src/backend/access/spgist/spgvacuum.c         |   4 +-
 src/backend/access/transam/xloginsert.c       |   2 +-
 src/backend/access/transam/xlogutils.c        |   3 +-
 src/backend/catalog/index.c                   |   1 -
 src/backend/commands/analyze.c                |   7 +-
 src/backend/commands/dbcommands.c             |   6 +-
 src/backend/commands/repack.c                 |   2 +-
 src/backend/commands/sequence.c               |   2 +-
 src/backend/commands/vacuum.c                 | 118 +----
 src/backend/commands/vacuumparallel.c         |  21 +-
 src/backend/postmaster/autovacuum.c           |  29 +-
 src/backend/postmaster/datachecksum_state.c   |  33 +-
 src/backend/storage/aio/read_stream.c         |  17 +-
 src/backend/storage/buffer/README             |  44 --
 src/backend/storage/buffer/bufmgr.c           | 185 +++-----
 src/backend/storage/buffer/freelist.c         | 434 +-----------------
 src/backend/storage/freespace/freespace.c     |   4 +-
 src/backend/storage/smgr/md.c                 |  11 +-
 src/backend/utils/activity/pgstat_io.c        |  45 +-
 src/backend/utils/init/globals.c              |   1 -
 src/backend/utils/misc/guc_parameters.dat     |  10 -
 src/backend/utils/misc/postgresql.conf.sample |   3 -
 src/bin/scripts/vacuumdb.c                    |  13 -
 src/bin/scripts/vacuuming.c                   |  21 -
 src/bin/scripts/vacuuming.h                   |   1 -
 src/include/access/genam.h                    |   1 -
 src/include/access/hash.h                     |   9 +-
 src/include/access/heapam.h                   |   4 +-
 src/include/access/hio.h                      |   1 -
 src/include/access/tableam.h                  |   8 +-
 src/include/commands/vacuum.h                 |   8 +-
 src/include/miscadmin.h                       |   9 -
 src/include/pgstat.h                          |   5 +-
 src/include/storage/buf.h                     |   7 -
 src/include/storage/buf_internals.h           |   6 +-
 src/include/storage/bufmgr.h                  |  32 +-
 src/include/storage/read_stream.h             |   5 +-
 src/include/utils/guc_hooks.h                 |   2 -
 src/test/modules/test_aio/test_aio.c          |   7 +-
 src/test/regress/expected/stats.out           | 110 +----
 src/test/regress/expected/vacuum.out          |  19 -
 src/test/regress/sql/stats.sql                |  56 +--
 src/test/regress/sql/vacuum.sql               |  15 -
 src/tools/pgindent/typedefs.list              |   2 -
 82 files changed, 261 insertions(+), 1554 deletions(-)

diff --git a/contrib/amcheck/expected/check_heap.out b/contrib/amcheck/expected/check_heap.out
index 979e5e84e72..569b0202f1c 100644
--- a/contrib/amcheck/expected/check_heap.out
+++ b/contrib/amcheck/expected/check_heap.out
@@ -67,11 +67,11 @@ INSERT INTO heaptest (a, b)
 	(SELECT gs, repeat('x', gs)
 		FROM generate_series(1,50) gs);
 -- pg_stat_io test:
--- verify_heapam always uses a BAS_BULKREAD BufferAccessStrategy, whereas a
--- sequential scan does so only if the table is large enough when compared to
--- shared buffers (see initscan()). CREATE DATABASE ... also unconditionally
--- uses a BAS_BULKREAD strategy, but we have chosen to use a tablespace and
--- verify_heapam to provide coverage instead of adding another expensive
+-- verify_heapam reads the heap through the buffer manager; with the
+-- cooling-stage clock sweep there are no per-strategy IO contexts, so the
+-- reads are counted in the 'normal' context.  CREATE DATABASE ... likewise
+-- reads through the normal context, but we have chosen to use a tablespace
+-- and verify_heapam to provide coverage instead of adding another expensive
 -- operation to the main regression test suite.
 --
 -- Create an alternative tablespace and move the heaptest table to it, causing
@@ -83,7 +83,7 @@ INSERT INTO heaptest (a, b)
 SET allow_in_place_tablespaces = true;
 CREATE TABLESPACE regress_test_stats_tblspc LOCATION '';
 SELECT sum(reads) AS stats_bulkreads_before
-  FROM pg_stat_io WHERE context = 'bulkread' \gset
+  FROM pg_stat_io WHERE context = 'normal' AND object = 'relation' \gset
 BEGIN;
 ALTER TABLE heaptest SET TABLESPACE regress_test_stats_tblspc;
 -- Check that valid options are not rejected nor corruption reported
@@ -111,7 +111,7 @@ SELECT * FROM verify_heapam(relation := 'heaptest', startblock := 0, endblock :=
 COMMIT;
 -- verify_heapam should have read in the page written out by
 --   ALTER TABLE ... SET TABLESPACE ...
--- causing an additional bulkread, which should be reflected in pg_stat_io.
+-- causing additional reads, which should be reflected in pg_stat_io.
 SELECT pg_stat_force_next_flush();
  pg_stat_force_next_flush 
 --------------------------
@@ -119,7 +119,7 @@ SELECT pg_stat_force_next_flush();
 (1 row)
 
 SELECT sum(reads) AS stats_bulkreads_after
-  FROM pg_stat_io WHERE context = 'bulkread' \gset
+  FROM pg_stat_io WHERE context = 'normal' AND object = 'relation' \gset
 SELECT :stats_bulkreads_after > :stats_bulkreads_before;
  ?column? 
 ----------
diff --git a/contrib/amcheck/sql/check_heap.sql b/contrib/amcheck/sql/check_heap.sql
index 1745bae634e..61f710dbeda 100644
--- a/contrib/amcheck/sql/check_heap.sql
+++ b/contrib/amcheck/sql/check_heap.sql
@@ -27,11 +27,11 @@ INSERT INTO heaptest (a, b)
 		FROM generate_series(1,50) gs);
 
 -- pg_stat_io test:
--- verify_heapam always uses a BAS_BULKREAD BufferAccessStrategy, whereas a
--- sequential scan does so only if the table is large enough when compared to
--- shared buffers (see initscan()). CREATE DATABASE ... also unconditionally
--- uses a BAS_BULKREAD strategy, but we have chosen to use a tablespace and
--- verify_heapam to provide coverage instead of adding another expensive
+-- verify_heapam reads the heap through the buffer manager; with the
+-- cooling-stage clock sweep there are no per-strategy IO contexts, so the
+-- reads are counted in the 'normal' context.  CREATE DATABASE ... likewise
+-- reads through the normal context, but we have chosen to use a tablespace
+-- and verify_heapam to provide coverage instead of adding another expensive
 -- operation to the main regression test suite.
 --
 -- Create an alternative tablespace and move the heaptest table to it, causing
@@ -43,7 +43,7 @@ INSERT INTO heaptest (a, b)
 SET allow_in_place_tablespaces = true;
 CREATE TABLESPACE regress_test_stats_tblspc LOCATION '';
 SELECT sum(reads) AS stats_bulkreads_before
-  FROM pg_stat_io WHERE context = 'bulkread' \gset
+  FROM pg_stat_io WHERE context = 'normal' AND object = 'relation' \gset
 BEGIN;
 ALTER TABLE heaptest SET TABLESPACE regress_test_stats_tblspc;
 -- Check that valid options are not rejected nor corruption reported
@@ -56,10 +56,10 @@ COMMIT;
 
 -- verify_heapam should have read in the page written out by
 --   ALTER TABLE ... SET TABLESPACE ...
--- causing an additional bulkread, which should be reflected in pg_stat_io.
+-- causing additional reads, which should be reflected in pg_stat_io.
 SELECT pg_stat_force_next_flush();
 SELECT sum(reads) AS stats_bulkreads_after
-  FROM pg_stat_io WHERE context = 'bulkread' \gset
+  FROM pg_stat_io WHERE context = 'normal' AND object = 'relation' \gset
 SELECT :stats_bulkreads_after > :stats_bulkreads_before;
 
 CREATE ROLE regress_heaptest_role;
diff --git a/contrib/amcheck/verify_gin.c b/contrib/amcheck/verify_gin.c
index fa06689ed5b..ef2e64475c6 100644
--- a/contrib/amcheck/verify_gin.c
+++ b/contrib/amcheck/verify_gin.c
@@ -63,8 +63,7 @@ static void gin_check_parent_keys_consistency(Relation rel,
 static void check_index_page(Relation rel, Buffer buffer, BlockNumber blockNo);
 static IndexTuple gin_refind_parent(Relation rel,
 									BlockNumber parentblkno,
-									BlockNumber childblkno,
-									BufferAccessStrategy strategy);
+									BlockNumber childblkno);
 static ItemId PageGetItemIdCareful(Relation rel, BlockNumber block, Page page,
 								   OffsetNumber offset);
 
@@ -133,7 +132,6 @@ ginReadTupleWithoutState(IndexTuple itup, int *nitems)
 static void
 gin_check_posting_tree_parent_keys_consistency(Relation rel, BlockNumber posting_tree_root)
 {
-	BufferAccessStrategy strategy = GetAccessStrategy(BAS_BULKREAD);
 	GinPostingTreeScanItem *stack;
 	MemoryContext mctx;
 	MemoryContext oldcontext;
@@ -171,8 +169,7 @@ gin_check_posting_tree_parent_keys_consistency(Relation rel, BlockNumber posting
 
 		CHECK_FOR_INTERRUPTS();
 
-		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno,
-									RBM_NORMAL, strategy);
+		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno, RBM_NORMAL);
 		LockBuffer(buffer, GIN_SHARE);
 		page = BufferGetPage(buffer);
 
@@ -391,7 +388,6 @@ gin_check_parent_keys_consistency(Relation rel,
 								  void *callback_state,
 								  bool readonly)
 {
-	BufferAccessStrategy strategy = GetAccessStrategy(BAS_BULKREAD);
 	GinScanItem *stack;
 	MemoryContext mctx;
 	MemoryContext oldcontext;
@@ -430,8 +426,7 @@ gin_check_parent_keys_consistency(Relation rel,
 
 		CHECK_FOR_INTERRUPTS();
 
-		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno,
-									RBM_NORMAL, strategy);
+		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno, RBM_NORMAL);
 		LockBuffer(buffer, GIN_SHARE);
 		page = BufferGetPage(buffer);
 		maxoff = PageGetMaxOffsetNumber(page);
@@ -567,7 +562,7 @@ gin_check_parent_keys_consistency(Relation rel,
 					 */
 					pfree(stack->parenttup);
 					stack->parenttup = gin_refind_parent(rel, stack->parentblk,
-														 stack->blkno, strategy);
+														 stack->blkno);
 
 					/* We found it - make a final check before failing */
 					if (!stack->parenttup)
@@ -719,7 +714,7 @@ check_index_page(Relation rel, Buffer buffer, BlockNumber blockNo)
  */
 static IndexTuple
 gin_refind_parent(Relation rel, BlockNumber parentblkno,
-				  BlockNumber childblkno, BufferAccessStrategy strategy)
+				  BlockNumber childblkno)
 {
 	Buffer		parentbuf;
 	Page		parentpage;
@@ -727,8 +722,7 @@ gin_refind_parent(Relation rel, BlockNumber parentblkno,
 				parent_maxoff;
 	IndexTuple	result = NULL;
 
-	parentbuf = ReadBufferExtended(rel, MAIN_FORKNUM, parentblkno, RBM_NORMAL,
-								   strategy);
+	parentbuf = ReadBufferExtended(rel, MAIN_FORKNUM, parentblkno, RBM_NORMAL);
 
 	LockBuffer(parentbuf, GIN_SHARE);
 	parentpage = BufferGetPage(parentbuf);
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 20ff58aa782..e4336eb5fc2 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -126,7 +126,6 @@ typedef struct HeapCheckContext
 	 * recent block in the buffer yielded by the read stream API.
 	 */
 	BlockNumber blkno;
-	BufferAccessStrategy bstrategy;
 	Buffer		buffer;
 	Page		page;
 
@@ -374,7 +373,6 @@ verify_heapam(PG_FUNCTION_ARGS)
 		PG_RETURN_NULL();
 	}
 
-	ctx.bstrategy = GetAccessStrategy(BAS_BULKREAD);
 	ctx.buffer = InvalidBuffer;
 	ctx.page = NULL;
 
@@ -472,7 +470,6 @@ verify_heapam(PG_FUNCTION_ARGS)
 	}
 
 	stream = read_stream_begin_relation(stream_flags,
-										ctx.bstrategy,
 										ctx.rel,
 										MAIN_FORKNUM,
 										stream_cb,
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 3ef2d66f826..18cd8928fdd 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -88,8 +88,6 @@ typedef struct BtreeCheckState
 	bool		checkunique;
 	/* Per-page context */
 	MemoryContext targetcontext;
-	/* Buffer access strategy */
-	BufferAccessStrategy checkstrategy;
 
 	/*
 	 * Info for uniqueness checking. Fill this field and the one below once
@@ -490,7 +488,6 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	state->targetcontext = AllocSetContextCreate(CurrentMemoryContext,
 												 "amcheck context",
 												 ALLOCSET_DEFAULT_SIZES);
-	state->checkstrategy = GetAccessStrategy(BAS_BULKREAD);
 
 	/* Get true root block from meta-page */
 	metapage = palloc_btree_page(state, BTREE_METAPAGE);
@@ -1115,7 +1112,7 @@ bt_recheck_sibling_links(BtreeCheckState *state,
 
 		/* Couple locks in the usual order for nbtree:  Left to right */
 		lbuf = ReadBufferExtended(state->rel, MAIN_FORKNUM, leftcurrent,
-								  RBM_NORMAL, state->checkstrategy);
+								  RBM_NORMAL);
 		LockBuffer(lbuf, BT_READ);
 		_bt_checkpage(state->rel, lbuf);
 		page = BufferGetPage(lbuf);
@@ -1138,8 +1135,7 @@ bt_recheck_sibling_links(BtreeCheckState *state,
 		if (newtargetblock != leftcurrent)
 		{
 			newtargetbuf = ReadBufferExtended(state->rel, MAIN_FORKNUM,
-											  newtargetblock, RBM_NORMAL,
-											  state->checkstrategy);
+											  newtargetblock, RBM_NORMAL);
 			LockBuffer(newtargetbuf, BT_READ);
 			_bt_checkpage(state->rel, newtargetbuf);
 			page = BufferGetPage(newtargetbuf);
@@ -3300,8 +3296,7 @@ palloc_btree_page(BtreeCheckState *state, BlockNumber blocknum)
 	 * We copy the page into local storage to avoid holding pin on the buffer
 	 * longer than we must.
 	 */
-	buffer = ReadBufferExtended(state->rel, MAIN_FORKNUM, blocknum, RBM_NORMAL,
-								state->checkstrategy);
+	buffer = ReadBufferExtended(state->rel, MAIN_FORKNUM, blocknum, RBM_NORMAL);
 	LockBuffer(buffer, BT_READ);
 
 	/*
diff --git a/contrib/bloom/blscan.c b/contrib/bloom/blscan.c
index 1a0e42021ec..ed62d5c04e6 100644
--- a/contrib/bloom/blscan.c
+++ b/contrib/bloom/blscan.c
@@ -80,7 +80,6 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BlockNumber blkno,
 				npages;
 	int			i;
-	BufferAccessStrategy bas;
 	BloomScanOpaque so = (BloomScanOpaque) scan->opaque;
 	BlockRangeReadStreamPrivate p;
 	ReadStream *stream;
@@ -113,11 +112,6 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		}
 	}
 
-	/*
-	 * We're going to read the whole index. This is why we use appropriate
-	 * buffer access strategy.
-	 */
-	bas = GetAccessStrategy(BAS_BULKREAD);
 	npages = RelationGetNumberOfBlocks(scan->indexRelation);
 	pgstat_count_index_scan(scan->indexRelation);
 	if (scan->instrument)
@@ -133,7 +127,6 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										bas,
 										scan->indexRelation,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -184,7 +177,6 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	Assert(read_stream_next_buffer(stream, NULL) == InvalidBuffer);
 	read_stream_end(stream);
-	FreeAccessStrategy(bas);
 
 	return ntids;
 }
diff --git a/contrib/bloom/blutils.c b/contrib/bloom/blutils.c
index 5111cdc6dd6..6c1ec15c59c 100644
--- a/contrib/bloom/blutils.c
+++ b/contrib/bloom/blutils.c
@@ -392,7 +392,7 @@ BloomNewBuffer(Relation index)
 	}
 
 	/* Must extend the file */
-	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM,
 							   EB_LOCK_FIRST);
 
 	return buffer;
@@ -460,7 +460,7 @@ BloomInitMetapage(Relation index, ForkNumber forknum)
 	 * block number 0 (BLOOM_METAPAGE_BLKNO).  No need to hold the extension
 	 * lock because there cannot be concurrent inserters yet.
 	 */
-	metaBuffer = ReadBufferExtended(index, forknum, P_NEW, RBM_NORMAL, NULL);
+	metaBuffer = ReadBufferExtended(index, forknum, P_NEW, RBM_NORMAL);
 	LockBuffer(metaBuffer, BUFFER_LOCK_EXCLUSIVE);
 	Assert(BufferGetBlockNumber(metaBuffer) == BLOOM_METAPAGE_BLKNO);
 
diff --git a/contrib/bloom/blvacuum.c b/contrib/bloom/blvacuum.c
index 6beb1c20ebb..08d7705e365 100644
--- a/contrib/bloom/blvacuum.c
+++ b/contrib/bloom/blvacuum.c
@@ -66,7 +66,6 @@ blbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										index,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -219,7 +218,6 @@ blvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										index,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/contrib/pageinspect/rawpage.c b/contrib/pageinspect/rawpage.c
index d136593edb2..bbe8edf9161 100644
--- a/contrib/pageinspect/rawpage.c
+++ b/contrib/pageinspect/rawpage.c
@@ -188,7 +188,7 @@ get_raw_page_internal(text *relname, ForkNumber forknum, BlockNumber blkno)
 
 	/* Take a verbatim copy of the page */
 
-	buf = ReadBufferExtended(rel, forknum, blkno, RBM_NORMAL, NULL);
+	buf = ReadBufferExtended(rel, forknum, blkno, RBM_NORMAL);
 	LockBuffer(buf, BUFFER_LOCK_SHARE);
 
 	memcpy(raw_page_data, BufferGetPage(buf), BLCKSZ);
diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index deb4c2671b5..33ad88ea8b1 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -630,7 +630,6 @@ autoprewarm_database_main(Datum main_arg)
 			stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 												READ_STREAM_DEFAULT |
 												READ_STREAM_USE_BATCHING,
-												NULL,
 												rel,
 												p.forknum,
 												apw_read_stream_next_block,
diff --git a/contrib/pg_prewarm/pg_prewarm.c b/contrib/pg_prewarm/pg_prewarm.c
index c2716086693..716a6754a7e 100644
--- a/contrib/pg_prewarm/pg_prewarm.c
+++ b/contrib/pg_prewarm/pg_prewarm.c
@@ -251,7 +251,6 @@ pg_prewarm(PG_FUNCTION_ARGS)
 		stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 											READ_STREAM_FULL |
 											READ_STREAM_USE_BATCHING,
-											NULL,
 											rel,
 											forkNumber,
 											block_range_read_stream_cb,
diff --git a/contrib/pg_visibility/pg_visibility.c b/contrib/pg_visibility/pg_visibility.c
index dfab0b64cf5..ccb829a09eb 100644
--- a/contrib/pg_visibility/pg_visibility.c
+++ b/contrib/pg_visibility/pg_visibility.c
@@ -488,7 +488,6 @@ collect_visibility_data(Oid relid, bool include_pd)
 	vbits	   *info;
 	BlockNumber blkno;
 	Buffer		vmbuffer = InvalidBuffer;
-	BufferAccessStrategy bstrategy = GetAccessStrategy(BAS_BULKREAD);
 	BlockRangeReadStreamPrivate p;
 	ReadStream *stream = NULL;
 
@@ -514,7 +513,6 @@ collect_visibility_data(Oid relid, bool include_pd)
 		 */
 		stream = read_stream_begin_relation(READ_STREAM_FULL |
 											READ_STREAM_USE_BATCHING,
-											bstrategy,
 											rel,
 											MAIN_FORKNUM,
 											block_range_read_stream_cb,
@@ -538,8 +536,7 @@ collect_visibility_data(Oid relid, bool include_pd)
 
 		/*
 		 * Page-level data requires reading every block, so only get it if the
-		 * caller needs it.  Use a buffer access strategy, too, to prevent
-		 * cache-trashing.
+		 * caller needs it.
 		 */
 		if (include_pd)
 		{
@@ -700,7 +697,6 @@ collect_corrupt_items(Oid relid, bool all_visible, bool all_frozen)
 	Relation	rel;
 	corrupt_items *items;
 	Buffer		vmbuffer = InvalidBuffer;
-	BufferAccessStrategy bstrategy = GetAccessStrategy(BAS_BULKREAD);
 	TransactionId OldestXmin = InvalidTransactionId;
 	struct collect_corrupt_items_read_stream_private p;
 	ReadStream *stream;
@@ -734,7 +730,6 @@ collect_corrupt_items(Oid relid, bool all_visible, bool all_frozen)
 	p.all_frozen = all_frozen;
 	p.all_visible = all_visible;
 	stream = read_stream_begin_relation(READ_STREAM_FULL,
-										bstrategy,
 										rel,
 										MAIN_FORKNUM,
 										collect_corrupt_items_read_stream_next_block,
diff --git a/contrib/pgstattuple/pgstatapprox.c b/contrib/pgstattuple/pgstatapprox.c
index 21e0b50fb4b..8e17d48991e 100644
--- a/contrib/pgstattuple/pgstatapprox.c
+++ b/contrib/pgstattuple/pgstatapprox.c
@@ -116,13 +116,11 @@ static void
 statapprox_heap(Relation rel, output_type *stat)
 {
 	BlockNumber nblocks;
-	BufferAccessStrategy bstrategy;
 	TransactionId OldestXmin;
 	StatApproxReadStreamPrivate p;
 	ReadStream *stream;
 
 	OldestXmin = GetOldestNonRemovableTransactionId(rel);
-	bstrategy = GetAccessStrategy(BAS_BULKREAD);
 
 	nblocks = RelationGetNumberOfBlocks(rel);
 
@@ -141,7 +139,6 @@ statapprox_heap(Relation rel, output_type *stat)
 	 * caution.
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_FULL,
-										bstrategy,
 										rel,
 										MAIN_FORKNUM,
 										statapprox_heap_read_stream_next,
diff --git a/contrib/pgstattuple/pgstatindex.c b/contrib/pgstattuple/pgstatindex.c
index 8951ad0aac4..a0922b386a0 100644
--- a/contrib/pgstattuple/pgstatindex.c
+++ b/contrib/pgstattuple/pgstatindex.c
@@ -217,7 +217,6 @@ pgstatindex_impl(Relation rel, FunctionCallInfo fcinfo)
 	BlockNumber nblocks;
 	BlockNumber blkno;
 	BTIndexStat indexStat;
-	BufferAccessStrategy bstrategy = GetAccessStrategy(BAS_BULKREAD);
 	BlockRangeReadStreamPrivate p;
 	ReadStream *stream;
 	BlockNumber startblk;
@@ -254,7 +253,7 @@ pgstatindex_impl(Relation rel, FunctionCallInfo fcinfo)
 	 * Read metapage
 	 */
 	{
-		Buffer		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, 0, RBM_NORMAL, bstrategy);
+		Buffer		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, 0, RBM_NORMAL);
 		Page		page = BufferGetPage(buffer);
 		BTMetaPageData *metad = BTPageGetMeta(page);
 
@@ -291,7 +290,6 @@ pgstatindex_impl(Relation rel, FunctionCallInfo fcinfo)
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										bstrategy,
 										rel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -612,7 +610,6 @@ pgstathashindex(PG_FUNCTION_ARGS)
 	BlockNumber blkno;
 	Relation	rel;
 	HashIndexStat stats;
-	BufferAccessStrategy bstrategy;
 	HeapTuple	tuple;
 	TupleDesc	tupleDesc;
 	Datum		values[8];
@@ -665,9 +662,6 @@ pgstathashindex(PG_FUNCTION_ARGS)
 	/* Get the current relation length */
 	nblocks = RelationGetNumberOfBlocks(rel);
 
-	/* prepare access strategy for this index */
-	bstrategy = GetAccessStrategy(BAS_BULKREAD);
-
 	/* Scan all blocks except the metapage (0th page) using streaming reads */
 	startblk = HASH_METAPAGE + 1;
 
@@ -680,7 +674,6 @@ pgstathashindex(PG_FUNCTION_ARGS)
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										bstrategy,
 										rel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/contrib/pgstattuple/pgstattuple.c b/contrib/pgstattuple/pgstattuple.c
index 6a7f8cb4a7c..90f7938e953 100644
--- a/contrib/pgstattuple/pgstattuple.c
+++ b/contrib/pgstattuple/pgstattuple.c
@@ -64,22 +64,18 @@ typedef struct pgstattuple_type
 	uint64		free_space;		/* free/reusable space in bytes */
 } pgstattuple_type;
 
-typedef void (*pgstat_page) (pgstattuple_type *, Relation, BlockNumber,
-							 BufferAccessStrategy);
+typedef void (*pgstat_page) (pgstattuple_type *, Relation, BlockNumber);
 
 static Datum build_pgstattuple_type(pgstattuple_type *stat,
 									FunctionCallInfo fcinfo);
 static Datum pgstat_relation(Relation rel, FunctionCallInfo fcinfo);
 static Datum pgstat_heap(Relation rel, FunctionCallInfo fcinfo);
 static void pgstat_btree_page(pgstattuple_type *stat,
-							  Relation rel, BlockNumber blkno,
-							  BufferAccessStrategy bstrategy);
+							  Relation rel, BlockNumber blkno);
 static void pgstat_hash_page(pgstattuple_type *stat,
-							 Relation rel, BlockNumber blkno,
-							 BufferAccessStrategy bstrategy);
+							 Relation rel, BlockNumber blkno);
 static void pgstat_gist_page(pgstattuple_type *stat,
-							 Relation rel, BlockNumber blkno,
-							 BufferAccessStrategy bstrategy);
+							 Relation rel, BlockNumber blkno);
 static Datum pgstat_index(Relation rel, BlockNumber start,
 						  pgstat_page pagefn, FunctionCallInfo fcinfo);
 static void pgstat_index_page(pgstattuple_type *stat, Page page,
@@ -376,7 +372,7 @@ pgstat_heap(Relation rel, FunctionCallInfo fcinfo)
 			CHECK_FOR_INTERRUPTS();
 
 			buffer = ReadBufferExtended(rel, MAIN_FORKNUM, block,
-										RBM_NORMAL, hscan->rs_strategy);
+										RBM_NORMAL);
 			LockBuffer(buffer, BUFFER_LOCK_SHARE);
 			stat.free_space += PageGetExactFreeSpace(BufferGetPage(buffer));
 			UnlockReleaseBuffer(buffer);
@@ -389,7 +385,7 @@ pgstat_heap(Relation rel, FunctionCallInfo fcinfo)
 		CHECK_FOR_INTERRUPTS();
 
 		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, block,
-									RBM_NORMAL, hscan->rs_strategy);
+									RBM_NORMAL);
 		LockBuffer(buffer, BUFFER_LOCK_SHARE);
 		stat.free_space += PageGetExactFreeSpace(BufferGetPage(buffer));
 		UnlockReleaseBuffer(buffer);
@@ -408,13 +404,12 @@ pgstat_heap(Relation rel, FunctionCallInfo fcinfo)
  * pgstat_btree_page -- check tuples in a btree page
  */
 static void
-pgstat_btree_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
-				  BufferAccessStrategy bstrategy)
+pgstat_btree_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno)
 {
 	Buffer		buf;
 	Page		page;
 
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, bstrategy);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 	LockBuffer(buf, BT_READ);
 	page = BufferGetPage(buf);
 
@@ -452,13 +447,12 @@ pgstat_btree_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
  * pgstat_hash_page -- check tuples in a hash page
  */
 static void
-pgstat_hash_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
-				 BufferAccessStrategy bstrategy)
+pgstat_hash_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno)
 {
 	Buffer		buf;
 	Page		page;
 
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, bstrategy);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 	LockBuffer(buf, HASH_READ);
 	page = BufferGetPage(buf);
 
@@ -500,13 +494,12 @@ pgstat_hash_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
  * pgstat_gist_page -- check tuples in a gist page
  */
 static void
-pgstat_gist_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
-				 BufferAccessStrategy bstrategy)
+pgstat_gist_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno)
 {
 	Buffer		buf;
 	Page		page;
 
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, bstrategy);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 	LockBuffer(buf, GIST_SHARE);
 	page = BufferGetPage(buf);
 	if (PageIsNew(page))
@@ -539,12 +532,8 @@ pgstat_index(Relation rel, BlockNumber start, pgstat_page pagefn,
 {
 	BlockNumber nblocks;
 	BlockNumber blkno;
-	BufferAccessStrategy bstrategy;
 	pgstattuple_type stat = {0};
 
-	/* prepare access strategy for this index */
-	bstrategy = GetAccessStrategy(BAS_BULKREAD);
-
 	blkno = start;
 	for (;;)
 	{
@@ -565,7 +554,7 @@ pgstat_index(Relation rel, BlockNumber start, pgstat_page pagefn,
 		{
 			CHECK_FOR_INTERRUPTS();
 
-			pagefn(&stat, rel, blkno, bstrategy);
+			pagefn(&stat, rel, blkno);
 		}
 	}
 
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0848c18d329..7cb8839245c 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2107,36 +2107,6 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
-     <varlistentry id="guc-vacuum-buffer-usage-limit" xreflabel="vacuum_buffer_usage_limit">
-      <term>
-       <varname>vacuum_buffer_usage_limit</varname> (<type>integer</type>)
-       <indexterm>
-        <primary><varname>vacuum_buffer_usage_limit</varname> configuration parameter</primary>
-       </indexterm>
-      </term>
-      <listitem>
-       <para>
-        Specifies the size of the
-        <glossterm linkend="glossary-buffer-access-strategy">Buffer Access Strategy</glossterm>
-        used by the <command>VACUUM</command> and <command>ANALYZE</command>
-        commands.  A setting of <literal>0</literal> will allow the operation
-        to use any number of <varname>shared_buffers</varname>.  Otherwise
-        valid sizes range from <literal>128 kB</literal> to
-        <literal>16 GB</literal>.  If the specified size would exceed 1/8 the
-        size of <varname>shared_buffers</varname>, the size is silently capped
-        to that value.  The default value is <literal>2MB</literal>.  If
-        this value is specified without units, it is taken as kilobytes.  This
-        parameter can be set at any time.  It can be overridden for
-        <xref linkend="sql-vacuum"/> and <xref linkend="sql-analyze"/>
-        when passing the <option>BUFFER_USAGE_LIMIT</option> option.  Higher
-        settings can allow <command>VACUUM</command> and
-        <command>ANALYZE</command> to run more quickly, but having too large a
-        setting may cause too many other useful pages to be evicted from
-        shared buffers.
-       </para>
-      </listitem>
-     </varlistentry>
-
      <varlistentry id="guc-logical-decoding-work-mem" xreflabel="logical_decoding_work_mem">
       <term><varname>logical_decoding_work_mem</varname> (<type>integer</type>)
       <indexterm>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index d1a20d001e9..18d17540430 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -2993,28 +2993,6 @@ description | Waiting for a newly initialized WAL file to reach durable storage
           <literal>init</literal>.
          </para>
         </listitem>
-        <listitem>
-         <para>
-          <literal>vacuum</literal>: I/O operations performed outside of shared
-          buffers while vacuuming and analyzing permanent relations. Temporary
-          table vacuums use the same local buffer pool as other temporary table
-          I/O operations and are tracked in <varname>context</varname>
-          <literal>normal</literal>.
-         </para>
-        </listitem>
-        <listitem>
-         <para>
-          <literal>bulkread</literal>: Certain large read I/O operations
-          done outside of shared buffers, for example, a sequential scan of a
-          large table.
-         </para>
-        </listitem>
-        <listitem>
-         <para>
-          <literal>bulkwrite</literal>: Certain large write I/O operations
-          done outside of shared buffers, such as <command>COPY</command>.
-         </para>
-        </listitem>
        </itemizedlist>
       </entry>
      </row>
@@ -3180,13 +3158,8 @@ description | Waiting for a newly initialized WAL file to reach durable storage
         buffer in order to make it available for another use.
        </para>
        <para>
-        In <varname>context</varname> <literal>normal</literal>, this counts
-        the number of times a block was evicted from a buffer and replaced with
-        another block. In <varname>context</varname>s
-        <literal>bulkwrite</literal>, <literal>bulkread</literal>, and
-        <literal>vacuum</literal>, this counts the number of times a block was
-        evicted from shared buffers in order to add the shared buffer to a
-        separate, size-limited ring buffer for use in a bulk I/O operation.
+        This counts the number of times a block was evicted from a buffer and
+        replaced with another block.
         </para>
       </entry>
      </row>
@@ -3197,10 +3170,9 @@ description | Waiting for a newly initialized WAL file to reach durable storage
         <structfield>reuses</structfield> <type>bigint</type>
        </para>
        <para>
-        The number of times an existing buffer in a size-limited ring buffer
-        outside of shared buffers was reused as part of an I/O operation in the
-        <literal>bulkread</literal>, <literal>bulkwrite</literal>, or
-        <literal>vacuum</literal> <varname>context</varname>s.
+        Always zero.  This column previously counted reuses of buffers in a
+        size-limited ring buffer (buffer access strategy); ring buffers have
+        been removed, so no reuses are tracked.
        </para>
       </entry>
      </row>
diff --git a/doc/src/sgml/ref/analyze.sgml b/doc/src/sgml/ref/analyze.sgml
index ec81f00fecf..7f5159111d8 100644
--- a/doc/src/sgml/ref/analyze.sgml
+++ b/doc/src/sgml/ref/analyze.sgml
@@ -27,7 +27,6 @@ ANALYZE [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] [ <r
 
     VERBOSE [ <replaceable class="parameter">boolean</replaceable> ]
     SKIP_LOCKED [ <replaceable class="parameter">boolean</replaceable> ]
-    BUFFER_USAGE_LIMIT <replaceable class="parameter">size</replaceable>
 
 <phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase>
 
@@ -88,26 +87,6 @@ ANALYZE [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] [ <r
     </listitem>
    </varlistentry>
 
-   <varlistentry>
-    <term><literal>BUFFER_USAGE_LIMIT</literal></term>
-    <listitem>
-     <para>
-      Specifies the
-      <glossterm linkend="glossary-buffer-access-strategy">Buffer Access Strategy</glossterm>
-      ring buffer size for <command>ANALYZE</command>.  This size is used to
-      calculate the number of shared buffers which will be reused as part of
-      this strategy.  <literal>0</literal> disables use of a
-      <literal>Buffer Access Strategy</literal>.   When this option is not
-      specified, <command>ANALYZE</command> uses the value from
-      <xref linkend="guc-vacuum-buffer-usage-limit"/>.  Higher settings can
-      allow <command>ANALYZE</command> to run more quickly, but having too
-      large a setting may cause too many other useful pages to be evicted from
-      shared buffers.  The minimum value is <literal>128 kB</literal> and the
-      maximum value is <literal>16 GB</literal>.
-     </para>
-    </listitem>
-   </varlistentry>
-
    <varlistentry>
     <term><replaceable class="parameter">boolean</replaceable></term>
     <listitem>
diff --git a/doc/src/sgml/ref/vacuum.sgml b/doc/src/sgml/ref/vacuum.sgml
index 38ee973ea05..d44a49f8efe 100644
--- a/doc/src/sgml/ref/vacuum.sgml
+++ b/doc/src/sgml/ref/vacuum.sgml
@@ -37,7 +37,6 @@ VACUUM [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] [ <re
     PARALLEL <replaceable class="parameter">integer</replaceable>
     SKIP_DATABASE_STATS [ <replaceable class="parameter">boolean</replaceable> ]
     ONLY_DATABASE_STATS [ <replaceable class="parameter">boolean</replaceable> ]
-    BUFFER_USAGE_LIMIT <replaceable class="parameter">size</replaceable>
     FULL [ <replaceable class="parameter">boolean</replaceable> ]
 
 <phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase>
@@ -309,30 +308,6 @@ VACUUM [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] [ <re
     </listitem>
    </varlistentry>
 
-   <varlistentry>
-    <term><literal>BUFFER_USAGE_LIMIT</literal></term>
-    <listitem>
-     <para>
-      Specifies the
-      <glossterm linkend="glossary-buffer-access-strategy">Buffer Access Strategy</glossterm>
-      ring buffer size for <command>VACUUM</command>.  This size is used to
-      calculate the number of shared buffers which will be reused as part of
-      this strategy.  <literal>0</literal> disables use of a
-      <literal>Buffer Access Strategy</literal>.  If <option>ANALYZE</option>
-      is also specified, the <option>BUFFER_USAGE_LIMIT</option> value is used
-      for both the vacuum and analyze stages.  This option can't be used with
-      the <option>FULL</option> option except if <option>ANALYZE</option> is
-      also specified.  When this option is not specified,
-      <command>VACUUM</command> uses the value from
-      <xref linkend="guc-vacuum-buffer-usage-limit"/>.  Higher settings can
-      allow <command>VACUUM</command> to run more quickly, but having too
-      large a setting may cause too many other useful pages to be evicted from
-      shared buffers.  The minimum value is <literal>128 kB</literal> and the
-      maximum value is <literal>16 GB</literal>.
-     </para>
-    </listitem>
-   </varlistentry>
-
    <varlistentry>
     <term><literal>FULL</literal></term>
     <listitem>
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index bdb30752e09..5d6539f5483 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -224,7 +224,7 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void form_and_spill_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
-static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
+static void brin_vacuum_scan(Relation idxrel);
 static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
 								BrinMemTuple *dtup, const Datum *values, const bool *nulls);
 static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
@@ -1129,7 +1129,7 @@ brinbuild(Relation heap, Relation index, IndexInfo *indexInfo)
 	 * whole relation will be rolled back.
 	 */
 
-	meta = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM, NULL,
+	meta = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM,
 							 EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 	Assert(BufferGetBlockNumber(meta) == BRIN_METAPAGE_BLKNO);
 
@@ -1281,7 +1281,7 @@ brinbuildempty(Relation index)
 	Buffer		metabuf;
 
 	/* An empty BRIN index has a metapage only. */
-	metabuf = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+	metabuf = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM,
 								EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 
 	/* Initialize and xlog metabuffer. */
@@ -1336,7 +1336,7 @@ brinvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
 	heapRel = table_open(IndexGetRelation(RelationGetRelid(info->index), false),
 						 AccessShareLock);
 
-	brin_vacuum_scan(info->index, info->strategy);
+	brin_vacuum_scan(info->index);
 
 	brinsummarize(info->index, heapRel, BRIN_ALL_BLOCKRANGES, false,
 				  &stats->num_index_tuples, &stats->num_index_tuples);
@@ -2171,7 +2171,7 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
  * and such.
  */
 static void
-brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
+brin_vacuum_scan(Relation idxrel)
 {
 	BlockRangeReadStreamPrivate p;
 	ReadStream *stream;
@@ -2187,7 +2187,6 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										strategy,
 										idxrel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/src/backend/access/brin/brin_revmap.c b/src/backend/access/brin/brin_revmap.c
index 233355cb2d5..01c4104ac88 100644
--- a/src/backend/access/brin/brin_revmap.c
+++ b/src/backend/access/brin/brin_revmap.c
@@ -559,7 +559,7 @@ revmap_physical_extend(BrinRevmap *revmap)
 	}
 	else
 	{
-		buf = ExtendBufferedRel(BMR_REL(irel), MAIN_FORKNUM, NULL,
+		buf = ExtendBufferedRel(BMR_REL(irel), MAIN_FORKNUM,
 								EB_LOCK_FIRST);
 		if (BufferGetBlockNumber(buf) != mapBlk)
 		{
diff --git a/src/backend/access/gin/gininsert.c b/src/backend/access/gin/gininsert.c
index cb9ed3b563c..32ad65cc95f 100644
--- a/src/backend/access/gin/gininsert.c
+++ b/src/backend/access/gin/gininsert.c
@@ -815,9 +815,9 @@ ginbuildempty(Relation index)
 				MetaBuffer;
 
 	/* An empty GIN index has two pages. */
-	MetaBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+	MetaBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM,
 								   EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
-	RootBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+	RootBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM,
 								   EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 
 	/* Initialize and xlog metabuffer and root buffer. */
diff --git a/src/backend/access/gin/ginutil.c b/src/backend/access/gin/ginutil.c
index e7cba81d477..2d7394aedd5 100644
--- a/src/backend/access/gin/ginutil.c
+++ b/src/backend/access/gin/ginutil.c
@@ -336,7 +336,7 @@ GinNewBuffer(Relation index)
 	}
 
 	/* Must extend the file */
-	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM,
 							   EB_LOCK_FIRST);
 
 	return buffer;
diff --git a/src/backend/access/gin/ginvacuum.c b/src/backend/access/gin/ginvacuum.c
index 840543eb664..86538de39d6 100644
--- a/src/backend/access/gin/ginvacuum.c
+++ b/src/backend/access/gin/ginvacuum.c
@@ -32,7 +32,6 @@ struct GinVacuumState
 	IndexBulkDeleteCallback callback;
 	void	   *callback_state;
 	GinState	ginstate;
-	BufferAccessStrategy strategy;
 	MemoryContext tmpCxt;
 };
 
@@ -289,7 +288,7 @@ ginScanPostingTreeToDelete(GinVacuumState *gvs, DataPageDeleteStack *myStackItem
 			childBuffer = ReadBufferExtended(gvs->index,
 											 MAIN_FORKNUM,
 											 PostingItemGetBlockNumber(pitem),
-											 RBM_NORMAL, gvs->strategy);
+											 RBM_NORMAL);
 			LockBuffer(childBuffer, GIN_EXCLUSIVE);
 
 			/* Allocate a child stack entry on first use; reuse thereafter */
@@ -389,7 +388,7 @@ ginVacuumPostingTreeLeaves(GinVacuumState *gvs, BlockNumber blkno)
 		PostingItem *pitem;
 
 		buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, gvs->strategy);
+									RBM_NORMAL);
 		LockBuffer(buffer, GIN_SHARE);
 		page = BufferGetPage(buffer);
 
@@ -430,7 +429,7 @@ ginVacuumPostingTreeLeaves(GinVacuumState *gvs, BlockNumber blkno)
 			break;
 
 		buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, gvs->strategy);
+									RBM_NORMAL);
 		LockBuffer(buffer, GIN_EXCLUSIVE);
 		page = BufferGetPage(buffer);
 	}
@@ -454,7 +453,7 @@ ginVacuumPostingTree(GinVacuumState *gvs, BlockNumber rootBlkno)
 		bool		deleted PG_USED_FOR_ASSERTS_ONLY;
 
 		buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, rootBlkno,
-									RBM_NORMAL, gvs->strategy);
+									RBM_NORMAL);
 
 		/*
 		 * Lock posting tree root for cleanup to ensure there are no
@@ -615,7 +614,6 @@ ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	gvs.index = index;
 	gvs.callback = callback;
 	gvs.callback_state = callback_state;
-	gvs.strategy = info->strategy;
 	initGinState(&gvs.ginstate, index);
 
 	/* first time through? */
@@ -636,7 +634,7 @@ ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	gvs.result = stats;
 
 	buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
-								RBM_NORMAL, info->strategy);
+								RBM_NORMAL);
 
 	/* find leaf page */
 	for (;;)
@@ -669,7 +667,7 @@ ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 
 		UnlockReleaseBuffer(buffer);
 		buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, info->strategy);
+									RBM_NORMAL);
 	}
 
 	/* right now we found leftmost page in entry's BTree */
@@ -712,7 +710,7 @@ ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 			break;
 
 		buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, info->strategy);
+									RBM_NORMAL);
 		LockBuffer(buffer, GIN_EXCLUSIVE);
 	}
 
@@ -794,7 +792,6 @@ ginvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										index,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/src/backend/access/gist/gist.c b/src/backend/access/gist/gist.c
index 8565e225be7..ae494742a4b 100644
--- a/src/backend/access/gist/gist.c
+++ b/src/backend/access/gist/gist.c
@@ -142,7 +142,7 @@ gistbuildempty(Relation index)
 	Buffer		buffer;
 
 	/* Initialize the root page */
-	buffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM,
 							   EB_SKIP_EXTENSION_LOCK | EB_LOCK_FIRST);
 
 	/* Initialize and xlog buffer */
diff --git a/src/backend/access/gist/gistutil.c b/src/backend/access/gist/gistutil.c
index 0f58f61879f..170174f62fd 100644
--- a/src/backend/access/gist/gistutil.c
+++ b/src/backend/access/gist/gistutil.c
@@ -877,7 +877,7 @@ gistNewBuffer(Relation r, Relation heaprel)
 	}
 
 	/* Must extend the file */
-	buffer = ExtendBufferedRel(BMR_REL(r), MAIN_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(r), MAIN_FORKNUM,
 							   EB_LOCK_FIRST);
 
 	return buffer;
diff --git a/src/backend/access/gist/gistvacuum.c b/src/backend/access/gist/gistvacuum.c
index 686a0418054..c366ed35ea8 100644
--- a/src/backend/access/gist/gistvacuum.c
+++ b/src/backend/access/gist/gistvacuum.c
@@ -218,7 +218,6 @@ gistvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										rel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -491,8 +490,7 @@ restart:
 		/* check for vacuum delay while not holding any buffer lock */
 		vacuum_delay_point(false);
 
-		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL,
-									info->strategy);
+		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 		goto restart;
 	}
 }
@@ -524,8 +522,7 @@ gistvacuum_delete_empty_pages(IndexVacuumInfo *info, GistVacState *vstate)
 		int			ntodelete;
 		int			deleted;
 
-		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, (BlockNumber) blkno,
-									RBM_NORMAL, info->strategy);
+		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, (BlockNumber) blkno, RBM_NORMAL);
 
 		LockBuffer(buffer, GIST_SHARE);
 		page = BufferGetPage(buffer);
@@ -590,8 +587,7 @@ gistvacuum_delete_empty_pages(IndexVacuumInfo *info, GistVacState *vstate)
 			if (PageGetMaxOffsetNumber(page) == FirstOffsetNumber)
 				break;
 
-			leafbuf = ReadBufferExtended(rel, MAIN_FORKNUM, leafs_to_delete[i],
-										 RBM_NORMAL, info->strategy);
+			leafbuf = ReadBufferExtended(rel, MAIN_FORKNUM, leafs_to_delete[i], RBM_NORMAL);
 			LockBuffer(leafbuf, GIST_EXCLUSIVE);
 			gistcheckpage(rel, leafbuf);
 
diff --git a/src/backend/access/hash/hash.c b/src/backend/access/hash/hash.c
index 8d8cd30dc38..1bbb7224bfc 100644
--- a/src/backend/access/hash/hash.c
+++ b/src/backend/access/hash/hash.c
@@ -542,7 +542,6 @@ hashbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										rel,
 										MAIN_FORKNUM,
 										hash_bulkdelete_read_stream_cb,
@@ -616,7 +615,7 @@ bucket_loop:
 
 		bucket_buf = buf;
 
-		hashbucketcleanup(rel, cur_bucket, bucket_buf, blkno, info->strategy,
+		hashbucketcleanup(rel, cur_bucket, bucket_buf, blkno,
 						  cachedmetap->hashm_maxbucket,
 						  cachedmetap->hashm_highmask,
 						  cachedmetap->hashm_lowmask, &tuples_removed,
@@ -765,7 +764,7 @@ hashvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
  */
 void
 hashbucketcleanup(Relation rel, Bucket cur_bucket, Buffer bucket_buf,
-				  BlockNumber bucket_blkno, BufferAccessStrategy bstrategy,
+				  BlockNumber bucket_blkno,
 				  uint32 maxbucket, uint32 highmask, uint32 lowmask,
 				  double *tuples_removed, double *num_index_tuples,
 				  bool split_cleanup,
@@ -935,9 +934,8 @@ hashbucketcleanup(Relation rel, Bucket cur_bucket, Buffer bucket_buf,
 		if (!BlockNumberIsValid(blkno))
 			break;
 
-		next_buf = _hash_getbuf_with_strategy(rel, blkno, HASH_WRITE,
-											  LH_OVERFLOW_PAGE,
-											  bstrategy);
+		next_buf = _hash_getbuf(rel, blkno, HASH_WRITE,
+								LH_OVERFLOW_PAGE);
 
 		/*
 		 * release the lock on previous page after acquiring the lock on next
@@ -1004,8 +1002,7 @@ hashbucketcleanup(Relation rel, Bucket cur_bucket, Buffer bucket_buf,
 	 * ordering of tuples for a scan that has started before it.
 	 */
 	if (bucket_dirty && IsBufferCleanupOK(bucket_buf))
-		_hash_squeezebucket(rel, cur_bucket, bucket_blkno, bucket_buf,
-							bstrategy);
+		_hash_squeezebucket(rel, cur_bucket, bucket_blkno, bucket_buf);
 	else
 		LockBuffer(bucket_buf, BUFFER_LOCK_UNLOCK);
 }
diff --git a/src/backend/access/hash/hashovfl.c b/src/backend/access/hash/hashovfl.c
index dbc57ef958c..5b35fed33f9 100644
--- a/src/backend/access/hash/hashovfl.c
+++ b/src/backend/access/hash/hashovfl.c
@@ -491,8 +491,7 @@ _hash_firstfreebit(uint32 map)
 BlockNumber
 _hash_freeovflpage(Relation rel, Buffer bucketbuf, Buffer ovflbuf,
 				   Buffer wbuf, IndexTuple *itups, OffsetNumber *itup_offsets,
-				   Size *tups_size, uint16 nitups,
-				   BufferAccessStrategy bstrategy)
+				   Size *tups_size, uint16 nitups)
 {
 	HashMetaPage metap;
 	Buffer		metabuf;
@@ -539,20 +538,16 @@ _hash_freeovflpage(Relation rel, Buffer bucketbuf, Buffer ovflbuf,
 		if (prevblkno == writeblkno)
 			prevbuf = wbuf;
 		else
-			prevbuf = _hash_getbuf_with_strategy(rel,
-												 prevblkno,
-												 HASH_WRITE,
-												 LH_BUCKET_PAGE | LH_OVERFLOW_PAGE,
-												 bstrategy);
+			prevbuf = _hash_getbuf(rel,
+								   prevblkno,
+								   HASH_WRITE,
+								   LH_BUCKET_PAGE | LH_OVERFLOW_PAGE);
 	}
 	if (BlockNumberIsValid(nextblkno))
-		nextbuf = _hash_getbuf_with_strategy(rel,
-											 nextblkno,
-											 HASH_WRITE,
-											 LH_OVERFLOW_PAGE,
-											 bstrategy);
-
-	/* Note: bstrategy is intentionally not used for metapage and bitmap */
+		nextbuf = _hash_getbuf(rel,
+							   nextblkno,
+							   HASH_WRITE,
+							   LH_OVERFLOW_PAGE);
 
 	/* Read the metapage so we can determine which bitmap page to use */
 	metabuf = _hash_getbuf(rel, HASH_METAPAGE, HASH_READ, LH_META_PAGE);
@@ -843,8 +838,7 @@ void
 _hash_squeezebucket(Relation rel,
 					Bucket bucket,
 					BlockNumber bucket_blkno,
-					Buffer bucket_buf,
-					BufferAccessStrategy bstrategy)
+					Buffer bucket_buf)
 {
 	BlockNumber wblkno;
 	BlockNumber rblkno;
@@ -886,11 +880,10 @@ _hash_squeezebucket(Relation rel,
 		rblkno = ropaque->hasho_nextblkno;
 		if (rbuf != InvalidBuffer)
 			_hash_relbuf(rel, rbuf);
-		rbuf = _hash_getbuf_with_strategy(rel,
-										  rblkno,
-										  HASH_WRITE,
-										  LH_OVERFLOW_PAGE,
-										  bstrategy);
+		rbuf = _hash_getbuf(rel,
+							rblkno,
+							HASH_WRITE,
+							LH_OVERFLOW_PAGE);
 		rpage = BufferGetPage(rbuf);
 		ropaque = HashPageGetOpaque(rpage);
 		Assert(ropaque->hasho_bucket == bucket);
@@ -952,11 +945,10 @@ readpage:
 
 				/* don't need to move to next page if we reached the read page */
 				if (wblkno != rblkno)
-					next_wbuf = _hash_getbuf_with_strategy(rel,
-														   wblkno,
-														   HASH_WRITE,
-														   LH_OVERFLOW_PAGE,
-														   bstrategy);
+					next_wbuf = _hash_getbuf(rel,
+											 wblkno,
+											 HASH_WRITE,
+											 LH_OVERFLOW_PAGE);
 
 				if (nitups > 0)
 				{
@@ -1098,7 +1090,7 @@ readpage:
 
 		/* free this overflow page (releases rbuf) */
 		_hash_freeovflpage(rel, bucket_buf, rbuf, wbuf, itups, itup_offsets,
-						   tups_size, nitups, bstrategy);
+						   tups_size, nitups);
 
 		/* be tidy */
 		for (i = 0; i < nitups; i++)
@@ -1115,11 +1107,10 @@ readpage:
 			return;
 		}
 
-		rbuf = _hash_getbuf_with_strategy(rel,
-										  rblkno,
-										  HASH_WRITE,
-										  LH_OVERFLOW_PAGE,
-										  bstrategy);
+		rbuf = _hash_getbuf(rel,
+							rblkno,
+							HASH_WRITE,
+							LH_OVERFLOW_PAGE);
 		rpage = BufferGetPage(rbuf);
 		ropaque = HashPageGetOpaque(rpage);
 		Assert(ropaque->hasho_bucket == bucket);
diff --git a/src/backend/access/hash/hashpage.c b/src/backend/access/hash/hashpage.c
index 8099b0d021f..bdf1a255f1f 100644
--- a/src/backend/access/hash/hashpage.c
+++ b/src/backend/access/hash/hashpage.c
@@ -139,8 +139,7 @@ _hash_getinitbuf(Relation rel, BlockNumber blkno)
 	if (blkno == P_NEW)
 		elog(ERROR, "hash AM does not use P_NEW");
 
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_ZERO_AND_LOCK,
-							 NULL);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_ZERO_AND_LOCK);
 
 	/* ref count and lock type are correct */
 
@@ -209,7 +208,7 @@ _hash_getnewbuf(Relation rel, BlockNumber blkno, ForkNumber forkNum)
 	/* smgr insists we explicitly extend the relation */
 	if (blkno == nblocks)
 	{
-		buf = ExtendBufferedRel(BMR_REL(rel), forkNum, NULL,
+		buf = ExtendBufferedRel(BMR_REL(rel), forkNum,
 								EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 		if (BufferGetBlockNumber(buf) != blkno)
 			elog(ERROR, "unexpected hash relation size: %u, should be %u",
@@ -217,8 +216,7 @@ _hash_getnewbuf(Relation rel, BlockNumber blkno, ForkNumber forkNum)
 	}
 	else
 	{
-		buf = ReadBufferExtended(rel, forkNum, blkno, RBM_ZERO_AND_LOCK,
-								 NULL);
+		buf = ReadBufferExtended(rel, forkNum, blkno, RBM_ZERO_AND_LOCK);
 	}
 
 	/* ref count and lock type are correct */
@@ -229,34 +227,6 @@ _hash_getnewbuf(Relation rel, BlockNumber blkno, ForkNumber forkNum)
 	return buf;
 }
 
-/*
- *	_hash_getbuf_with_strategy() -- Get a buffer with nondefault strategy.
- *
- *		This is identical to _hash_getbuf() but also allows a buffer access
- *		strategy to be specified.  We use this for VACUUM operations.
- */
-Buffer
-_hash_getbuf_with_strategy(Relation rel, BlockNumber blkno,
-						   int access, int flags,
-						   BufferAccessStrategy bstrategy)
-{
-	Buffer		buf;
-
-	if (blkno == P_NEW)
-		elog(ERROR, "hash AM does not use P_NEW");
-
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, bstrategy);
-
-	if (access != HASH_NOLOCK)
-		LockBuffer(buf, access);
-
-	/* ref count and lock type are correct */
-
-	_hash_checkpage(rel, buf, flags);
-
-	return buf;
-}
-
 /*
  *	_hash_relbuf() -- release a locked buffer.
  *
@@ -758,7 +728,7 @@ restart_expand:
 		/* Release the metapage lock. */
 		LockBuffer(metabuf, BUFFER_LOCK_UNLOCK);
 
-		hashbucketcleanup(rel, old_bucket, buf_oblkno, start_oblkno, NULL,
+		hashbucketcleanup(rel, old_bucket, buf_oblkno, start_oblkno,
 						  maxbucket, highmask, lowmask, NULL, NULL, true,
 						  NULL, NULL);
 
@@ -1333,7 +1303,7 @@ _hash_splitbucket(Relation rel,
 	{
 		LockBuffer(bucket_nbuf, BUFFER_LOCK_UNLOCK);
 		hashbucketcleanup(rel, obucket, bucket_obuf,
-						  BufferGetBlockNumber(bucket_obuf), NULL,
+						  BufferGetBlockNumber(bucket_obuf),
 						  maxbucket, highmask, lowmask, NULL, NULL, true,
 						  NULL, NULL);
 	}
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 9cdc221675b..48e7c421bf4 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -359,7 +359,6 @@ static void
 initscan(HeapScanDesc scan, ScanKey key, bool keep_startblock)
 {
 	ParallelBlockTableScanDesc bpscan = NULL;
-	bool		allow_strat;
 	bool		allow_sync;
 
 	/*
@@ -396,24 +395,10 @@ initscan(HeapScanDesc scan, ScanKey key, bool keep_startblock)
 	if (!RelationUsesLocalBuffers(scan->rs_base.rs_rd) &&
 		scan->rs_nblocks > NBuffers / 4)
 	{
-		allow_strat = (scan->rs_base.rs_flags & SO_ALLOW_STRAT) != 0;
 		allow_sync = (scan->rs_base.rs_flags & SO_ALLOW_SYNC) != 0;
 	}
 	else
-		allow_strat = allow_sync = false;
-
-	if (allow_strat)
-	{
-		/* During a rescan, keep the previous strategy object. */
-		if (scan->rs_strategy == NULL)
-			scan->rs_strategy = GetAccessStrategy(BAS_BULKREAD);
-	}
-	else
-	{
-		if (scan->rs_strategy != NULL)
-			FreeAccessStrategy(scan->rs_strategy);
-		scan->rs_strategy = NULL;
-	}
+		allow_sync = false;
 
 	if (scan->rs_base.rs_parallel != NULL)
 	{
@@ -1202,7 +1187,6 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 	scan->rs_base.rs_flags = flags;
 	scan->rs_base.rs_parallel = parallel_scan;
 	scan->rs_base.rs_instrument = NULL;
-	scan->rs_strategy = NULL;	/* set in initscan */
 	scan->rs_cbuf = InvalidBuffer;
 
 	/*
@@ -1273,8 +1257,7 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 
 	/*
 	 * Set up a read stream for sequential scans and TID range scans. This
-	 * should be done after initscan() because initscan() allocates the
-	 * BufferAccessStrategy object passed to the read stream API.
+	 * should be done after initscan().
 	 */
 	if (scan->rs_base.rs_flags & SO_TYPE_SEQSCAN ||
 		scan->rs_base.rs_flags & SO_TYPE_TIDRANGESCAN)
@@ -1295,7 +1278,6 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 		 */
 		scan->rs_read_stream = read_stream_begin_relation(READ_STREAM_SEQUENTIAL |
 														  READ_STREAM_USE_BATCHING,
-														  scan->rs_strategy,
 														  scan->rs_base.rs_rd,
 														  MAIN_FORKNUM,
 														  cb,
@@ -1306,7 +1288,6 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 	{
 		scan->rs_read_stream = read_stream_begin_relation(READ_STREAM_DEFAULT |
 														  READ_STREAM_USE_BATCHING,
-														  scan->rs_strategy,
 														  scan->rs_base.rs_rd,
 														  MAIN_FORKNUM,
 														  bitmapheap_stream_read_next,
@@ -1402,9 +1383,6 @@ heap_endscan(TableScanDesc sscan)
 	if (BufferIsValid(scan->rs_vmbuffer))
 		ReleaseBuffer(scan->rs_vmbuffer);
 
-	/*
-	 * Must free the read stream before freeing the BufferAccessStrategy.
-	 */
 	if (scan->rs_read_stream)
 		read_stream_end(scan->rs_read_stream);
 
@@ -1416,9 +1394,6 @@ heap_endscan(TableScanDesc sscan)
 	if (scan->rs_base.rs_key)
 		pfree(scan->rs_base.rs_key);
 
-	if (scan->rs_strategy != NULL)
-		FreeAccessStrategy(scan->rs_strategy);
-
 	if (scan->rs_parallelworkerdata != NULL)
 		pfree(scan->rs_parallelworkerdata);
 
@@ -1939,7 +1914,6 @@ GetBulkInsertState(void)
 	BulkInsertState bistate;
 
 	bistate = (BulkInsertState) palloc_object(BulkInsertStateData);
-	bistate->strategy = GetAccessStrategy(BAS_BULKWRITE);
 	bistate->current_buf = InvalidBuffer;
 	bistate->next_free = InvalidBlockNumber;
 	bistate->last_free = InvalidBlockNumber;
@@ -1955,7 +1929,6 @@ FreeBulkInsertState(BulkInsertState bistate)
 {
 	if (bistate->current_buf != InvalidBuffer)
 		ReleaseBuffer(bistate->current_buf);
-	FreeAccessStrategy(bistate->strategy);
 	pfree(bistate);
 }
 
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index bf87430cf01..d14658d7218 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -2213,9 +2213,9 @@ heapam_scan_sample_next_block(TableScanDesc scan, SampleScanState *scanstate)
 	 */
 	CHECK_FOR_INTERRUPTS();
 
-	/* Read page using selected strategy */
+	/* Read page */
 	hscan->rs_cbuf = ReadBufferExtended(hscan->rs_base.rs_rd, MAIN_FORKNUM,
-										blockno, RBM_NORMAL, hscan->rs_strategy);
+										blockno, RBM_NORMAL);
 
 	/* in pagemode, prune the page and determine visible tuple offsets */
 	if (hscan->rs_base.rs_flags & SO_ALLOW_PAGEMODE)
diff --git a/src/backend/access/heap/hio.c b/src/backend/access/heap/hio.c
index e96e0f77d92..9bf7abc247f 100644
--- a/src/backend/access/heap/hio.c
+++ b/src/backend/access/heap/hio.c
@@ -80,7 +80,8 @@ RelationPutHeapTuple(Relation relation,
 }
 
 /*
- * Read in a buffer in mode, using bulk-insert strategy if bistate isn't NULL.
+ * Read in a buffer in mode, using the bistate's pinned target page if
+ * available.
  */
 static Buffer
 ReadBufferBI(Relation relation, BlockNumber targetBlock,
@@ -91,7 +92,7 @@ ReadBufferBI(Relation relation, BlockNumber targetBlock,
 	/* If not bulk-insert, exactly like ReadBuffer */
 	if (!bistate)
 		return ReadBufferExtended(relation, MAIN_FORKNUM, targetBlock,
-								  mode, NULL);
+								  mode);
 
 	/* If we have the desired block already pinned, re-pin and return it */
 	if (bistate->current_buf != InvalidBuffer)
@@ -113,9 +114,9 @@ ReadBufferBI(Relation relation, BlockNumber targetBlock,
 		bistate->current_buf = InvalidBuffer;
 	}
 
-	/* Perform a read using the buffer strategy */
+	/* Perform the read */
 	buffer = ReadBufferExtended(relation, MAIN_FORKNUM, targetBlock,
-								mode, bistate->strategy);
+								mode);
 
 	/* Save the selected block as target for future inserts */
 	IncrBufferRefCount(buffer);
@@ -337,7 +338,6 @@ RelationAddBlocks(Relation relation, BulkInsertState bistate,
 	 * way larger.
 	 */
 	first_block = ExtendBufferedRelBy(BMR_REL(relation), MAIN_FORKNUM,
-									  bistate ? bistate->strategy : NULL,
 									  EB_LOCK_FIRST,
 									  extend_by_pages,
 									  victim_buffers,
@@ -484,8 +484,7 @@ RelationAddBlocks(Relation relation, BulkInsertState bistate,
  *
  *	The caller can also provide a BulkInsertState object to optimize many
  *	insertions into the same relation.  This keeps a pin on the current
- *	insertion target page (to save pin/unpin cycles) and also passes a
- *	BULKWRITE buffer selection strategy object to the buffer manager.
+ *	insertion target page (to save pin/unpin cycles).
  *	Passing NULL for bistate selects the default behavior.
  *
  *	We don't fill existing pages further than the fillfactor, except for large
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 39395aed0d5..8bc8e470af3 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -257,7 +257,6 @@ typedef struct LVRelState
 	int			nindexes;
 
 	/* Buffer access strategy and parallel vacuum state */
-	BufferAccessStrategy bstrategy;
 	ParallelVacuumState *pvs;
 
 	/* Aggressive VACUUM? (must set relfrozenxid >= FreezeLimit) */
@@ -621,8 +620,7 @@ heap_vacuum_eager_scan_setup(LVRelState *vacrel, const VacuumParams *params)
  *		and locked the relation.
  */
 void
-heap_vacuum_rel(Relation rel, const VacuumParams *params,
-				BufferAccessStrategy bstrategy)
+heap_vacuum_rel(Relation rel, const VacuumParams *params)
 {
 	LVRelState *vacrel;
 	bool		verbose,
@@ -699,7 +697,6 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
 	vacrel->rel = rel;
 	vac_open_indexes(vacrel->rel, RowExclusiveLock, &vacrel->nindexes,
 					 &vacrel->indrels);
-	vacrel->bstrategy = bstrategy;
 	if (instrument && vacrel->nindexes > 0)
 	{
 		/* Copy index names used by instrumentation (not error reporting) */
@@ -1311,7 +1308,6 @@ lazy_scan_heap(LVRelState *vacrel)
 	 * explicit work in heap_vac_scan_next_block.
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE,
-										vacrel->bstrategy,
 										vacrel->rel,
 										MAIN_FORKNUM,
 										heap_vac_scan_next_block,
@@ -2670,7 +2666,6 @@ lazy_vacuum_heap_rel(LVRelState *vacrel)
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_USE_BATCHING,
-										vacrel->bstrategy,
 										vacrel->rel,
 										MAIN_FORKNUM,
 										vacuum_reap_lp_read_stream_next,
@@ -2904,13 +2899,6 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel)
 
 		VacuumFailsafeActive = true;
 
-		/*
-		 * Abandon use of a buffer access strategy to allow use of all of
-		 * shared buffers.  We assume the caller who allocated the memory for
-		 * the BufferAccessStrategy will free it.
-		 */
-		vacrel->bstrategy = NULL;
-
 		/* Disable index vacuuming, index cleanup, and heap rel truncation */
 		vacrel->do_index_vacuuming = false;
 		vacrel->do_index_cleanup = false;
@@ -3023,7 +3011,6 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	ivinfo.estimated_count = true;
 	ivinfo.message_level = DEBUG2;
 	ivinfo.num_heap_tuples = reltuples;
-	ivinfo.strategy = vacrel->bstrategy;
 
 	/*
 	 * Update error traceback information.
@@ -3074,7 +3061,6 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	ivinfo.message_level = DEBUG2;
 
 	ivinfo.num_heap_tuples = reltuples;
-	ivinfo.strategy = vacrel->bstrategy;
 
 	/*
 	 * Update error traceback information.
@@ -3354,8 +3340,7 @@ count_nondeletable_pages(LVRelState *vacrel, bool *lock_waiter_detected)
 			prefetchedUntil = prefetchStart;
 		}
 
-		buf = ReadBufferExtended(vacrel->rel, MAIN_FORKNUM, blkno, RBM_NORMAL,
-								 vacrel->bstrategy);
+		buf = ReadBufferExtended(vacrel->rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 
 		/* In this phase we only need shared access to the buffer */
 		LockBuffer(buf, BUFFER_LOCK_SHARE);
@@ -3446,8 +3431,7 @@ dead_items_alloc(LVRelState *vacrel, int nworkers)
 			vacrel->pvs = parallel_vacuum_init(vacrel->rel, vacrel->indrels,
 											   vacrel->nindexes, nworkers,
 											   vac_work_mem,
-											   vacrel->verbose ? INFO : DEBUG2,
-											   vacrel->bstrategy);
+											   vacrel->verbose ? INFO : DEBUG2);
 
 		/*
 		 * If parallel mode started, dead_items and dead_items_info spaces are
diff --git a/src/backend/access/heap/visibilitymap.c b/src/backend/access/heap/visibilitymap.c
index 4fd470702aa..b2612b1f710 100644
--- a/src/backend/access/heap/visibilitymap.c
+++ b/src/backend/access/heap/visibilitymap.c
@@ -575,7 +575,7 @@ vm_readbuf(Relation rel, BlockNumber blkno, bool extend)
 	}
 	else
 		buf = ReadBufferExtended(rel, VISIBILITYMAP_FORKNUM, blkno,
-								 RBM_ZERO_ON_ERROR, NULL);
+								 RBM_ZERO_ON_ERROR);
 
 	/*
 	 * Initializing the page when needed is trickier than it looks, because of
@@ -611,7 +611,7 @@ vm_extend(Relation rel, BlockNumber vm_nblocks)
 {
 	Buffer		buf;
 
-	buf = ExtendBufferedRelTo(BMR_REL(rel), VISIBILITYMAP_FORKNUM, NULL,
+	buf = ExtendBufferedRelTo(BMR_REL(rel), VISIBILITYMAP_FORKNUM,
 							  EB_CREATE_FORK_IF_NEEDED |
 							  EB_CLEAR_SIZE_CACHE,
 							  vm_nblocks,
diff --git a/src/backend/access/nbtree/nbtpage.c b/src/backend/access/nbtree/nbtpage.c
index 109017d6b52..7b5fc16d75c 100644
--- a/src/backend/access/nbtree/nbtpage.c
+++ b/src/backend/access/nbtree/nbtpage.c
@@ -960,7 +960,7 @@ _bt_allocbuf(Relation rel, Relation heaprel)
 	 * otherwise would make, as we can't use _bt_lockbuf() without introducing
 	 * a race.
 	 */
-	buf = ExtendBufferedRel(BMR_REL(rel), MAIN_FORKNUM, NULL, EB_LOCK_FIRST);
+	buf = ExtendBufferedRel(BMR_REL(rel), MAIN_FORKNUM, EB_LOCK_FIRST);
 	if (!RelationUsesLocalBuffers(rel))
 		VALGRIND_MAKE_MEM_DEFINED(BufferGetPage(buf), BLCKSZ);
 
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 3df2c752ead..379a2d37def 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -1324,7 +1324,6 @@ btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										rel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -1730,8 +1729,7 @@ backtrack:
 		 * recycle all-zero pages, not fail.  Also, we want to use a
 		 * nondefault buffer access strategy.
 		 */
-		buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL,
-								 info->strategy);
+		buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 		goto backtrack;
 	}
 
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index f2ee333f60d..785e385aab6 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -432,7 +432,7 @@ SpGistNewBuffer(Relation index)
 		ReleaseBuffer(buffer);
 	}
 
-	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM,
 							   EB_LOCK_FIRST);
 
 	return buffer;
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index c461f8dc02d..9f644edaf82 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -704,8 +704,7 @@ spgprocesspending(spgBulkDeleteState *bds)
 
 		/* examine the referenced page */
 		blkno = ItemPointerGetBlockNumber(&pitem->tid);
-		buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, bds->info->strategy);
+		buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno, RBM_NORMAL);
 		LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE);
 		page = BufferGetPage(buffer);
 
@@ -830,7 +829,6 @@ spgvacuumscan(spgBulkDeleteState *bds)
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										bds->info->strategy,
 										index,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/src/backend/access/transam/xloginsert.c b/src/backend/access/transam/xloginsert.c
index f2e10b82b7d..0c16b7fef0f 100644
--- a/src/backend/access/transam/xloginsert.c
+++ b/src/backend/access/transam/xloginsert.c
@@ -1348,7 +1348,7 @@ log_newpage_range(Relation rel, ForkNumber forknum,
 		while (nbufs < XLR_MAX_BLOCK_ID && blkno < endblk)
 		{
 			Buffer		buf = ReadBufferExtended(rel, forknum, blkno,
-												 RBM_NORMAL, NULL);
+												 RBM_NORMAL);
 
 			LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
 
diff --git a/src/backend/access/transam/xlogutils.c b/src/backend/access/transam/xlogutils.c
index d8c179c5dcc..e5be49f8b5d 100644
--- a/src/backend/access/transam/xlogutils.c
+++ b/src/backend/access/transam/xlogutils.c
@@ -519,7 +519,7 @@ XLogReadBufferExtended(RelFileLocator rlocator, ForkNumber forknum,
 	{
 		/* page exists in file */
 		buffer = ReadBufferWithoutRelcache(rlocator, forknum, blkno,
-										   mode, NULL, true);
+										   mode, true);
 	}
 	else
 	{
@@ -536,7 +536,6 @@ XLogReadBufferExtended(RelFileLocator rlocator, ForkNumber forknum,
 		Assert(InRecovery);
 		buffer = ExtendBufferedRelTo(BMR_SMGR(smgr, RELPERSISTENCE_PERMANENT),
 									 forknum,
-									 NULL,
 									 EB_PERFORMING_RECOVERY |
 									 EB_SKIP_EXTENSION_LOCK,
 									 blkno + 1,
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 81bba4beac7..b0a7b8a33cd 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -3431,7 +3431,6 @@ validate_index(Oid heapId, Oid indexId, Snapshot snapshot)
 	ivinfo.estimated_count = true;
 	ivinfo.message_level = DEBUG2;
 	ivinfo.num_heap_tuples = heapRelation->rd_rel->reltuples;
-	ivinfo.strategy = NULL;
 
 	/*
 	 * Encode TIDs as int8 values for the sort, rather than directly sorting
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index f66e80b757c..9b117a53124 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -72,7 +72,6 @@ int			default_statistics_target = 100;
 
 /* A few variables that don't seem worth passing around as parameters */
 static MemoryContext anl_context = NULL;
-static BufferAccessStrategy vac_strategy;
 
 
 static void do_analyze_rel(Relation onerel,
@@ -108,8 +107,7 @@ static Datum ind_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
  */
 void
 analyze_rel(Oid relid, RangeVar *relation,
-			const VacuumParams *params, List *va_cols, bool in_outer_xact,
-			BufferAccessStrategy bstrategy)
+			const VacuumParams *params, List *va_cols, bool in_outer_xact)
 {
 	Relation	onerel;
 	int			elevel;
@@ -124,7 +122,6 @@ analyze_rel(Oid relid, RangeVar *relation,
 		elevel = DEBUG2;
 
 	/* Set up static variables */
-	vac_strategy = bstrategy;
 
 	/*
 	 * Check for user-requested abort.
@@ -730,7 +727,6 @@ do_analyze_rel(Relation onerel, const VacuumParams *params,
 			ivinfo.estimated_count = true;
 			ivinfo.message_level = elevel;
 			ivinfo.num_heap_tuples = onerel->rd_rel->reltuples;
-			ivinfo.strategy = vac_strategy;
 
 			stats = index_vacuum_cleanup(&ivinfo, NULL);
 
@@ -1302,7 +1298,6 @@ acquire_sample_rows(Relation onerel, int elevel,
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_USE_BATCHING,
-										vac_strategy,
 										scan->rs_rd,
 										MAIN_FORKNUM,
 										block_sampling_read_stream_next,
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index f0819d15ab7..6f65d48b6bd 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -262,7 +262,6 @@ ScanSourceDatabasePgClass(Oid tbid, Oid dbid, char *srcpath)
 	LockRelId	relid;
 	Snapshot	snapshot;
 	SMgrRelation smgr;
-	BufferAccessStrategy bstrategy;
 
 	/* Get pg_class relfilenumber. */
 	relfilenumber = RelationMapOidToFilenumberForDatabase(srcpath,
@@ -282,9 +281,6 @@ ScanSourceDatabasePgClass(Oid tbid, Oid dbid, char *srcpath)
 	nblocks = smgrnblocks(smgr, MAIN_FORKNUM);
 	smgrclose(smgr);
 
-	/* Use a buffer access strategy since this is a bulk read operation. */
-	bstrategy = GetAccessStrategy(BAS_BULKREAD);
-
 	/*
 	 * As explained in the function header comments, we need a snapshot that
 	 * will see all committed transactions as committed, and our transaction
@@ -299,7 +295,7 @@ ScanSourceDatabasePgClass(Oid tbid, Oid dbid, char *srcpath)
 		CHECK_FOR_INTERRUPTS();
 
 		buf = ReadBufferWithoutRelcache(rlocator, MAIN_FORKNUM, blkno,
-										RBM_NORMAL, bstrategy, true);
+										RBM_NORMAL, true);
 
 		LockBuffer(buf, BUFFER_LOCK_SHARE);
 		page = BufferGetPage(buf);
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 02883fe34a4..d8c15a8b1bc 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -2447,7 +2447,7 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 			if (params->options & CLUOPT_VERBOSE)
 				vac_params.options |= VACOPT_VERBOSE;
 			analyze_rel(tableOid, NULL, &vac_params,
-						stmt->relation->va_cols, true, NULL);
+						stmt->relation->va_cols, true);
 			PopActiveSnapshot();
 			CommandCounterIncrement();
 		}
diff --git a/src/backend/commands/sequence.c b/src/backend/commands/sequence.c
index 551667650ba..ac1ab948211 100644
--- a/src/backend/commands/sequence.c
+++ b/src/backend/commands/sequence.c
@@ -358,7 +358,7 @@ fill_seq_fork_with_data(Relation rel, HeapTuple tuple, ForkNumber forkNum)
 
 	/* Initialize first page of relation with special magic number */
 
-	buf = ExtendBufferedRel(BMR_REL(rel), forkNum, NULL,
+	buf = ExtendBufferedRel(BMR_REL(rel), forkNum,
 							EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 	Assert(BufferGetBlockNumber(buf) == 0);
 
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 38539a6fd3d..66acd27bd48 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -127,32 +127,11 @@ static void vac_truncate_clog(TransactionId frozenXID,
 							  TransactionId lastSaneFrozenXid,
 							  MultiXactId lastSaneMinMulti);
 static bool vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
-					   BufferAccessStrategy bstrategy, bool isTopLevel);
+					   bool isTopLevel);
 static double compute_parallel_delay(void);
 static VacOptValue get_vacoptval_from_boolean(DefElem *def);
 static bool vac_tid_reaped(ItemPointer itemptr, void *state);
 
-/*
- * GUC check function to ensure GUC value specified is within the allowable
- * range.
- */
-bool
-check_vacuum_buffer_usage_limit(int *newval, void **extra,
-								GucSource source)
-{
-	/* Value upper and lower hard limits are inclusive */
-	if (*newval == 0 || (*newval >= MIN_BAS_VAC_RING_SIZE_KB &&
-						 *newval <= MAX_BAS_VAC_RING_SIZE_KB))
-		return true;
-
-	/* Value does not fall within any allowable range */
-	GUC_check_errdetail("\"%s\" must be 0 or between %d kB and %d kB.",
-						"vacuum_buffer_usage_limit",
-						MIN_BAS_VAC_RING_SIZE_KB, MAX_BAS_VAC_RING_SIZE_KB);
-
-	return false;
-}
-
 /*
  * Primary entry point for manual VACUUM and ANALYZE commands
  *
@@ -163,7 +142,6 @@ void
 ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 {
 	VacuumParams params;
-	BufferAccessStrategy bstrategy = NULL;
 	bool		verbose = false;
 	bool		skip_locked = false;
 	bool		analyze = false;
@@ -172,7 +150,6 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 	bool		disable_page_skipping = false;
 	bool		process_main = true;
 	bool		process_toast = true;
-	int			ring_size;
 	bool		skip_database_stats = false;
 	bool		only_database_stats = false;
 	MemoryContext vac_context;
@@ -188,12 +165,6 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 	/* Will be set later if we recurse to a TOAST table. */
 	params.toast_parent = InvalidOid;
 
-	/*
-	 * Set this to an invalid value so it is clear whether or not a
-	 * BUFFER_USAGE_LIMIT was specified when making the access strategy.
-	 */
-	ring_size = -1;
-
 	/* Parse options list */
 	foreach(lc, vacstmt->options)
 	{
@@ -204,32 +175,6 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 			verbose = defGetBoolean(opt);
 		else if (strcmp(opt->defname, "skip_locked") == 0)
 			skip_locked = defGetBoolean(opt);
-		else if (strcmp(opt->defname, "buffer_usage_limit") == 0)
-		{
-			const char *hintmsg;
-			int			result;
-			char	   *vac_buffer_size;
-
-			vac_buffer_size = defGetString(opt);
-
-			/*
-			 * Check that the specified value is valid and the size falls
-			 * within the hard upper and lower limits if it is not 0.
-			 */
-			if (!parse_int(vac_buffer_size, &result, GUC_UNIT_KB, &hintmsg) ||
-				(result != 0 &&
-				 (result < MIN_BAS_VAC_RING_SIZE_KB || result > MAX_BAS_VAC_RING_SIZE_KB)))
-			{
-				ereport(ERROR,
-						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-						 errmsg("%s option must be 0 or between %d kB and %d kB",
-								"BUFFER_USAGE_LIMIT",
-								MIN_BAS_VAC_RING_SIZE_KB, MAX_BAS_VAC_RING_SIZE_KB),
-						 hintmsg ? errhint_internal("%s", _(hintmsg)) : 0));
-			}
-
-			ring_size = result;
-		}
 		else if (!vacstmt->is_vacuumcmd)
 			ereport(ERROR,
 					(errcode(ERRCODE_SYNTAX_ERROR),
@@ -325,17 +270,6 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("VACUUM FULL cannot be performed in parallel")));
 
-	/*
-	 * BUFFER_USAGE_LIMIT does nothing for VACUUM (FULL) so just raise an
-	 * ERROR for that case.  VACUUM (FULL, ANALYZE) does make use of it, so
-	 * we'll permit that.
-	 */
-	if (ring_size != -1 && (params.options & VACOPT_FULL) &&
-		!(params.options & VACOPT_ANALYZE))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("BUFFER_USAGE_LIMIT cannot be specified for VACUUM FULL")));
-
 	/*
 	 * Make sure VACOPT_ANALYZE is specified if any column lists are present.
 	 */
@@ -431,38 +365,8 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 										"Vacuum",
 										ALLOCSET_DEFAULT_SIZES);
 
-	/*
-	 * Make a buffer strategy object in the cross-transaction memory context.
-	 * We needn't bother making this for VACUUM (FULL) or VACUUM
-	 * (ONLY_DATABASE_STATS) as they'll not make use of it.  VACUUM (FULL,
-	 * ANALYZE) is possible, so we'd better ensure that we make a strategy
-	 * when we see ANALYZE.
-	 */
-	if ((params.options & (VACOPT_ONLY_DATABASE_STATS |
-						   VACOPT_FULL)) == 0 ||
-		(params.options & VACOPT_ANALYZE) != 0)
-	{
-
-		MemoryContext old_context = MemoryContextSwitchTo(vac_context);
-
-		Assert(ring_size >= -1);
-
-		/*
-		 * If BUFFER_USAGE_LIMIT was specified by the VACUUM or ANALYZE
-		 * command, it overrides the value of VacuumBufferUsageLimit.  Either
-		 * value may be 0, in which case GetAccessStrategyWithSize() will
-		 * return NULL, effectively allowing full use of shared buffers.
-		 */
-		if (ring_size == -1)
-			ring_size = VacuumBufferUsageLimit;
-
-		bstrategy = GetAccessStrategyWithSize(BAS_VACUUM, ring_size);
-
-		MemoryContextSwitchTo(old_context);
-	}
-
 	/* Now go through the common routine */
-	vacuum(vacstmt->rels, &params, bstrategy, vac_context, isTopLevel);
+	vacuum(vacstmt->rels, &params, vac_context, isTopLevel);
 
 	/* Finally, clean up the vacuum memory context */
 	MemoryContextDelete(vac_context);
@@ -479,19 +383,13 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
  * params contains a set of parameters that can be used to customize the
  * behavior.
  *
- * bstrategy may be passed in as NULL when the caller does not want to
- * restrict the number of shared_buffers that VACUUM / ANALYZE can use,
- * otherwise, the caller must build a BufferAccessStrategy with the number of
- * shared_buffers that VACUUM / ANALYZE should try to limit themselves to
- * using.
- *
  * isTopLevel should be passed down from ProcessUtility.
  *
  * It is the caller's responsibility that all parameters are allocated in a
  * memory context that will not disappear at transaction commit.
  */
 void
-vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrategy,
+vacuum(List *relations, const VacuumParams *params,
 	   MemoryContext vac_context, bool isTopLevel)
 {
 	static bool in_vacuum = false;
@@ -630,7 +528,7 @@ vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrate
 
 			if (params->options & VACOPT_VACUUM)
 			{
-				if (!vacuum_rel(vrel->oid, vrel->relation, *params, bstrategy,
+				if (!vacuum_rel(vrel->oid, vrel->relation, *params,
 								isTopLevel))
 					continue;
 			}
@@ -649,7 +547,7 @@ vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrate
 				}
 
 				analyze_rel(vrel->oid, vrel->relation, params,
-							vrel->va_cols, in_outer_xact, bstrategy);
+							vrel->va_cols, in_outer_xact);
 
 				if (use_own_xacts)
 				{
@@ -2010,7 +1908,7 @@ vac_truncate_clog(TransactionId frozenXID,
  */
 static bool
 vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
-		   BufferAccessStrategy bstrategy, bool isTopLevel)
+		   bool isTopLevel)
 {
 	LOCKMODE	lmode;
 	Relation	rel;
@@ -2307,7 +2205,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 			rel = NULL;
 		}
 		else
-			table_relation_vacuum(rel, &params, bstrategy);
+			table_relation_vacuum(rel, &params);
 	}
 
 	/* Roll back any GUC changes executed by index functions */
@@ -2344,7 +2242,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 		toast_vacuum_params.options |= VACOPT_PROCESS_MAIN;
 		toast_vacuum_params.toast_parent = relid;
 
-		vacuum_rel(toast_relid, NULL, toast_vacuum_params, bstrategy,
+		vacuum_rel(toast_relid, NULL, toast_vacuum_params,
 				   isTopLevel);
 	}
 
diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c
index 41cefcfde54..8b4b835db9b 100644
--- a/src/backend/commands/vacuumparallel.c
+++ b/src/backend/commands/vacuumparallel.c
@@ -125,12 +125,6 @@ typedef struct PVShared
 	 */
 	int			maintenance_work_mem_worker;
 
-	/*
-	 * The number of buffers each worker's Buffer Access Strategy ring should
-	 * contain.
-	 */
-	int			ring_nbuffers;
-
 	/*
 	 * Shared vacuum cost balance.  During parallel vacuum,
 	 * VacuumSharedCostBalance points to this value and it accumulates the
@@ -257,9 +251,6 @@ struct ParallelVacuumState
 	int			nindexes_parallel_cleanup;
 	int			nindexes_parallel_condcleanup;
 
-	/* Buffer access strategy used by leader process */
-	BufferAccessStrategy bstrategy;
-
 	/*
 	 * Error reporting state.  The error callback is set only for workers
 	 * processes during parallel index vacuum.
@@ -304,7 +295,7 @@ static void parallel_vacuum_dsm_detach(dsm_segment *seg, Datum arg);
 ParallelVacuumState *
 parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes,
 					 int nrequested_workers, int vac_work_mem,
-					 int elevel, BufferAccessStrategy bstrategy)
+					 int elevel)
 {
 	ParallelVacuumState *pvs;
 	ParallelContext *pcxt;
@@ -345,7 +336,6 @@ parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes,
 	pvs->indrels = indrels;
 	pvs->nindexes = nindexes;
 	pvs->will_parallel_vacuum = will_parallel_vacuum;
-	pvs->bstrategy = bstrategy;
 	pvs->heaprel = rel;
 
 	EnterParallelMode();
@@ -447,9 +437,6 @@ parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes,
 	shared->dead_items_handle = TidStoreGetHandle(dead_items);
 	shared->dead_items_dsa_handle = dsa_get_handle(TidStoreGetDSA(dead_items));
 
-	/* Use the same buffer size for all workers */
-	shared->ring_nbuffers = GetAccessStrategyBufferCount(bstrategy);
-
 	pg_atomic_init_u32(&(shared->cost_balance), 0);
 	pg_atomic_init_u32(&(shared->active_nworkers), 0);
 	pg_atomic_init_u32(&(shared->idx), 0);
@@ -1091,7 +1078,6 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
 	ivinfo.message_level = DEBUG2;
 	ivinfo.estimated_count = pvs->shared->estimated_count;
 	ivinfo.num_heap_tuples = pvs->shared->reltuples;
-	ivinfo.strategy = pvs->bstrategy;
 
 	/* Update error traceback information */
 	pvs->indname = pstrdup(RelationGetRelationName(indrel));
@@ -1294,10 +1280,6 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
 	pvs.indname = NULL;
 	pvs.status = PARALLEL_INDVAC_STATUS_INITIAL;
 
-	/* Each parallel VACUUM worker gets its own access strategy. */
-	pvs.bstrategy = GetAccessStrategyWithSize(BAS_VACUUM,
-											  shared->ring_nbuffers * (BLCKSZ / 1024));
-
 	/* Setup error traceback support for ereport() */
 	errcallback.callback = parallel_vacuum_error_callback;
 	errcallback.arg = &pvs;
@@ -1328,7 +1310,6 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
 
 	vac_close_indexes(nindexes, indrels, RowExclusiveLock);
 	table_close(rel, ShareUpdateExclusiveLock);
-	FreeAccessStrategy(pvs.bstrategy);
 
 	if (shared->is_autovacuum)
 		pv_shared_cost_params = NULL;
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 45abf48768a..e021e798805 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -386,8 +386,7 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
 									  bool *dovacuum, bool *doanalyze, bool *wraparound,
 									  AutoVacuumScores *scores);
 
-static void autovacuum_do_vac_analyze(autovac_table *tab,
-									  BufferAccessStrategy bstrategy);
+static void autovacuum_do_vac_analyze(autovac_table *tab);
 static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
 										 TupleDesc pg_class_desc);
 static void perform_work_item(AutoVacuumWorkItem *workitem);
@@ -1936,7 +1935,6 @@ do_autovacuum(void)
 	HASHCTL		ctl;
 	HTAB	   *table_toast_map;
 	ListCell   *volatile cell;
-	BufferAccessStrategy bstrategy;
 	ScanKeyData key;
 	TupleDesc	pg_class_desc;
 	int			effective_multixact_freeze_max_age;
@@ -2322,23 +2320,6 @@ do_autovacuum(void)
 		autovacuum_analyze_score_weight != 0.0)
 		list_sort(tables_to_process, TableToProcessComparator);
 
-	/*
-	 * Optionally, create a buffer access strategy object for VACUUM to use.
-	 * We use the same BufferAccessStrategy object for all tables VACUUMed by
-	 * this worker to prevent autovacuum from blowing out shared buffers.
-	 *
-	 * VacuumBufferUsageLimit being set to 0 results in
-	 * GetAccessStrategyWithSize returning NULL, effectively meaning we can
-	 * use up to all of shared buffers.
-	 *
-	 * If we later enter failsafe mode on any of the tables being vacuumed, we
-	 * will cease use of the BufferAccessStrategy only for that table.
-	 *
-	 * XXX should we consider adding code to adjust the size of this if
-	 * VacuumBufferUsageLimit changes?
-	 */
-	bstrategy = GetAccessStrategyWithSize(BAS_VACUUM, VacuumBufferUsageLimit);
-
 	/*
 	 * create a memory context to act as fake PortalContext, so that the
 	 * contexts created in the vacuum code are cleaned up for each table.
@@ -2516,7 +2497,7 @@ do_autovacuum(void)
 			MemoryContextSwitchTo(PortalContext);
 
 			/* have at it */
-			autovacuum_do_vac_analyze(tab, bstrategy);
+			autovacuum_do_vac_analyze(tab);
 
 			/*
 			 * Clear a possible query-cancel signal, to avoid a late reaction
@@ -2636,8 +2617,6 @@ deleted:
 #ifdef USE_VALGRIND
 	hash_destroy(table_toast_map);
 	FreeTupleDesc(pg_class_desc);
-	if (bstrategy)
-		pfree(bstrategy);
 #endif
 
 	/* Run the rest in xact context, mainly to avoid Valgrind leak warnings */
@@ -3348,7 +3327,7 @@ relation_needs_vacanalyze(Oid relid,
  * disappear at transaction commit.
  */
 static void
-autovacuum_do_vac_analyze(autovac_table *tab, BufferAccessStrategy bstrategy)
+autovacuum_do_vac_analyze(autovac_table *tab)
 {
 	RangeVar   *rangevar;
 	VacuumRelation *rel;
@@ -3371,7 +3350,7 @@ autovacuum_do_vac_analyze(autovac_table *tab, BufferAccessStrategy bstrategy)
 	rel_list = list_make1(rel);
 	MemoryContextSwitchTo(old_context);
 
-	vacuum(rel_list, &tab->at_params, bstrategy, vac_context, true);
+	vacuum(rel_list, &tab->at_params, vac_context, true);
 
 	MemoryContextDelete(vac_context);
 }
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 73dc539836b..b6612f1e517 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -390,7 +390,7 @@ static List *BuildRelationList(bool temp_relations, bool include_shared);
 static void FreeDatabaseList(List *dblist);
 static DataChecksumsWorkerResult ProcessDatabase(DataChecksumsWorkerDatabase *db);
 static bool ProcessAllDatabases(void);
-static bool ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy);
+static bool ProcessSingleRelationFork(Relation reln, ForkNumber forkNum);
 static void ResetDataChecksumsProgressCounters(void);
 static void launcher_cancel_handler(SIGNAL_ARGS);
 static void WaitForAllTransactionsToFinish(void);
@@ -687,7 +687,7 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
  * error is raised in the lower levels.
  */
 static bool
-ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy)
+ProcessSingleRelationFork(Relation reln, ForkNumber forkNum)
 {
 	BlockNumber numblocks = RelationGetNumberOfBlocksInFork(reln, forkNum);
 	char		activity[NAMEDATALEN * 2 + 128];
@@ -722,7 +722,7 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg
 	 */
 	for (BlockNumber blknum = 0; blknum < numblocks; blknum++)
 	{
-		Buffer		buf = ReadBufferExtended(reln, forkNum, blknum, RBM_NORMAL, strategy);
+		Buffer		buf = ReadBufferExtended(reln, forkNum, blknum, RBM_NORMAL);
 
 		/* Need to get an exclusive lock to mark the buffer as dirty */
 		LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
@@ -808,7 +808,7 @@ ResetDataChecksumsProgressCounters(void)
  * error is raised in the lower levels.
  */
 static bool
-ProcessSingleRelationByOid(Oid relationId, BufferAccessStrategy strategy)
+ProcessSingleRelationByOid(Oid relationId)
 {
 	Relation	rel;
 	bool		aborted = false;
@@ -835,7 +835,7 @@ ProcessSingleRelationByOid(Oid relationId, BufferAccessStrategy strategy)
 	{
 		if (smgrexists(rel->rd_smgr, fnum))
 		{
-			if (!ProcessSingleRelationFork(rel, fnum, strategy))
+			if (!ProcessSingleRelationFork(rel, fnum))
 			{
 				aborted = true;
 				break;
@@ -1582,7 +1582,6 @@ DataChecksumsWorkerMain(Datum arg)
 	Oid			dboid;
 	List	   *RelationList = NIL;
 	List	   *InitialTempTableList = NIL;
-	BufferAccessStrategy strategy;
 	bool		aborted = false;
 	int64		rels_done;
 	bool		process_shared;
@@ -1650,11 +1649,6 @@ DataChecksumsWorkerMain(Datum arg)
 	VacuumUpdateCosts();
 	VacuumCostBalance = 0;
 
-	/*
-	 * Create and set the vacuum strategy as our buffer strategy.
-	 */
-	strategy = GetAccessStrategy(BAS_VACUUM);
-
 	RelationList = BuildRelationList(false, process_shared);
 
 	/* Update the total number of relations to be processed in this DB. */
@@ -1676,9 +1670,7 @@ DataChecksumsWorkerMain(Datum arg)
 	rels_done = 0;
 	foreach_oid(reloid, RelationList)
 	{
-		bool		costs_updated = false;
-
-		if (!ProcessSingleRelationByOid(reloid, strategy))
+		if (!ProcessSingleRelationByOid(reloid))
 		{
 			aborted = true;
 			break;
@@ -1694,8 +1686,7 @@ DataChecksumsWorkerMain(Datum arg)
 
 		/*
 		 * Check if the cost settings changed during runtime and if so, update
-		 * to reflect the new values and signal that the access strategy needs
-		 * to be refreshed.
+		 * to reflect the new values.
 		 */
 		LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
 		if (DataChecksumState->worker_invocation != worker_invocation)
@@ -1706,7 +1697,6 @@ DataChecksumsWorkerMain(Datum arg)
 		if ((DataChecksumState->launch_cost_delay != DataChecksumState->cost_delay)
 			|| (DataChecksumState->launch_cost_limit != DataChecksumState->cost_limit))
 		{
-			costs_updated = true;
 			VacuumCostDelay = DataChecksumState->launch_cost_delay;
 			VacuumCostLimit = DataChecksumState->launch_cost_limit;
 			VacuumUpdateCosts();
@@ -1714,19 +1704,10 @@ DataChecksumsWorkerMain(Datum arg)
 			DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
 			DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
 		}
-		else
-			costs_updated = false;
 		LWLockRelease(DataChecksumsWorkerLock);
-
-		if (costs_updated)
-		{
-			FreeAccessStrategy(strategy);
-			strategy = GetAccessStrategy(BAS_VACUUM);
-		}
 	}
 
 	list_free(RelationList);
-	FreeAccessStrategy(strategy);
 
 	if (aborted || abort_requested)
 	{
diff --git a/src/backend/storage/aio/read_stream.c b/src/backend/storage/aio/read_stream.c
index a318539e56c..08d344f063d 100644
--- a/src/backend/storage/aio/read_stream.c
+++ b/src/backend/storage/aio/read_stream.c
@@ -757,7 +757,6 @@ read_stream_look_ahead(ReadStream *stream)
  */
 static ReadStream *
 read_stream_begin_impl(int flags,
-					   BufferAccessStrategy strategy,
 					   Relation rel,
 					   SMgrRelation smgr,
 					   char persistence,
@@ -771,7 +770,6 @@ read_stream_begin_impl(int flags,
 	int16		queue_size;
 	int16		queue_overflow;
 	int			max_ios;
-	int			strategy_pin_limit;
 	uint32		max_pinned_buffers;
 	uint32		max_possible_buffer_limit;
 	Oid			tablespace_id;
@@ -835,10 +833,6 @@ read_stream_begin_impl(int flags,
 	max_pinned_buffers = Min(max_pinned_buffers,
 							 PG_INT16_MAX - queue_overflow - 1);
 
-	/* Give the strategy a chance to limit the number of buffers we pin. */
-	strategy_pin_limit = GetAccessStrategyPinLimit(strategy);
-	max_pinned_buffers = Min(strategy_pin_limit, max_pinned_buffers);
-
 	/*
 	 * Also limit our queue to the maximum number of pins we could ever be
 	 * allowed to acquire according to the buffer manager.  We may not really
@@ -962,7 +956,6 @@ read_stream_begin_impl(int flags,
 		stream->ios[i].op.smgr = smgr;
 		stream->ios[i].op.persistence = persistence;
 		stream->ios[i].op.forknum = forknum;
-		stream->ios[i].op.strategy = strategy;
 	}
 
 	return stream;
@@ -974,7 +967,6 @@ read_stream_begin_impl(int flags,
  */
 ReadStream *
 read_stream_begin_relation(int flags,
-						   BufferAccessStrategy strategy,
 						   Relation rel,
 						   ForkNumber forknum,
 						   ReadStreamBlockNumberCB callback,
@@ -982,7 +974,6 @@ read_stream_begin_relation(int flags,
 						   size_t per_buffer_data_size)
 {
 	return read_stream_begin_impl(flags,
-								  strategy,
 								  rel,
 								  RelationGetSmgr(rel),
 								  rel->rd_rel->relpersistence,
@@ -998,7 +989,6 @@ read_stream_begin_relation(int flags,
  */
 ReadStream *
 read_stream_begin_smgr_relation(int flags,
-								BufferAccessStrategy strategy,
 								SMgrRelation smgr,
 								char smgr_persistence,
 								ForkNumber forknum,
@@ -1007,7 +997,6 @@ read_stream_begin_smgr_relation(int flags,
 								size_t per_buffer_data_size)
 {
 	return read_stream_begin_impl(flags,
-								  strategy,
 								  NULL,
 								  smgr,
 								  smgr_persistence,
@@ -1370,13 +1359,11 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data)
  * Transitional support for code that would like to perform or skip reads
  * itself, without using the stream.  Returns, and consumes, the next block
  * number that would be read by the stream's look-ahead algorithm, or
- * InvalidBlockNumber if the end of the stream is reached.  Also reports the
- * strategy that would be used to read it.
+ * InvalidBlockNumber if the end of the stream is reached.
  */
 BlockNumber
-read_stream_next_block(ReadStream *stream, BufferAccessStrategy *strategy)
+read_stream_next_block(ReadStream *stream)
 {
-	*strategy = stream->ios[0].op.strategy;
 	return read_stream_get_block(stream, NULL);
 }
 
diff --git a/src/backend/storage/buffer/README b/src/backend/storage/buffer/README
index b332e002ba1..58df6bd011a 100644
--- a/src/backend/storage/buffer/README
+++ b/src/backend/storage/buffer/README
@@ -203,50 +203,6 @@ have to give up and try another buffer.  This however is not a concern
 of the basic select-a-victim-buffer algorithm.)
 
 
-Buffer Ring Replacement Strategy
----------------------------------
-
-When running a query that needs to access a large number of pages just once,
-such as VACUUM or a large sequential scan, a different strategy is used.
-A page that has been touched only by such a scan is unlikely to be needed
-again soon, so instead of running the normal clock-sweep algorithm and
-blowing out the entire buffer cache, a small ring of buffers is allocated
-using the normal clock-sweep algorithm and those buffers are reused for the
-whole scan.  This also implies that much of the write traffic caused by such
-a statement will be done by the backend itself and not pushed off onto other
-processes.
-
-For sequential scans, a 256KB ring is used. That's small enough to fit in L2
-cache, which makes transferring pages from OS cache to shared buffer cache
-efficient.  Even less would often be enough, but the ring must be big enough
-to accommodate all pages in the scan that are pinned concurrently.  256KB
-should also be enough to leave a small cache trail for other backends to
-join in a synchronized seq scan.  If a ring buffer is dirtied and its LSN
-updated, we would normally have to write and flush WAL before we could
-re-use the buffer; in this case we instead discard the buffer from the ring
-and (later) choose a replacement using the normal clock-sweep algorithm.
-Hence this strategy works best for scans that are read-only (or at worst
-update hint bits).  In a scan that modifies every page in the scan, like a
-bulk UPDATE or DELETE, the buffers in the ring will always be dirtied and
-the ring strategy effectively degrades to the normal strategy.
-
-VACUUM uses a ring like sequential scans, however, the size of this ring is
-controlled by the vacuum_buffer_usage_limit GUC.  Dirty pages are not removed
-from the ring.  Instead, the WAL is flushed if needed to allow reuse of the
-buffers.  Before introducing the buffer ring strategy in 8.3, VACUUM's buffers
-were sent to the freelist, which was effectively a buffer ring of 1 buffer,
-resulting in excessive WAL flushing.
-
-Bulk writes work similarly to VACUUM.  Currently this applies only to
-COPY IN and CREATE TABLE AS SELECT.  (Might it be interesting to make
-seqscan UPDATE and DELETE use the bulkwrite strategy?)  For bulk writes
-we use a ring size of 16MB (but not more than 1/8th of shared_buffers).
-Smaller sizes have been shown to result in the COPY blocking too often
-for WAL flushes.  While it's okay for a background vacuum to be slowed by
-doing its own WAL flushing, we'd prefer that COPY not be subject to that,
-so we let it use up a bit more of the buffer arena.
-
-
 Background Writer's Processing
 ------------------------------
 
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 1f5a00849e6..cc2fab959df 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -612,10 +612,9 @@ ForgetPrivateRefCountEntry(PrivateRefCountEntry *ref)
 static Buffer ReadBuffer_common(Relation rel,
 								SMgrRelation smgr, char smgr_persistence,
 								ForkNumber forkNum, BlockNumber blockNum,
-								ReadBufferMode mode, BufferAccessStrategy strategy);
+								ReadBufferMode mode);
 static BlockNumber ExtendBufferedRelCommon(BufferManagerRelation bmr,
 										   ForkNumber fork,
-										   BufferAccessStrategy strategy,
 										   uint32 flags,
 										   uint32 extend_by,
 										   BlockNumber extend_upto,
@@ -623,13 +622,12 @@ static BlockNumber ExtendBufferedRelCommon(BufferManagerRelation bmr,
 										   uint32 *extended_by);
 static BlockNumber ExtendBufferedRelShared(BufferManagerRelation bmr,
 										   ForkNumber fork,
-										   BufferAccessStrategy strategy,
 										   uint32 flags,
 										   uint32 extend_by,
 										   BlockNumber extend_upto,
 										   Buffer *buffers,
 										   uint32 *extended_by);
-static bool PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy,
+static bool PinBuffer(BufferDesc *buf,
 					  bool skip_if_not_valid);
 static void PinBuffer_Locked(BufferDesc *buf);
 static void UnpinBuffer(BufferDesc *buf);
@@ -645,7 +643,6 @@ static inline BufferDesc *BufferAlloc(SMgrRelation smgr,
 									  char relpersistence,
 									  ForkNumber forkNum,
 									  BlockNumber blockNum,
-									  BufferAccessStrategy strategy,
 									  bool *foundPtr, IOContext io_context);
 static bool AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress);
 static void CheckReadBuffersOperation(ReadBuffersOperation *operation, bool is_complete);
@@ -654,7 +651,7 @@ static pg_always_inline void TrackBufferHit(IOObject io_object,
 											IOContext io_context,
 											Relation rel, char persistence, SMgrRelation smgr,
 											ForkNumber forknum, BlockNumber blocknum);
-static Buffer GetVictimBuffer(BufferAccessStrategy strategy, IOContext io_context);
+static Buffer GetVictimBuffer(IOContext io_context);
 static void FlushUnlockedBuffer(BufferDesc *buf, SMgrRelation reln,
 								IOObject io_object, IOContext io_context);
 static void FlushBuffer(BufferDesc *buf, SMgrRelation reln,
@@ -858,7 +855,7 @@ ReadRecentBuffer(RelFileLocator rlocator, ForkNumber forkNum, BlockNumber blockN
 		 * pin.
 		 */
 		if (BufferTagsEqual(&tag, &bufHdr->tag) &&
-			PinBuffer(bufHdr, NULL, true))
+			PinBuffer(bufHdr, true))
 		{
 			if (BufferTagsEqual(&tag, &bufHdr->tag))
 			{
@@ -874,12 +871,12 @@ ReadRecentBuffer(RelFileLocator rlocator, ForkNumber forkNum, BlockNumber blockN
 
 /*
  * ReadBuffer -- a shorthand for ReadBufferExtended, for reading from main
- *		fork with RBM_NORMAL mode and default strategy.
+ *		fork with RBM_NORMAL mode.
  */
 Buffer
 ReadBuffer(Relation reln, BlockNumber blockNum)
 {
-	return ReadBufferExtended(reln, MAIN_FORKNUM, blockNum, RBM_NORMAL, NULL);
+	return ReadBufferExtended(reln, MAIN_FORKNUM, blockNum, RBM_NORMAL);
 }
 
 /*
@@ -919,13 +916,10 @@ ReadBuffer(Relation reln, BlockNumber blockNum)
  * a cleanup-strength lock on the page.
  *
  * RBM_NORMAL_NO_LOG mode is treated the same as RBM_NORMAL here.
- *
- * If strategy is not NULL, a nondefault buffer access strategy is used.
- * See buffer/README for details.
  */
 inline Buffer
 ReadBufferExtended(Relation reln, ForkNumber forkNum, BlockNumber blockNum,
-				   ReadBufferMode mode, BufferAccessStrategy strategy)
+				   ReadBufferMode mode)
 {
 	Buffer		buf;
 
@@ -935,7 +929,7 @@ ReadBufferExtended(Relation reln, ForkNumber forkNum, BlockNumber blockNum,
 	 * ReadBuffer_common().
 	 */
 	buf = ReadBuffer_common(reln, RelationGetSmgr(reln), 0,
-							forkNum, blockNum, mode, strategy);
+							forkNum, blockNum, mode);
 
 	return buf;
 }
@@ -954,14 +948,14 @@ ReadBufferExtended(Relation reln, ForkNumber forkNum, BlockNumber blockNum,
 Buffer
 ReadBufferWithoutRelcache(RelFileLocator rlocator, ForkNumber forkNum,
 						  BlockNumber blockNum, ReadBufferMode mode,
-						  BufferAccessStrategy strategy, bool permanent)
+						  bool permanent)
 {
 	SMgrRelation smgr = smgropen(rlocator, INVALID_PROC_NUMBER);
 
 	return ReadBuffer_common(NULL, smgr,
 							 permanent ? RELPERSISTENCE_PERMANENT : RELPERSISTENCE_UNLOGGED,
 							 forkNum, blockNum,
-							 mode, strategy);
+							 mode);
 }
 
 /*
@@ -970,13 +964,12 @@ ReadBufferWithoutRelcache(RelFileLocator rlocator, ForkNumber forkNum,
 Buffer
 ExtendBufferedRel(BufferManagerRelation bmr,
 				  ForkNumber forkNum,
-				  BufferAccessStrategy strategy,
 				  uint32 flags)
 {
 	Buffer		buf;
 	uint32		extend_by = 1;
 
-	ExtendBufferedRelBy(bmr, forkNum, strategy, flags, extend_by,
+	ExtendBufferedRelBy(bmr, forkNum, flags, extend_by,
 						&buf, &extend_by);
 
 	return buf;
@@ -1002,7 +995,6 @@ ExtendBufferedRel(BufferManagerRelation bmr,
 BlockNumber
 ExtendBufferedRelBy(BufferManagerRelation bmr,
 					ForkNumber fork,
-					BufferAccessStrategy strategy,
 					uint32 flags,
 					uint32 extend_by,
 					Buffer *buffers,
@@ -1015,7 +1007,7 @@ ExtendBufferedRelBy(BufferManagerRelation bmr,
 	if (bmr.relpersistence == '\0')
 		bmr.relpersistence = bmr.rel->rd_rel->relpersistence;
 
-	return ExtendBufferedRelCommon(bmr, fork, strategy, flags,
+	return ExtendBufferedRelCommon(bmr, fork, flags,
 								   extend_by, InvalidBlockNumber,
 								   buffers, extended_by);
 }
@@ -1031,7 +1023,6 @@ ExtendBufferedRelBy(BufferManagerRelation bmr,
 Buffer
 ExtendBufferedRelTo(BufferManagerRelation bmr,
 					ForkNumber fork,
-					BufferAccessStrategy strategy,
 					uint32 flags,
 					BlockNumber extend_to,
 					ReadBufferMode mode)
@@ -1097,7 +1088,7 @@ ExtendBufferedRelTo(BufferManagerRelation bmr,
 		if ((uint64) current_size + num_pages > extend_to)
 			num_pages = extend_to - current_size;
 
-		first_block = ExtendBufferedRelCommon(bmr, fork, strategy, flags,
+		first_block = ExtendBufferedRelCommon(bmr, fork, flags,
 											  num_pages, extend_to,
 											  buffers, &extended_by);
 
@@ -1123,7 +1114,7 @@ ExtendBufferedRelTo(BufferManagerRelation bmr,
 	{
 		Assert(extended_by == 0);
 		buffer = ReadBuffer_common(bmr.rel, BMR_GET_SMGR(bmr), bmr.relpersistence,
-								   fork, extend_to - 1, mode, strategy);
+								   fork, extend_to - 1, mode);
 	}
 
 	return buffer;
@@ -1226,7 +1217,6 @@ PinBufferForBlock(Relation rel,
 				  char persistence,
 				  ForkNumber forkNum,
 				  BlockNumber blockNum,
-				  BufferAccessStrategy strategy,
 				  IOObject io_object,
 				  IOContext io_context,
 				  bool *foundPtr)
@@ -1250,7 +1240,7 @@ PinBufferForBlock(Relation rel,
 		bufHdr = LocalBufferAlloc(smgr, forkNum, blockNum, foundPtr);
 	else
 		bufHdr = BufferAlloc(smgr, persistence, forkNum, blockNum,
-							 strategy, foundPtr, io_context);
+							 foundPtr, io_context);
 
 	if (*foundPtr)
 		TrackBufferHit(io_object, io_context, rel, persistence, smgr, forkNum, blockNum);
@@ -1276,8 +1266,7 @@ PinBufferForBlock(Relation rel,
 static pg_always_inline Buffer
 ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
 				  ForkNumber forkNum,
-				  BlockNumber blockNum, ReadBufferMode mode,
-				  BufferAccessStrategy strategy)
+				  BlockNumber blockNum, ReadBufferMode mode)
 {
 	ReadBuffersOperation operation;
 	Buffer		buffer;
@@ -1313,7 +1302,7 @@ ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
 		if (mode == RBM_ZERO_AND_LOCK || mode == RBM_ZERO_AND_CLEANUP_LOCK)
 			flags |= EB_LOCK_FIRST;
 
-		return ExtendBufferedRel(BMR_REL(rel), forkNum, strategy, flags);
+		return ExtendBufferedRel(BMR_REL(rel), forkNum, flags);
 	}
 
 	if (rel)
@@ -1335,12 +1324,12 @@ ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
 		}
 		else
 		{
-			io_context = IOContextForStrategy(strategy);
+			io_context = IOCONTEXT_NORMAL;
 			io_object = IOOBJECT_RELATION;
 		}
 
 		buffer = PinBufferForBlock(rel, smgr, persistence,
-								   forkNum, blockNum, strategy,
+								   forkNum, blockNum,
 								   io_object, io_context, &found);
 		ZeroAndLockBuffer(buffer, mode, found);
 		return buffer;
@@ -1358,7 +1347,6 @@ ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
 	operation.rel = rel;
 	operation.persistence = persistence;
 	operation.forknum = forkNum;
-	operation.strategy = strategy;
 	if (StartReadBuffer(&operation,
 						&buffer,
 						blockNum,
@@ -1399,7 +1387,7 @@ StartReadBuffersImpl(ReadBuffersOperation *operation,
 	}
 	else
 	{
-		io_context = IOContextForStrategy(operation->strategy);
+		io_context = IOCONTEXT_NORMAL;
 		io_object = IOOBJECT_RELATION;
 	}
 
@@ -1450,7 +1438,6 @@ StartReadBuffersImpl(ReadBuffersOperation *operation,
 										   operation->persistence,
 										   operation->forknum,
 										   blockNum + i,
-										   operation->strategy,
 										   io_object, io_context,
 										   &found);
 		}
@@ -1771,7 +1758,7 @@ WaitReadBuffers(ReadBuffersOperation *operation)
 	}
 	else
 	{
-		io_context = IOContextForStrategy(operation->strategy);
+		io_context = IOCONTEXT_NORMAL;
 		io_object = IOOBJECT_RELATION;
 	}
 
@@ -1961,7 +1948,7 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress)
 	}
 	else
 	{
-		io_context = IOContextForStrategy(operation->strategy);
+		io_context = IOCONTEXT_NORMAL;
 		io_object = IOOBJECT_RELATION;
 	}
 
@@ -2180,24 +2167,18 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress)
  *		buffer.  If no buffer exists already, selects a replacement victim and
  *		evicts the old page, but does NOT read in new page.
  *
- * "strategy" can be a buffer replacement strategy object, or NULL for
- * the default strategy.  The selected buffer's usage_count is advanced when
- * using the default strategy, but otherwise possibly not (see PinBuffer).
- *
  * The returned buffer is pinned and is already marked as holding the
  * desired page.  If it already did have the desired page, *foundPtr is
  * set true.  Otherwise, *foundPtr is set false.
  *
- * io_context is passed as an output parameter to avoid calling
- * IOContextForStrategy() when there is a shared buffers hit and no IO
- * statistics need be captured.
+ * io_context is passed as an output parameter to avoid capturing IO
+ * statistics when there is a shared buffers hit and no IO occurs.
  *
  * No locks are held either at entry or exit.
  */
 static pg_always_inline BufferDesc *
 BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 			BlockNumber blockNum,
-			BufferAccessStrategy strategy,
 			bool *foundPtr, IOContext io_context)
 {
 	BufferTag	newTag;			/* identity of requested block */
@@ -2235,7 +2216,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 		 */
 		buf = GetBufferDescriptor(existing_buf_id);
 
-		valid = PinBuffer(buf, strategy, false);
+		valid = PinBuffer(buf, false);
 
 		/* Can release the mapping lock as soon as we've pinned it */
 		LWLockRelease(newPartitionLock);
@@ -2266,7 +2247,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	 * don't hold any conflicting locks. If so we'll have to undo our work
 	 * later.
 	 */
-	victim_buffer = GetVictimBuffer(strategy, io_context);
+	victim_buffer = GetVictimBuffer(io_context);
 	victim_buf_hdr = GetBufferDescriptor(victim_buffer - 1);
 
 	/*
@@ -2297,7 +2278,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 
 		existing_buf_hdr = GetBufferDescriptor(existing_buf_id);
 
-		valid = PinBuffer(existing_buf_hdr, strategy, false);
+		valid = PinBuffer(existing_buf_hdr, false);
 
 		/* Can release the mapping lock as soon as we've pinned it */
 		LWLockRelease(newPartitionLock);
@@ -2335,9 +2316,12 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	 * just like permanent relations.
 	 */
 	set_bits |= BM_TAG_VALID;
-	/* Admit the newly loaded page COOL (probation); a second access via
+
+	/*
+	 * Admit the newly loaded page COOL (probation); a second access via
 	 * PinBuffer promotes it to HOT.  This is what makes a one-touch scan
-	 * self-evicting -- see the cooling-state notes in buf_internals.h. */
+	 * self-evicting -- see the cooling-state notes in buf_internals.h.
+	 */
 	if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM)
 		set_bits |= BM_PERMANENT;
 
@@ -2549,12 +2533,11 @@ InvalidateVictimBuffer(BufferDesc *buf_hdr)
 }
 
 static Buffer
-GetVictimBuffer(BufferAccessStrategy strategy, IOContext io_context)
+GetVictimBuffer(IOContext io_context)
 {
 	BufferDesc *buf_hdr;
 	Buffer		buf;
 	uint64		buf_state;
-	bool		from_ring;
 
 	/*
 	 * Ensure, before we pin a victim buffer, that there's a free refcount
@@ -2570,7 +2553,7 @@ again:
 	 * Select a victim buffer.  The buffer is returned pinned and owned by
 	 * this backend.
 	 */
-	buf_hdr = StrategyGetBuffer(strategy, &buf_state, &from_ring);
+	buf_hdr = StrategyGetBuffer(&buf_state);
 	buf = BufferDescriptorGetBuffer(buf_hdr);
 
 	/*
@@ -2614,26 +2597,6 @@ again:
 			goto again;
 		}
 
-		/*
-		 * If using a nondefault strategy, and this victim came from the
-		 * strategy ring, let the strategy decide whether to reject it when
-		 * reusing it would require a WAL flush.  This only applies to
-		 * permanent buffers; unlogged buffers can have fake LSNs, so
-		 * XLogNeedsFlush() is not meaningful for them.
-		 *
-		 * We need to hold the content lock in at least share-exclusive mode
-		 * to safely inspect the page LSN, so this couldn't have been done
-		 * inside StrategyGetBuffer().
-		 */
-		if (strategy && from_ring &&
-			buf_state & BM_PERMANENT &&
-			XLogNeedsFlush(BufferGetLSN(buf_hdr)) &&
-			StrategyRejectBuffer(strategy, buf_hdr, from_ring))
-		{
-			UnlockReleaseBuffer(buf);
-			goto again;
-		}
-
 		/* OK, do the I/O */
 		FlushBuffer(buf_hdr, NULL, IOOBJECT_RELATION, io_context);
 		LockBuffer(buf, BUFFER_LOCK_UNLOCK);
@@ -2646,23 +2609,15 @@ again:
 	if (buf_state & BM_VALID)
 	{
 		/*
-		 * When a BufferAccessStrategy is in use, blocks evicted from shared
-		 * buffers are counted as IOOP_EVICT in the corresponding context
-		 * (e.g. IOCONTEXT_BULKWRITE). Shared buffers are evicted by a
-		 * strategy in two cases: 1) while initially claiming buffers for the
-		 * strategy ring 2) to replace an existing strategy ring buffer
-		 * because it is pinned or in use and cannot be reused.
+		 * Blocks evicted from shared buffers are counted as IOOP_EVICT.
 		 *
-		 * Blocks evicted from buffers already in the strategy ring are
-		 * counted as IOOP_REUSE in the corresponding strategy context.
-		 *
-		 * At this point, we can accurately count evictions and reuses,
-		 * because we have successfully claimed the valid buffer. Previously,
-		 * we may have been forced to release the buffer due to concurrent
-		 * pinners or erroring out.
+		 * At this point, we can accurately count evictions, because we have
+		 * successfully claimed the valid buffer. Previously, we may have been
+		 * forced to release the buffer due to concurrent pinners or erroring
+		 * out.
 		 */
 		pgstat_count_io_op(IOOBJECT_RELATION, io_context,
-						   from_ring ? IOOP_REUSE : IOOP_EVICT, 1, 0);
+						   IOOP_EVICT, 1, 0);
 	}
 
 	/*
@@ -2754,7 +2709,6 @@ LimitAdditionalPins(uint32 *additional_pins)
 static BlockNumber
 ExtendBufferedRelCommon(BufferManagerRelation bmr,
 						ForkNumber fork,
-						BufferAccessStrategy strategy,
 						uint32 flags,
 						uint32 extend_by,
 						BlockNumber extend_upto,
@@ -2789,7 +2743,7 @@ ExtendBufferedRelCommon(BufferManagerRelation bmr,
 											 buffers, &extend_by);
 	}
 	else
-		first_block = ExtendBufferedRelShared(bmr, fork, strategy, flags,
+		first_block = ExtendBufferedRelShared(bmr, fork, flags,
 											  extend_by, extend_upto,
 											  buffers, &extend_by);
 	*extended_by = extend_by;
@@ -2812,7 +2766,6 @@ ExtendBufferedRelCommon(BufferManagerRelation bmr,
 static BlockNumber
 ExtendBufferedRelShared(BufferManagerRelation bmr,
 						ForkNumber fork,
-						BufferAccessStrategy strategy,
 						uint32 flags,
 						uint32 extend_by,
 						BlockNumber extend_upto,
@@ -2820,7 +2773,7 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 						uint32 *extended_by)
 {
 	BlockNumber first_block;
-	IOContext	io_context = IOContextForStrategy(strategy);
+	IOContext	io_context = IOCONTEXT_NORMAL;
 	instr_time	io_start;
 
 	LimitAdditionalPins(&extend_by);
@@ -2839,7 +2792,7 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 	{
 		Block		buf_block;
 
-		buffers[i] = GetVictimBuffer(strategy, io_context);
+		buffers[i] = GetVictimBuffer(io_context);
 		buf_block = BufHdrGetBlock(GetBufferDescriptor(buffers[i] - 1));
 
 		/* new buffers are zero-filled */
@@ -2957,7 +2910,7 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 			 * Pin the existing buffer before releasing the partition lock,
 			 * preventing it from being evicted.
 			 */
-			valid = PinBuffer(existing_hdr, strategy, false);
+			valid = PinBuffer(existing_hdr, false);
 
 			LWLockRelease(partition_lock);
 			UnpinBuffer(victim_buf_hdr);
@@ -3007,8 +2960,11 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 			victim_buf_hdr->tag = tag;
 
 			set_bits |= BM_TAG_VALID;
-			/* Admit COOL (probation); see the comment at the other admission
-			 * site and the cooling-state notes in buf_internals.h. */
+
+			/*
+			 * Admit COOL (probation); see the comment at the other admission
+			 * site and the cooling-state notes in buf_internals.h.
+			 */
 			if (bmr.relpersistence == RELPERSISTENCE_PERMANENT || fork == INIT_FORKNUM)
 				set_bits |= BM_PERMANENT;
 
@@ -3275,13 +3231,8 @@ ReleaseAndReadBuffer(Buffer buffer,
 /*
  * PinBuffer -- make buffer unavailable for replacement.
  *
- * For the default access strategy, the buffer's usage_count is incremented
- * when we first pin it; for other strategies we just make sure the usage_count
- * isn't zero.  (The idea of the latter is that we don't want synchronized
- * heap scans to inflate the count, but we need it to not be zero to discourage
- * other backends from stealing buffers from our ring.  As long as we cycle
- * through the ring faster than the global clock-sweep cycles, buffers in
- * our ring won't be chosen as victims for replacement by other backends.)
+ * The buffer's cooling state is promoted to HOT (the 2Q rescue) when we pin
+ * it; see the cooling-state notes in buf_internals.h.
  *
  * This should be applied only to shared buffers, never local ones.
  *
@@ -3298,7 +3249,7 @@ ReleaseAndReadBuffer(Buffer buffer,
  * (recently) invalid and has not been pinned.
  */
 static bool
-PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy,
+PinBuffer(BufferDesc *buf,
 		  bool skip_if_not_valid)
 {
 	Buffer		b = BufferDescriptorGetBuffer(buf);
@@ -3396,7 +3347,7 @@ PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy,
  * ResourceOwnerEnlarge(CurrentResourceOwner);
  *
  * Currently, no callers of this function want to modify the buffer's
- * usage_count at all, so there's no need for a strategy parameter.
+ * cooling state at all.
  * Also we don't bother with a BM_VALID test (the caller could check that for
  * itself).
  *
@@ -4674,22 +4625,8 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object,
 			  false);
 
 	/*
-	 * When a strategy is in use, only flushes of dirty buffers already in the
-	 * strategy ring are counted as strategy writes (IOCONTEXT
-	 * [BULKREAD|BULKWRITE|VACUUM] IOOP_WRITE) for the purpose of IO
-	 * statistics tracking.
-	 *
-	 * If a shared buffer initially added to the ring must be flushed before
-	 * being used, this is counted as an IOCONTEXT_NORMAL IOOP_WRITE.
-	 *
-	 * If a shared buffer which was added to the ring later because the
-	 * current strategy buffer is pinned or in use or because all strategy
-	 * buffers were dirty and rejected (for BAS_BULKREAD operations only)
-	 * requires flushing, this is counted as an IOCONTEXT_NORMAL IOOP_WRITE
-	 * (from_ring will be false).
-	 *
-	 * When a strategy is not in use, the write can only be a "regular" write
-	 * of a dirty shared buffer (IOCONTEXT_NORMAL IOOP_WRITE).
+	 * All writes of a dirty shared buffer are counted as an IOCONTEXT_NORMAL
+	 * IOOP_WRITE for the purpose of IO statistics tracking.
 	 */
 	pgstat_count_io_op_time(io_object, io_context,
 							IOOP_WRITE, io_start, 1, BLCKSZ);
@@ -5450,8 +5387,6 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 	BlockNumber nblocks;
 	BlockNumber blkno;
 	PGIOAlignedBlock buf;
-	BufferAccessStrategy bstrategy_src;
-	BufferAccessStrategy bstrategy_dst;
 	BlockRangeReadStreamPrivate p;
 	ReadStream *src_stream;
 	SMgrRelation src_smgr;
@@ -5479,10 +5414,6 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 	smgrextend(smgropen(dstlocator, INVALID_PROC_NUMBER), forkNum, nblocks - 1,
 			   buf.data, true);
 
-	/* This is a bulk operation, so use buffer access strategies. */
-	bstrategy_src = GetAccessStrategy(BAS_BULKREAD);
-	bstrategy_dst = GetAccessStrategy(BAS_BULKWRITE);
-
 	/* Initialize streaming read */
 	p.current_blocknum = 0;
 	p.last_exclusive = nblocks;
@@ -5494,7 +5425,6 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 	 */
 	src_stream = read_stream_begin_smgr_relation(READ_STREAM_FULL |
 												 READ_STREAM_USE_BATCHING,
-												 bstrategy_src,
 												 src_smgr,
 												 permanent ? RELPERSISTENCE_PERMANENT : RELPERSISTENCE_UNLOGGED,
 												 forkNum,
@@ -5514,7 +5444,7 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 
 		dstBuf = ReadBufferWithoutRelcache(dstlocator, forkNum,
 										   BufferGetBlockNumber(srcBuf),
-										   RBM_ZERO_AND_LOCK, bstrategy_dst,
+										   RBM_ZERO_AND_LOCK,
 										   permanent);
 		dstPage = BufferGetPage(dstBuf);
 
@@ -5535,9 +5465,6 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 	}
 	Assert(read_stream_next_buffer(src_stream, NULL) == InvalidBuffer);
 	read_stream_end(src_stream);
-
-	FreeAccessStrategy(bstrategy_src);
-	FreeAccessStrategy(bstrategy_dst);
 }
 
 /* ---------------------------------------------------------------------
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index e8cf13f3362..ee62b336680 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -77,40 +77,6 @@ const ShmemCallbacks StrategyCtlShmemCallbacks = {
 	.init_fn = StrategyCtlShmemInit,
 };
 
-/*
- * Private (non-shared) state for managing a ring of shared buffers to re-use.
- * This is currently the only kind of BufferAccessStrategy object, but someday
- * we might have more kinds.
- */
-typedef struct BufferAccessStrategyData
-{
-	/* Overall strategy type */
-	BufferAccessStrategyType btype;
-	/* Number of elements in buffers[] array */
-	int			nbuffers;
-
-	/*
-	 * Index of the "current" slot in the ring, ie, the one most recently
-	 * returned by GetBufferFromRing.
-	 */
-	int			current;
-
-	/*
-	 * Array of buffer numbers.  InvalidBuffer (that is, zero) indicates we
-	 * have not yet selected a buffer for this ring slot.  For allocation
-	 * simplicity this is palloc'd together with the fixed fields of the
-	 * struct.
-	 */
-	Buffer		buffers[FLEXIBLE_ARRAY_MEMBER];
-}			BufferAccessStrategyData;
-
-
-/* Prototypes for internal functions */
-static BufferDesc *GetBufferFromRing(BufferAccessStrategy strategy,
-									 uint64 *buf_state);
-static void AddBufferToRing(BufferAccessStrategy strategy,
-							BufferDesc *buf);
-
 /*
  * Per-backend state for the batched clock sweep.  Each backend claims a run
  * of consecutive clock-hand values with a single atomic fetch-add and then
@@ -157,8 +123,8 @@ ClockSweepTick(void)
 			 * must hold the spinlock so StrategySyncStart() can read
 			 * nextVictimBuffer and completePasses consistently.
 			 *
-			 * With batching, multiple backends may each land a fetch-add
-			 * that returns a value past NBuffers in the same pass.  After
+			 * With batching, multiple backends may each land a fetch-add that
+			 * returns a value past NBuffers in the same pass.  After
 			 * acquiring the spinlock we re-read the counter: if another
 			 * backend already wrapped it below NBuffers we are done.
 			 */
@@ -214,8 +180,6 @@ ClockSweepTick(void)
  *	GetVictimBuffer(). The only hard requirement GetVictimBuffer() has is that
  *	the selected buffer must not currently be pinned by anyone.
  *
- *	strategy is a BufferAccessStrategy object, or NULL for default strategy.
- *
  *	It is the callers responsibility to ensure the buffer ownership can be
  *	tracked via TrackNewBufferPin().
  *
@@ -223,29 +187,13 @@ ClockSweepTick(void)
  *	before returning.
  */
 BufferDesc *
-StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_ring)
+StrategyGetBuffer(uint64 *buf_state)
 {
 	BufferDesc *buf;
 	int			bgwprocno;
 	int			trycounter;
 	bool		force_cool;
 
-	*from_ring = false;
-
-	/*
-	 * If given a strategy object, see whether it can select a buffer. We
-	 * assume strategy objects don't need buffer_strategy_lock.
-	 */
-	if (strategy != NULL)
-	{
-		buf = GetBufferFromRing(strategy, buf_state);
-		if (buf != NULL)
-		{
-			*from_ring = true;
-			return buf;
-		}
-	}
-
 	/*
 	 * If asked, we need to waken the bgwriter. Since we don't want to rely on
 	 * a spinlock for this we force a read from shared memory once, and then
@@ -274,8 +222,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 
 	/*
 	 * We count buffer allocation requests so that the bgwriter can estimate
-	 * the rate of buffer consumption.  Note that buffers recycled by a
-	 * strategy object are intentionally not counted here.
+	 * the rate of buffer consumption.
 	 */
 	pg_atomic_fetch_add_u32(&StrategyControl->numBufferAllocs, 1);
 
@@ -347,7 +294,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 				if (!force_cool)
 				{
 					no_progress = true;
-					break;			/* advance the hand, look for COOL */
+					break;		/* advance the hand, look for COOL */
 				}
 
 				if (BUF_STATE_GET_REFBIT(local_buf_state))
@@ -387,8 +334,6 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 												   local_buf_state))
 				{
 					/* Found a usable buffer */
-					if (strategy != NULL)
-						AddBufferToRing(strategy, buf);
 					*buf_state = local_buf_state;
 
 					TrackNewBufferPin(BufferDescriptorGetBuffer(buf));
@@ -399,11 +344,12 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 		}
 
 		/*
-		 * A tick that made no progress toward a victim counts down trycounter.
-		 * A full unproductive pass escalates to force_cool (cool HOT buffers
-		 * into victims); a second unproductive full pass means everything is
-		 * pinned, so fail rather than spin forever.  (A failed CAS above is
-		 * neither progress nor a full miss: we simply retry the same buffer.)
+		 * A tick that made no progress toward a victim counts down
+		 * trycounter. A full unproductive pass escalates to force_cool (cool
+		 * HOT buffers into victims); a second unproductive full pass means
+		 * everything is pinned, so fail rather than spin forever.  (A failed
+		 * CAS above is neither progress nor a full miss: we simply retry the
+		 * same buffer.)
 		 */
 		if (no_progress && --trycounter == 0)
 		{
@@ -531,361 +477,3 @@ StrategyCtlShmemInit(void *arg)
 	else
 		StrategyControl->batchSize = 1;
 }
-
-
-/* ----------------------------------------------------------------
- *				Backend-private buffer ring management
- * ----------------------------------------------------------------
- */
-
-
-/*
- * GetAccessStrategy -- create a BufferAccessStrategy object
- *
- * The object is allocated in the current memory context.
- */
-BufferAccessStrategy
-GetAccessStrategy(BufferAccessStrategyType btype)
-{
-	int			ring_size_kb;
-
-	/*
-	 * Select ring size to use.  See buffer/README for rationales.
-	 *
-	 * Note: if you change the ring size for BAS_BULKREAD, see also
-	 * SYNC_SCAN_REPORT_INTERVAL in access/heap/syncscan.c.
-	 */
-	switch (btype)
-	{
-		case BAS_NORMAL:
-			/* if someone asks for NORMAL, just give 'em a "default" object */
-			return NULL;
-
-		case BAS_BULKREAD:
-			{
-				int			ring_max_kb;
-
-				/*
-				 * The ring always needs to be large enough to allow some
-				 * separation in time between providing a buffer to the user
-				 * of the strategy and that buffer being reused. Otherwise the
-				 * user's pin will prevent reuse of the buffer, even without
-				 * concurrent activity.
-				 *
-				 * We also need to ensure the ring always is large enough for
-				 * SYNC_SCAN_REPORT_INTERVAL, as noted above.
-				 *
-				 * Thus we start out a minimal size and increase the size
-				 * further if appropriate.
-				 */
-				ring_size_kb = 256;
-
-				/*
-				 * There's no point in a larger ring if we won't be allowed to
-				 * pin sufficiently many buffers.  But we never limit to less
-				 * than the minimal size above.
-				 */
-				ring_max_kb = GetPinLimit() * (BLCKSZ / 1024);
-				ring_max_kb = Max(ring_size_kb, ring_max_kb);
-
-				/*
-				 * We would like the ring to additionally have space for the
-				 * configured degree of IO concurrency. While being read in,
-				 * buffers can obviously not yet be reused.
-				 *
-				 * Each IO can be up to io_combine_limit blocks large, and we
-				 * want to start up to effective_io_concurrency IOs.
-				 *
-				 * Note that effective_io_concurrency may be 0, which disables
-				 * AIO.
-				 */
-				ring_size_kb += (BLCKSZ / 1024) *
-					io_combine_limit * effective_io_concurrency;
-
-				if (ring_size_kb > ring_max_kb)
-					ring_size_kb = ring_max_kb;
-				break;
-			}
-		case BAS_BULKWRITE:
-			ring_size_kb = 16 * 1024;
-			break;
-		case BAS_VACUUM:
-			ring_size_kb = 2048;
-			break;
-
-		default:
-			elog(ERROR, "unrecognized buffer access strategy: %d",
-				 (int) btype);
-			return NULL;		/* keep compiler quiet */
-	}
-
-	return GetAccessStrategyWithSize(btype, ring_size_kb);
-}
-
-/*
- * GetAccessStrategyWithSize -- create a BufferAccessStrategy object with a
- *		number of buffers equivalent to the passed in size.
- *
- * If the given ring size is 0, no BufferAccessStrategy will be created and
- * the function will return NULL.  ring_size_kb must not be negative.
- */
-BufferAccessStrategy
-GetAccessStrategyWithSize(BufferAccessStrategyType btype, int ring_size_kb)
-{
-	int			ring_buffers;
-	BufferAccessStrategy strategy;
-
-	Assert(ring_size_kb >= 0);
-
-	/* Figure out how many buffers ring_size_kb is */
-	ring_buffers = ring_size_kb / (BLCKSZ / 1024);
-
-	/* 0 means unlimited, so no BufferAccessStrategy required */
-	if (ring_buffers == 0)
-		return NULL;
-
-	/* Cap to 1/8th of shared_buffers */
-	ring_buffers = Min(NBuffers / 8, ring_buffers);
-
-	/* NBuffers should never be less than 16, so this shouldn't happen */
-	Assert(ring_buffers > 0);
-
-	/* Allocate the object and initialize all elements to zeroes */
-	strategy = (BufferAccessStrategy)
-		palloc0(offsetof(BufferAccessStrategyData, buffers) +
-				ring_buffers * sizeof(Buffer));
-
-	/* Set fields that don't start out zero */
-	strategy->btype = btype;
-	strategy->nbuffers = ring_buffers;
-
-	return strategy;
-}
-
-/*
- * GetAccessStrategyBufferCount -- an accessor for the number of buffers in
- *		the ring
- *
- * Returns 0 on NULL input to match behavior of GetAccessStrategyWithSize()
- * returning NULL with 0 size.
- */
-int
-GetAccessStrategyBufferCount(BufferAccessStrategy strategy)
-{
-	if (strategy == NULL)
-		return 0;
-
-	return strategy->nbuffers;
-}
-
-/*
- * GetAccessStrategyPinLimit -- get cap of number of buffers that should be pinned
- *
- * When pinning extra buffers to look ahead, users of a ring-based strategy are
- * in danger of pinning too much of the ring at once while performing look-ahead.
- * For some strategies, that means "escaping" from the ring, and in others it
- * means forcing dirty data to disk very frequently with associated WAL
- * flushing.  Since external code has no insight into any of that, allow
- * individual strategy types to expose a clamp that should be applied when
- * deciding on a maximum number of buffers to pin at once.
- *
- * Callers should combine this number with other relevant limits and take the
- * minimum.
- */
-int
-GetAccessStrategyPinLimit(BufferAccessStrategy strategy)
-{
-	if (strategy == NULL)
-		return NBuffers;
-
-	switch (strategy->btype)
-	{
-		case BAS_BULKREAD:
-
-			/*
-			 * Since BAS_BULKREAD uses StrategyRejectBuffer(), dirty buffers
-			 * shouldn't be a problem and the caller is free to pin up to the
-			 * entire ring at once.
-			 */
-			return strategy->nbuffers;
-
-		default:
-
-			/*
-			 * Tell caller not to pin more than half the buffers in the ring.
-			 * This is a trade-off between look ahead distance and deferring
-			 * writeback and associated WAL traffic.
-			 */
-			return strategy->nbuffers / 2;
-	}
-}
-
-/*
- * FreeAccessStrategy -- release a BufferAccessStrategy object
- *
- * A simple pfree would do at the moment, but we would prefer that callers
- * don't assume that much about the representation of BufferAccessStrategy.
- */
-void
-FreeAccessStrategy(BufferAccessStrategy strategy)
-{
-	/* don't crash if called on a "default" strategy */
-	if (strategy != NULL)
-		pfree(strategy);
-}
-
-/*
- * GetBufferFromRing -- returns a buffer from the ring, or NULL if the
- *		ring is empty / not usable.
- *
- * The buffer is pinned and marked as owned, using TrackNewBufferPin(), before
- * returning.
- */
-static BufferDesc *
-GetBufferFromRing(BufferAccessStrategy strategy, uint64 *buf_state)
-{
-	BufferDesc *buf;
-	Buffer		bufnum;
-	uint64		old_buf_state;
-	uint64		local_buf_state;	/* to avoid repeated (de-)referencing */
-
-
-	/* Advance to next ring slot */
-	if (++strategy->current >= strategy->nbuffers)
-		strategy->current = 0;
-
-	/*
-	 * If the slot hasn't been filled yet, tell the caller to allocate a new
-	 * buffer with the normal allocation strategy.  He will then fill this
-	 * slot by calling AddBufferToRing with the new buffer.
-	 */
-	bufnum = strategy->buffers[strategy->current];
-	if (bufnum == InvalidBuffer)
-		return NULL;
-
-	buf = GetBufferDescriptor(bufnum - 1);
-
-	/*
-	 * Check whether the buffer can be used and pin it if so. Do this using a
-	 * CAS loop, to avoid having to lock the buffer header.
-	 */
-	old_buf_state = pg_atomic_read_u64(&buf->state);
-	for (;;)
-	{
-		local_buf_state = old_buf_state;
-
-		/*
-		 * If the buffer is pinned we cannot use it under any circumstances.
-		 *
-		 * With the cooling-state replacement the field holds only COOL or HOT,
-		 * so the stock "usage_count > 1 means another backend touched it"
-		 * heuristic no longer applies: a ring element is reusable whenever it
-		 * is unpinned.  (The whole ring mechanism is removed in a later patch;
-		 * scan resistance is now intrinsic to the sweep.)
-		 */
-		if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0)
-			break;
-
-		/* See equivalent code in PinBuffer() */
-		if (unlikely(local_buf_state & BM_LOCKED))
-		{
-			old_buf_state = WaitBufHdrUnlocked(buf);
-			continue;
-		}
-
-		/* pin the buffer if the CAS succeeds */
-		local_buf_state += BUF_REFCOUNT_ONE;
-
-		if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
-										   local_buf_state))
-		{
-			*buf_state = local_buf_state;
-
-			TrackNewBufferPin(BufferDescriptorGetBuffer(buf));
-			return buf;
-		}
-	}
-
-	/*
-	 * Tell caller to allocate a new buffer with the normal allocation
-	 * strategy.  He'll then replace this ring element via AddBufferToRing.
-	 */
-	return NULL;
-}
-
-/*
- * AddBufferToRing -- add a buffer to the buffer ring
- *
- * Caller must hold the buffer header spinlock on the buffer.  Since this
- * is called with the spinlock held, it had better be quite cheap.
- */
-static void
-AddBufferToRing(BufferAccessStrategy strategy, BufferDesc *buf)
-{
-	strategy->buffers[strategy->current] = BufferDescriptorGetBuffer(buf);
-}
-
-/*
- * Utility function returning the IOContext of a given BufferAccessStrategy's
- * strategy ring.
- */
-IOContext
-IOContextForStrategy(BufferAccessStrategy strategy)
-{
-	if (!strategy)
-		return IOCONTEXT_NORMAL;
-
-	switch (strategy->btype)
-	{
-		case BAS_NORMAL:
-
-			/*
-			 * Currently, GetAccessStrategy() returns NULL for
-			 * BufferAccessStrategyType BAS_NORMAL, so this case is
-			 * unreachable.
-			 */
-			pg_unreachable();
-			return IOCONTEXT_NORMAL;
-		case BAS_BULKREAD:
-			return IOCONTEXT_BULKREAD;
-		case BAS_BULKWRITE:
-			return IOCONTEXT_BULKWRITE;
-		case BAS_VACUUM:
-			return IOCONTEXT_VACUUM;
-	}
-
-	elog(ERROR, "unrecognized BufferAccessStrategyType: %d", strategy->btype);
-	pg_unreachable();
-}
-
-/*
- * StrategyRejectBuffer -- consider rejecting a dirty buffer
- *
- * When a nondefault strategy is used, the buffer manager calls this function
- * when it turns out that the buffer selected by StrategyGetBuffer needs to
- * be written out and doing so would require flushing WAL too.  This gives us
- * a chance to choose a different victim.
- *
- * Returns true if buffer manager should ask for a new victim, and false
- * if this buffer should be written and re-used.
- */
-bool
-StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf, bool from_ring)
-{
-	/* We only do this in bulkread mode */
-	if (strategy->btype != BAS_BULKREAD)
-		return false;
-
-	/* Don't muck with behavior of normal buffer-replacement strategy */
-	if (!from_ring ||
-		strategy->buffers[strategy->current] != BufferDescriptorGetBuffer(buf))
-		return false;
-
-	/*
-	 * Remove the dirty buffer from the ring; necessary to prevent infinite
-	 * loop if all ring members are dirty.
-	 */
-	strategy->buffers[strategy->current] = InvalidBuffer;
-
-	return true;
-}
diff --git a/src/backend/storage/freespace/freespace.c b/src/backend/storage/freespace/freespace.c
index 006edab9d77..29cb2d70f66 100644
--- a/src/backend/storage/freespace/freespace.c
+++ b/src/backend/storage/freespace/freespace.c
@@ -603,7 +603,7 @@ fsm_readbuf(Relation rel, FSMAddress addr, bool extend)
 			return InvalidBuffer;
 	}
 	else
-		buf = ReadBufferExtended(rel, FSM_FORKNUM, blkno, RBM_ZERO_ON_ERROR, NULL);
+		buf = ReadBufferExtended(rel, FSM_FORKNUM, blkno, RBM_ZERO_ON_ERROR);
 
 	/*
 	 * Initializing the page when needed is trickier than it looks, because of
@@ -638,7 +638,7 @@ fsm_readbuf(Relation rel, FSMAddress addr, bool extend)
 static Buffer
 fsm_extend(Relation rel, BlockNumber fsm_nblocks)
 {
-	return ExtendBufferedRelTo(BMR_REL(rel), FSM_FORKNUM, NULL,
+	return ExtendBufferedRelTo(BMR_REL(rel), FSM_FORKNUM,
 							   EB_CREATE_FORK_IF_NEEDED |
 							   EB_CLEAR_SIZE_CACHE,
 							   fsm_nblocks,
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 718c1cfc0f9..3c3e35ab8a8 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -1540,15 +1540,8 @@ register_dirty_segment(SMgrRelation reln, ForkNumber forknum, MdfdVec *seg)
 							FilePathName(seg->mdfd_vfd))));
 
 		/*
-		 * We have no way of knowing if the current IOContext is
-		 * IOCONTEXT_NORMAL or IOCONTEXT_[BULKREAD, BULKWRITE, VACUUM] at this
-		 * point, so count the fsync as being in the IOCONTEXT_NORMAL
-		 * IOContext. This is probably okay, because the number of backend
-		 * fsyncs doesn't say anything about the efficacy of the
-		 * BufferAccessStrategy. And counting both fsyncs done in
-		 * IOCONTEXT_NORMAL and IOCONTEXT_[BULKREAD, BULKWRITE, VACUUM] under
-		 * IOCONTEXT_NORMAL is likely clearer when investigating the number of
-		 * backend fsyncs.
+		 * Count the fsync in the IOCONTEXT_NORMAL IOContext, the only
+		 * IOContext relations are read/written under.
 		 */
 		pgstat_count_io_op_time(IOOBJECT_RELATION, IOCONTEXT_NORMAL,
 								IOOP_FSYNC, io_start, 1, 0);
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 38bae7b15d2..d66a23749d5 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -241,16 +241,10 @@ pgstat_get_io_context_name(IOContext io_context)
 {
 	switch (io_context)
 	{
-		case IOCONTEXT_BULKREAD:
-			return "bulkread";
-		case IOCONTEXT_BULKWRITE:
-			return "bulkwrite";
 		case IOCONTEXT_INIT:
 			return "init";
 		case IOCONTEXT_NORMAL:
 			return "normal";
-		case IOCONTEXT_VACUUM:
-			return "vacuum";
 	}
 
 	elog(ERROR, "unrecognized IOContext value: %d", io_context);
@@ -451,18 +445,6 @@ pgstat_tracks_io_object(BackendType bktype, IOObject io_object,
 	 * IOContexts, and, while it may not be inherently incorrect for them to
 	 * do so, excluding those rows from the view makes the view easier to use.
 	 */
-	if ((bktype == B_CHECKPOINTER || bktype == B_BG_WRITER) &&
-		(io_context == IOCONTEXT_BULKREAD ||
-		 io_context == IOCONTEXT_BULKWRITE ||
-		 io_context == IOCONTEXT_VACUUM))
-		return false;
-
-	if (bktype == B_AUTOVAC_LAUNCHER && io_context == IOCONTEXT_VACUUM)
-		return false;
-
-	if ((bktype == B_AUTOVAC_WORKER || bktype == B_AUTOVAC_LAUNCHER) &&
-		io_context == IOCONTEXT_BULKWRITE)
-		return false;
 
 	return true;
 }
@@ -479,8 +461,6 @@ bool
 pgstat_tracks_io_op(BackendType bktype, IOObject io_object,
 					IOContext io_context, IOOp io_op)
 {
-	bool		strategy_io_context;
-
 	/* if (io_context, io_object) will never collect stats, we're done */
 	if (!pgstat_tracks_io_object(bktype, io_object, io_context))
 		return false;
@@ -521,17 +501,13 @@ pgstat_tracks_io_op(BackendType bktype, IOObject io_object,
 	/*
 	 * Some IOOps are not valid in certain IOContexts and some IOOps are only
 	 * valid in certain contexts.
+	 *
+	 * IOOP_REUSE was only relevant when a BufferAccessStrategy was in use.
+	 * Buffer access strategies (ring buffers) have been removed -- scan
+	 * resistance is now intrinsic to the cooling-stage clock sweep -- so
+	 * IOOP_REUSE never occurs.
 	 */
-	if (io_context == IOCONTEXT_BULKREAD && io_op == IOOP_EXTEND)
-		return false;
-
-	strategy_io_context = io_context == IOCONTEXT_BULKREAD ||
-		io_context == IOCONTEXT_BULKWRITE || io_context == IOCONTEXT_VACUUM;
-
-	/*
-	 * IOOP_REUSE is only relevant when a BufferAccessStrategy is in use.
-	 */
-	if (!strategy_io_context && io_op == IOOP_REUSE)
+	if (io_op == IOOP_REUSE)
 		return false;
 
 	/*
@@ -545,14 +521,5 @@ pgstat_tracks_io_op(BackendType bktype, IOObject io_object,
 		!(io_op == IOOP_WRITE || io_op == IOOP_READ || io_op == IOOP_FSYNC))
 		return false;
 
-	/*
-	 * IOOP_FSYNC IOOps done by a backend using a BufferAccessStrategy are
-	 * counted in the IOCONTEXT_NORMAL IOContext. See comment in
-	 * register_dirty_segment() for more details.
-	 */
-	if (strategy_io_context && io_op == IOOP_FSYNC)
-		return false;
-
-
 	return true;
 }
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index bbd28d14d99..555c04272f5 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -149,7 +149,6 @@ int			autovacuum_max_parallel_workers = 0;
 int			MaxBackends = 0;
 
 /* GUC parameters for vacuum */
-int			VacuumBufferUsageLimit = 2048;
 
 int			VacuumCostPageHit = 1;
 int			VacuumCostPageMiss = 2;
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index d421cdbde76..b1ea81f536c 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -3326,16 +3326,6 @@
   boot_val => 'DEFAULT_UPDATE_PROCESS_TITLE',
 },
 
-{ name => 'vacuum_buffer_usage_limit', type => 'int', context => 'PGC_USERSET', group => 'RESOURCES_MEM',
-  short_desc => 'Sets the buffer pool size for VACUUM, ANALYZE, and autovacuum.',
-  flags => 'GUC_UNIT_KB',
-  variable => 'VacuumBufferUsageLimit',
-  boot_val => '2048',
-  min => '0',
-  max => 'MAX_BAS_VAC_RING_SIZE_KB',
-  check_hook => 'check_vacuum_buffer_usage_limit',
-},
-
 { name => 'vacuum_cost_delay', type => 'real', context => 'PGC_USERSET', group => 'VACUUM_COST_DELAY',
   short_desc => 'Vacuum cost delay in milliseconds.',
   flags => 'GUC_UNIT_MS',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 7958653077b..cb1eb8ef3ff 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -164,9 +164,6 @@
                                         #   mmap
                                         # (change requires restart)
 #min_dynamic_shared_memory = 0MB        # (change requires restart)
-#vacuum_buffer_usage_limit = 2MB        # size of vacuum and analyze buffer access strategy ring;
-                                        # 0 to disable vacuum buffer access strategy;
-                                        # range 128kB to 16GB
 
 # SLRU buffers (change requires restart)
 #commit_timestamp_buffers = 0           # memory for pg_commit_ts (0 = auto)
diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index f8158ca6a78..f78e57ff2c3 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -57,7 +57,6 @@ main(int argc, char *argv[])
 		{"no-truncate", no_argument, NULL, 10},
 		{"no-process-toast", no_argument, NULL, 11},
 		{"no-process-main", no_argument, NULL, 12},
-		{"buffer-usage-limit", required_argument, NULL, 13},
 		{"missing-stats-only", no_argument, NULL, 14},
 		{"dry-run", no_argument, NULL, 15},
 		{NULL, 0, NULL, 0}
@@ -202,9 +201,6 @@ main(int argc, char *argv[])
 			case 12:
 				vacopts.process_main = false;
 				break;
-			case 13:
-				vacopts.buffer_usage_limit = escape_quotes(optarg);
-				break;
 			case 14:
 				vacopts.missing_stats_only = true;
 				break;
@@ -290,14 +286,6 @@ main(int argc, char *argv[])
 		pg_fatal("cannot use the \"%s\" option with the \"%s\" option",
 				 "no-index-cleanup", "force-index-cleanup");
 
-	/*
-	 * buffer-usage-limit is not allowed with VACUUM FULL unless ANALYZE is
-	 * included too.
-	 */
-	if (vacopts.buffer_usage_limit && vacopts.full && !vacopts.and_analyze)
-		pg_fatal("cannot use the \"%s\" option with the \"%s\" option",
-				 "buffer-usage-limit", "full");
-
 	/*
 	 * Prohibit --missing-stats-only without --analyze-only or
 	 * --analyze-in-stages.
@@ -352,7 +340,6 @@ help(const char *progname)
 	printf(_("  %s [OPTION]... [DBNAME]\n"), progname);
 	printf(_("\nOptions:\n"));
 	printf(_("  -a, --all                       vacuum all databases\n"));
-	printf(_("      --buffer-usage-limit=SIZE   size of ring buffer used for vacuum\n"));
 	printf(_("  -d, --dbname=DBNAME             database to vacuum\n"));
 	printf(_("      --disable-page-skipping     disable all page-skipping behavior\n"));
 	printf(_("      --dry-run                   show the commands that would be sent to the server\n"));
diff --git a/src/bin/scripts/vacuuming.c b/src/bin/scripts/vacuuming.c
index 855a5754c98..078a3bbb516 100644
--- a/src/bin/scripts/vacuuming.c
+++ b/src/bin/scripts/vacuuming.c
@@ -264,13 +264,6 @@ vacuum_one_database(ConnParams *cparams,
 				 "--parallel", "13");
 	}
 
-	if (vacopts->buffer_usage_limit && PQserverVersion(conn) < 160000)
-	{
-		PQfinish(conn);
-		pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s",
-				 "--buffer-usage-limit", "16");
-	}
-
 	if (vacopts->missing_stats_only && PQserverVersion(conn) < 150000)
 	{
 		PQfinish(conn);
@@ -865,13 +858,6 @@ prepare_vacuum_command(PGconn *conn, PQExpBuffer sql,
 				appendPQExpBuffer(sql, "%sVERBOSE", sep);
 				sep = comma;
 			}
-			if (vacopts->buffer_usage_limit)
-			{
-				Assert(serverVersion >= 160000);
-				appendPQExpBuffer(sql, "%sBUFFER_USAGE_LIMIT '%s'", sep,
-								  vacopts->buffer_usage_limit);
-				sep = comma;
-			}
 			if (sep != paren)
 				appendPQExpBufferChar(sql, ')');
 		}
@@ -974,13 +960,6 @@ prepare_vacuum_command(PGconn *conn, PQExpBuffer sql,
 								  vacopts->parallel_workers);
 				sep = comma;
 			}
-			if (vacopts->buffer_usage_limit)
-			{
-				Assert(serverVersion >= 160000);
-				appendPQExpBuffer(sql, "%sBUFFER_USAGE_LIMIT '%s'", sep,
-								  vacopts->buffer_usage_limit);
-				sep = comma;
-			}
 			if (sep != paren)
 				appendPQExpBufferChar(sql, ')');
 		}
diff --git a/src/bin/scripts/vacuuming.h b/src/bin/scripts/vacuuming.h
index 5a491db2526..83a2469a6aa 100644
--- a/src/bin/scripts/vacuuming.h
+++ b/src/bin/scripts/vacuuming.h
@@ -49,7 +49,6 @@ typedef struct vacuumingOptions
 	bool		process_main;
 	bool		process_toast;
 	bool		skip_database_stats;
-	char	   *buffer_usage_limit;
 	bool		missing_stats_only;
 	bool		echo;
 	bool		quiet;
diff --git a/src/include/access/genam.h b/src/include/access/genam.h
index 68bfe405db3..bc87f6f7cc0 100644
--- a/src/include/access/genam.h
+++ b/src/include/access/genam.h
@@ -58,7 +58,6 @@ typedef struct IndexVacuumInfo
 	bool		estimated_count;	/* num_heap_tuples is an estimate */
 	int			message_level;	/* ereport level for progress messages */
 	double		num_heap_tuples;	/* tuples remaining in heap */
-	BufferAccessStrategy strategy;	/* access strategy for reads */
 } IndexVacuumInfo;
 
 /*
diff --git a/src/include/access/hash.h b/src/include/access/hash.h
index a8702f0e5ea..7c9268fe953 100644
--- a/src/include/access/hash.h
+++ b/src/include/access/hash.h
@@ -405,12 +405,11 @@ extern void _hash_pgaddmultitup(Relation rel, Buffer buf, IndexTuple *itups,
 extern Buffer _hash_addovflpage(Relation rel, Buffer metabuf, Buffer buf, bool retain_pin);
 extern BlockNumber _hash_freeovflpage(Relation rel, Buffer bucketbuf, Buffer ovflbuf,
 									  Buffer wbuf, IndexTuple *itups, OffsetNumber *itup_offsets,
-									  Size *tups_size, uint16 nitups, BufferAccessStrategy bstrategy);
+									  Size *tups_size, uint16 nitups);
 extern void _hash_initbitmapbuffer(Buffer buf, uint16 bmsize, bool initpage);
 extern void _hash_squeezebucket(Relation rel,
 								Bucket bucket, BlockNumber bucket_blkno,
-								Buffer bucket_buf,
-								BufferAccessStrategy bstrategy);
+								Buffer bucket_buf);
 extern uint32 _hash_ovflblkno_to_bitno(HashMetaPage metap, BlockNumber ovflblkno);
 
 /* hashpage.c */
@@ -428,9 +427,6 @@ extern void _hash_initbuf(Buffer buf, uint32 max_bucket, uint32 num_bucket,
 						  uint32 flag, bool initpage);
 extern Buffer _hash_getnewbuf(Relation rel, BlockNumber blkno,
 							  ForkNumber forkNum);
-extern Buffer _hash_getbuf_with_strategy(Relation rel, BlockNumber blkno,
-										 int access, int flags,
-										 BufferAccessStrategy bstrategy);
 extern void _hash_relbuf(Relation rel, Buffer buf);
 extern void _hash_dropbuf(Relation rel, Buffer buf);
 extern void _hash_dropscanbuf(Relation rel, HashScanOpaque so);
@@ -481,7 +477,6 @@ extern void _hash_kill_items(IndexScanDesc scan);
 /* hash.c */
 extern void hashbucketcleanup(Relation rel, Bucket cur_bucket,
 							  Buffer bucket_buf, BlockNumber bucket_blkno,
-							  BufferAccessStrategy bstrategy,
 							  uint32 maxbucket, uint32 highmask, uint32 lowmask,
 							  double *tuples_removed, double *num_index_tuples,
 							  bool split_cleanup,
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 5176478c295..0967b659794 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -72,8 +72,6 @@ typedef struct HeapScanDescData
 	Buffer		rs_cbuf;		/* current buffer in scan, if any */
 	/* NB: if rs_cbuf is not InvalidBuffer, we hold a pin on that buffer */
 
-	BufferAccessStrategy rs_strategy;	/* access strategy for reads */
-
 	HeapTupleData rs_ctup;		/* current tuple in scan, if any */
 
 	/* For scans that stream reads */
@@ -466,7 +464,7 @@ extern void log_heap_prune_and_freeze(Relation relation, Buffer buffer,
 
 /* in heap/vacuumlazy.c */
 extern void heap_vacuum_rel(Relation rel,
-							const VacuumParams *params, BufferAccessStrategy bstrategy);
+							const VacuumParams *params);
 #ifdef USE_ASSERT_CHECKING
 extern bool heap_page_is_all_visible(Relation rel, Buffer buf,
 									 GlobalVisState *vistest,
diff --git a/src/include/access/hio.h b/src/include/access/hio.h
index 60cfc375fd5..63bfcc884ee 100644
--- a/src/include/access/hio.h
+++ b/src/include/access/hio.h
@@ -28,7 +28,6 @@
  */
 typedef struct BulkInsertStateData
 {
-	BufferAccessStrategy strategy;	/* our BULKWRITE strategy object */
 	Buffer		current_buf;	/* current insertion target page */
 
 	/*
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index f2c36696bca..83ba0f3ca73 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -689,8 +689,7 @@ typedef struct TableAmRoutine
 	 * integrate with autovacuum's scheduling.
 	 */
 	void		(*relation_vacuum) (Relation rel,
-									const VacuumParams *params,
-									BufferAccessStrategy bstrategy);
+									const VacuumParams *params);
 
 	/*
 	 * Prepare to analyze block `blockno` of `scan`. The scan has been started
@@ -1774,10 +1773,9 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
  * routine, even if (for ANALYZE) it is part of the same VACUUM command.
  */
 static inline void
-table_relation_vacuum(Relation rel, const VacuumParams *params,
-					  BufferAccessStrategy bstrategy)
+table_relation_vacuum(Relation rel, const VacuumParams *params)
 {
-	rel->rd_tableam->relation_vacuum(rel, params, bstrategy);
+	rel->rd_tableam->relation_vacuum(rel, params);
 }
 
 /*
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 956d9cea36d..14a9b530bcf 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -363,7 +363,7 @@ extern PGDLLIMPORT int64 parallel_vacuum_worker_delay_ns;
 /* in commands/vacuum.c */
 extern void ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel);
 extern void vacuum(List *relations, const VacuumParams *params,
-				   BufferAccessStrategy bstrategy, MemoryContext vac_context,
+				   MemoryContext vac_context,
 				   bool isTopLevel);
 extern void vac_open_indexes(Relation relation, LOCKMODE lockmode,
 							 int *nindexes, Relation **Irel);
@@ -407,8 +407,7 @@ extern void VacuumUpdateCosts(void);
 /* in commands/vacuumparallel.c */
 extern ParallelVacuumState *parallel_vacuum_init(Relation rel, Relation *indrels,
 												 int nindexes, int nrequested_workers,
-												 int vac_work_mem, int elevel,
-												 BufferAccessStrategy bstrategy);
+												 int vac_work_mem, int elevel);
 extern void parallel_vacuum_end(ParallelVacuumState *pvs, IndexBulkDeleteResult **istats);
 extern TidStore *parallel_vacuum_get_dead_items(ParallelVacuumState *pvs,
 												VacDeadItemsInfo **dead_items_info_p);
@@ -428,8 +427,7 @@ extern void parallel_vacuum_main(dsm_segment *seg, shm_toc *toc);
 
 /* in commands/analyze.c */
 extern void analyze_rel(Oid relid, RangeVar *relation,
-						const VacuumParams *params, List *va_cols, bool in_outer_xact,
-						BufferAccessStrategy bstrategy);
+						const VacuumParams *params, List *va_cols, bool in_outer_xact);
 extern bool attribute_is_analyzable(Relation onerel, int attnum, Form_pg_attribute attr,
 									int *p_attstattarget);
 extern bool std_typanalyze(VacAttrStats *stats);
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 7170a4bff98..c5d582b152b 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -273,15 +273,6 @@ extern PGDLLIMPORT double hash_mem_multiplier;
 extern PGDLLIMPORT int maintenance_work_mem;
 extern PGDLLIMPORT int max_parallel_maintenance_workers;
 
-/*
- * Upper and lower hard limits for the buffer access strategy ring size
- * specified by the VacuumBufferUsageLimit GUC and BUFFER_USAGE_LIMIT option
- * to VACUUM and ANALYZE.
- */
-#define MIN_BAS_VAC_RING_SIZE_KB 128
-#define MAX_BAS_VAC_RING_SIZE_KB (16 * 1024 * 1024)
-
-extern PGDLLIMPORT int VacuumBufferUsageLimit;
 extern PGDLLIMPORT int VacuumCostPageHit;
 extern PGDLLIMPORT int VacuumCostPageMiss;
 extern PGDLLIMPORT int VacuumCostPageDirty;
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..68b4e5093a2 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -287,14 +287,11 @@ typedef enum IOObject
 
 typedef enum IOContext
 {
-	IOCONTEXT_BULKREAD,
-	IOCONTEXT_BULKWRITE,
 	IOCONTEXT_INIT,
 	IOCONTEXT_NORMAL,
-	IOCONTEXT_VACUUM,
 } IOContext;
 
-#define IOCONTEXT_NUM_TYPES (IOCONTEXT_VACUUM + 1)
+#define IOCONTEXT_NUM_TYPES (IOCONTEXT_NORMAL + 1)
 
 /*
  * Enumeration of IO operations.
diff --git a/src/include/storage/buf.h b/src/include/storage/buf.h
index b21445522b1..fa942a64f05 100644
--- a/src/include/storage/buf.h
+++ b/src/include/storage/buf.h
@@ -36,11 +36,4 @@ typedef int Buffer;
  */
 #define BufferIsLocal(buffer)	((buffer) < 0)
 
-/*
- * Buffer access strategy objects.
- *
- * BufferAccessStrategyData is private to freelist.c
- */
-typedef struct BufferAccessStrategyData *BufferAccessStrategy;
-
 #endif							/* BUF_H */
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index 131df126cda..3b44ad3e0b6 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -632,11 +632,7 @@ extern void TerminateBufferIO(BufferDesc *buf, bool clear_dirty, uint64 set_flag
 
 
 /* freelist.c */
-extern IOContext IOContextForStrategy(BufferAccessStrategy strategy);
-extern BufferDesc *StrategyGetBuffer(BufferAccessStrategy strategy,
-									 uint64 *buf_state, bool *from_ring);
-extern bool StrategyRejectBuffer(BufferAccessStrategy strategy,
-								 BufferDesc *buf, bool from_ring);
+extern BufferDesc *StrategyGetBuffer(uint64 *buf_state);
 
 extern int	StrategySyncStart(uint32 *complete_passes, uint32 *num_buf_alloc);
 extern void StrategyNotifyBgWriter(int bgwprocno);
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 6837b35fc6d..36023792f36 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -25,21 +25,6 @@
 
 typedef void *Block;
 
-/*
- * Possible arguments for GetAccessStrategy().
- *
- * If adding a new BufferAccessStrategyType, also add a new IOContext so
- * IO statistics using this strategy are tracked.
- */
-typedef enum BufferAccessStrategyType
-{
-	BAS_NORMAL,					/* Normal random access */
-	BAS_BULKREAD,				/* Large read-only scan (hint bit updates are
-								 * ok) */
-	BAS_BULKWRITE,				/* Large multi-block write (e.g. COPY IN) */
-	BAS_VACUUM,					/* VACUUM */
-} BufferAccessStrategyType;
-
 /* Possible modes for ReadBufferExtended() */
 typedef enum
 {
@@ -135,7 +120,6 @@ struct ReadBuffersOperation
 	SMgrRelation smgr;
 	char		persistence;
 	ForkNumber	forknum;
-	BufferAccessStrategy strategy;
 
 	/*
 	 * The following private members are private state for communication
@@ -235,11 +219,10 @@ extern bool ReadRecentBuffer(RelFileLocator rlocator, ForkNumber forkNum,
 							 BlockNumber blockNum, Buffer recent_buffer);
 extern Buffer ReadBuffer(Relation reln, BlockNumber blockNum);
 extern Buffer ReadBufferExtended(Relation reln, ForkNumber forkNum,
-								 BlockNumber blockNum, ReadBufferMode mode,
-								 BufferAccessStrategy strategy);
+								 BlockNumber blockNum, ReadBufferMode mode);
 extern Buffer ReadBufferWithoutRelcache(RelFileLocator rlocator,
 										ForkNumber forkNum, BlockNumber blockNum,
-										ReadBufferMode mode, BufferAccessStrategy strategy,
+										ReadBufferMode mode,
 										bool permanent);
 
 extern bool StartReadBuffer(ReadBuffersOperation *operation,
@@ -266,18 +249,15 @@ extern Buffer ReleaseAndReadBuffer(Buffer buffer, Relation relation,
 
 extern Buffer ExtendBufferedRel(BufferManagerRelation bmr,
 								ForkNumber forkNum,
-								BufferAccessStrategy strategy,
 								uint32 flags);
 extern BlockNumber ExtendBufferedRelBy(BufferManagerRelation bmr,
 									   ForkNumber fork,
-									   BufferAccessStrategy strategy,
 									   uint32 flags,
 									   uint32 extend_by,
 									   Buffer *buffers,
 									   uint32 *extended_by);
 extern Buffer ExtendBufferedRelTo(BufferManagerRelation bmr,
 								  ForkNumber fork,
-								  BufferAccessStrategy strategy,
 								  uint32 flags,
 								  BlockNumber extend_to,
 								  ReadBufferMode mode);
@@ -376,14 +356,6 @@ extern void AtProcExit_LocalBuffers(void);
 
 /* in freelist.c */
 
-extern BufferAccessStrategy GetAccessStrategy(BufferAccessStrategyType btype);
-extern BufferAccessStrategy GetAccessStrategyWithSize(BufferAccessStrategyType btype,
-													  int ring_size_kb);
-extern int	GetAccessStrategyBufferCount(BufferAccessStrategy strategy);
-extern int	GetAccessStrategyPinLimit(BufferAccessStrategy strategy);
-
-extern void FreeAccessStrategy(BufferAccessStrategy strategy);
-
 
 /* inline functions */
 
diff --git a/src/include/storage/read_stream.h b/src/include/storage/read_stream.h
index 48995c6d534..9e5fbbed9e8 100644
--- a/src/include/storage/read_stream.h
+++ b/src/include/storage/read_stream.h
@@ -83,17 +83,14 @@ extern BlockNumber block_range_read_stream_cb(ReadStream *stream,
 											  void *callback_private_data,
 											  void *per_buffer_data);
 extern ReadStream *read_stream_begin_relation(int flags,
-											  BufferAccessStrategy strategy,
 											  Relation rel,
 											  ForkNumber forknum,
 											  ReadStreamBlockNumberCB callback,
 											  void *callback_private_data,
 											  size_t per_buffer_data_size);
 extern Buffer read_stream_next_buffer(ReadStream *stream, void **per_buffer_data);
-extern BlockNumber read_stream_next_block(ReadStream *stream,
-										  BufferAccessStrategy *strategy);
+extern BlockNumber read_stream_next_block(ReadStream *stream);
 extern ReadStream *read_stream_begin_smgr_relation(int flags,
-												   BufferAccessStrategy strategy,
 												   SMgrRelation smgr,
 												   char smgr_persistence,
 												   ForkNumber forknum,
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 307f4fbaefe..3f28558d47c 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -31,8 +31,6 @@ extern void assign_application_name(const char *newval, void *extra);
 extern const char *show_archive_command(void);
 extern bool check_autovacuum_work_mem(int *newval, void **extra,
 									  GucSource source);
-extern bool check_vacuum_buffer_usage_limit(int *newval, void **extra,
-											GucSource source);
 extern bool check_backtrace_functions(char **newval, void **extra,
 									  GucSource source);
 extern void assign_backtrace_functions(const char *newval, void *extra);
diff --git a/src/test/modules/test_aio/test_aio.c b/src/test/modules/test_aio/test_aio.c
index 6270775af7c..8d7b2a728d6 100644
--- a/src/test/modules/test_aio/test_aio.c
+++ b/src/test/modules/test_aio/test_aio.c
@@ -194,7 +194,6 @@ grow_rel(PG_FUNCTION_ARGS)
 
 		ExtendBufferedRelBy(BMR_REL(rel),
 							MAIN_FORKNUM,
-							NULL,
 							0,
 							extend_by_pages,
 							victim_buffers,
@@ -231,7 +230,7 @@ modify_rel_block(PG_FUNCTION_ARGS)
 	rel = relation_open(relid, AccessExclusiveLock);
 
 	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno,
-							 RBM_ZERO_ON_ERROR, NULL);
+							 RBM_ZERO_ON_ERROR);
 
 	LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
 
@@ -331,7 +330,7 @@ create_toy_buffer(Relation rel, BlockNumber blkno)
 	uint64		unset_bits = 0;
 
 	/* place buffer in shared buffers without erroring out */
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_ZERO_AND_LOCK, NULL);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_ZERO_AND_LOCK);
 	LockBuffer(buf, BUFFER_LOCK_UNLOCK);
 
 	if (RelationUsesLocalBuffers(rel))
@@ -737,7 +736,6 @@ read_buffers(PG_FUNCTION_ARGS)
 		operation->rel = rel;
 		operation->smgr = smgr;
 		operation->persistence = rel->rd_rel->relpersistence;
-		operation->strategy = NULL;
 		operation->forknum = MAIN_FORKNUM;
 
 		io_reqds[nios] = StartReadBuffers(operation,
@@ -879,7 +877,6 @@ read_stream_for_blocks(PG_FUNCTION_ARGS)
 	rel = relation_open(relid, AccessShareLock);
 
 	stream = read_stream_begin_relation(READ_STREAM_FULL,
-										NULL,
 										rel,
 										MAIN_FORKNUM,
 										read_stream_for_blocks_cb,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index 03cbc1cdef5..beb876c48b4 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -16,22 +16,16 @@ SHOW track_counts;  -- must be on
 SELECT backend_type, object, context FROM pg_stat_io
   ORDER BY backend_type COLLATE "C", object COLLATE "C", context COLLATE "C";
 backend_type|object|context
-autovacuum launcher|relation|bulkread
 autovacuum launcher|relation|init
 autovacuum launcher|relation|normal
 autovacuum launcher|wal|init
 autovacuum launcher|wal|normal
-autovacuum worker|relation|bulkread
 autovacuum worker|relation|init
 autovacuum worker|relation|normal
-autovacuum worker|relation|vacuum
 autovacuum worker|wal|init
 autovacuum worker|wal|normal
-background worker|relation|bulkread
-background worker|relation|bulkwrite
 background worker|relation|init
 background worker|relation|normal
-background worker|relation|vacuum
 background worker|temp relation|normal
 background worker|wal|init
 background worker|wal|normal
@@ -43,67 +37,43 @@ checkpointer|relation|init
 checkpointer|relation|normal
 checkpointer|wal|init
 checkpointer|wal|normal
-client backend|relation|bulkread
-client backend|relation|bulkwrite
 client backend|relation|init
 client backend|relation|normal
-client backend|relation|vacuum
 client backend|temp relation|normal
 client backend|wal|init
 client backend|wal|normal
-datachecksums launcher|relation|bulkread
-datachecksums launcher|relation|bulkwrite
 datachecksums launcher|relation|init
 datachecksums launcher|relation|normal
-datachecksums launcher|relation|vacuum
 datachecksums launcher|temp relation|normal
 datachecksums launcher|wal|init
 datachecksums launcher|wal|normal
-datachecksums worker|relation|bulkread
-datachecksums worker|relation|bulkwrite
 datachecksums worker|relation|init
 datachecksums worker|relation|normal
-datachecksums worker|relation|vacuum
 datachecksums worker|temp relation|normal
 datachecksums worker|wal|init
 datachecksums worker|wal|normal
-io worker|relation|bulkread
-io worker|relation|bulkwrite
 io worker|relation|init
 io worker|relation|normal
-io worker|relation|vacuum
 io worker|temp relation|normal
 io worker|wal|init
 io worker|wal|normal
-slotsync worker|relation|bulkread
-slotsync worker|relation|bulkwrite
 slotsync worker|relation|init
 slotsync worker|relation|normal
-slotsync worker|relation|vacuum
 slotsync worker|temp relation|normal
 slotsync worker|wal|init
 slotsync worker|wal|normal
-standalone backend|relation|bulkread
-standalone backend|relation|bulkwrite
 standalone backend|relation|init
 standalone backend|relation|normal
-standalone backend|relation|vacuum
 standalone backend|wal|init
 standalone backend|wal|normal
-startup|relation|bulkread
-startup|relation|bulkwrite
 startup|relation|init
 startup|relation|normal
-startup|relation|vacuum
 startup|wal|init
 startup|wal|normal
 walreceiver|wal|init
 walreceiver|wal|normal
-walsender|relation|bulkread
-walsender|relation|bulkwrite
 walsender|relation|init
 walsender|relation|normal
-walsender|relation|vacuum
 walsender|temp relation|normal
 walsender|wal|init
 walsender|wal|normal
@@ -111,7 +81,7 @@ walsummarizer|wal|init
 walsummarizer|wal|normal
 walwriter|wal|init
 walwriter|wal|normal
-(95 rows)
+(65 rows)
 \a
 -- List of registered statistics kinds.
 SELECT id, name, fixed_amount,
@@ -1757,80 +1727,20 @@ SELECT :io_sum_local_new_tblspc_writes > :io_sum_local_after_writes;
 (1 row)
 
 RESET temp_buffers;
--- Test that reuse of strategy buffers and reads of blocks into these reused
--- buffers while VACUUMing are tracked in pg_stat_io. If there is sufficient
--- demand for shared buffers from concurrent queries, some buffers may be
--- pinned by other backends before they can be reused. In such cases, the
--- backend will evict a buffer from outside the ring and add it to the
--- ring. This is considered an eviction and not a reuse.
--- Set wal_skip_threshold smaller than the expected size of
--- test_io_vac_strategy so that, even if wal_level is minimal, VACUUM FULL will
--- fsync the newly rewritten test_io_vac_strategy instead of writing it to WAL.
--- Writing it to WAL will result in the newly written relation pages being in
--- shared buffers -- preventing us from testing BAS_VACUUM BufferAccessStrategy
--- reads.
-SET wal_skip_threshold = '1 kB';
-SELECT sum(reuses) AS reuses, sum(reads) AS reads, sum(evictions) AS evictions
-  FROM pg_stat_io WHERE context = 'vacuum' \gset io_sum_vac_strategy_before_
-CREATE TABLE test_io_vac_strategy(a int, b int) WITH (autovacuum_enabled = 'false');
-INSERT INTO test_io_vac_strategy SELECT i, i from generate_series(1, 4500)i;
--- Ensure that the next VACUUM will need to perform IO by rewriting the table
--- first with VACUUM (FULL).
-VACUUM (FULL) test_io_vac_strategy;
--- Use the minimum BUFFER_USAGE_LIMIT to cause reuses or evictions with the
--- smallest table possible.
-VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) test_io_vac_strategy;
-SELECT pg_stat_force_next_flush();
- pg_stat_force_next_flush 
---------------------------
- 
-(1 row)
-
-SELECT sum(reuses) AS reuses, sum(reads) AS reads, sum(evictions) AS evictions
-  FROM pg_stat_io WHERE context = 'vacuum' \gset io_sum_vac_strategy_after_
-SELECT :io_sum_vac_strategy_after_reads > :io_sum_vac_strategy_before_reads;
- ?column? 
-----------
- t
-(1 row)
-
-SELECT (:io_sum_vac_strategy_after_reuses + :io_sum_vac_strategy_after_evictions) >
-  (:io_sum_vac_strategy_before_reuses + :io_sum_vac_strategy_before_evictions);
- ?column? 
-----------
- t
-(1 row)
-
-RESET wal_skip_threshold;
--- Test that extends done by a CTAS, which uses a BAS_BULKWRITE
--- BufferAccessStrategy, are tracked in pg_stat_io.
-SELECT sum(extends) AS io_sum_bulkwrite_strategy_extends_before
-  FROM pg_stat_io WHERE context = 'bulkwrite' \gset
-CREATE TABLE test_io_bulkwrite_strategy AS SELECT i FROM generate_series(1,100)i;
-SELECT pg_stat_force_next_flush();
- pg_stat_force_next_flush 
---------------------------
- 
-(1 row)
-
-SELECT sum(extends) AS io_sum_bulkwrite_strategy_extends_after
-  FROM pg_stat_io WHERE context = 'bulkwrite' \gset
-SELECT :io_sum_bulkwrite_strategy_extends_after > :io_sum_bulkwrite_strategy_extends_before;
- ?column? 
-----------
- t
-(1 row)
-
 -- Test IO stats reset
+-- Note: reuses is intentionally excluded from these totals.  With the
+-- BufferAccessStrategy ring buffers removed, IOOP_REUSE is never tracked, so
+-- the reuses column is NULL for every row; summing it would make the whole
+-- total NULL.
 SELECT pg_stat_have_stats('io', 0, 0);
  pg_stat_have_stats 
 --------------------
  t
 (1 row)
 
-SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS io_stats_pre_reset
+SELECT sum(evictions) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS io_stats_pre_reset
   FROM pg_stat_io \gset
-SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_pre_reset
+SELECT sum(evictions) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_pre_reset
   FROM pg_stat_get_backend_io(pg_backend_pid()) \gset
 SELECT pg_stat_reset_shared('io');
  pg_stat_reset_shared 
@@ -1838,7 +1748,7 @@ SELECT pg_stat_reset_shared('io');
  
 (1 row)
 
-SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS io_stats_post_reset
+SELECT sum(evictions) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS io_stats_post_reset
   FROM pg_stat_io \gset
 SELECT :io_stats_post_reset < :io_stats_pre_reset;
  ?column? 
@@ -1846,7 +1756,7 @@ SELECT :io_stats_post_reset < :io_stats_pre_reset;
  t
 (1 row)
 
-SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_post_reset
+SELECT sum(evictions) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_post_reset
   FROM pg_stat_get_backend_io(pg_backend_pid()) \gset
 -- pg_stat_reset_shared() did not reset backend IO stats
 SELECT :my_io_stats_pre_reset <= :my_io_stats_post_reset;
@@ -1862,7 +1772,7 @@ SELECT pg_stat_reset_backend_stats(pg_backend_pid());
  
 (1 row)
 
-SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_post_backend_reset
+SELECT sum(evictions) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_post_backend_reset
   FROM pg_stat_get_backend_io(pg_backend_pid()) \gset
 SELECT :my_io_stats_pre_reset > :my_io_stats_post_backend_reset;
  ?column? 
diff --git a/src/test/regress/expected/vacuum.out b/src/test/regress/expected/vacuum.out
index d4696bc3325..85d06907cec 100644
--- a/src/test/regress/expected/vacuum.out
+++ b/src/test/regress/expected/vacuum.out
@@ -526,25 +526,6 @@ SELECT t.relfilenode = :toast_filenode AS is_same_toast_filenode
  f
 (1 row)
 
--- BUFFER_USAGE_LIMIT option
-VACUUM (BUFFER_USAGE_LIMIT '512 kB') vac_option_tab;
-ANALYZE (BUFFER_USAGE_LIMIT '512 kB') vac_option_tab;
--- try disabling the buffer usage limit
-VACUUM (BUFFER_USAGE_LIMIT 0) vac_option_tab;
-ANALYZE (BUFFER_USAGE_LIMIT 0) vac_option_tab;
--- value exceeds max size error
-VACUUM (BUFFER_USAGE_LIMIT 16777220) vac_option_tab;
-ERROR:  BUFFER_USAGE_LIMIT option must be 0 or between 128 kB and 16777216 kB
--- value is less than min size error
-VACUUM (BUFFER_USAGE_LIMIT 120) vac_option_tab;
-ERROR:  BUFFER_USAGE_LIMIT option must be 0 or between 128 kB and 16777216 kB
--- integer overflow error
-VACUUM (BUFFER_USAGE_LIMIT 10000000000) vac_option_tab;
-ERROR:  BUFFER_USAGE_LIMIT option must be 0 or between 128 kB and 16777216 kB
-HINT:  Value exceeds integer range.
--- incompatible with VACUUM FULL error
-VACUUM (BUFFER_USAGE_LIMIT '512 kB', FULL) vac_option_tab;
-ERROR:  BUFFER_USAGE_LIMIT cannot be specified for VACUUM FULL
 -- SKIP_DATABASE_STATS option
 VACUUM (SKIP_DATABASE_STATS) vactst;
 -- ONLY_DATABASE_STATS option
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index 4c265d1245c..4eb48114e80 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -814,65 +814,27 @@ SELECT sum(writes) AS io_sum_local_new_tblspc_writes
 SELECT :io_sum_local_new_tblspc_writes > :io_sum_local_after_writes;
 RESET temp_buffers;
 
--- Test that reuse of strategy buffers and reads of blocks into these reused
--- buffers while VACUUMing are tracked in pg_stat_io. If there is sufficient
--- demand for shared buffers from concurrent queries, some buffers may be
--- pinned by other backends before they can be reused. In such cases, the
--- backend will evict a buffer from outside the ring and add it to the
--- ring. This is considered an eviction and not a reuse.
-
--- Set wal_skip_threshold smaller than the expected size of
--- test_io_vac_strategy so that, even if wal_level is minimal, VACUUM FULL will
--- fsync the newly rewritten test_io_vac_strategy instead of writing it to WAL.
--- Writing it to WAL will result in the newly written relation pages being in
--- shared buffers -- preventing us from testing BAS_VACUUM BufferAccessStrategy
--- reads.
-SET wal_skip_threshold = '1 kB';
-SELECT sum(reuses) AS reuses, sum(reads) AS reads, sum(evictions) AS evictions
-  FROM pg_stat_io WHERE context = 'vacuum' \gset io_sum_vac_strategy_before_
-CREATE TABLE test_io_vac_strategy(a int, b int) WITH (autovacuum_enabled = 'false');
-INSERT INTO test_io_vac_strategy SELECT i, i from generate_series(1, 4500)i;
--- Ensure that the next VACUUM will need to perform IO by rewriting the table
--- first with VACUUM (FULL).
-VACUUM (FULL) test_io_vac_strategy;
--- Use the minimum BUFFER_USAGE_LIMIT to cause reuses or evictions with the
--- smallest table possible.
-VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) test_io_vac_strategy;
-SELECT pg_stat_force_next_flush();
-SELECT sum(reuses) AS reuses, sum(reads) AS reads, sum(evictions) AS evictions
-  FROM pg_stat_io WHERE context = 'vacuum' \gset io_sum_vac_strategy_after_
-SELECT :io_sum_vac_strategy_after_reads > :io_sum_vac_strategy_before_reads;
-SELECT (:io_sum_vac_strategy_after_reuses + :io_sum_vac_strategy_after_evictions) >
-  (:io_sum_vac_strategy_before_reuses + :io_sum_vac_strategy_before_evictions);
-RESET wal_skip_threshold;
-
--- Test that extends done by a CTAS, which uses a BAS_BULKWRITE
--- BufferAccessStrategy, are tracked in pg_stat_io.
-SELECT sum(extends) AS io_sum_bulkwrite_strategy_extends_before
-  FROM pg_stat_io WHERE context = 'bulkwrite' \gset
-CREATE TABLE test_io_bulkwrite_strategy AS SELECT i FROM generate_series(1,100)i;
-SELECT pg_stat_force_next_flush();
-SELECT sum(extends) AS io_sum_bulkwrite_strategy_extends_after
-  FROM pg_stat_io WHERE context = 'bulkwrite' \gset
-SELECT :io_sum_bulkwrite_strategy_extends_after > :io_sum_bulkwrite_strategy_extends_before;
-
 -- Test IO stats reset
+-- Note: reuses is intentionally excluded from these totals.  With the
+-- BufferAccessStrategy ring buffers removed, IOOP_REUSE is never tracked, so
+-- the reuses column is NULL for every row; summing it would make the whole
+-- total NULL.
 SELECT pg_stat_have_stats('io', 0, 0);
-SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS io_stats_pre_reset
+SELECT sum(evictions) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS io_stats_pre_reset
   FROM pg_stat_io \gset
-SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_pre_reset
+SELECT sum(evictions) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_pre_reset
   FROM pg_stat_get_backend_io(pg_backend_pid()) \gset
 SELECT pg_stat_reset_shared('io');
-SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS io_stats_post_reset
+SELECT sum(evictions) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS io_stats_post_reset
   FROM pg_stat_io \gset
 SELECT :io_stats_post_reset < :io_stats_pre_reset;
-SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_post_reset
+SELECT sum(evictions) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_post_reset
   FROM pg_stat_get_backend_io(pg_backend_pid()) \gset
 -- pg_stat_reset_shared() did not reset backend IO stats
 SELECT :my_io_stats_pre_reset <= :my_io_stats_post_reset;
 -- but pg_stat_reset_backend_stats() does
 SELECT pg_stat_reset_backend_stats(pg_backend_pid());
-SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_post_backend_reset
+SELECT sum(evictions) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_post_backend_reset
   FROM pg_stat_get_backend_io(pg_backend_pid()) \gset
 SELECT :my_io_stats_pre_reset > :my_io_stats_post_backend_reset;
 
diff --git a/src/test/regress/sql/vacuum.sql b/src/test/regress/sql/vacuum.sql
index 247b8e23b23..506c4389765 100644
--- a/src/test/regress/sql/vacuum.sql
+++ b/src/test/regress/sql/vacuum.sql
@@ -390,21 +390,6 @@ SELECT t.relfilenode = :toast_filenode AS is_same_toast_filenode
   FROM pg_class c, pg_class t
   WHERE c.reltoastrelid = t.oid AND c.relname = 'vac_option_tab';
 
--- BUFFER_USAGE_LIMIT option
-VACUUM (BUFFER_USAGE_LIMIT '512 kB') vac_option_tab;
-ANALYZE (BUFFER_USAGE_LIMIT '512 kB') vac_option_tab;
--- try disabling the buffer usage limit
-VACUUM (BUFFER_USAGE_LIMIT 0) vac_option_tab;
-ANALYZE (BUFFER_USAGE_LIMIT 0) vac_option_tab;
--- value exceeds max size error
-VACUUM (BUFFER_USAGE_LIMIT 16777220) vac_option_tab;
--- value is less than min size error
-VACUUM (BUFFER_USAGE_LIMIT 120) vac_option_tab;
--- integer overflow error
-VACUUM (BUFFER_USAGE_LIMIT 10000000000) vac_option_tab;
--- incompatible with VACUUM FULL error
-VACUUM (BUFFER_USAGE_LIMIT '512 kB', FULL) vac_option_tab;
-
 -- SKIP_DATABASE_STATS option
 VACUUM (SKIP_DATABASE_STATS) vactst;
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 56c1f997f88..064a9878e72 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -355,8 +355,6 @@ BtreeLevel
 Bucket
 BufFile
 Buffer
-BufferAccessStrategy
-BufferAccessStrategyType
 BufferCacheOsPagesContext
 BufferCacheOsPagesRec
 BufferDesc
-- 
2.51.2



^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention
  2026-04-25 20:08 [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-04-27 14:15 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-04-28 14:30   ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 14:00     ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 18:07       ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-11 15:54         ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-14 17:53           ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
@ 2026-07-20 15:36             ` Greg Burd <greg@burd.me>
  2 siblings, 0 replies; 21+ messages in thread

From: Greg Burd @ 2026-07-20 15:36 UTC (permalink / raw)
  To: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>

v7 is attached, rebased on master (2521f9e).

best.

-greg

Attachments:

  [text/x-patch] v7-0001-Batch-the-clock-sweep-to-reduce-nextVictimBuffer-.patch (9.7K, ../../8ffd4113-1d35-4d11-9d6b-4b49eac9985c@app.fastmail.com/2-v7-0001-Batch-the-clock-sweep-to-reduce-nextVictimBuffer-.patch)
  download | inline diff:
From aeca7d52aa309cafd68ef8fdd0be5b792426a9b7 Mon Sep 17 00:00:00 2001
From: Greg Burd <greg@burd.me>
Date: Mon, 6 Jul 2026 17:25:00 -0400
Subject: [PATCH v7 1/3] Batch the clock sweep to reduce nextVictimBuffer
 atomic contention

StrategyGetBuffer() advances the shared clock hand, nextVictimBuffer, with a
pg_atomic_fetch_add_u32(..., 1) on every tick.  On a multi-socket system the
cache line holding that counter has to travel over the interconnect on each
operation, pushing a sweep tick from ~20ns (same-socket, line warm in L1/L2)
into the ~100-200ns range.  Under eviction pressure with hundreds of backends
in StrategyGetBuffer() concurrently, that single cache line becomes the
dominant cost of the sweep, visible as elevated bus-cycles and cache-misses in
a perf profile.

Have each backend claim a run of consecutive buffer IDs from the shared hand
with a single fetch-add and then iterate through them privately.  The sweep
still advances through the pool in order, each buffer is still visited exactly
once per complete pass, and the meaning of the clock state is unchanged; only
the temporal ordering of visits within a pass changes, which the algorithm
does not depend on.  The contended atomic now fires roughly once per batch
rather than once per buffer.

The batch is one cache line's worth of clock-hand values --
PG_CACHE_LINE_SIZE / sizeof(uint32) -- capped at NBuffers so a claim can never
wrap the pool more than once.  Batching only helps when the counter's cache
line actually bounces between sockets, so it is enabled only on multi-node
NUMA hardware (pg_numa_get_max_node() >= 1); on a single socket, or where
libnuma is unavailable, the batch size stays 1 and the code path is
byte-identical to the stock clock sweep.

Wraparound handling is adjusted: with batching, several backends can each see
a fetch-add return a value past NBuffers within the same pass.  Any such
backend takes buffer_strategy_lock, re-reads the counter, and if it is still
out of range wraps it with a single CAS and increments completePasses.
StrategySyncStart() continues to see a consistent (nextVictimBuffer,
completePasses) pair.

This is the batched-clock-sweep idea from Jim Mlodgenski's pgsql-hackers
thread, adapted to derive the batch size from the platform cache-line size.

Co-authored-by: Jim Mlodgenski <mlodj@amazon.com>
---
 src/backend/storage/buffer/freelist.c | 152 ++++++++++++++++++--------
 1 file changed, 109 insertions(+), 43 deletions(-)

diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index fdb5bad..46104b3 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -22,6 +22,7 @@
 #include "storage/proc.h"
 #include "storage/shmem.h"
 #include "storage/subsystems.h"
+#include "port/pg_numa.h"
 
 #define INT_ACCESS_ONCE(var)	((int)(*((volatile int *)&(var))))
 
@@ -100,68 +101,110 @@ static BufferDesc *GetBufferFromRing(BufferAccessStrategy strategy,
 static void AddBufferToRing(BufferAccessStrategy strategy,
 							BufferDesc *buf);
 
+/*
+ * Per-backend state for the batched clock sweep.  Each backend claims a run
+ * of consecutive clock-hand values with a single atomic fetch-add and then
+ * iterates through them privately, so the contended nextVictimBuffer cache
+ * line is touched roughly 1/batch as often.  MyBatchPos is the next hand
+ * value to hand out; MyBatchEnd is one past the end of the claimed run.  Both
+ * are absolute (monotonically increasing) hand values; the buffer id is the
+ * value modulo NBuffers.
+ */
+static uint32 MyBatchPos = 0;
+static uint32 MyBatchEnd = 0;
+
+/*
+ * Number of clock-hand values a backend claims per atomic fetch-add,
+ * computed once at startup (see StrategyCtlShmemInit).  When batching is
+ * enabled it is one cache line's worth of hand advance, so concurrent
+ * backends sweep non-overlapping, cache-line-sized runs of the pool; the
+ * global sweep order is preserved (each buffer is still visited exactly once
+ * per pass).  Batching is enabled only on multi-node NUMA hardware; otherwise
+ * this stays 1 and the sweep is byte-identical to the stock clock.
+ */
+static uint32 ClockSweepBatchSize = 1;
+
 /*
  * ClockSweepTick - Helper routine for StrategyGetBuffer()
  *
- * Move the clock hand one buffer ahead of its current position and return the
- * id of the buffer now under the hand.
+ * Return the next buffer to consider for eviction.  Backends claim batches of
+ * consecutive buffer IDs from the shared clock hand, then iterate through
+ * them locally without further atomic operations.  This preserves the global
+ * sweep order while reducing contention on the shared counter.
  */
 static inline uint32
 ClockSweepTick(void)
 {
 	uint32		victim;
 
-	/*
-	 * Atomically move hand ahead one buffer - if there's several processes
-	 * doing this, this can lead to buffers being returned slightly out of
-	 * apparent order.
-	 */
-	victim =
-		pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer, 1);
-
-	if (victim >= NBuffers)
+	if (MyBatchPos >= MyBatchEnd)
 	{
-		uint32		originalVictim = victim;
-
-		/* always wrap what we look up in BufferDescriptors */
-		victim = victim % NBuffers;
-
 		/*
-		 * If we're the one that just caused a wraparound, force
-		 * completePasses to be incremented while holding the spinlock. We
-		 * need the spinlock so StrategySyncStart() can return a consistent
-		 * value consisting of nextVictimBuffer and completePasses.
+		 * Claim a fresh batch from the shared clock hand.  This is the only
+		 * atomic operation per batch, reducing contention by the batch size.
 		 */
-		if (victim == 0)
-		{
-			uint32		expected;
-			uint32		wrapped;
-			bool		success = false;
+		uint32		start;
+		uint32		batch_size = ClockSweepBatchSize;
 
-			expected = originalVictim + 1;
+		start = pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer,
+										batch_size);
 
-			while (!success)
-			{
-				/*
-				 * Acquire the spinlock while increasing completePasses. That
-				 * allows other readers to read nextVictimBuffer and
-				 * completePasses in a consistent manner which is required for
-				 * StrategySyncStart().  In theory delaying the increment
-				 * could lead to an overflow of nextVictimBuffers, but that's
-				 * highly unlikely and wouldn't be particularly harmful.
-				 */
-				SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
+		if (start >= (uint32) NBuffers)
+		{
+			start = start % NBuffers;
 
-				wrapped = expected % NBuffers;
+			/*
+			 * The counter has grown past NBuffers; try to wrap it back.  We
+			 * must hold the spinlock so StrategySyncStart() can read
+			 * nextVictimBuffer and completePasses consistently.
+			 *
+			 * With batching, multiple backends may each land a fetch-add
+			 * that returns a value past NBuffers in the same pass.  After
+			 * acquiring the spinlock we re-read the counter: if another
+			 * backend already wrapped it below NBuffers we are done.
+			 */
+			SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
+			{
+				uint32		current;
+				uint32		wrapped;
 
-				success = pg_atomic_compare_exchange_u32(&StrategyControl->nextVictimBuffer,
-														 &expected, wrapped);
-				if (success)
-					StrategyControl->completePasses++;
-				SpinLockRelease(&StrategyControl->buffer_strategy_lock);
+				current = pg_atomic_read_u32(&StrategyControl->nextVictimBuffer);
+				if (current >= (uint32) NBuffers)
+				{
+					wrapped = current % NBuffers;
+					if (pg_atomic_compare_exchange_u32(&StrategyControl->nextVictimBuffer,
+													   &current, wrapped))
+						StrategyControl->completePasses++;
+				}
 			}
+			SpinLockRelease(&StrategyControl->buffer_strategy_lock);
+		}
+		else if (start + batch_size > (uint32) NBuffers)
+		{
+			/*
+			 * The fetch-add returned a value below NBuffers, but this batch
+			 * spans the wrap point (start .. NBuffers-1 then 0 .. k are
+			 * iterated locally via "% NBuffers").  That crossing is a
+			 * complete pass too, but the wrap-and-increment path above only
+			 * fires when the fetch-add return is itself >= NBuffers, so
+			 * without this it would go uncounted and completePasses would
+			 * drift low (it feeds bgwriter pacing / StrategySyncStart, not
+			 * correctness).  The batch is capped at NBuffers, so a batch
+			 * spans the wrap at most once; count it under the spinlock for a
+			 * consistent (nextVictimBuffer, completePasses) pair.
+			 */
+			SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
+			StrategyControl->completePasses++;
+			SpinLockRelease(&StrategyControl->buffer_strategy_lock);
 		}
+
+		MyBatchPos = start;
+		MyBatchEnd = start + batch_size;
 	}
+
+	victim = MyBatchPos % NBuffers;
+	MyBatchPos++;
+
 	return victim;
 }
 
@@ -408,6 +451,29 @@ StrategyCtlShmemInit(void *arg)
 
 	/* No pending notification */
 	StrategyControl->bgwprocno = -1;
+
+	/*
+	 * Decide whether to batch the clock sweep.
+	 *
+	 * Batching claims a run of consecutive buffer IDs per atomic fetch-add so
+	 * concurrent backends touch the shared nextVictimBuffer cache line ~1/batch
+	 * as often -- a win only when that line actually bounces across sockets,
+	 * i.e. on multi-node NUMA hardware.  On a single socket the atomic is
+	 * already node-local and batching would only make backends skip ahead for
+	 * no benefit, so we fall back to batch size 1 there (byte-identical to the
+	 * stock one-buffer-at-a-time clock sweep).
+	 *
+	 * Batch (> 1) only when libnuma reports more than one node
+	 * (pg_numa_get_max_node() >= 1); pg_numa_init() returns -1 when NUMA is
+	 * unavailable.  (Benchmarking showed the win holds with or without huge
+	 * pages, so huge-page availability is intentionally not part of the gate.)
+	 */
+	if (pg_numa_init() != -1 &&
+		pg_numa_get_max_node() >= 1)
+		ClockSweepBatchSize = Min(PG_CACHE_LINE_SIZE / (uint32) sizeof(uint32),
+								  (uint32) NBuffers);
+	else
+		ClockSweepBatchSize = 1;
 }
 
 
-- 
2.50.1



  [text/x-patch] v7-0002-Replace-the-usage_count-clock-sweep-with-a-coolin.patch (30.4K, ../../8ffd4113-1d35-4d11-9d6b-4b49eac9985c@app.fastmail.com/3-v7-0002-Replace-the-usage_count-clock-sweep-with-a-coolin.patch)
  download | inline diff:
From 18a4cd1b6904f80a2574f8390378b6e1d9dc9f80 Mon Sep 17 00:00:00 2001
From: Greg Burd <greg@burd.me>
Date: Mon, 6 Jul 2026 19:22:45 -0400
Subject: [PATCH v7 2/3] Replace the usage_count clock sweep with a
 cooling-stage evictor

Replace the 0..5 usage_count buffer-replacement policy with a cooling-stage
clock (the LeanStore / 2Q-A1 model): a buffer is simply HOT (recently used)
or COOL (an eviction candidate), with "pinned" being the existing refcount.
There is no per-buffer access counter.

  - A demand-loaded page is admitted COOL (probationary), not HOT.  A second
    access via PinBuffer promotes it COOL -> HOT (the rescue).  So a page
    touched once -- a sequential scan -- fills and drains the COOL stage and
    is evicted from it without ever displacing the HOT working set.  Scan
    resistance is intrinsic to the replacement algorithm, which is what lets a
    later commit remove the BufferAccessStrategy ring buffers entirely.

  - The foreground sweep in StrategyGetBuffer() reclaims an already-COOL,
    unpinned buffer, pinning it with a CAS so a racing PinBuffer always wins.
    Promotion (COOL -> HOT) and demotion (HOT -> COOL) are single-bit
    transitions; only the eviction claim is a CAS.

  - The background writer maintains the supply of COOL victims.  As its LRU
    scan runs ahead of the clock hand it demotes HOT buffers to COOL, so the
    foreground finds a victim in a single pass rather than having to cool
    buffers itself.  The demotion is demand-driven -- bounded by the predicted
    allocation for the next cycle -- so it stages just enough COOL buffers
    without cooling the whole pool, and it is done under the buffer header
    lock the scan already holds.  A single second-chance reference bit
    (set by PinBuffer, cleared on the bgwriter's first pass over a HOT buffer)
    spares a recently-accessed buffer one cooling pass, keeping the genuinely
    hot set out of the COOL stage under scan pressure.  BgBufferSync also
    tracks the reusable-buffer density it directly observes on a shorter
    smoothing window, so a burst of probationary/scan COOL pages is followed
    promptly rather than averaged away.  Under a bulk-dirtying workload the
    per-cycle clean-write cap is raised from bgwriter_lru_maxpages to predicted
    demand so the bgwriter keeps supplying clean victims, rather than the
    foreground sweep having to flush dirty victims inline; normal workloads,
    where demand is below the cap, are unaffected.

The 4-bit usage_count field is reinterpreted in place: bit 0 is the HOT/COOL
state (BUF_COOLSTATE_ONE), bit 1 the reference bit (BUF_REFBIT).  The 64-bit
buffer-state layout -- refcount, flag and lock offsets and their StaticAsserts
-- is unchanged; only the meaning of the field and the instructions that touch
it change.  A StaticAssert requires the field to be at least 2 bits wide so a
future width change cannot push the reference bit into the flag bits.
BM_MAX_USAGE_COUNT becomes BUF_COOLSTATE_HOT (1), so the pin fast path
saturates at HOT.  Local (temp-table) buffers get the same two-state
treatment; being single-backend they need no background cooler.

Because the reference bit shares the field, the full 4-bit value can be 0..3;
readers that mean "the cooling state" must use BUF_STATE_GET_COOLSTATE(), which
masks to bit 0 and returns only 0 (COOL) or 1 (HOT), never the raw field.
contrib/pg_buffercache is updated accordingly: its usagecount column and the
pg_buffercache_summary average report the cooling state (0 = COOL, 1 = HOT),
and pg_buffercache_usage_counts() buckets on it.  Using the raw 4-bit getter
there would index its BM_MAX_USAGE_COUNT+1 = 2-element arrays with values up
to 3 and overrun the stack; masking to the cooling bit keeps the index in
range.  The reference bit is deliberately not exposed as usagecount.

Depends on the batched clock sweep from the previous commit.
---
 contrib/pg_buffercache/pg_buffercache_pages.c |   6 +-
 src/backend/storage/buffer/bufmgr.c           | 124 +++++++++++----
 src/backend/storage/buffer/freelist.c         | 141 ++++++++++++------
 src/backend/storage/buffer/localbuf.c         |  13 +-
 src/include/storage/buf_internals.h           |  63 +++++++-
 5 files changed, 260 insertions(+), 87 deletions(-)

diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index 5104559..b94e6f6 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -161,7 +161,7 @@ pg_buffercache_pages(PG_FUNCTION_ARGS)
 		reldatabase = bufHdr->tag.dbOid;
 		forknum = BufTagGetForkNum(&bufHdr->tag);
 		blocknum = bufHdr->tag.blockNum;
-		usagecount = BUF_STATE_GET_USAGECOUNT(buf_state);
+		usagecount = BUF_STATE_GET_COOLSTATE(buf_state);
 		pinning_backends = BUF_STATE_GET_REFCOUNT(buf_state);
 
 		if (buf_state & BM_DIRTY)
@@ -605,7 +605,7 @@ pg_buffercache_summary(PG_FUNCTION_ARGS)
 		if (buf_state & BM_VALID)
 		{
 			buffers_used++;
-			usagecount_total += BUF_STATE_GET_USAGECOUNT(buf_state);
+			usagecount_total += BUF_STATE_GET_COOLSTATE(buf_state);
 
 			if (buf_state & BM_DIRTY)
 				buffers_dirty++;
@@ -655,7 +655,7 @@ pg_buffercache_usage_counts(PG_FUNCTION_ARGS)
 
 		CHECK_FOR_INTERRUPTS();
 
-		usage_count = BUF_STATE_GET_USAGECOUNT(buf_state);
+		usage_count = BUF_STATE_GET_COOLSTATE(buf_state);
 		usage_counts[usage_count]++;
 
 		if (buf_state & BM_DIRTY)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 3908529..1f5a008 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -83,6 +83,7 @@
 /* Bits in SyncOneBuffer's return value */
 #define BUF_WRITTEN				0x01
 #define BUF_REUSABLE			0x02
+#define BUF_COOLED				0x04
 
 #define RELS_BSEARCH_THRESHOLD		20
 
@@ -634,7 +635,7 @@ static void PinBuffer_Locked(BufferDesc *buf);
 static void UnpinBuffer(BufferDesc *buf);
 static void UnpinBufferNoOwner(BufferDesc *buf);
 static void BufferSync(int flags);
-static int	SyncOneBuffer(int buf_id, bool skip_recently_used,
+static int	SyncOneBuffer(int buf_id, bool skip_recently_used, bool cool_if_hot,
 						  WritebackContext *wb_context);
 static void WaitIO(BufferDesc *buf);
 static void AbortBufferIO(Buffer buffer);
@@ -2333,7 +2334,10 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	 * checkpoints, except for their "init" forks, which need to be treated
 	 * just like permanent relations.
 	 */
-	set_bits |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
+	set_bits |= BM_TAG_VALID;
+	/* Admit the newly loaded page COOL (probation); a second access via
+	 * PinBuffer promotes it to HOT.  This is what makes a one-touch scan
+	 * self-evicting -- see the cooling-state notes in buf_internals.h. */
 	if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM)
 		set_bits |= BM_PERMANENT;
 
@@ -3002,7 +3006,9 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 
 			victim_buf_hdr->tag = tag;
 
-			set_bits |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
+			set_bits |= BM_TAG_VALID;
+			/* Admit COOL (probation); see the comment at the other admission
+			 * site and the cooling-state notes in buf_internals.h. */
 			if (bmr.relpersistence == RELPERSISTENCE_PERMANENT || fork == INIT_FORKNUM)
 				set_bits |= BM_PERMANENT;
 
@@ -3332,21 +3338,17 @@ PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy,
 			/* increase refcount */
 			buf_state += BUF_REFCOUNT_ONE;
 
-			if (strategy == NULL)
-			{
-				/* Default case: increase usagecount unless already max. */
-				if (BUF_STATE_GET_USAGECOUNT(buf_state) < BM_MAX_USAGE_COUNT)
-					buf_state += BUF_USAGECOUNT_ONE;
-			}
-			else
-			{
-				/*
-				 * Ring buffers shouldn't evict others from pool.  Thus we
-				 * don't make usagecount more than 1.
-				 */
-				if (BUF_STATE_GET_USAGECOUNT(buf_state) == 0)
-					buf_state += BUF_USAGECOUNT_ONE;
-			}
+			/*
+			 * Accessing a resident buffer promotes it to HOT (the 2Q rescue):
+			 * a page loaded COOL on probation becomes part of the hot working
+			 * set on its second touch.  BM_MAX_USAGE_COUNT is
+			 * BUF_COOLSTATE_HOT (1), so this saturates at HOT and never
+			 * overflows the field.  We also set the second-chance ref bit so
+			 * the bgwriter's next cooling pass spares this recently-used buffer.
+			 */
+			if (BUF_STATE_GET_COOLSTATE(buf_state) < BUF_COOLSTATE_HOT)
+				buf_state += BUF_COOLSTATE_ONE;
+			buf_state |= BUF_REFBIT;
 
 			if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
 											   buf_state))
@@ -3785,7 +3787,7 @@ BufferSync(int flags)
 		 */
 		if (pg_atomic_read_u64(&bufHdr->state) & BM_CHECKPOINT_NEEDED)
 		{
-			if (SyncOneBuffer(buf_id, false, &wb_context) & BUF_WRITTEN)
+			if (SyncOneBuffer(buf_id, false, false, &wb_context) & BUF_WRITTEN)
 			{
 				TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
 				PendingCheckpointerStats.buffers_written++;
@@ -3876,6 +3878,19 @@ BgBufferSync(WritebackContext *wb_context)
 	float		smoothing_samples = 16;
 	float		scan_whole_pool_milliseconds = 120000.0;
 
+	/*
+	 * The cleaner scan directly observes the reusable (COOL, unpinned) buffer
+	 * density over the region it walks, which -- with the cooling-stage
+	 * evictor -- is exactly the sweep's victim predicate.  That observation is
+	 * ground truth for the buffers about to be reused, whereas the strategy
+	 * scan's positional proxy (strategy_delta/recent_alloc) blurs a pool whose
+	 * COOL population is spatially clustered (a scan burst leaves whole regions
+	 * COOL, hot OLTP regions not).  So we let the cleaner's own sample adapt on
+	 * a shorter window than the strategy proxy, tracking a burst of
+	 * probationary/scan COOL pages within a cycle or two instead of lagging it.
+	 */
+	float		cleaner_smoothing_samples = 4;
+
 	/* Used to compute how far we scan ahead */
 	long		strategy_delta;
 	int			bufs_to_lap;
@@ -3889,6 +3904,7 @@ BgBufferSync(WritebackContext *wb_context)
 	int			num_to_scan;
 	int			num_written;
 	int			reusable_buffers;
+	int			write_limit;
 
 	/* Variables for final smoothed_density update */
 	long		new_strategy_delta;
@@ -4063,8 +4079,22 @@ BgBufferSync(WritebackContext *wb_context)
 	 * Now write out dirty reusable buffers, working forward from the
 	 * next_to_clean point, until we have lapped the strategy scan, or cleaned
 	 * enough buffers to match our estimate of the next cycle's allocation
-	 * requirements, or hit the bgwriter_lru_maxpages limit.
-	 */
+	 * requirements, or hit the write limit.
+	 *
+	 * The per-cycle write cap is normally bgwriter_lru_maxpages.  But under a
+	 * bulk-dirtying workload (COPY, bulk UPDATE, VACUUM) the pool fills with
+	 * dirty COOL buffers faster than that fixed cap can clean, so the
+	 * foreground clock sweep is forced to flush dirty victims inline -- the
+	 * very cost the cooling-stage evictor is meant to keep off the critical
+	 * path.  When predicted demand (upcoming_alloc_est) exceeds the fixed cap,
+	 * raise the limit to meet demand so the bgwriter stays ahead and supplies
+	 * clean victims.  This stays bounded (by demand and by lapping the
+	 * strategy point), so it cannot run away, and normal workloads are
+	 * unaffected because there upcoming_alloc_est <= bgwriter_lru_maxpages.
+	 */
+	write_limit = bgwriter_lru_maxpages;
+	if (upcoming_alloc_est > write_limit)
+		write_limit = upcoming_alloc_est;
 
 	num_to_scan = bufs_to_lap;
 	num_written = 0;
@@ -4073,7 +4103,7 @@ BgBufferSync(WritebackContext *wb_context)
 	/* Execute the LRU scan */
 	while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
 	{
-		int			sync_state = SyncOneBuffer(next_to_clean, true,
+		int			sync_state = SyncOneBuffer(next_to_clean, true, true,
 											   wb_context);
 
 		if (++next_to_clean >= NBuffers)
@@ -4086,7 +4116,7 @@ BgBufferSync(WritebackContext *wb_context)
 		if (sync_state & BUF_WRITTEN)
 		{
 			reusable_buffers++;
-			if (++num_written >= bgwriter_lru_maxpages)
+			if (++num_written >= write_limit)
 			{
 				PendingBgWriterStats.maxwritten_clean++;
 				break;
@@ -4121,7 +4151,7 @@ BgBufferSync(WritebackContext *wb_context)
 	{
 		scans_per_alloc = (float) new_strategy_delta / (float) new_recent_alloc;
 		smoothed_density += (scans_per_alloc - smoothed_density) /
-			smoothing_samples;
+			cleaner_smoothing_samples;
 
 #ifdef BGW_DEBUG
 		elog(DEBUG2, "bgwriter: cleaner density alloc=%u scan=%ld density=%.2f new smoothed=%.2f",
@@ -4140,16 +4170,26 @@ BgBufferSync(WritebackContext *wb_context)
  * If skip_recently_used is true, we don't write currently-pinned buffers, nor
  * buffers marked recently used, as these are not replacement candidates.
  *
+ * If cool_if_hot is true (the bgwriter's LRU scan), an unpinned HOT buffer is
+ * demoted HOT -> COOL as we pass it, pre-staging eviction candidates so the
+ * foreground clock sweep finds a COOL victim in a single pass instead of
+ * having to cool buffers itself (force_cool).  The demotion is done under the
+ * buffer header lock we already hold, so it needs no CAS and cannot race a
+ * concurrent demotion.  A concurrent PinBuffer promotes it back to HOT, which
+ * is the intended 2Q behavior (a re-accessed buffer is rescued).
+ *
  * Returns a bitmask containing the following flag bits:
  *	BUF_WRITTEN: we wrote the buffer.
  *	BUF_REUSABLE: buffer is available for replacement, ie, it has
- *		pin count 0 and usage count 0.
+ *		pin count 0 and is COOL (an eviction candidate).
+ *	BUF_COOLED: we demoted this buffer HOT -> COOL this call.
  *
  * (BUF_WRITTEN could be set in error if FlushBuffer finds the buffer clean
  * after locking it, but we don't care all that much.)
  */
 static int
-SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
+SyncOneBuffer(int buf_id, bool skip_recently_used, bool cool_if_hot,
+			  WritebackContext *wb_context)
 {
 	BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
 	int			result = 0;
@@ -4171,8 +4211,38 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 	 */
 	buf_state = LockBufHdr(bufHdr);
 
+	/*
+	 * Pre-cool with a second chance: if asked, act on an unpinned HOT buffer.
+	 * If its ref bit is set (accessed since our last pass), clear the ref bit
+	 * and leave it HOT -- a recently-used buffer earns one reprieve, keeping
+	 * the hot working set out of the COOL stage under scan pressure.  Only a
+	 * HOT buffer whose ref bit is already clear is demoted HOT -> COOL,
+	 * pre-staging it as an eviction candidate for the foreground sweep.  We
+	 * hold the header lock, so each transition is a plain masked store applied
+	 * atomically by UnlockBufHdrExt; we re-lock to continue the dirty-write
+	 * inspection below.
+	 */
+	if (cool_if_hot &&
+		BUF_STATE_GET_REFCOUNT(buf_state) == 0 &&
+		BUF_STATE_GET_COOLSTATE(buf_state) != BUF_COOLSTATE_COOL)
+	{
+		if (BUF_STATE_GET_REFBIT(buf_state))
+		{
+			/* second chance: consume the ref bit, stay HOT */
+			UnlockBufHdrExt(bufHdr, buf_state, 0, BUF_REFBIT, 0);
+			buf_state = LockBufHdr(bufHdr);
+		}
+		else
+		{
+			/* not re-accessed since last pass: demote to COOL */
+			UnlockBufHdrExt(bufHdr, buf_state, 0, BUF_USAGECOUNT_MASK, 0);
+			buf_state = LockBufHdr(bufHdr);
+			result |= BUF_COOLED;
+		}
+	}
+
 	if (BUF_STATE_GET_REFCOUNT(buf_state) == 0 &&
-		BUF_STATE_GET_USAGECOUNT(buf_state) == 0)
+		BUF_STATE_GET_COOLSTATE(buf_state) == BUF_COOLSTATE_COOL)
 	{
 		result |= BUF_REUSABLE;
 	}
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index 46104b3..e8cf13f 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -49,6 +49,16 @@ typedef struct
 	uint32		completePasses; /* Complete cycles of the clock-sweep */
 	pg_atomic_uint32 numBufferAllocs;	/* Buffers allocated since last reset */
 
+	/*
+	 * Number of clock-hand values a backend claims per atomic fetch-add.
+	 * Computed once at startup (see StrategyCtlShmemInit).  Kept in shared
+	 * memory rather than a backend-local static so that EXEC_BACKEND children
+	 * (which do not inherit the postmaster's statics) see the same value; a
+	 * backend-local copy would silently reset to 1 there, disabling batching
+	 * on Windows.
+	 */
+	uint32		batchSize;
+
 	/*
 	 * Bgworker process to be notified upon activity or -1 if none. See
 	 * StrategyNotifyBgWriter.
@@ -113,17 +123,6 @@ static void AddBufferToRing(BufferAccessStrategy strategy,
 static uint32 MyBatchPos = 0;
 static uint32 MyBatchEnd = 0;
 
-/*
- * Number of clock-hand values a backend claims per atomic fetch-add,
- * computed once at startup (see StrategyCtlShmemInit).  When batching is
- * enabled it is one cache line's worth of hand advance, so concurrent
- * backends sweep non-overlapping, cache-line-sized runs of the pool; the
- * global sweep order is preserved (each buffer is still visited exactly once
- * per pass).  Batching is enabled only on multi-node NUMA hardware; otherwise
- * this stays 1 and the sweep is byte-identical to the stock clock.
- */
-static uint32 ClockSweepBatchSize = 1;
-
 /*
  * ClockSweepTick - Helper routine for StrategyGetBuffer()
  *
@@ -144,7 +143,7 @@ ClockSweepTick(void)
 		 * atomic operation per batch, reducing contention by the batch size.
 		 */
 		uint32		start;
-		uint32		batch_size = ClockSweepBatchSize;
+		uint32		batch_size = StrategyControl->batchSize;
 
 		start = pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer,
 										batch_size);
@@ -229,6 +228,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 	BufferDesc *buf;
 	int			bgwprocno;
 	int			trycounter;
+	bool		force_cool;
 
 	*from_ring = false;
 
@@ -279,12 +279,32 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 	 */
 	pg_atomic_fetch_add_u32(&StrategyControl->numBufferAllocs, 1);
 
-	/* Use the "clock sweep" algorithm to find a free buffer */
+	/*
+	 * Use the cooling-stage clock sweep to find a victim.
+	 *
+	 * A buffer is HOT (recently used) or COOL (an eviction candidate).  We
+	 * prefer to reclaim an already-COOL buffer and demote a HOT buffer to
+	 * COOL only once a full sweep has found no COOL victim (force_cool) -- so
+	 * an abundant supply of COOL/probationary pages (e.g. a scan) is drained
+	 * before the hot working set is cooled.  Newly loaded pages are admitted
+	 * COOL (see BufferAlloc), so scan resistance falls out of the algorithm.
+	 *
+	 * trycounter bounds the search.  Any tick that does not produce a victim
+	 * and does not make progress -- a pinned buffer, or a HOT buffer skipped
+	 * on a prefer-COOL pass -- decrements it.  Cooling a HOT buffer (under
+	 * force_cool) is progress and resets it.  When a full pass (NBuffers)
+	 * makes no progress we escalate to force_cool so the next pass cools HOT
+	 * buffers into victims; if a force_cool pass ALSO makes no progress every
+	 * buffer is pinned and we fail, matching the stock "no unpinned buffers
+	 * available" contract.
+	 */
 	trycounter = NBuffers;
+	force_cool = false;
 	for (;;)
 	{
 		uint64		old_buf_state;
 		uint64		local_buf_state;
+		bool		no_progress = false;
 
 		buf = GetBufferDescriptor(ClockSweepTick());
 
@@ -297,25 +317,10 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 		{
 			local_buf_state = old_buf_state;
 
-			/*
-			 * If the buffer is pinned or has a nonzero usage_count, we cannot
-			 * use it; decrement the usage_count (unless pinned) and keep
-			 * scanning.
-			 */
-
+			/* If the buffer is pinned we cannot use it; keep scanning. */
 			if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0)
 			{
-				if (--trycounter == 0)
-				{
-					/*
-					 * We've scanned all the buffers without making any state
-					 * changes, so all the buffers are pinned (or were when we
-					 * looked at them). We could hope that someone will free
-					 * one eventually, but it's probably better to fail than
-					 * to risk getting stuck in an infinite loop.
-					 */
-					elog(ERROR, "no unpinned buffers available");
-				}
+				no_progress = true;
 				break;
 			}
 
@@ -326,20 +331,56 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 				continue;
 			}
 
-			if (BUF_STATE_GET_USAGECOUNT(local_buf_state) != 0)
+			if (BUF_STATE_GET_COOLSTATE(local_buf_state) != BUF_COOLSTATE_COOL)
 			{
-				local_buf_state -= BUF_USAGECOUNT_ONE;
+				/*
+				 * HOT buffer.  Prefer a COOL victim: on a normal pass just
+				 * advance the hand (no progress).  Under force_cool apply the
+				 * same second-chance rule the bgwriter's pre-cooling uses: a
+				 * HOT buffer whose ref bit is set (recently accessed) has the
+				 * ref bit cleared and is left HOT this pass; only a HOT buffer
+				 * whose ref bit is already clear is demoted HOT -> COOL.  This
+				 * keeps a just-touched buffer from being cooled the instant the
+				 * bgwriter falls behind and the foreground has to cool for
+				 * itself.  Either transition is progress toward a victim.
+				 */
+				if (!force_cool)
+				{
+					no_progress = true;
+					break;			/* advance the hand, look for COOL */
+				}
+
+				if (BUF_STATE_GET_REFBIT(local_buf_state))
+					local_buf_state &= ~BUF_REFBIT;			/* second chance: clear ref, stay HOT */
+				else
+					local_buf_state &= ~BUF_USAGECOUNT_MASK;	/* HOT -> COOL, clear ref bit */
 
 				if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
 												   local_buf_state))
 				{
+					/*
+					 * Making a cooling-state transition is progress toward a
+					 * victim, so reset the counter.  Stay in force_cool: we made
+					 * a transition but have not yet produced a reclaimable COOL
+					 * victim (a ref-clear leaves the buffer HOT; a demote leaves
+					 * it COOL for a later tick to claim), and dropping out here
+					 * would waste a full no-progress pass re-escalating.  An
+					 * all-HOT, all-recently-referenced pool thus takes up to ~3
+					 * full passes to yield a victim (discover no COOL, clear ref
+					 * bits, cool + reclaim) -- still within the stock clock's
+					 * worst case (up to BM_MAX_USAGE_COUNT+1 passes), and in
+					 * practice the bgwriter pre-cooling keeps a COOL victim
+					 * available so force_cool rarely fires at all.  force_cool
+					 * ends naturally once a COOL victim is found and returned
+					 * below.
+					 */
 					trycounter = NBuffers;
 					break;
 				}
 			}
 			else
 			{
-				/* pin the buffer if the CAS succeeds */
+				/* COOL and unpinned: claim it.  Pin if the CAS succeeds. */
 				local_buf_state += BUF_REFCOUNT_ONE;
 
 				if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
@@ -356,6 +397,21 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 				}
 			}
 		}
+
+		/*
+		 * A tick that made no progress toward a victim counts down trycounter.
+		 * A full unproductive pass escalates to force_cool (cool HOT buffers
+		 * into victims); a second unproductive full pass means everything is
+		 * pinned, so fail rather than spin forever.  (A failed CAS above is
+		 * neither progress nor a full miss: we simply retry the same buffer.)
+		 */
+		if (no_progress && --trycounter == 0)
+		{
+			if (force_cool)
+				elog(ERROR, "no unpinned buffers available");
+			force_cool = true;
+			trycounter = NBuffers;
+		}
 	}
 }
 
@@ -470,10 +526,10 @@ StrategyCtlShmemInit(void *arg)
 	 */
 	if (pg_numa_init() != -1 &&
 		pg_numa_get_max_node() >= 1)
-		ClockSweepBatchSize = Min(PG_CACHE_LINE_SIZE / (uint32) sizeof(uint32),
-								  (uint32) NBuffers);
+		StrategyControl->batchSize = Min(PG_CACHE_LINE_SIZE / (uint32) sizeof(uint32),
+										 (uint32) NBuffers);
 	else
-		ClockSweepBatchSize = 1;
+		StrategyControl->batchSize = 1;
 }
 
 
@@ -721,14 +777,13 @@ GetBufferFromRing(BufferAccessStrategy strategy, uint64 *buf_state)
 		/*
 		 * If the buffer is pinned we cannot use it under any circumstances.
 		 *
-		 * If usage_count is 0 or 1 then the buffer is fair game (we expect 1,
-		 * since our own previous usage of the ring element would have left it
-		 * there, but it might've been decremented by clock-sweep since then).
-		 * A higher usage_count indicates someone else has touched the buffer,
-		 * so we shouldn't re-use it.
+		 * With the cooling-state replacement the field holds only COOL or HOT,
+		 * so the stock "usage_count > 1 means another backend touched it"
+		 * heuristic no longer applies: a ring element is reusable whenever it
+		 * is unpinned.  (The whole ring mechanism is removed in a later patch;
+		 * scan resistance is now intrinsic to the sweep.)
 		 */
-		if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0
-			|| BUF_STATE_GET_USAGECOUNT(local_buf_state) > 1)
+		if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0)
 			break;
 
 		/* See equivalent code in PinBuffer() */
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 4870c8e..21c0809 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -167,7 +167,7 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
 
 		buf_state = pg_atomic_read_u64(&bufHdr->state);
 		buf_state &= ~(BUF_FLAG_MASK | BUF_USAGECOUNT_MASK);
-		buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
+		buf_state |= BM_TAG_VALID;	/* admit COOL (probation) */
 		pg_atomic_unlocked_write_u64(&bufHdr->state, buf_state);
 
 		*foundPtr = false;
@@ -248,9 +248,10 @@ GetLocalVictimBuffer(void)
 		{
 			uint64		buf_state = pg_atomic_read_u64(&bufHdr->state);
 
-			if (BUF_STATE_GET_USAGECOUNT(buf_state) > 0)
+			if (BUF_STATE_GET_COOLSTATE(buf_state) != BUF_COOLSTATE_COOL)
 			{
-				buf_state -= BUF_USAGECOUNT_ONE;
+				/* HOT: give it a second chance, cool it and keep scanning. */
+				buf_state -= BUF_COOLSTATE_ONE;
 				pg_atomic_unlocked_write_u64(&bufHdr->state, buf_state);
 				trycounter = NLocBuffer;
 			}
@@ -454,7 +455,7 @@ ExtendBufferedRelLocal(BufferManagerRelation bmr,
 
 			victim_buf_hdr->tag = tag;
 
-			buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
+			buf_state |= BM_TAG_VALID;	/* admit COOL (probation) */
 
 			pg_atomic_unlocked_write_u64(&victim_buf_hdr->state, buf_state);
 
@@ -839,9 +840,9 @@ PinLocalBuffer(BufferDesc *buf_hdr, bool adjust_usagecount)
 		NLocalPinnedBuffers++;
 		buf_state += BUF_REFCOUNT_ONE;
 		if (adjust_usagecount &&
-			BUF_STATE_GET_USAGECOUNT(buf_state) < BM_MAX_USAGE_COUNT)
+			BUF_STATE_GET_COOLSTATE(buf_state) < BUF_COOLSTATE_HOT)
 		{
-			buf_state += BUF_USAGECOUNT_ONE;
+			buf_state += BUF_COOLSTATE_ONE;
 		}
 		pg_atomic_unlocked_write_u64(&buf_hdr->state, buf_state);
 
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index e4ff561..131df12 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -67,6 +67,50 @@ StaticAssertDecl(BUF_REFCOUNT_BITS + BUF_USAGECOUNT_BITS + BUF_FLAG_BITS + BUF_L
 #define BUF_USAGECOUNT_ONE \
 	(UINT64CONST(1) << BUF_REFCOUNT_BITS)
 
+/*
+ * Cooling state (LeanStore / 2Q-A1 cooling-stage clock sweep).
+ *
+ * The field historically used for the 0..5 usage_count now holds a single
+ * cooling-state bit: HOT (recently accessed, not an eviction candidate) or
+ * COOL (an eviction candidate).  We reuse BUF_USAGECOUNT_ONE as the unit so
+ * the buffer-state bit geography -- refcount, flag, and lock offsets, and the
+ * 64-bit StaticAsserts -- is unchanged; only the meaning of the field and the
+ * instructions that touch it change.
+ *
+ * A demand-loaded page is admitted COOL (probation); a second access promotes
+ * it to HOT (the rescue).  The sweep prefers COOL victims and demotes HOT to
+ * COOL only when a full pass finds no COOL victim.  So a one-touch scan fills
+ * and drains the COOL stage without displacing the HOT working set -- scan
+ * resistance intrinsic to the replacement algorithm.
+ */
+#define BUF_COOLSTATE_COOL	0
+#define BUF_COOLSTATE_HOT	1
+#define BUF_COOLSTATE_ONE	BUF_USAGECOUNT_ONE
+
+/*
+ * Second-chance reference bit, bit 1 of the (former usagecount) field, one
+ * position above the cooling-state bit.  PinBuffer sets it on every access.
+ * The bgwriter's pre-cooling gives a HOT buffer a second chance: the first
+ * time it passes a HOT buffer whose ref bit is set, it clears the ref bit and
+ * leaves the buffer HOT; only a HOT buffer whose ref bit is already clear (not
+ * re-accessed since the previous bgwriter pass) is demoted to COOL.  This
+ * keeps genuinely-hot pages out of the COOL stage (protecting the working set
+ * from being cooled under scan pressure) while leaving the foreground sweep a
+ * single-pass search over the pre-staged COOL buffers.  A separate bit (not a
+ * count) so it stays a plain masked store under the header lock.
+ */
+#define BUF_REFBIT			(UINT64CONST(2) << BUF_REFCOUNT_BITS)
+#define BUF_STATE_GET_REFBIT(state)	(((state) & BUF_REFBIT) != 0)
+
+/*
+ * The cooling state occupies bit 0 and the reference bit occupies bit 1 of
+ * the (former usagecount) field, so the field must be at least 2 bits wide.
+ * Assert it here so a future change to BUF_USAGECOUNT_BITS cannot silently
+ * push BUF_REFBIT up into the flag bits.
+ */
+StaticAssertDecl(BUF_USAGECOUNT_BITS >= 2,
+				 "cooling state + reference bit need at least 2 bits in the usagecount field");
+
 /* flags related definitions */
 #define BUF_FLAG_SHIFT \
 	(BUF_REFCOUNT_BITS + BUF_USAGECOUNT_BITS)
@@ -92,6 +136,11 @@ StaticAssertDecl(BUF_REFCOUNT_BITS + BUF_USAGECOUNT_BITS + BUF_FLAG_BITS + BUF_L
 #define BUF_STATE_GET_USAGECOUNT(state) \
 	((uint32)(((state) & BUF_USAGECOUNT_MASK) >> BUF_USAGECOUNT_SHIFT))
 
+/* Cooling state (HOT/COOL) from buffer state -- bit 0 of the field only, so
+ * the second-chance ref bit (bit 1) does not perturb the HOT/COOL test. */
+#define BUF_STATE_GET_COOLSTATE(state) \
+	((uint32) (((state) >> BUF_USAGECOUNT_SHIFT) & 1))
+
 /*
  * Flags for buffer descriptors
  *
@@ -134,17 +183,15 @@ StaticAssertDecl(MAX_BACKENDS_BITS <= (BUF_LOCK_BITS - 2),
 
 
 /*
- * The maximum allowed value of usage_count represents a tradeoff between
- * accuracy and speed of the clock-sweep buffer management algorithm.  A
- * large value (comparable to NBuffers) would approximate LRU semantics.
- * But it can take as many as BM_MAX_USAGE_COUNT+1 complete cycles of the
- * clock-sweep hand to find a free buffer, so in practice we don't want the
- * value to be very large.
+ * The cooling state is a single bit (HOT/COOL); the maximum value stored in
+ * the field is therefore BUF_COOLSTATE_HOT.  Retained under the historical
+ * name BM_MAX_USAGE_COUNT so the pin fast path ("promote unless already at
+ * max") reads naturally.
  */
-#define BM_MAX_USAGE_COUNT	5
+#define BM_MAX_USAGE_COUNT	BUF_COOLSTATE_HOT
 
 StaticAssertDecl(BM_MAX_USAGE_COUNT < (UINT64CONST(1) << BUF_USAGECOUNT_BITS),
-				 "BM_MAX_USAGE_COUNT doesn't fit in BUF_USAGECOUNT_BITS bits");
+				 "cooling state doesn't fit in BUF_USAGECOUNT_BITS bits");
 
 /*
  * Buffer tag identifies which disk block the buffer contains.
-- 
2.50.1



  [text/x-patch] v7-0003-Remove-BufferAccessStrategy-scan-resistance-is-no.patch (180.5K, ../../8ffd4113-1d35-4d11-9d6b-4b49eac9985c@app.fastmail.com/4-v7-0003-Remove-BufferAccessStrategy-scan-resistance-is-no.patch)
  download | inline diff:
From d605c07cc755ba130045d811c2e577d443b96c40 Mon Sep 17 00:00:00 2001
From: Greg Burd <greg@burd.me>
Date: Tue, 7 Jul 2026 08:45:39 -0400
Subject: [PATCH v7 3/3] Remove BufferAccessStrategy; scan resistance is now
 intrinsic

The cooling-stage evictor admits demand-loaded pages COOL and promotes them to
HOT only on a second access, so a one-touch sequential scan fills and drains
the COOL stage without displacing the hot working set.  Scan resistance is
therefore a property of the replacement algorithm itself, and the
BufferAccessStrategy ring buffers that previously provided it are dead weight.
Remove them end to end.

Deleted:
  - the BufferAccessStrategy type and the BufferAccessStrategyType enum
    (BAS_NORMAL/BULKREAD/BULKWRITE/VACUUM);
  - the ring machinery in freelist.c (GetAccessStrategy[WithSize],
    GetAccessStrategyBufferCount, GetAccessStrategyPinLimit,
    FreeAccessStrategy, GetBufferFromRing, AddBufferToRing,
    StrategyRejectBuffer, IOContextForStrategy);
  - the strategy parameter from ReadBufferExtended, ReadBufferWithoutRelcache,
    the ExtendBufferedRel* family, StrategyGetBuffer, read_stream_begin_*,
    and every scan/vacuum/analyze/index-AM caller;
  - the strategy fields on HeapScanDescData, IndexScanDescData,
    BulkInsertStateData, ReadBuffersOperation, and ReadStream;
  - _hash_getbuf_with_strategy (identical to _hash_getbuf without a strategy).

pg_stat_io's per-strategy IO contexts collapse: IOCONTEXT_BULKREAD,
IOCONTEXT_BULKWRITE and IOCONTEXT_VACUUM are removed, leaving IOCONTEXT_INIT
and IOCONTEXT_NORMAL.  IOOP_REUSE only ever occurred while recycling a ring
buffer, so it is no longer tracked; GetVictimBuffer counts IOOP_EVICT only.

The vacuum_buffer_usage_limit GUC and the VACUUM/ANALYZE (BUFFER_USAGE_LIMIT
...) option are removed, along with the VacuumBufferUsageLimit global, the
ring-size plumbing through VacuumParams and parallel vacuum, and vacuumdb's
--buffer-usage-limit client option.  read_stream's per-backend pin budget is
now enforced solely by GetPinLimit()/GetLocalPinLimit(), which already applied
and is unchanged for the (formerly universal) no-strategy case.

Documentation and the stats/amcheck regression tests are updated to drop the
removed contexts and options.
---
 contrib/amcheck/expected/check_heap.out       |  16 +-
 contrib/amcheck/sql/check_heap.sql            |  16 +-
 contrib/amcheck/verify_gin.c                  |  18 +-
 contrib/amcheck/verify_heapam.c               |   3 -
 contrib/amcheck/verify_nbtree.c               |  11 +-
 contrib/bloom/blscan.c                        |   8 -
 contrib/bloom/blutils.c                       |   4 +-
 contrib/bloom/blvacuum.c                      |   2 -
 contrib/pageinspect/rawpage.c                 |   2 +-
 contrib/pg_prewarm/autoprewarm.c              |   1 -
 contrib/pg_prewarm/pg_prewarm.c               |   1 -
 contrib/pg_visibility/pg_visibility.c         |   7 +-
 contrib/pgstattuple/pgstatapprox.c            |   3 -
 contrib/pgstattuple/pgstatindex.c             |   9 +-
 contrib/pgstattuple/pgstattuple.c             |  37 +-
 doc/src/sgml/config.sgml                      |  30 --
 doc/src/sgml/monitoring.sgml                  |  38 +-
 doc/src/sgml/ref/analyze.sgml                 |  21 -
 doc/src/sgml/ref/vacuum.sgml                  |  25 -
 src/backend/access/brin/brin.c                |  11 +-
 src/backend/access/brin/brin_revmap.c         |   2 +-
 src/backend/access/gin/gininsert.c            |   4 +-
 src/backend/access/gin/ginutil.c              |   2 +-
 src/backend/access/gin/ginvacuum.c            |  17 +-
 src/backend/access/gist/gist.c                |   2 +-
 src/backend/access/gist/gistutil.c            |   2 +-
 src/backend/access/gist/gistvacuum.c          |  10 +-
 src/backend/access/hash/hash.c                |  13 +-
 src/backend/access/hash/hashovfl.c            |  55 +--
 src/backend/access/hash/hashpage.c            |  40 +-
 src/backend/access/heap/heapam.c              |  31 +-
 src/backend/access/heap/heapam_handler.c      |   4 +-
 src/backend/access/heap/hio.c                 |  13 +-
 src/backend/access/heap/vacuumlazy.c          |  22 +-
 src/backend/access/heap/visibilitymap.c       |   4 +-
 src/backend/access/nbtree/nbtpage.c           |   2 +-
 src/backend/access/nbtree/nbtree.c            |   4 +-
 src/backend/access/spgist/spgutils.c          |   2 +-
 src/backend/access/spgist/spgvacuum.c         |   4 +-
 src/backend/access/transam/xloginsert.c       |   2 +-
 src/backend/access/transam/xlogutils.c        |   3 +-
 src/backend/catalog/index.c                   |   1 -
 src/backend/commands/analyze.c                |   7 +-
 src/backend/commands/dbcommands.c             |   6 +-
 src/backend/commands/repack.c                 |   2 +-
 src/backend/commands/sequence.c               |   2 +-
 src/backend/commands/vacuum.c                 | 118 +----
 src/backend/commands/vacuumparallel.c         |  21 +-
 src/backend/postmaster/autovacuum.c           |  29 +-
 src/backend/postmaster/datachecksum_state.c   |  33 +-
 src/backend/storage/aio/read_stream.c         |  17 +-
 src/backend/storage/buffer/README             |  44 --
 src/backend/storage/buffer/bufmgr.c           | 185 +++-----
 src/backend/storage/buffer/freelist.c         | 434 +-----------------
 src/backend/storage/freespace/freespace.c     |   4 +-
 src/backend/storage/smgr/md.c                 |  11 +-
 src/backend/utils/activity/pgstat_io.c        |  60 +--
 src/backend/utils/init/globals.c              |   1 -
 src/backend/utils/misc/guc_parameters.dat     |  10 -
 src/backend/utils/misc/postgresql.conf.sample |   3 -
 src/bin/scripts/vacuumdb.c                    |  13 -
 src/bin/scripts/vacuuming.c                   |  21 -
 src/bin/scripts/vacuuming.h                   |   1 -
 src/include/access/genam.h                    |   1 -
 src/include/access/hash.h                     |   9 +-
 src/include/access/heapam.h                   |   4 +-
 src/include/access/hio.h                      |   1 -
 src/include/access/tableam.h                  |   8 +-
 src/include/commands/vacuum.h                 |   8 +-
 src/include/miscadmin.h                       |   9 -
 src/include/pgstat.h                          |   5 +-
 src/include/storage/buf.h                     |   7 -
 src/include/storage/buf_internals.h           |   6 +-
 src/include/storage/bufmgr.h                  |  32 +-
 src/include/storage/read_stream.h             |   5 +-
 src/include/utils/guc_hooks.h                 |   2 -
 src/test/modules/test_aio/test_aio.c          |   7 +-
 src/test/regress/expected/stats.out           | 107 +----
 src/test/regress/expected/vacuum.out          |  19 -
 src/test/regress/sql/stats.sql                |  56 +--
 src/test/regress/sql/vacuum.sql               |  15 -
 src/tools/pgindent/typedefs.list              |   2 -
 82 files changed, 268 insertions(+), 1559 deletions(-)

diff --git a/contrib/amcheck/expected/check_heap.out b/contrib/amcheck/expected/check_heap.out
index 979e5e8..569b020 100644
--- a/contrib/amcheck/expected/check_heap.out
+++ b/contrib/amcheck/expected/check_heap.out
@@ -67,11 +67,11 @@ INSERT INTO heaptest (a, b)
 	(SELECT gs, repeat('x', gs)
 		FROM generate_series(1,50) gs);
 -- pg_stat_io test:
--- verify_heapam always uses a BAS_BULKREAD BufferAccessStrategy, whereas a
--- sequential scan does so only if the table is large enough when compared to
--- shared buffers (see initscan()). CREATE DATABASE ... also unconditionally
--- uses a BAS_BULKREAD strategy, but we have chosen to use a tablespace and
--- verify_heapam to provide coverage instead of adding another expensive
+-- verify_heapam reads the heap through the buffer manager; with the
+-- cooling-stage clock sweep there are no per-strategy IO contexts, so the
+-- reads are counted in the 'normal' context.  CREATE DATABASE ... likewise
+-- reads through the normal context, but we have chosen to use a tablespace
+-- and verify_heapam to provide coverage instead of adding another expensive
 -- operation to the main regression test suite.
 --
 -- Create an alternative tablespace and move the heaptest table to it, causing
@@ -83,7 +83,7 @@ INSERT INTO heaptest (a, b)
 SET allow_in_place_tablespaces = true;
 CREATE TABLESPACE regress_test_stats_tblspc LOCATION '';
 SELECT sum(reads) AS stats_bulkreads_before
-  FROM pg_stat_io WHERE context = 'bulkread' \gset
+  FROM pg_stat_io WHERE context = 'normal' AND object = 'relation' \gset
 BEGIN;
 ALTER TABLE heaptest SET TABLESPACE regress_test_stats_tblspc;
 -- Check that valid options are not rejected nor corruption reported
@@ -111,7 +111,7 @@ SELECT * FROM verify_heapam(relation := 'heaptest', startblock := 0, endblock :=
 COMMIT;
 -- verify_heapam should have read in the page written out by
 --   ALTER TABLE ... SET TABLESPACE ...
--- causing an additional bulkread, which should be reflected in pg_stat_io.
+-- causing additional reads, which should be reflected in pg_stat_io.
 SELECT pg_stat_force_next_flush();
  pg_stat_force_next_flush 
 --------------------------
@@ -119,7 +119,7 @@ SELECT pg_stat_force_next_flush();
 (1 row)
 
 SELECT sum(reads) AS stats_bulkreads_after
-  FROM pg_stat_io WHERE context = 'bulkread' \gset
+  FROM pg_stat_io WHERE context = 'normal' AND object = 'relation' \gset
 SELECT :stats_bulkreads_after > :stats_bulkreads_before;
  ?column? 
 ----------
diff --git a/contrib/amcheck/sql/check_heap.sql b/contrib/amcheck/sql/check_heap.sql
index 1745bae..61f710d 100644
--- a/contrib/amcheck/sql/check_heap.sql
+++ b/contrib/amcheck/sql/check_heap.sql
@@ -27,11 +27,11 @@ INSERT INTO heaptest (a, b)
 		FROM generate_series(1,50) gs);
 
 -- pg_stat_io test:
--- verify_heapam always uses a BAS_BULKREAD BufferAccessStrategy, whereas a
--- sequential scan does so only if the table is large enough when compared to
--- shared buffers (see initscan()). CREATE DATABASE ... also unconditionally
--- uses a BAS_BULKREAD strategy, but we have chosen to use a tablespace and
--- verify_heapam to provide coverage instead of adding another expensive
+-- verify_heapam reads the heap through the buffer manager; with the
+-- cooling-stage clock sweep there are no per-strategy IO contexts, so the
+-- reads are counted in the 'normal' context.  CREATE DATABASE ... likewise
+-- reads through the normal context, but we have chosen to use a tablespace
+-- and verify_heapam to provide coverage instead of adding another expensive
 -- operation to the main regression test suite.
 --
 -- Create an alternative tablespace and move the heaptest table to it, causing
@@ -43,7 +43,7 @@ INSERT INTO heaptest (a, b)
 SET allow_in_place_tablespaces = true;
 CREATE TABLESPACE regress_test_stats_tblspc LOCATION '';
 SELECT sum(reads) AS stats_bulkreads_before
-  FROM pg_stat_io WHERE context = 'bulkread' \gset
+  FROM pg_stat_io WHERE context = 'normal' AND object = 'relation' \gset
 BEGIN;
 ALTER TABLE heaptest SET TABLESPACE regress_test_stats_tblspc;
 -- Check that valid options are not rejected nor corruption reported
@@ -56,10 +56,10 @@ COMMIT;
 
 -- verify_heapam should have read in the page written out by
 --   ALTER TABLE ... SET TABLESPACE ...
--- causing an additional bulkread, which should be reflected in pg_stat_io.
+-- causing additional reads, which should be reflected in pg_stat_io.
 SELECT pg_stat_force_next_flush();
 SELECT sum(reads) AS stats_bulkreads_after
-  FROM pg_stat_io WHERE context = 'bulkread' \gset
+  FROM pg_stat_io WHERE context = 'normal' AND object = 'relation' \gset
 SELECT :stats_bulkreads_after > :stats_bulkreads_before;
 
 CREATE ROLE regress_heaptest_role;
diff --git a/contrib/amcheck/verify_gin.c b/contrib/amcheck/verify_gin.c
index fa06689..ef2e644 100644
--- a/contrib/amcheck/verify_gin.c
+++ b/contrib/amcheck/verify_gin.c
@@ -63,8 +63,7 @@ static void gin_check_parent_keys_consistency(Relation rel,
 static void check_index_page(Relation rel, Buffer buffer, BlockNumber blockNo);
 static IndexTuple gin_refind_parent(Relation rel,
 									BlockNumber parentblkno,
-									BlockNumber childblkno,
-									BufferAccessStrategy strategy);
+									BlockNumber childblkno);
 static ItemId PageGetItemIdCareful(Relation rel, BlockNumber block, Page page,
 								   OffsetNumber offset);
 
@@ -133,7 +132,6 @@ ginReadTupleWithoutState(IndexTuple itup, int *nitems)
 static void
 gin_check_posting_tree_parent_keys_consistency(Relation rel, BlockNumber posting_tree_root)
 {
-	BufferAccessStrategy strategy = GetAccessStrategy(BAS_BULKREAD);
 	GinPostingTreeScanItem *stack;
 	MemoryContext mctx;
 	MemoryContext oldcontext;
@@ -171,8 +169,7 @@ gin_check_posting_tree_parent_keys_consistency(Relation rel, BlockNumber posting
 
 		CHECK_FOR_INTERRUPTS();
 
-		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno,
-									RBM_NORMAL, strategy);
+		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno, RBM_NORMAL);
 		LockBuffer(buffer, GIN_SHARE);
 		page = BufferGetPage(buffer);
 
@@ -391,7 +388,6 @@ gin_check_parent_keys_consistency(Relation rel,
 								  void *callback_state,
 								  bool readonly)
 {
-	BufferAccessStrategy strategy = GetAccessStrategy(BAS_BULKREAD);
 	GinScanItem *stack;
 	MemoryContext mctx;
 	MemoryContext oldcontext;
@@ -430,8 +426,7 @@ gin_check_parent_keys_consistency(Relation rel,
 
 		CHECK_FOR_INTERRUPTS();
 
-		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno,
-									RBM_NORMAL, strategy);
+		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno, RBM_NORMAL);
 		LockBuffer(buffer, GIN_SHARE);
 		page = BufferGetPage(buffer);
 		maxoff = PageGetMaxOffsetNumber(page);
@@ -567,7 +562,7 @@ gin_check_parent_keys_consistency(Relation rel,
 					 */
 					pfree(stack->parenttup);
 					stack->parenttup = gin_refind_parent(rel, stack->parentblk,
-														 stack->blkno, strategy);
+														 stack->blkno);
 
 					/* We found it - make a final check before failing */
 					if (!stack->parenttup)
@@ -719,7 +714,7 @@ check_index_page(Relation rel, Buffer buffer, BlockNumber blockNo)
  */
 static IndexTuple
 gin_refind_parent(Relation rel, BlockNumber parentblkno,
-				  BlockNumber childblkno, BufferAccessStrategy strategy)
+				  BlockNumber childblkno)
 {
 	Buffer		parentbuf;
 	Page		parentpage;
@@ -727,8 +722,7 @@ gin_refind_parent(Relation rel, BlockNumber parentblkno,
 				parent_maxoff;
 	IndexTuple	result = NULL;
 
-	parentbuf = ReadBufferExtended(rel, MAIN_FORKNUM, parentblkno, RBM_NORMAL,
-								   strategy);
+	parentbuf = ReadBufferExtended(rel, MAIN_FORKNUM, parentblkno, RBM_NORMAL);
 
 	LockBuffer(parentbuf, GIN_SHARE);
 	parentpage = BufferGetPage(parentbuf);
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 20ff58a..e4336eb 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -126,7 +126,6 @@ typedef struct HeapCheckContext
 	 * recent block in the buffer yielded by the read stream API.
 	 */
 	BlockNumber blkno;
-	BufferAccessStrategy bstrategy;
 	Buffer		buffer;
 	Page		page;
 
@@ -374,7 +373,6 @@ verify_heapam(PG_FUNCTION_ARGS)
 		PG_RETURN_NULL();
 	}
 
-	ctx.bstrategy = GetAccessStrategy(BAS_BULKREAD);
 	ctx.buffer = InvalidBuffer;
 	ctx.page = NULL;
 
@@ -472,7 +470,6 @@ verify_heapam(PG_FUNCTION_ARGS)
 	}
 
 	stream = read_stream_begin_relation(stream_flags,
-										ctx.bstrategy,
 										ctx.rel,
 										MAIN_FORKNUM,
 										stream_cb,
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 3ef2d66..18cd892 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -88,8 +88,6 @@ typedef struct BtreeCheckState
 	bool		checkunique;
 	/* Per-page context */
 	MemoryContext targetcontext;
-	/* Buffer access strategy */
-	BufferAccessStrategy checkstrategy;
 
 	/*
 	 * Info for uniqueness checking. Fill this field and the one below once
@@ -490,7 +488,6 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 	state->targetcontext = AllocSetContextCreate(CurrentMemoryContext,
 												 "amcheck context",
 												 ALLOCSET_DEFAULT_SIZES);
-	state->checkstrategy = GetAccessStrategy(BAS_BULKREAD);
 
 	/* Get true root block from meta-page */
 	metapage = palloc_btree_page(state, BTREE_METAPAGE);
@@ -1115,7 +1112,7 @@ bt_recheck_sibling_links(BtreeCheckState *state,
 
 		/* Couple locks in the usual order for nbtree:  Left to right */
 		lbuf = ReadBufferExtended(state->rel, MAIN_FORKNUM, leftcurrent,
-								  RBM_NORMAL, state->checkstrategy);
+								  RBM_NORMAL);
 		LockBuffer(lbuf, BT_READ);
 		_bt_checkpage(state->rel, lbuf);
 		page = BufferGetPage(lbuf);
@@ -1138,8 +1135,7 @@ bt_recheck_sibling_links(BtreeCheckState *state,
 		if (newtargetblock != leftcurrent)
 		{
 			newtargetbuf = ReadBufferExtended(state->rel, MAIN_FORKNUM,
-											  newtargetblock, RBM_NORMAL,
-											  state->checkstrategy);
+											  newtargetblock, RBM_NORMAL);
 			LockBuffer(newtargetbuf, BT_READ);
 			_bt_checkpage(state->rel, newtargetbuf);
 			page = BufferGetPage(newtargetbuf);
@@ -3300,8 +3296,7 @@ palloc_btree_page(BtreeCheckState *state, BlockNumber blocknum)
 	 * We copy the page into local storage to avoid holding pin on the buffer
 	 * longer than we must.
 	 */
-	buffer = ReadBufferExtended(state->rel, MAIN_FORKNUM, blocknum, RBM_NORMAL,
-								state->checkstrategy);
+	buffer = ReadBufferExtended(state->rel, MAIN_FORKNUM, blocknum, RBM_NORMAL);
 	LockBuffer(buffer, BT_READ);
 
 	/*
diff --git a/contrib/bloom/blscan.c b/contrib/bloom/blscan.c
index 1a0e420..ed62d5c 100644
--- a/contrib/bloom/blscan.c
+++ b/contrib/bloom/blscan.c
@@ -80,7 +80,6 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BlockNumber blkno,
 				npages;
 	int			i;
-	BufferAccessStrategy bas;
 	BloomScanOpaque so = (BloomScanOpaque) scan->opaque;
 	BlockRangeReadStreamPrivate p;
 	ReadStream *stream;
@@ -113,11 +112,6 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 		}
 	}
 
-	/*
-	 * We're going to read the whole index. This is why we use appropriate
-	 * buffer access strategy.
-	 */
-	bas = GetAccessStrategy(BAS_BULKREAD);
 	npages = RelationGetNumberOfBlocks(scan->indexRelation);
 	pgstat_count_index_scan(scan->indexRelation);
 	if (scan->instrument)
@@ -133,7 +127,6 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										bas,
 										scan->indexRelation,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -184,7 +177,6 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	Assert(read_stream_next_buffer(stream, NULL) == InvalidBuffer);
 	read_stream_end(stream);
-	FreeAccessStrategy(bas);
 
 	return ntids;
 }
diff --git a/contrib/bloom/blutils.c b/contrib/bloom/blutils.c
index 5111cdc..6c1ec15 100644
--- a/contrib/bloom/blutils.c
+++ b/contrib/bloom/blutils.c
@@ -392,7 +392,7 @@ BloomNewBuffer(Relation index)
 	}
 
 	/* Must extend the file */
-	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM,
 							   EB_LOCK_FIRST);
 
 	return buffer;
@@ -460,7 +460,7 @@ BloomInitMetapage(Relation index, ForkNumber forknum)
 	 * block number 0 (BLOOM_METAPAGE_BLKNO).  No need to hold the extension
 	 * lock because there cannot be concurrent inserters yet.
 	 */
-	metaBuffer = ReadBufferExtended(index, forknum, P_NEW, RBM_NORMAL, NULL);
+	metaBuffer = ReadBufferExtended(index, forknum, P_NEW, RBM_NORMAL);
 	LockBuffer(metaBuffer, BUFFER_LOCK_EXCLUSIVE);
 	Assert(BufferGetBlockNumber(metaBuffer) == BLOOM_METAPAGE_BLKNO);
 
diff --git a/contrib/bloom/blvacuum.c b/contrib/bloom/blvacuum.c
index 6beb1c2..08d7705 100644
--- a/contrib/bloom/blvacuum.c
+++ b/contrib/bloom/blvacuum.c
@@ -66,7 +66,6 @@ blbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										index,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -219,7 +218,6 @@ blvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										index,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/contrib/pageinspect/rawpage.c b/contrib/pageinspect/rawpage.c
index d136593..bbe8edf 100644
--- a/contrib/pageinspect/rawpage.c
+++ b/contrib/pageinspect/rawpage.c
@@ -188,7 +188,7 @@ get_raw_page_internal(text *relname, ForkNumber forknum, BlockNumber blkno)
 
 	/* Take a verbatim copy of the page */
 
-	buf = ReadBufferExtended(rel, forknum, blkno, RBM_NORMAL, NULL);
+	buf = ReadBufferExtended(rel, forknum, blkno, RBM_NORMAL);
 	LockBuffer(buf, BUFFER_LOCK_SHARE);
 
 	memcpy(raw_page_data, BufferGetPage(buf), BLCKSZ);
diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index deb4c26..33ad88e 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -630,7 +630,6 @@ autoprewarm_database_main(Datum main_arg)
 			stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 												READ_STREAM_DEFAULT |
 												READ_STREAM_USE_BATCHING,
-												NULL,
 												rel,
 												p.forknum,
 												apw_read_stream_next_block,
diff --git a/contrib/pg_prewarm/pg_prewarm.c b/contrib/pg_prewarm/pg_prewarm.c
index c271608..716a675 100644
--- a/contrib/pg_prewarm/pg_prewarm.c
+++ b/contrib/pg_prewarm/pg_prewarm.c
@@ -251,7 +251,6 @@ pg_prewarm(PG_FUNCTION_ARGS)
 		stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 											READ_STREAM_FULL |
 											READ_STREAM_USE_BATCHING,
-											NULL,
 											rel,
 											forkNumber,
 											block_range_read_stream_cb,
diff --git a/contrib/pg_visibility/pg_visibility.c b/contrib/pg_visibility/pg_visibility.c
index dfab0b6..ccb829a 100644
--- a/contrib/pg_visibility/pg_visibility.c
+++ b/contrib/pg_visibility/pg_visibility.c
@@ -488,7 +488,6 @@ collect_visibility_data(Oid relid, bool include_pd)
 	vbits	   *info;
 	BlockNumber blkno;
 	Buffer		vmbuffer = InvalidBuffer;
-	BufferAccessStrategy bstrategy = GetAccessStrategy(BAS_BULKREAD);
 	BlockRangeReadStreamPrivate p;
 	ReadStream *stream = NULL;
 
@@ -514,7 +513,6 @@ collect_visibility_data(Oid relid, bool include_pd)
 		 */
 		stream = read_stream_begin_relation(READ_STREAM_FULL |
 											READ_STREAM_USE_BATCHING,
-											bstrategy,
 											rel,
 											MAIN_FORKNUM,
 											block_range_read_stream_cb,
@@ -538,8 +536,7 @@ collect_visibility_data(Oid relid, bool include_pd)
 
 		/*
 		 * Page-level data requires reading every block, so only get it if the
-		 * caller needs it.  Use a buffer access strategy, too, to prevent
-		 * cache-trashing.
+		 * caller needs it.
 		 */
 		if (include_pd)
 		{
@@ -700,7 +697,6 @@ collect_corrupt_items(Oid relid, bool all_visible, bool all_frozen)
 	Relation	rel;
 	corrupt_items *items;
 	Buffer		vmbuffer = InvalidBuffer;
-	BufferAccessStrategy bstrategy = GetAccessStrategy(BAS_BULKREAD);
 	TransactionId OldestXmin = InvalidTransactionId;
 	struct collect_corrupt_items_read_stream_private p;
 	ReadStream *stream;
@@ -734,7 +730,6 @@ collect_corrupt_items(Oid relid, bool all_visible, bool all_frozen)
 	p.all_frozen = all_frozen;
 	p.all_visible = all_visible;
 	stream = read_stream_begin_relation(READ_STREAM_FULL,
-										bstrategy,
 										rel,
 										MAIN_FORKNUM,
 										collect_corrupt_items_read_stream_next_block,
diff --git a/contrib/pgstattuple/pgstatapprox.c b/contrib/pgstattuple/pgstatapprox.c
index 21e0b50..8e17d48 100644
--- a/contrib/pgstattuple/pgstatapprox.c
+++ b/contrib/pgstattuple/pgstatapprox.c
@@ -116,13 +116,11 @@ static void
 statapprox_heap(Relation rel, output_type *stat)
 {
 	BlockNumber nblocks;
-	BufferAccessStrategy bstrategy;
 	TransactionId OldestXmin;
 	StatApproxReadStreamPrivate p;
 	ReadStream *stream;
 
 	OldestXmin = GetOldestNonRemovableTransactionId(rel);
-	bstrategy = GetAccessStrategy(BAS_BULKREAD);
 
 	nblocks = RelationGetNumberOfBlocks(rel);
 
@@ -141,7 +139,6 @@ statapprox_heap(Relation rel, output_type *stat)
 	 * caution.
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_FULL,
-										bstrategy,
 										rel,
 										MAIN_FORKNUM,
 										statapprox_heap_read_stream_next,
diff --git a/contrib/pgstattuple/pgstatindex.c b/contrib/pgstattuple/pgstatindex.c
index 8951ad0..a0922b3 100644
--- a/contrib/pgstattuple/pgstatindex.c
+++ b/contrib/pgstattuple/pgstatindex.c
@@ -217,7 +217,6 @@ pgstatindex_impl(Relation rel, FunctionCallInfo fcinfo)
 	BlockNumber nblocks;
 	BlockNumber blkno;
 	BTIndexStat indexStat;
-	BufferAccessStrategy bstrategy = GetAccessStrategy(BAS_BULKREAD);
 	BlockRangeReadStreamPrivate p;
 	ReadStream *stream;
 	BlockNumber startblk;
@@ -254,7 +253,7 @@ pgstatindex_impl(Relation rel, FunctionCallInfo fcinfo)
 	 * Read metapage
 	 */
 	{
-		Buffer		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, 0, RBM_NORMAL, bstrategy);
+		Buffer		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, 0, RBM_NORMAL);
 		Page		page = BufferGetPage(buffer);
 		BTMetaPageData *metad = BTPageGetMeta(page);
 
@@ -291,7 +290,6 @@ pgstatindex_impl(Relation rel, FunctionCallInfo fcinfo)
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										bstrategy,
 										rel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -612,7 +610,6 @@ pgstathashindex(PG_FUNCTION_ARGS)
 	BlockNumber blkno;
 	Relation	rel;
 	HashIndexStat stats;
-	BufferAccessStrategy bstrategy;
 	HeapTuple	tuple;
 	TupleDesc	tupleDesc;
 	Datum		values[8];
@@ -665,9 +662,6 @@ pgstathashindex(PG_FUNCTION_ARGS)
 	/* Get the current relation length */
 	nblocks = RelationGetNumberOfBlocks(rel);
 
-	/* prepare access strategy for this index */
-	bstrategy = GetAccessStrategy(BAS_BULKREAD);
-
 	/* Scan all blocks except the metapage (0th page) using streaming reads */
 	startblk = HASH_METAPAGE + 1;
 
@@ -680,7 +674,6 @@ pgstathashindex(PG_FUNCTION_ARGS)
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										bstrategy,
 										rel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/contrib/pgstattuple/pgstattuple.c b/contrib/pgstattuple/pgstattuple.c
index 6a7f8cb..90f7938 100644
--- a/contrib/pgstattuple/pgstattuple.c
+++ b/contrib/pgstattuple/pgstattuple.c
@@ -64,22 +64,18 @@ typedef struct pgstattuple_type
 	uint64		free_space;		/* free/reusable space in bytes */
 } pgstattuple_type;
 
-typedef void (*pgstat_page) (pgstattuple_type *, Relation, BlockNumber,
-							 BufferAccessStrategy);
+typedef void (*pgstat_page) (pgstattuple_type *, Relation, BlockNumber);
 
 static Datum build_pgstattuple_type(pgstattuple_type *stat,
 									FunctionCallInfo fcinfo);
 static Datum pgstat_relation(Relation rel, FunctionCallInfo fcinfo);
 static Datum pgstat_heap(Relation rel, FunctionCallInfo fcinfo);
 static void pgstat_btree_page(pgstattuple_type *stat,
-							  Relation rel, BlockNumber blkno,
-							  BufferAccessStrategy bstrategy);
+							  Relation rel, BlockNumber blkno);
 static void pgstat_hash_page(pgstattuple_type *stat,
-							 Relation rel, BlockNumber blkno,
-							 BufferAccessStrategy bstrategy);
+							 Relation rel, BlockNumber blkno);
 static void pgstat_gist_page(pgstattuple_type *stat,
-							 Relation rel, BlockNumber blkno,
-							 BufferAccessStrategy bstrategy);
+							 Relation rel, BlockNumber blkno);
 static Datum pgstat_index(Relation rel, BlockNumber start,
 						  pgstat_page pagefn, FunctionCallInfo fcinfo);
 static void pgstat_index_page(pgstattuple_type *stat, Page page,
@@ -376,7 +372,7 @@ pgstat_heap(Relation rel, FunctionCallInfo fcinfo)
 			CHECK_FOR_INTERRUPTS();
 
 			buffer = ReadBufferExtended(rel, MAIN_FORKNUM, block,
-										RBM_NORMAL, hscan->rs_strategy);
+										RBM_NORMAL);
 			LockBuffer(buffer, BUFFER_LOCK_SHARE);
 			stat.free_space += PageGetExactFreeSpace(BufferGetPage(buffer));
 			UnlockReleaseBuffer(buffer);
@@ -389,7 +385,7 @@ pgstat_heap(Relation rel, FunctionCallInfo fcinfo)
 		CHECK_FOR_INTERRUPTS();
 
 		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, block,
-									RBM_NORMAL, hscan->rs_strategy);
+									RBM_NORMAL);
 		LockBuffer(buffer, BUFFER_LOCK_SHARE);
 		stat.free_space += PageGetExactFreeSpace(BufferGetPage(buffer));
 		UnlockReleaseBuffer(buffer);
@@ -408,13 +404,12 @@ pgstat_heap(Relation rel, FunctionCallInfo fcinfo)
  * pgstat_btree_page -- check tuples in a btree page
  */
 static void
-pgstat_btree_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
-				  BufferAccessStrategy bstrategy)
+pgstat_btree_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno)
 {
 	Buffer		buf;
 	Page		page;
 
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, bstrategy);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 	LockBuffer(buf, BT_READ);
 	page = BufferGetPage(buf);
 
@@ -452,13 +447,12 @@ pgstat_btree_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
  * pgstat_hash_page -- check tuples in a hash page
  */
 static void
-pgstat_hash_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
-				 BufferAccessStrategy bstrategy)
+pgstat_hash_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno)
 {
 	Buffer		buf;
 	Page		page;
 
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, bstrategy);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 	LockBuffer(buf, HASH_READ);
 	page = BufferGetPage(buf);
 
@@ -500,13 +494,12 @@ pgstat_hash_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
  * pgstat_gist_page -- check tuples in a gist page
  */
 static void
-pgstat_gist_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno,
-				 BufferAccessStrategy bstrategy)
+pgstat_gist_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno)
 {
 	Buffer		buf;
 	Page		page;
 
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, bstrategy);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 	LockBuffer(buf, GIST_SHARE);
 	page = BufferGetPage(buf);
 	if (PageIsNew(page))
@@ -539,12 +532,8 @@ pgstat_index(Relation rel, BlockNumber start, pgstat_page pagefn,
 {
 	BlockNumber nblocks;
 	BlockNumber blkno;
-	BufferAccessStrategy bstrategy;
 	pgstattuple_type stat = {0};
 
-	/* prepare access strategy for this index */
-	bstrategy = GetAccessStrategy(BAS_BULKREAD);
-
 	blkno = start;
 	for (;;)
 	{
@@ -565,7 +554,7 @@ pgstat_index(Relation rel, BlockNumber start, pgstat_page pagefn,
 		{
 			CHECK_FOR_INTERRUPTS();
 
-			pagefn(&stat, rel, blkno, bstrategy);
+			pagefn(&stat, rel, blkno);
 		}
 	}
 
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index aa7b1bd..e993ab3 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2107,36 +2107,6 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
-     <varlistentry id="guc-vacuum-buffer-usage-limit" xreflabel="vacuum_buffer_usage_limit">
-      <term>
-       <varname>vacuum_buffer_usage_limit</varname> (<type>integer</type>)
-       <indexterm>
-        <primary><varname>vacuum_buffer_usage_limit</varname> configuration parameter</primary>
-       </indexterm>
-      </term>
-      <listitem>
-       <para>
-        Specifies the size of the
-        <glossterm linkend="glossary-buffer-access-strategy">Buffer Access Strategy</glossterm>
-        used by the <command>VACUUM</command> and <command>ANALYZE</command>
-        commands.  A setting of <literal>0</literal> will allow the operation
-        to use any number of <varname>shared_buffers</varname>.  Otherwise
-        valid sizes range from <literal>128 kB</literal> to
-        <literal>16 GB</literal>.  If the specified size would exceed 1/8 the
-        size of <varname>shared_buffers</varname>, the size is silently capped
-        to that value.  The default value is <literal>2MB</literal>.  If
-        this value is specified without units, it is taken as kilobytes.  This
-        parameter can be set at any time.  It can be overridden for
-        <xref linkend="sql-vacuum"/> and <xref linkend="sql-analyze"/>
-        when passing the <option>BUFFER_USAGE_LIMIT</option> option.  Higher
-        settings can allow <command>VACUUM</command> and
-        <command>ANALYZE</command> to run more quickly, but having too large a
-        setting may cause too many other useful pages to be evicted from
-        shared buffers.
-       </para>
-      </listitem>
-     </varlistentry>
-
      <varlistentry id="guc-logical-decoding-work-mem" xreflabel="logical_decoding_work_mem">
       <term><varname>logical_decoding_work_mem</varname> (<type>integer</type>)
       <indexterm>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index d1a20d0..18d1754 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -2993,28 +2993,6 @@ description | Waiting for a newly initialized WAL file to reach durable storage
           <literal>init</literal>.
          </para>
         </listitem>
-        <listitem>
-         <para>
-          <literal>vacuum</literal>: I/O operations performed outside of shared
-          buffers while vacuuming and analyzing permanent relations. Temporary
-          table vacuums use the same local buffer pool as other temporary table
-          I/O operations and are tracked in <varname>context</varname>
-          <literal>normal</literal>.
-         </para>
-        </listitem>
-        <listitem>
-         <para>
-          <literal>bulkread</literal>: Certain large read I/O operations
-          done outside of shared buffers, for example, a sequential scan of a
-          large table.
-         </para>
-        </listitem>
-        <listitem>
-         <para>
-          <literal>bulkwrite</literal>: Certain large write I/O operations
-          done outside of shared buffers, such as <command>COPY</command>.
-         </para>
-        </listitem>
        </itemizedlist>
       </entry>
      </row>
@@ -3180,13 +3158,8 @@ description | Waiting for a newly initialized WAL file to reach durable storage
         buffer in order to make it available for another use.
        </para>
        <para>
-        In <varname>context</varname> <literal>normal</literal>, this counts
-        the number of times a block was evicted from a buffer and replaced with
-        another block. In <varname>context</varname>s
-        <literal>bulkwrite</literal>, <literal>bulkread</literal>, and
-        <literal>vacuum</literal>, this counts the number of times a block was
-        evicted from shared buffers in order to add the shared buffer to a
-        separate, size-limited ring buffer for use in a bulk I/O operation.
+        This counts the number of times a block was evicted from a buffer and
+        replaced with another block.
         </para>
       </entry>
      </row>
@@ -3197,10 +3170,9 @@ description | Waiting for a newly initialized WAL file to reach durable storage
         <structfield>reuses</structfield> <type>bigint</type>
        </para>
        <para>
-        The number of times an existing buffer in a size-limited ring buffer
-        outside of shared buffers was reused as part of an I/O operation in the
-        <literal>bulkread</literal>, <literal>bulkwrite</literal>, or
-        <literal>vacuum</literal> <varname>context</varname>s.
+        Always zero.  This column previously counted reuses of buffers in a
+        size-limited ring buffer (buffer access strategy); ring buffers have
+        been removed, so no reuses are tracked.
        </para>
       </entry>
      </row>
diff --git a/doc/src/sgml/ref/analyze.sgml b/doc/src/sgml/ref/analyze.sgml
index ec81f00..7f51591 100644
--- a/doc/src/sgml/ref/analyze.sgml
+++ b/doc/src/sgml/ref/analyze.sgml
@@ -27,7 +27,6 @@ ANALYZE [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] [ <r
 
     VERBOSE [ <replaceable class="parameter">boolean</replaceable> ]
     SKIP_LOCKED [ <replaceable class="parameter">boolean</replaceable> ]
-    BUFFER_USAGE_LIMIT <replaceable class="parameter">size</replaceable>
 
 <phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase>
 
@@ -88,26 +87,6 @@ ANALYZE [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] [ <r
     </listitem>
    </varlistentry>
 
-   <varlistentry>
-    <term><literal>BUFFER_USAGE_LIMIT</literal></term>
-    <listitem>
-     <para>
-      Specifies the
-      <glossterm linkend="glossary-buffer-access-strategy">Buffer Access Strategy</glossterm>
-      ring buffer size for <command>ANALYZE</command>.  This size is used to
-      calculate the number of shared buffers which will be reused as part of
-      this strategy.  <literal>0</literal> disables use of a
-      <literal>Buffer Access Strategy</literal>.   When this option is not
-      specified, <command>ANALYZE</command> uses the value from
-      <xref linkend="guc-vacuum-buffer-usage-limit"/>.  Higher settings can
-      allow <command>ANALYZE</command> to run more quickly, but having too
-      large a setting may cause too many other useful pages to be evicted from
-      shared buffers.  The minimum value is <literal>128 kB</literal> and the
-      maximum value is <literal>16 GB</literal>.
-     </para>
-    </listitem>
-   </varlistentry>
-
    <varlistentry>
     <term><replaceable class="parameter">boolean</replaceable></term>
     <listitem>
diff --git a/doc/src/sgml/ref/vacuum.sgml b/doc/src/sgml/ref/vacuum.sgml
index 38ee973..d44a49f 100644
--- a/doc/src/sgml/ref/vacuum.sgml
+++ b/doc/src/sgml/ref/vacuum.sgml
@@ -37,7 +37,6 @@ VACUUM [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] [ <re
     PARALLEL <replaceable class="parameter">integer</replaceable>
     SKIP_DATABASE_STATS [ <replaceable class="parameter">boolean</replaceable> ]
     ONLY_DATABASE_STATS [ <replaceable class="parameter">boolean</replaceable> ]
-    BUFFER_USAGE_LIMIT <replaceable class="parameter">size</replaceable>
     FULL [ <replaceable class="parameter">boolean</replaceable> ]
 
 <phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase>
@@ -309,30 +308,6 @@ VACUUM [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] [ <re
     </listitem>
    </varlistentry>
 
-   <varlistentry>
-    <term><literal>BUFFER_USAGE_LIMIT</literal></term>
-    <listitem>
-     <para>
-      Specifies the
-      <glossterm linkend="glossary-buffer-access-strategy">Buffer Access Strategy</glossterm>
-      ring buffer size for <command>VACUUM</command>.  This size is used to
-      calculate the number of shared buffers which will be reused as part of
-      this strategy.  <literal>0</literal> disables use of a
-      <literal>Buffer Access Strategy</literal>.  If <option>ANALYZE</option>
-      is also specified, the <option>BUFFER_USAGE_LIMIT</option> value is used
-      for both the vacuum and analyze stages.  This option can't be used with
-      the <option>FULL</option> option except if <option>ANALYZE</option> is
-      also specified.  When this option is not specified,
-      <command>VACUUM</command> uses the value from
-      <xref linkend="guc-vacuum-buffer-usage-limit"/>.  Higher settings can
-      allow <command>VACUUM</command> to run more quickly, but having too
-      large a setting may cause too many other useful pages to be evicted from
-      shared buffers.  The minimum value is <literal>128 kB</literal> and the
-      maximum value is <literal>16 GB</literal>.
-     </para>
-    </listitem>
-   </varlistentry>
-
    <varlistentry>
     <term><literal>FULL</literal></term>
     <listitem>
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index bdb3075..5d6539f 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -224,7 +224,7 @@ static void form_and_insert_tuple(BrinBuildState *state);
 static void form_and_spill_tuple(BrinBuildState *state);
 static void union_tuples(BrinDesc *bdesc, BrinMemTuple *a,
 						 BrinTuple *b);
-static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy);
+static void brin_vacuum_scan(Relation idxrel);
 static bool add_values_to_range(Relation idxRel, BrinDesc *bdesc,
 								BrinMemTuple *dtup, const Datum *values, const bool *nulls);
 static bool check_null_keys(BrinValues *bval, ScanKey *nullkeys, int nnullkeys);
@@ -1129,7 +1129,7 @@ brinbuild(Relation heap, Relation index, IndexInfo *indexInfo)
 	 * whole relation will be rolled back.
 	 */
 
-	meta = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM, NULL,
+	meta = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM,
 							 EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 	Assert(BufferGetBlockNumber(meta) == BRIN_METAPAGE_BLKNO);
 
@@ -1281,7 +1281,7 @@ brinbuildempty(Relation index)
 	Buffer		metabuf;
 
 	/* An empty BRIN index has a metapage only. */
-	metabuf = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+	metabuf = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM,
 								EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 
 	/* Initialize and xlog metabuffer. */
@@ -1336,7 +1336,7 @@ brinvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
 	heapRel = table_open(IndexGetRelation(RelationGetRelid(info->index), false),
 						 AccessShareLock);
 
-	brin_vacuum_scan(info->index, info->strategy);
+	brin_vacuum_scan(info->index);
 
 	brinsummarize(info->index, heapRel, BRIN_ALL_BLOCKRANGES, false,
 				  &stats->num_index_tuples, &stats->num_index_tuples);
@@ -2171,7 +2171,7 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b)
  * and such.
  */
 static void
-brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
+brin_vacuum_scan(Relation idxrel)
 {
 	BlockRangeReadStreamPrivate p;
 	ReadStream *stream;
@@ -2187,7 +2187,6 @@ brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy)
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										strategy,
 										idxrel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/src/backend/access/brin/brin_revmap.c b/src/backend/access/brin/brin_revmap.c
index 233355c..01c4104 100644
--- a/src/backend/access/brin/brin_revmap.c
+++ b/src/backend/access/brin/brin_revmap.c
@@ -559,7 +559,7 @@ revmap_physical_extend(BrinRevmap *revmap)
 	}
 	else
 	{
-		buf = ExtendBufferedRel(BMR_REL(irel), MAIN_FORKNUM, NULL,
+		buf = ExtendBufferedRel(BMR_REL(irel), MAIN_FORKNUM,
 								EB_LOCK_FIRST);
 		if (BufferGetBlockNumber(buf) != mapBlk)
 		{
diff --git a/src/backend/access/gin/gininsert.c b/src/backend/access/gin/gininsert.c
index cb9ed3b..32ad65c 100644
--- a/src/backend/access/gin/gininsert.c
+++ b/src/backend/access/gin/gininsert.c
@@ -815,9 +815,9 @@ ginbuildempty(Relation index)
 				MetaBuffer;
 
 	/* An empty GIN index has two pages. */
-	MetaBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+	MetaBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM,
 								   EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
-	RootBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+	RootBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM,
 								   EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 
 	/* Initialize and xlog metabuffer and root buffer. */
diff --git a/src/backend/access/gin/ginutil.c b/src/backend/access/gin/ginutil.c
index e7cba81..2d7394a 100644
--- a/src/backend/access/gin/ginutil.c
+++ b/src/backend/access/gin/ginutil.c
@@ -336,7 +336,7 @@ GinNewBuffer(Relation index)
 	}
 
 	/* Must extend the file */
-	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM,
 							   EB_LOCK_FIRST);
 
 	return buffer;
diff --git a/src/backend/access/gin/ginvacuum.c b/src/backend/access/gin/ginvacuum.c
index 840543e..86538de 100644
--- a/src/backend/access/gin/ginvacuum.c
+++ b/src/backend/access/gin/ginvacuum.c
@@ -32,7 +32,6 @@ struct GinVacuumState
 	IndexBulkDeleteCallback callback;
 	void	   *callback_state;
 	GinState	ginstate;
-	BufferAccessStrategy strategy;
 	MemoryContext tmpCxt;
 };
 
@@ -289,7 +288,7 @@ ginScanPostingTreeToDelete(GinVacuumState *gvs, DataPageDeleteStack *myStackItem
 			childBuffer = ReadBufferExtended(gvs->index,
 											 MAIN_FORKNUM,
 											 PostingItemGetBlockNumber(pitem),
-											 RBM_NORMAL, gvs->strategy);
+											 RBM_NORMAL);
 			LockBuffer(childBuffer, GIN_EXCLUSIVE);
 
 			/* Allocate a child stack entry on first use; reuse thereafter */
@@ -389,7 +388,7 @@ ginVacuumPostingTreeLeaves(GinVacuumState *gvs, BlockNumber blkno)
 		PostingItem *pitem;
 
 		buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, gvs->strategy);
+									RBM_NORMAL);
 		LockBuffer(buffer, GIN_SHARE);
 		page = BufferGetPage(buffer);
 
@@ -430,7 +429,7 @@ ginVacuumPostingTreeLeaves(GinVacuumState *gvs, BlockNumber blkno)
 			break;
 
 		buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, gvs->strategy);
+									RBM_NORMAL);
 		LockBuffer(buffer, GIN_EXCLUSIVE);
 		page = BufferGetPage(buffer);
 	}
@@ -454,7 +453,7 @@ ginVacuumPostingTree(GinVacuumState *gvs, BlockNumber rootBlkno)
 		bool		deleted PG_USED_FOR_ASSERTS_ONLY;
 
 		buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, rootBlkno,
-									RBM_NORMAL, gvs->strategy);
+									RBM_NORMAL);
 
 		/*
 		 * Lock posting tree root for cleanup to ensure there are no
@@ -615,7 +614,6 @@ ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	gvs.index = index;
 	gvs.callback = callback;
 	gvs.callback_state = callback_state;
-	gvs.strategy = info->strategy;
 	initGinState(&gvs.ginstate, index);
 
 	/* first time through? */
@@ -636,7 +634,7 @@ ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	gvs.result = stats;
 
 	buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
-								RBM_NORMAL, info->strategy);
+								RBM_NORMAL);
 
 	/* find leaf page */
 	for (;;)
@@ -669,7 +667,7 @@ ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 
 		UnlockReleaseBuffer(buffer);
 		buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, info->strategy);
+									RBM_NORMAL);
 	}
 
 	/* right now we found leftmost page in entry's BTree */
@@ -712,7 +710,7 @@ ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 			break;
 
 		buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, info->strategy);
+									RBM_NORMAL);
 		LockBuffer(buffer, GIN_EXCLUSIVE);
 	}
 
@@ -794,7 +792,6 @@ ginvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										index,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/src/backend/access/gist/gist.c b/src/backend/access/gist/gist.c
index 8565e22..ae49474 100644
--- a/src/backend/access/gist/gist.c
+++ b/src/backend/access/gist/gist.c
@@ -142,7 +142,7 @@ gistbuildempty(Relation index)
 	Buffer		buffer;
 
 	/* Initialize the root page */
-	buffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM,
 							   EB_SKIP_EXTENSION_LOCK | EB_LOCK_FIRST);
 
 	/* Initialize and xlog buffer */
diff --git a/src/backend/access/gist/gistutil.c b/src/backend/access/gist/gistutil.c
index 0f58f61..170174f 100644
--- a/src/backend/access/gist/gistutil.c
+++ b/src/backend/access/gist/gistutil.c
@@ -877,7 +877,7 @@ gistNewBuffer(Relation r, Relation heaprel)
 	}
 
 	/* Must extend the file */
-	buffer = ExtendBufferedRel(BMR_REL(r), MAIN_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(r), MAIN_FORKNUM,
 							   EB_LOCK_FIRST);
 
 	return buffer;
diff --git a/src/backend/access/gist/gistvacuum.c b/src/backend/access/gist/gistvacuum.c
index 686a041..c366ed3 100644
--- a/src/backend/access/gist/gistvacuum.c
+++ b/src/backend/access/gist/gistvacuum.c
@@ -218,7 +218,6 @@ gistvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										rel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -491,8 +490,7 @@ restart:
 		/* check for vacuum delay while not holding any buffer lock */
 		vacuum_delay_point(false);
 
-		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL,
-									info->strategy);
+		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 		goto restart;
 	}
 }
@@ -524,8 +522,7 @@ gistvacuum_delete_empty_pages(IndexVacuumInfo *info, GistVacState *vstate)
 		int			ntodelete;
 		int			deleted;
 
-		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, (BlockNumber) blkno,
-									RBM_NORMAL, info->strategy);
+		buffer = ReadBufferExtended(rel, MAIN_FORKNUM, (BlockNumber) blkno, RBM_NORMAL);
 
 		LockBuffer(buffer, GIST_SHARE);
 		page = BufferGetPage(buffer);
@@ -590,8 +587,7 @@ gistvacuum_delete_empty_pages(IndexVacuumInfo *info, GistVacState *vstate)
 			if (PageGetMaxOffsetNumber(page) == FirstOffsetNumber)
 				break;
 
-			leafbuf = ReadBufferExtended(rel, MAIN_FORKNUM, leafs_to_delete[i],
-										 RBM_NORMAL, info->strategy);
+			leafbuf = ReadBufferExtended(rel, MAIN_FORKNUM, leafs_to_delete[i], RBM_NORMAL);
 			LockBuffer(leafbuf, GIST_EXCLUSIVE);
 			gistcheckpage(rel, leafbuf);
 
diff --git a/src/backend/access/hash/hash.c b/src/backend/access/hash/hash.c
index 8d8cd30..1bbb722 100644
--- a/src/backend/access/hash/hash.c
+++ b/src/backend/access/hash/hash.c
@@ -542,7 +542,6 @@ hashbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										rel,
 										MAIN_FORKNUM,
 										hash_bulkdelete_read_stream_cb,
@@ -616,7 +615,7 @@ bucket_loop:
 
 		bucket_buf = buf;
 
-		hashbucketcleanup(rel, cur_bucket, bucket_buf, blkno, info->strategy,
+		hashbucketcleanup(rel, cur_bucket, bucket_buf, blkno,
 						  cachedmetap->hashm_maxbucket,
 						  cachedmetap->hashm_highmask,
 						  cachedmetap->hashm_lowmask, &tuples_removed,
@@ -765,7 +764,7 @@ hashvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
  */
 void
 hashbucketcleanup(Relation rel, Bucket cur_bucket, Buffer bucket_buf,
-				  BlockNumber bucket_blkno, BufferAccessStrategy bstrategy,
+				  BlockNumber bucket_blkno,
 				  uint32 maxbucket, uint32 highmask, uint32 lowmask,
 				  double *tuples_removed, double *num_index_tuples,
 				  bool split_cleanup,
@@ -935,9 +934,8 @@ hashbucketcleanup(Relation rel, Bucket cur_bucket, Buffer bucket_buf,
 		if (!BlockNumberIsValid(blkno))
 			break;
 
-		next_buf = _hash_getbuf_with_strategy(rel, blkno, HASH_WRITE,
-											  LH_OVERFLOW_PAGE,
-											  bstrategy);
+		next_buf = _hash_getbuf(rel, blkno, HASH_WRITE,
+								LH_OVERFLOW_PAGE);
 
 		/*
 		 * release the lock on previous page after acquiring the lock on next
@@ -1004,8 +1002,7 @@ hashbucketcleanup(Relation rel, Bucket cur_bucket, Buffer bucket_buf,
 	 * ordering of tuples for a scan that has started before it.
 	 */
 	if (bucket_dirty && IsBufferCleanupOK(bucket_buf))
-		_hash_squeezebucket(rel, cur_bucket, bucket_blkno, bucket_buf,
-							bstrategy);
+		_hash_squeezebucket(rel, cur_bucket, bucket_blkno, bucket_buf);
 	else
 		LockBuffer(bucket_buf, BUFFER_LOCK_UNLOCK);
 }
diff --git a/src/backend/access/hash/hashovfl.c b/src/backend/access/hash/hashovfl.c
index dbc57ef..5b35fed 100644
--- a/src/backend/access/hash/hashovfl.c
+++ b/src/backend/access/hash/hashovfl.c
@@ -491,8 +491,7 @@ _hash_firstfreebit(uint32 map)
 BlockNumber
 _hash_freeovflpage(Relation rel, Buffer bucketbuf, Buffer ovflbuf,
 				   Buffer wbuf, IndexTuple *itups, OffsetNumber *itup_offsets,
-				   Size *tups_size, uint16 nitups,
-				   BufferAccessStrategy bstrategy)
+				   Size *tups_size, uint16 nitups)
 {
 	HashMetaPage metap;
 	Buffer		metabuf;
@@ -539,20 +538,16 @@ _hash_freeovflpage(Relation rel, Buffer bucketbuf, Buffer ovflbuf,
 		if (prevblkno == writeblkno)
 			prevbuf = wbuf;
 		else
-			prevbuf = _hash_getbuf_with_strategy(rel,
-												 prevblkno,
-												 HASH_WRITE,
-												 LH_BUCKET_PAGE | LH_OVERFLOW_PAGE,
-												 bstrategy);
+			prevbuf = _hash_getbuf(rel,
+								   prevblkno,
+								   HASH_WRITE,
+								   LH_BUCKET_PAGE | LH_OVERFLOW_PAGE);
 	}
 	if (BlockNumberIsValid(nextblkno))
-		nextbuf = _hash_getbuf_with_strategy(rel,
-											 nextblkno,
-											 HASH_WRITE,
-											 LH_OVERFLOW_PAGE,
-											 bstrategy);
-
-	/* Note: bstrategy is intentionally not used for metapage and bitmap */
+		nextbuf = _hash_getbuf(rel,
+							   nextblkno,
+							   HASH_WRITE,
+							   LH_OVERFLOW_PAGE);
 
 	/* Read the metapage so we can determine which bitmap page to use */
 	metabuf = _hash_getbuf(rel, HASH_METAPAGE, HASH_READ, LH_META_PAGE);
@@ -843,8 +838,7 @@ void
 _hash_squeezebucket(Relation rel,
 					Bucket bucket,
 					BlockNumber bucket_blkno,
-					Buffer bucket_buf,
-					BufferAccessStrategy bstrategy)
+					Buffer bucket_buf)
 {
 	BlockNumber wblkno;
 	BlockNumber rblkno;
@@ -886,11 +880,10 @@ _hash_squeezebucket(Relation rel,
 		rblkno = ropaque->hasho_nextblkno;
 		if (rbuf != InvalidBuffer)
 			_hash_relbuf(rel, rbuf);
-		rbuf = _hash_getbuf_with_strategy(rel,
-										  rblkno,
-										  HASH_WRITE,
-										  LH_OVERFLOW_PAGE,
-										  bstrategy);
+		rbuf = _hash_getbuf(rel,
+							rblkno,
+							HASH_WRITE,
+							LH_OVERFLOW_PAGE);
 		rpage = BufferGetPage(rbuf);
 		ropaque = HashPageGetOpaque(rpage);
 		Assert(ropaque->hasho_bucket == bucket);
@@ -952,11 +945,10 @@ readpage:
 
 				/* don't need to move to next page if we reached the read page */
 				if (wblkno != rblkno)
-					next_wbuf = _hash_getbuf_with_strategy(rel,
-														   wblkno,
-														   HASH_WRITE,
-														   LH_OVERFLOW_PAGE,
-														   bstrategy);
+					next_wbuf = _hash_getbuf(rel,
+											 wblkno,
+											 HASH_WRITE,
+											 LH_OVERFLOW_PAGE);
 
 				if (nitups > 0)
 				{
@@ -1098,7 +1090,7 @@ readpage:
 
 		/* free this overflow page (releases rbuf) */
 		_hash_freeovflpage(rel, bucket_buf, rbuf, wbuf, itups, itup_offsets,
-						   tups_size, nitups, bstrategy);
+						   tups_size, nitups);
 
 		/* be tidy */
 		for (i = 0; i < nitups; i++)
@@ -1115,11 +1107,10 @@ readpage:
 			return;
 		}
 
-		rbuf = _hash_getbuf_with_strategy(rel,
-										  rblkno,
-										  HASH_WRITE,
-										  LH_OVERFLOW_PAGE,
-										  bstrategy);
+		rbuf = _hash_getbuf(rel,
+							rblkno,
+							HASH_WRITE,
+							LH_OVERFLOW_PAGE);
 		rpage = BufferGetPage(rbuf);
 		ropaque = HashPageGetOpaque(rpage);
 		Assert(ropaque->hasho_bucket == bucket);
diff --git a/src/backend/access/hash/hashpage.c b/src/backend/access/hash/hashpage.c
index c50f9a4..5447006 100644
--- a/src/backend/access/hash/hashpage.c
+++ b/src/backend/access/hash/hashpage.c
@@ -139,8 +139,7 @@ _hash_getinitbuf(Relation rel, BlockNumber blkno)
 	if (blkno == P_NEW)
 		elog(ERROR, "hash AM does not use P_NEW");
 
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_ZERO_AND_LOCK,
-							 NULL);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_ZERO_AND_LOCK);
 
 	/* ref count and lock type are correct */
 
@@ -209,7 +208,7 @@ _hash_getnewbuf(Relation rel, BlockNumber blkno, ForkNumber forkNum)
 	/* smgr insists we explicitly extend the relation */
 	if (blkno == nblocks)
 	{
-		buf = ExtendBufferedRel(BMR_REL(rel), forkNum, NULL,
+		buf = ExtendBufferedRel(BMR_REL(rel), forkNum,
 								EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 		if (BufferGetBlockNumber(buf) != blkno)
 			elog(ERROR, "unexpected hash relation size: %u, should be %u",
@@ -217,8 +216,7 @@ _hash_getnewbuf(Relation rel, BlockNumber blkno, ForkNumber forkNum)
 	}
 	else
 	{
-		buf = ReadBufferExtended(rel, forkNum, blkno, RBM_ZERO_AND_LOCK,
-								 NULL);
+		buf = ReadBufferExtended(rel, forkNum, blkno, RBM_ZERO_AND_LOCK);
 	}
 
 	/* ref count and lock type are correct */
@@ -229,34 +227,6 @@ _hash_getnewbuf(Relation rel, BlockNumber blkno, ForkNumber forkNum)
 	return buf;
 }
 
-/*
- *	_hash_getbuf_with_strategy() -- Get a buffer with nondefault strategy.
- *
- *		This is identical to _hash_getbuf() but also allows a buffer access
- *		strategy to be specified.  We use this for VACUUM operations.
- */
-Buffer
-_hash_getbuf_with_strategy(Relation rel, BlockNumber blkno,
-						   int access, int flags,
-						   BufferAccessStrategy bstrategy)
-{
-	Buffer		buf;
-
-	if (blkno == P_NEW)
-		elog(ERROR, "hash AM does not use P_NEW");
-
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, bstrategy);
-
-	if (access != HASH_NOLOCK)
-		LockBuffer(buf, access);
-
-	/* ref count and lock type are correct */
-
-	_hash_checkpage(rel, buf, flags);
-
-	return buf;
-}
-
 /*
  *	_hash_relbuf() -- release a locked buffer.
  *
@@ -758,7 +728,7 @@ restart_expand:
 		/* Release the metapage lock. */
 		LockBuffer(metabuf, BUFFER_LOCK_UNLOCK);
 
-		hashbucketcleanup(rel, old_bucket, buf_oblkno, start_oblkno, NULL,
+		hashbucketcleanup(rel, old_bucket, buf_oblkno, start_oblkno,
 						  maxbucket, highmask, lowmask, NULL, NULL, true,
 						  NULL, NULL);
 
@@ -1333,7 +1303,7 @@ _hash_splitbucket(Relation rel,
 	{
 		LockBuffer(bucket_nbuf, BUFFER_LOCK_UNLOCK);
 		hashbucketcleanup(rel, obucket, bucket_obuf,
-						  BufferGetBlockNumber(bucket_obuf), NULL,
+						  BufferGetBlockNumber(bucket_obuf),
 						  maxbucket, highmask, lowmask, NULL, NULL, true,
 						  NULL, NULL);
 	}
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 8b488cf..36a7d58 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -360,7 +360,6 @@ static void
 initscan(HeapScanDesc scan, ScanKey key, bool keep_startblock)
 {
 	ParallelBlockTableScanDesc bpscan = NULL;
-	bool		allow_strat;
 	bool		allow_sync;
 
 	/*
@@ -397,24 +396,10 @@ initscan(HeapScanDesc scan, ScanKey key, bool keep_startblock)
 	if (!RelationUsesLocalBuffers(scan->rs_base.rs_rd) &&
 		scan->rs_nblocks > NBuffers / 4)
 	{
-		allow_strat = (scan->rs_base.rs_flags & SO_ALLOW_STRAT) != 0;
 		allow_sync = (scan->rs_base.rs_flags & SO_ALLOW_SYNC) != 0;
 	}
 	else
-		allow_strat = allow_sync = false;
-
-	if (allow_strat)
-	{
-		/* During a rescan, keep the previous strategy object. */
-		if (scan->rs_strategy == NULL)
-			scan->rs_strategy = GetAccessStrategy(BAS_BULKREAD);
-	}
-	else
-	{
-		if (scan->rs_strategy != NULL)
-			FreeAccessStrategy(scan->rs_strategy);
-		scan->rs_strategy = NULL;
-	}
+		allow_sync = false;
 
 	if (scan->rs_base.rs_parallel != NULL)
 	{
@@ -1203,7 +1188,6 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 	scan->rs_base.rs_flags = flags;
 	scan->rs_base.rs_parallel = parallel_scan;
 	scan->rs_base.rs_instrument = NULL;
-	scan->rs_strategy = NULL;	/* set in initscan */
 	scan->rs_cbuf = InvalidBuffer;
 
 	/*
@@ -1274,8 +1258,7 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 
 	/*
 	 * Set up a read stream for sequential scans and TID range scans. This
-	 * should be done after initscan() because initscan() allocates the
-	 * BufferAccessStrategy object passed to the read stream API.
+	 * should be done after initscan().
 	 */
 	if (scan->rs_base.rs_flags & SO_TYPE_SEQSCAN ||
 		scan->rs_base.rs_flags & SO_TYPE_TIDRANGESCAN)
@@ -1296,7 +1279,6 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 		 */
 		scan->rs_read_stream = read_stream_begin_relation(READ_STREAM_SEQUENTIAL |
 														  READ_STREAM_USE_BATCHING,
-														  scan->rs_strategy,
 														  scan->rs_base.rs_rd,
 														  MAIN_FORKNUM,
 														  cb,
@@ -1307,7 +1289,6 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 	{
 		scan->rs_read_stream = read_stream_begin_relation(READ_STREAM_DEFAULT |
 														  READ_STREAM_USE_BATCHING,
-														  scan->rs_strategy,
 														  scan->rs_base.rs_rd,
 														  MAIN_FORKNUM,
 														  bitmapheap_stream_read_next,
@@ -1403,9 +1384,6 @@ heap_endscan(TableScanDesc sscan)
 	if (BufferIsValid(scan->rs_vmbuffer))
 		ReleaseBuffer(scan->rs_vmbuffer);
 
-	/*
-	 * Must free the read stream before freeing the BufferAccessStrategy.
-	 */
 	if (scan->rs_read_stream)
 		read_stream_end(scan->rs_read_stream);
 
@@ -1417,9 +1395,6 @@ heap_endscan(TableScanDesc sscan)
 	if (scan->rs_base.rs_key)
 		pfree(scan->rs_base.rs_key);
 
-	if (scan->rs_strategy != NULL)
-		FreeAccessStrategy(scan->rs_strategy);
-
 	if (scan->rs_parallelworkerdata != NULL)
 		pfree(scan->rs_parallelworkerdata);
 
@@ -1940,7 +1915,6 @@ GetBulkInsertState(void)
 	BulkInsertState bistate;
 
 	bistate = (BulkInsertState) palloc_object(BulkInsertStateData);
-	bistate->strategy = GetAccessStrategy(BAS_BULKWRITE);
 	bistate->current_buf = InvalidBuffer;
 	bistate->next_free = InvalidBlockNumber;
 	bistate->last_free = InvalidBlockNumber;
@@ -1956,7 +1930,6 @@ FreeBulkInsertState(BulkInsertState bistate)
 {
 	if (bistate->current_buf != InvalidBuffer)
 		ReleaseBuffer(bistate->current_buf);
-	FreeAccessStrategy(bistate->strategy);
 	pfree(bistate);
 }
 
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index bf87430..d14658d 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -2213,9 +2213,9 @@ heapam_scan_sample_next_block(TableScanDesc scan, SampleScanState *scanstate)
 	 */
 	CHECK_FOR_INTERRUPTS();
 
-	/* Read page using selected strategy */
+	/* Read page */
 	hscan->rs_cbuf = ReadBufferExtended(hscan->rs_base.rs_rd, MAIN_FORKNUM,
-										blockno, RBM_NORMAL, hscan->rs_strategy);
+										blockno, RBM_NORMAL);
 
 	/* in pagemode, prune the page and determine visible tuple offsets */
 	if (hscan->rs_base.rs_flags & SO_ALLOW_PAGEMODE)
diff --git a/src/backend/access/heap/hio.c b/src/backend/access/heap/hio.c
index fb4ffb9..05f33a7 100644
--- a/src/backend/access/heap/hio.c
+++ b/src/backend/access/heap/hio.c
@@ -80,7 +80,8 @@ RelationPutHeapTuple(Relation relation,
 }
 
 /*
- * Read in a buffer in mode, using bulk-insert strategy if bistate isn't NULL.
+ * Read in a buffer in mode, using the bistate's pinned target page if
+ * available.
  */
 static Buffer
 ReadBufferBI(Relation relation, BlockNumber targetBlock,
@@ -91,7 +92,7 @@ ReadBufferBI(Relation relation, BlockNumber targetBlock,
 	/* If not bulk-insert, exactly like ReadBuffer */
 	if (!bistate)
 		return ReadBufferExtended(relation, MAIN_FORKNUM, targetBlock,
-								  mode, NULL);
+								  mode);
 
 	/* If we have the desired block already pinned, re-pin and return it */
 	if (bistate->current_buf != InvalidBuffer)
@@ -113,9 +114,9 @@ ReadBufferBI(Relation relation, BlockNumber targetBlock,
 		bistate->current_buf = InvalidBuffer;
 	}
 
-	/* Perform a read using the buffer strategy */
+	/* Perform the read */
 	buffer = ReadBufferExtended(relation, MAIN_FORKNUM, targetBlock,
-								mode, bistate->strategy);
+								mode);
 
 	/* Save the selected block as target for future inserts */
 	IncrBufferRefCount(buffer);
@@ -337,7 +338,6 @@ RelationAddBlocks(Relation relation, BulkInsertState bistate,
 	 * way larger.
 	 */
 	first_block = ExtendBufferedRelBy(BMR_REL(relation), MAIN_FORKNUM,
-									  bistate ? bistate->strategy : NULL,
 									  EB_LOCK_FIRST,
 									  extend_by_pages,
 									  victim_buffers,
@@ -484,8 +484,7 @@ RelationAddBlocks(Relation relation, BulkInsertState bistate,
  *
  *	The caller can also provide a BulkInsertState object to optimize many
  *	insertions into the same relation.  This keeps a pin on the current
- *	insertion target page (to save pin/unpin cycles) and also passes a
- *	BULKWRITE buffer selection strategy object to the buffer manager.
+ *	insertion target page (to save pin/unpin cycles).
  *	Passing NULL for bistate selects the default behavior.
  *
  *	We don't fill existing pages further than the fillfactor, except for large
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 39395ae..8bc8e47 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -257,7 +257,6 @@ typedef struct LVRelState
 	int			nindexes;
 
 	/* Buffer access strategy and parallel vacuum state */
-	BufferAccessStrategy bstrategy;
 	ParallelVacuumState *pvs;
 
 	/* Aggressive VACUUM? (must set relfrozenxid >= FreezeLimit) */
@@ -621,8 +620,7 @@ heap_vacuum_eager_scan_setup(LVRelState *vacrel, const VacuumParams *params)
  *		and locked the relation.
  */
 void
-heap_vacuum_rel(Relation rel, const VacuumParams *params,
-				BufferAccessStrategy bstrategy)
+heap_vacuum_rel(Relation rel, const VacuumParams *params)
 {
 	LVRelState *vacrel;
 	bool		verbose,
@@ -699,7 +697,6 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
 	vacrel->rel = rel;
 	vac_open_indexes(vacrel->rel, RowExclusiveLock, &vacrel->nindexes,
 					 &vacrel->indrels);
-	vacrel->bstrategy = bstrategy;
 	if (instrument && vacrel->nindexes > 0)
 	{
 		/* Copy index names used by instrumentation (not error reporting) */
@@ -1311,7 +1308,6 @@ lazy_scan_heap(LVRelState *vacrel)
 	 * explicit work in heap_vac_scan_next_block.
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE,
-										vacrel->bstrategy,
 										vacrel->rel,
 										MAIN_FORKNUM,
 										heap_vac_scan_next_block,
@@ -2670,7 +2666,6 @@ lazy_vacuum_heap_rel(LVRelState *vacrel)
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_USE_BATCHING,
-										vacrel->bstrategy,
 										vacrel->rel,
 										MAIN_FORKNUM,
 										vacuum_reap_lp_read_stream_next,
@@ -2904,13 +2899,6 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel)
 
 		VacuumFailsafeActive = true;
 
-		/*
-		 * Abandon use of a buffer access strategy to allow use of all of
-		 * shared buffers.  We assume the caller who allocated the memory for
-		 * the BufferAccessStrategy will free it.
-		 */
-		vacrel->bstrategy = NULL;
-
 		/* Disable index vacuuming, index cleanup, and heap rel truncation */
 		vacrel->do_index_vacuuming = false;
 		vacrel->do_index_cleanup = false;
@@ -3023,7 +3011,6 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	ivinfo.estimated_count = true;
 	ivinfo.message_level = DEBUG2;
 	ivinfo.num_heap_tuples = reltuples;
-	ivinfo.strategy = vacrel->bstrategy;
 
 	/*
 	 * Update error traceback information.
@@ -3074,7 +3061,6 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat,
 	ivinfo.message_level = DEBUG2;
 
 	ivinfo.num_heap_tuples = reltuples;
-	ivinfo.strategy = vacrel->bstrategy;
 
 	/*
 	 * Update error traceback information.
@@ -3354,8 +3340,7 @@ count_nondeletable_pages(LVRelState *vacrel, bool *lock_waiter_detected)
 			prefetchedUntil = prefetchStart;
 		}
 
-		buf = ReadBufferExtended(vacrel->rel, MAIN_FORKNUM, blkno, RBM_NORMAL,
-								 vacrel->bstrategy);
+		buf = ReadBufferExtended(vacrel->rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 
 		/* In this phase we only need shared access to the buffer */
 		LockBuffer(buf, BUFFER_LOCK_SHARE);
@@ -3446,8 +3431,7 @@ dead_items_alloc(LVRelState *vacrel, int nworkers)
 			vacrel->pvs = parallel_vacuum_init(vacrel->rel, vacrel->indrels,
 											   vacrel->nindexes, nworkers,
 											   vac_work_mem,
-											   vacrel->verbose ? INFO : DEBUG2,
-											   vacrel->bstrategy);
+											   vacrel->verbose ? INFO : DEBUG2);
 
 		/*
 		 * If parallel mode started, dead_items and dead_items_info spaces are
diff --git a/src/backend/access/heap/visibilitymap.c b/src/backend/access/heap/visibilitymap.c
index 1d50e41..2b5fb6a 100644
--- a/src/backend/access/heap/visibilitymap.c
+++ b/src/backend/access/heap/visibilitymap.c
@@ -580,7 +580,7 @@ vm_readbuf(Relation rel, BlockNumber blkno, bool extend)
 	}
 	else
 		buf = ReadBufferExtended(rel, VISIBILITYMAP_FORKNUM, blkno,
-								 RBM_ZERO_ON_ERROR, NULL);
+								 RBM_ZERO_ON_ERROR);
 
 	/*
 	 * Initializing the page when needed is trickier than it looks, because of
@@ -616,7 +616,7 @@ vm_extend(Relation rel, BlockNumber vm_nblocks)
 {
 	Buffer		buf;
 
-	buf = ExtendBufferedRelTo(BMR_REL(rel), VISIBILITYMAP_FORKNUM, NULL,
+	buf = ExtendBufferedRelTo(BMR_REL(rel), VISIBILITYMAP_FORKNUM,
 							  EB_CREATE_FORK_IF_NEEDED |
 							  EB_CLEAR_SIZE_CACHE,
 							  vm_nblocks,
diff --git a/src/backend/access/nbtree/nbtpage.c b/src/backend/access/nbtree/nbtpage.c
index 109017d..7b5fc16 100644
--- a/src/backend/access/nbtree/nbtpage.c
+++ b/src/backend/access/nbtree/nbtpage.c
@@ -960,7 +960,7 @@ _bt_allocbuf(Relation rel, Relation heaprel)
 	 * otherwise would make, as we can't use _bt_lockbuf() without introducing
 	 * a race.
 	 */
-	buf = ExtendBufferedRel(BMR_REL(rel), MAIN_FORKNUM, NULL, EB_LOCK_FIRST);
+	buf = ExtendBufferedRel(BMR_REL(rel), MAIN_FORKNUM, EB_LOCK_FIRST);
 	if (!RelationUsesLocalBuffers(rel))
 		VALGRIND_MAKE_MEM_DEFINED(BufferGetPage(buf), BLCKSZ);
 
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 3df2c75..379a2d3 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -1324,7 +1324,6 @@ btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										info->strategy,
 										rel,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
@@ -1730,8 +1729,7 @@ backtrack:
 		 * recycle all-zero pages, not fail.  Also, we want to use a
 		 * nondefault buffer access strategy.
 		 */
-		buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL,
-								 info->strategy);
+		buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL);
 		goto backtrack;
 	}
 
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index f2ee333..785e385 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -432,7 +432,7 @@ SpGistNewBuffer(Relation index)
 		ReleaseBuffer(buffer);
 	}
 
-	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM, NULL,
+	buffer = ExtendBufferedRel(BMR_REL(index), MAIN_FORKNUM,
 							   EB_LOCK_FIRST);
 
 	return buffer;
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index c461f8d..9f644ed 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -704,8 +704,7 @@ spgprocesspending(spgBulkDeleteState *bds)
 
 		/* examine the referenced page */
 		blkno = ItemPointerGetBlockNumber(&pitem->tid);
-		buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
-									RBM_NORMAL, bds->info->strategy);
+		buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno, RBM_NORMAL);
 		LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE);
 		page = BufferGetPage(buffer);
 
@@ -830,7 +829,6 @@ spgvacuumscan(spgBulkDeleteState *bds)
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_FULL |
 										READ_STREAM_USE_BATCHING,
-										bds->info->strategy,
 										index,
 										MAIN_FORKNUM,
 										block_range_read_stream_cb,
diff --git a/src/backend/access/transam/xloginsert.c b/src/backend/access/transam/xloginsert.c
index f2e10b8..0c16b7f 100644
--- a/src/backend/access/transam/xloginsert.c
+++ b/src/backend/access/transam/xloginsert.c
@@ -1348,7 +1348,7 @@ log_newpage_range(Relation rel, ForkNumber forknum,
 		while (nbufs < XLR_MAX_BLOCK_ID && blkno < endblk)
 		{
 			Buffer		buf = ReadBufferExtended(rel, forknum, blkno,
-												 RBM_NORMAL, NULL);
+												 RBM_NORMAL);
 
 			LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
 
diff --git a/src/backend/access/transam/xlogutils.c b/src/backend/access/transam/xlogutils.c
index 58b9dab..a054ff8 100644
--- a/src/backend/access/transam/xlogutils.c
+++ b/src/backend/access/transam/xlogutils.c
@@ -519,7 +519,7 @@ XLogReadBufferExtended(RelFileLocator rlocator, ForkNumber forknum,
 	{
 		/* page exists in file */
 		buffer = ReadBufferWithoutRelcache(rlocator, forknum, blkno,
-										   mode, NULL, true);
+										   mode, true);
 	}
 	else
 	{
@@ -536,7 +536,6 @@ XLogReadBufferExtended(RelFileLocator rlocator, ForkNumber forknum,
 		Assert(InRecovery);
 		buffer = ExtendBufferedRelTo(BMR_SMGR(smgr, RELPERSISTENCE_PERMANENT),
 									 forknum,
-									 NULL,
 									 EB_PERFORMING_RECOVERY |
 									 EB_SKIP_EXTENSION_LOCK,
 									 blkno + 1,
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 81bba4b..b0a7b8a 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -3431,7 +3431,6 @@ validate_index(Oid heapId, Oid indexId, Snapshot snapshot)
 	ivinfo.estimated_count = true;
 	ivinfo.message_level = DEBUG2;
 	ivinfo.num_heap_tuples = heapRelation->rd_rel->reltuples;
-	ivinfo.strategy = NULL;
 
 	/*
 	 * Encode TIDs as int8 values for the sort, rather than directly sorting
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index f66e80b..9b117a5 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -72,7 +72,6 @@ int			default_statistics_target = 100;
 
 /* A few variables that don't seem worth passing around as parameters */
 static MemoryContext anl_context = NULL;
-static BufferAccessStrategy vac_strategy;
 
 
 static void do_analyze_rel(Relation onerel,
@@ -108,8 +107,7 @@ static Datum ind_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
  */
 void
 analyze_rel(Oid relid, RangeVar *relation,
-			const VacuumParams *params, List *va_cols, bool in_outer_xact,
-			BufferAccessStrategy bstrategy)
+			const VacuumParams *params, List *va_cols, bool in_outer_xact)
 {
 	Relation	onerel;
 	int			elevel;
@@ -124,7 +122,6 @@ analyze_rel(Oid relid, RangeVar *relation,
 		elevel = DEBUG2;
 
 	/* Set up static variables */
-	vac_strategy = bstrategy;
 
 	/*
 	 * Check for user-requested abort.
@@ -730,7 +727,6 @@ do_analyze_rel(Relation onerel, const VacuumParams *params,
 			ivinfo.estimated_count = true;
 			ivinfo.message_level = elevel;
 			ivinfo.num_heap_tuples = onerel->rd_rel->reltuples;
-			ivinfo.strategy = vac_strategy;
 
 			stats = index_vacuum_cleanup(&ivinfo, NULL);
 
@@ -1302,7 +1298,6 @@ acquire_sample_rows(Relation onerel, int elevel,
 	 */
 	stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE |
 										READ_STREAM_USE_BATCHING,
-										vac_strategy,
 										scan->rs_rd,
 										MAIN_FORKNUM,
 										block_sampling_read_stream_next,
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 51dcbd9..c7a7a90 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -262,7 +262,6 @@ ScanSourceDatabasePgClass(Oid tbid, Oid dbid, char *srcpath)
 	LockRelId	relid;
 	Snapshot	snapshot;
 	SMgrRelation smgr;
-	BufferAccessStrategy bstrategy;
 
 	/* Get pg_class relfilenumber. */
 	relfilenumber = RelationMapOidToFilenumberForDatabase(srcpath,
@@ -282,9 +281,6 @@ ScanSourceDatabasePgClass(Oid tbid, Oid dbid, char *srcpath)
 	nblocks = smgrnblocks(smgr, MAIN_FORKNUM);
 	smgrclose(smgr);
 
-	/* Use a buffer access strategy since this is a bulk read operation. */
-	bstrategy = GetAccessStrategy(BAS_BULKREAD);
-
 	/*
 	 * As explained in the function header comments, we need a snapshot that
 	 * will see all committed transactions as committed, and our transaction
@@ -299,7 +295,7 @@ ScanSourceDatabasePgClass(Oid tbid, Oid dbid, char *srcpath)
 		CHECK_FOR_INTERRUPTS();
 
 		buf = ReadBufferWithoutRelcache(rlocator, MAIN_FORKNUM, blkno,
-										RBM_NORMAL, bstrategy, true);
+										RBM_NORMAL, true);
 
 		LockBuffer(buf, BUFFER_LOCK_SHARE);
 		page = BufferGetPage(buf);
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 02883fe..d8c15a8 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -2447,7 +2447,7 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 			if (params->options & CLUOPT_VERBOSE)
 				vac_params.options |= VACOPT_VERBOSE;
 			analyze_rel(tableOid, NULL, &vac_params,
-						stmt->relation->va_cols, true, NULL);
+						stmt->relation->va_cols, true);
 			PopActiveSnapshot();
 			CommandCounterIncrement();
 		}
diff --git a/src/backend/commands/sequence.c b/src/backend/commands/sequence.c
index 5516676..ac1ab94 100644
--- a/src/backend/commands/sequence.c
+++ b/src/backend/commands/sequence.c
@@ -358,7 +358,7 @@ fill_seq_fork_with_data(Relation rel, HeapTuple tuple, ForkNumber forkNum)
 
 	/* Initialize first page of relation with special magic number */
 
-	buf = ExtendBufferedRel(BMR_REL(rel), forkNum, NULL,
+	buf = ExtendBufferedRel(BMR_REL(rel), forkNum,
 							EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
 	Assert(BufferGetBlockNumber(buf) == 0);
 
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 38539a6..66acd27 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -127,32 +127,11 @@ static void vac_truncate_clog(TransactionId frozenXID,
 							  TransactionId lastSaneFrozenXid,
 							  MultiXactId lastSaneMinMulti);
 static bool vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
-					   BufferAccessStrategy bstrategy, bool isTopLevel);
+					   bool isTopLevel);
 static double compute_parallel_delay(void);
 static VacOptValue get_vacoptval_from_boolean(DefElem *def);
 static bool vac_tid_reaped(ItemPointer itemptr, void *state);
 
-/*
- * GUC check function to ensure GUC value specified is within the allowable
- * range.
- */
-bool
-check_vacuum_buffer_usage_limit(int *newval, void **extra,
-								GucSource source)
-{
-	/* Value upper and lower hard limits are inclusive */
-	if (*newval == 0 || (*newval >= MIN_BAS_VAC_RING_SIZE_KB &&
-						 *newval <= MAX_BAS_VAC_RING_SIZE_KB))
-		return true;
-
-	/* Value does not fall within any allowable range */
-	GUC_check_errdetail("\"%s\" must be 0 or between %d kB and %d kB.",
-						"vacuum_buffer_usage_limit",
-						MIN_BAS_VAC_RING_SIZE_KB, MAX_BAS_VAC_RING_SIZE_KB);
-
-	return false;
-}
-
 /*
  * Primary entry point for manual VACUUM and ANALYZE commands
  *
@@ -163,7 +142,6 @@ void
 ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 {
 	VacuumParams params;
-	BufferAccessStrategy bstrategy = NULL;
 	bool		verbose = false;
 	bool		skip_locked = false;
 	bool		analyze = false;
@@ -172,7 +150,6 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 	bool		disable_page_skipping = false;
 	bool		process_main = true;
 	bool		process_toast = true;
-	int			ring_size;
 	bool		skip_database_stats = false;
 	bool		only_database_stats = false;
 	MemoryContext vac_context;
@@ -188,12 +165,6 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 	/* Will be set later if we recurse to a TOAST table. */
 	params.toast_parent = InvalidOid;
 
-	/*
-	 * Set this to an invalid value so it is clear whether or not a
-	 * BUFFER_USAGE_LIMIT was specified when making the access strategy.
-	 */
-	ring_size = -1;
-
 	/* Parse options list */
 	foreach(lc, vacstmt->options)
 	{
@@ -204,32 +175,6 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 			verbose = defGetBoolean(opt);
 		else if (strcmp(opt->defname, "skip_locked") == 0)
 			skip_locked = defGetBoolean(opt);
-		else if (strcmp(opt->defname, "buffer_usage_limit") == 0)
-		{
-			const char *hintmsg;
-			int			result;
-			char	   *vac_buffer_size;
-
-			vac_buffer_size = defGetString(opt);
-
-			/*
-			 * Check that the specified value is valid and the size falls
-			 * within the hard upper and lower limits if it is not 0.
-			 */
-			if (!parse_int(vac_buffer_size, &result, GUC_UNIT_KB, &hintmsg) ||
-				(result != 0 &&
-				 (result < MIN_BAS_VAC_RING_SIZE_KB || result > MAX_BAS_VAC_RING_SIZE_KB)))
-			{
-				ereport(ERROR,
-						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-						 errmsg("%s option must be 0 or between %d kB and %d kB",
-								"BUFFER_USAGE_LIMIT",
-								MIN_BAS_VAC_RING_SIZE_KB, MAX_BAS_VAC_RING_SIZE_KB),
-						 hintmsg ? errhint_internal("%s", _(hintmsg)) : 0));
-			}
-
-			ring_size = result;
-		}
 		else if (!vacstmt->is_vacuumcmd)
 			ereport(ERROR,
 					(errcode(ERRCODE_SYNTAX_ERROR),
@@ -325,17 +270,6 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("VACUUM FULL cannot be performed in parallel")));
 
-	/*
-	 * BUFFER_USAGE_LIMIT does nothing for VACUUM (FULL) so just raise an
-	 * ERROR for that case.  VACUUM (FULL, ANALYZE) does make use of it, so
-	 * we'll permit that.
-	 */
-	if (ring_size != -1 && (params.options & VACOPT_FULL) &&
-		!(params.options & VACOPT_ANALYZE))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("BUFFER_USAGE_LIMIT cannot be specified for VACUUM FULL")));
-
 	/*
 	 * Make sure VACOPT_ANALYZE is specified if any column lists are present.
 	 */
@@ -431,38 +365,8 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 										"Vacuum",
 										ALLOCSET_DEFAULT_SIZES);
 
-	/*
-	 * Make a buffer strategy object in the cross-transaction memory context.
-	 * We needn't bother making this for VACUUM (FULL) or VACUUM
-	 * (ONLY_DATABASE_STATS) as they'll not make use of it.  VACUUM (FULL,
-	 * ANALYZE) is possible, so we'd better ensure that we make a strategy
-	 * when we see ANALYZE.
-	 */
-	if ((params.options & (VACOPT_ONLY_DATABASE_STATS |
-						   VACOPT_FULL)) == 0 ||
-		(params.options & VACOPT_ANALYZE) != 0)
-	{
-
-		MemoryContext old_context = MemoryContextSwitchTo(vac_context);
-
-		Assert(ring_size >= -1);
-
-		/*
-		 * If BUFFER_USAGE_LIMIT was specified by the VACUUM or ANALYZE
-		 * command, it overrides the value of VacuumBufferUsageLimit.  Either
-		 * value may be 0, in which case GetAccessStrategyWithSize() will
-		 * return NULL, effectively allowing full use of shared buffers.
-		 */
-		if (ring_size == -1)
-			ring_size = VacuumBufferUsageLimit;
-
-		bstrategy = GetAccessStrategyWithSize(BAS_VACUUM, ring_size);
-
-		MemoryContextSwitchTo(old_context);
-	}
-
 	/* Now go through the common routine */
-	vacuum(vacstmt->rels, &params, bstrategy, vac_context, isTopLevel);
+	vacuum(vacstmt->rels, &params, vac_context, isTopLevel);
 
 	/* Finally, clean up the vacuum memory context */
 	MemoryContextDelete(vac_context);
@@ -479,19 +383,13 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
  * params contains a set of parameters that can be used to customize the
  * behavior.
  *
- * bstrategy may be passed in as NULL when the caller does not want to
- * restrict the number of shared_buffers that VACUUM / ANALYZE can use,
- * otherwise, the caller must build a BufferAccessStrategy with the number of
- * shared_buffers that VACUUM / ANALYZE should try to limit themselves to
- * using.
- *
  * isTopLevel should be passed down from ProcessUtility.
  *
  * It is the caller's responsibility that all parameters are allocated in a
  * memory context that will not disappear at transaction commit.
  */
 void
-vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrategy,
+vacuum(List *relations, const VacuumParams *params,
 	   MemoryContext vac_context, bool isTopLevel)
 {
 	static bool in_vacuum = false;
@@ -630,7 +528,7 @@ vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrate
 
 			if (params->options & VACOPT_VACUUM)
 			{
-				if (!vacuum_rel(vrel->oid, vrel->relation, *params, bstrategy,
+				if (!vacuum_rel(vrel->oid, vrel->relation, *params,
 								isTopLevel))
 					continue;
 			}
@@ -649,7 +547,7 @@ vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrate
 				}
 
 				analyze_rel(vrel->oid, vrel->relation, params,
-							vrel->va_cols, in_outer_xact, bstrategy);
+							vrel->va_cols, in_outer_xact);
 
 				if (use_own_xacts)
 				{
@@ -2010,7 +1908,7 @@ vac_truncate_clog(TransactionId frozenXID,
  */
 static bool
 vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
-		   BufferAccessStrategy bstrategy, bool isTopLevel)
+		   bool isTopLevel)
 {
 	LOCKMODE	lmode;
 	Relation	rel;
@@ -2307,7 +2205,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 			rel = NULL;
 		}
 		else
-			table_relation_vacuum(rel, &params, bstrategy);
+			table_relation_vacuum(rel, &params);
 	}
 
 	/* Roll back any GUC changes executed by index functions */
@@ -2344,7 +2242,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 		toast_vacuum_params.options |= VACOPT_PROCESS_MAIN;
 		toast_vacuum_params.toast_parent = relid;
 
-		vacuum_rel(toast_relid, NULL, toast_vacuum_params, bstrategy,
+		vacuum_rel(toast_relid, NULL, toast_vacuum_params,
 				   isTopLevel);
 	}
 
diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c
index 41cefcf..8b4b835 100644
--- a/src/backend/commands/vacuumparallel.c
+++ b/src/backend/commands/vacuumparallel.c
@@ -125,12 +125,6 @@ typedef struct PVShared
 	 */
 	int			maintenance_work_mem_worker;
 
-	/*
-	 * The number of buffers each worker's Buffer Access Strategy ring should
-	 * contain.
-	 */
-	int			ring_nbuffers;
-
 	/*
 	 * Shared vacuum cost balance.  During parallel vacuum,
 	 * VacuumSharedCostBalance points to this value and it accumulates the
@@ -257,9 +251,6 @@ struct ParallelVacuumState
 	int			nindexes_parallel_cleanup;
 	int			nindexes_parallel_condcleanup;
 
-	/* Buffer access strategy used by leader process */
-	BufferAccessStrategy bstrategy;
-
 	/*
 	 * Error reporting state.  The error callback is set only for workers
 	 * processes during parallel index vacuum.
@@ -304,7 +295,7 @@ static void parallel_vacuum_dsm_detach(dsm_segment *seg, Datum arg);
 ParallelVacuumState *
 parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes,
 					 int nrequested_workers, int vac_work_mem,
-					 int elevel, BufferAccessStrategy bstrategy)
+					 int elevel)
 {
 	ParallelVacuumState *pvs;
 	ParallelContext *pcxt;
@@ -345,7 +336,6 @@ parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes,
 	pvs->indrels = indrels;
 	pvs->nindexes = nindexes;
 	pvs->will_parallel_vacuum = will_parallel_vacuum;
-	pvs->bstrategy = bstrategy;
 	pvs->heaprel = rel;
 
 	EnterParallelMode();
@@ -447,9 +437,6 @@ parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes,
 	shared->dead_items_handle = TidStoreGetHandle(dead_items);
 	shared->dead_items_dsa_handle = dsa_get_handle(TidStoreGetDSA(dead_items));
 
-	/* Use the same buffer size for all workers */
-	shared->ring_nbuffers = GetAccessStrategyBufferCount(bstrategy);
-
 	pg_atomic_init_u32(&(shared->cost_balance), 0);
 	pg_atomic_init_u32(&(shared->active_nworkers), 0);
 	pg_atomic_init_u32(&(shared->idx), 0);
@@ -1091,7 +1078,6 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel,
 	ivinfo.message_level = DEBUG2;
 	ivinfo.estimated_count = pvs->shared->estimated_count;
 	ivinfo.num_heap_tuples = pvs->shared->reltuples;
-	ivinfo.strategy = pvs->bstrategy;
 
 	/* Update error traceback information */
 	pvs->indname = pstrdup(RelationGetRelationName(indrel));
@@ -1294,10 +1280,6 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
 	pvs.indname = NULL;
 	pvs.status = PARALLEL_INDVAC_STATUS_INITIAL;
 
-	/* Each parallel VACUUM worker gets its own access strategy. */
-	pvs.bstrategy = GetAccessStrategyWithSize(BAS_VACUUM,
-											  shared->ring_nbuffers * (BLCKSZ / 1024));
-
 	/* Setup error traceback support for ereport() */
 	errcallback.callback = parallel_vacuum_error_callback;
 	errcallback.arg = &pvs;
@@ -1328,7 +1310,6 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc)
 
 	vac_close_indexes(nindexes, indrels, RowExclusiveLock);
 	table_close(rel, ShareUpdateExclusiveLock);
-	FreeAccessStrategy(pvs.bstrategy);
 
 	if (shared->is_autovacuum)
 		pv_shared_cost_params = NULL;
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 45abf48..e021e79 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -386,8 +386,7 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
 									  bool *dovacuum, bool *doanalyze, bool *wraparound,
 									  AutoVacuumScores *scores);
 
-static void autovacuum_do_vac_analyze(autovac_table *tab,
-									  BufferAccessStrategy bstrategy);
+static void autovacuum_do_vac_analyze(autovac_table *tab);
 static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
 										 TupleDesc pg_class_desc);
 static void perform_work_item(AutoVacuumWorkItem *workitem);
@@ -1936,7 +1935,6 @@ do_autovacuum(void)
 	HASHCTL		ctl;
 	HTAB	   *table_toast_map;
 	ListCell   *volatile cell;
-	BufferAccessStrategy bstrategy;
 	ScanKeyData key;
 	TupleDesc	pg_class_desc;
 	int			effective_multixact_freeze_max_age;
@@ -2322,23 +2320,6 @@ do_autovacuum(void)
 		autovacuum_analyze_score_weight != 0.0)
 		list_sort(tables_to_process, TableToProcessComparator);
 
-	/*
-	 * Optionally, create a buffer access strategy object for VACUUM to use.
-	 * We use the same BufferAccessStrategy object for all tables VACUUMed by
-	 * this worker to prevent autovacuum from blowing out shared buffers.
-	 *
-	 * VacuumBufferUsageLimit being set to 0 results in
-	 * GetAccessStrategyWithSize returning NULL, effectively meaning we can
-	 * use up to all of shared buffers.
-	 *
-	 * If we later enter failsafe mode on any of the tables being vacuumed, we
-	 * will cease use of the BufferAccessStrategy only for that table.
-	 *
-	 * XXX should we consider adding code to adjust the size of this if
-	 * VacuumBufferUsageLimit changes?
-	 */
-	bstrategy = GetAccessStrategyWithSize(BAS_VACUUM, VacuumBufferUsageLimit);
-
 	/*
 	 * create a memory context to act as fake PortalContext, so that the
 	 * contexts created in the vacuum code are cleaned up for each table.
@@ -2516,7 +2497,7 @@ do_autovacuum(void)
 			MemoryContextSwitchTo(PortalContext);
 
 			/* have at it */
-			autovacuum_do_vac_analyze(tab, bstrategy);
+			autovacuum_do_vac_analyze(tab);
 
 			/*
 			 * Clear a possible query-cancel signal, to avoid a late reaction
@@ -2636,8 +2617,6 @@ deleted:
 #ifdef USE_VALGRIND
 	hash_destroy(table_toast_map);
 	FreeTupleDesc(pg_class_desc);
-	if (bstrategy)
-		pfree(bstrategy);
 #endif
 
 	/* Run the rest in xact context, mainly to avoid Valgrind leak warnings */
@@ -3348,7 +3327,7 @@ relation_needs_vacanalyze(Oid relid,
  * disappear at transaction commit.
  */
 static void
-autovacuum_do_vac_analyze(autovac_table *tab, BufferAccessStrategy bstrategy)
+autovacuum_do_vac_analyze(autovac_table *tab)
 {
 	RangeVar   *rangevar;
 	VacuumRelation *rel;
@@ -3371,7 +3350,7 @@ autovacuum_do_vac_analyze(autovac_table *tab, BufferAccessStrategy bstrategy)
 	rel_list = list_make1(rel);
 	MemoryContextSwitchTo(old_context);
 
-	vacuum(rel_list, &tab->at_params, bstrategy, vac_context, true);
+	vacuum(rel_list, &tab->at_params, vac_context, true);
 
 	MemoryContextDelete(vac_context);
 }
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 73dc539..b6612f1 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -390,7 +390,7 @@ static List *BuildRelationList(bool temp_relations, bool include_shared);
 static void FreeDatabaseList(List *dblist);
 static DataChecksumsWorkerResult ProcessDatabase(DataChecksumsWorkerDatabase *db);
 static bool ProcessAllDatabases(void);
-static bool ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy);
+static bool ProcessSingleRelationFork(Relation reln, ForkNumber forkNum);
 static void ResetDataChecksumsProgressCounters(void);
 static void launcher_cancel_handler(SIGNAL_ARGS);
 static void WaitForAllTransactionsToFinish(void);
@@ -687,7 +687,7 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
  * error is raised in the lower levels.
  */
 static bool
-ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy)
+ProcessSingleRelationFork(Relation reln, ForkNumber forkNum)
 {
 	BlockNumber numblocks = RelationGetNumberOfBlocksInFork(reln, forkNum);
 	char		activity[NAMEDATALEN * 2 + 128];
@@ -722,7 +722,7 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg
 	 */
 	for (BlockNumber blknum = 0; blknum < numblocks; blknum++)
 	{
-		Buffer		buf = ReadBufferExtended(reln, forkNum, blknum, RBM_NORMAL, strategy);
+		Buffer		buf = ReadBufferExtended(reln, forkNum, blknum, RBM_NORMAL);
 
 		/* Need to get an exclusive lock to mark the buffer as dirty */
 		LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
@@ -808,7 +808,7 @@ ResetDataChecksumsProgressCounters(void)
  * error is raised in the lower levels.
  */
 static bool
-ProcessSingleRelationByOid(Oid relationId, BufferAccessStrategy strategy)
+ProcessSingleRelationByOid(Oid relationId)
 {
 	Relation	rel;
 	bool		aborted = false;
@@ -835,7 +835,7 @@ ProcessSingleRelationByOid(Oid relationId, BufferAccessStrategy strategy)
 	{
 		if (smgrexists(rel->rd_smgr, fnum))
 		{
-			if (!ProcessSingleRelationFork(rel, fnum, strategy))
+			if (!ProcessSingleRelationFork(rel, fnum))
 			{
 				aborted = true;
 				break;
@@ -1582,7 +1582,6 @@ DataChecksumsWorkerMain(Datum arg)
 	Oid			dboid;
 	List	   *RelationList = NIL;
 	List	   *InitialTempTableList = NIL;
-	BufferAccessStrategy strategy;
 	bool		aborted = false;
 	int64		rels_done;
 	bool		process_shared;
@@ -1650,11 +1649,6 @@ DataChecksumsWorkerMain(Datum arg)
 	VacuumUpdateCosts();
 	VacuumCostBalance = 0;
 
-	/*
-	 * Create and set the vacuum strategy as our buffer strategy.
-	 */
-	strategy = GetAccessStrategy(BAS_VACUUM);
-
 	RelationList = BuildRelationList(false, process_shared);
 
 	/* Update the total number of relations to be processed in this DB. */
@@ -1676,9 +1670,7 @@ DataChecksumsWorkerMain(Datum arg)
 	rels_done = 0;
 	foreach_oid(reloid, RelationList)
 	{
-		bool		costs_updated = false;
-
-		if (!ProcessSingleRelationByOid(reloid, strategy))
+		if (!ProcessSingleRelationByOid(reloid))
 		{
 			aborted = true;
 			break;
@@ -1694,8 +1686,7 @@ DataChecksumsWorkerMain(Datum arg)
 
 		/*
 		 * Check if the cost settings changed during runtime and if so, update
-		 * to reflect the new values and signal that the access strategy needs
-		 * to be refreshed.
+		 * to reflect the new values.
 		 */
 		LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
 		if (DataChecksumState->worker_invocation != worker_invocation)
@@ -1706,7 +1697,6 @@ DataChecksumsWorkerMain(Datum arg)
 		if ((DataChecksumState->launch_cost_delay != DataChecksumState->cost_delay)
 			|| (DataChecksumState->launch_cost_limit != DataChecksumState->cost_limit))
 		{
-			costs_updated = true;
 			VacuumCostDelay = DataChecksumState->launch_cost_delay;
 			VacuumCostLimit = DataChecksumState->launch_cost_limit;
 			VacuumUpdateCosts();
@@ -1714,19 +1704,10 @@ DataChecksumsWorkerMain(Datum arg)
 			DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
 			DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
 		}
-		else
-			costs_updated = false;
 		LWLockRelease(DataChecksumsWorkerLock);
-
-		if (costs_updated)
-		{
-			FreeAccessStrategy(strategy);
-			strategy = GetAccessStrategy(BAS_VACUUM);
-		}
 	}
 
 	list_free(RelationList);
-	FreeAccessStrategy(strategy);
 
 	if (aborted || abort_requested)
 	{
diff --git a/src/backend/storage/aio/read_stream.c b/src/backend/storage/aio/read_stream.c
index a318539..08d344f 100644
--- a/src/backend/storage/aio/read_stream.c
+++ b/src/backend/storage/aio/read_stream.c
@@ -757,7 +757,6 @@ read_stream_look_ahead(ReadStream *stream)
  */
 static ReadStream *
 read_stream_begin_impl(int flags,
-					   BufferAccessStrategy strategy,
 					   Relation rel,
 					   SMgrRelation smgr,
 					   char persistence,
@@ -771,7 +770,6 @@ read_stream_begin_impl(int flags,
 	int16		queue_size;
 	int16		queue_overflow;
 	int			max_ios;
-	int			strategy_pin_limit;
 	uint32		max_pinned_buffers;
 	uint32		max_possible_buffer_limit;
 	Oid			tablespace_id;
@@ -835,10 +833,6 @@ read_stream_begin_impl(int flags,
 	max_pinned_buffers = Min(max_pinned_buffers,
 							 PG_INT16_MAX - queue_overflow - 1);
 
-	/* Give the strategy a chance to limit the number of buffers we pin. */
-	strategy_pin_limit = GetAccessStrategyPinLimit(strategy);
-	max_pinned_buffers = Min(strategy_pin_limit, max_pinned_buffers);
-
 	/*
 	 * Also limit our queue to the maximum number of pins we could ever be
 	 * allowed to acquire according to the buffer manager.  We may not really
@@ -962,7 +956,6 @@ read_stream_begin_impl(int flags,
 		stream->ios[i].op.smgr = smgr;
 		stream->ios[i].op.persistence = persistence;
 		stream->ios[i].op.forknum = forknum;
-		stream->ios[i].op.strategy = strategy;
 	}
 
 	return stream;
@@ -974,7 +967,6 @@ read_stream_begin_impl(int flags,
  */
 ReadStream *
 read_stream_begin_relation(int flags,
-						   BufferAccessStrategy strategy,
 						   Relation rel,
 						   ForkNumber forknum,
 						   ReadStreamBlockNumberCB callback,
@@ -982,7 +974,6 @@ read_stream_begin_relation(int flags,
 						   size_t per_buffer_data_size)
 {
 	return read_stream_begin_impl(flags,
-								  strategy,
 								  rel,
 								  RelationGetSmgr(rel),
 								  rel->rd_rel->relpersistence,
@@ -998,7 +989,6 @@ read_stream_begin_relation(int flags,
  */
 ReadStream *
 read_stream_begin_smgr_relation(int flags,
-								BufferAccessStrategy strategy,
 								SMgrRelation smgr,
 								char smgr_persistence,
 								ForkNumber forknum,
@@ -1007,7 +997,6 @@ read_stream_begin_smgr_relation(int flags,
 								size_t per_buffer_data_size)
 {
 	return read_stream_begin_impl(flags,
-								  strategy,
 								  NULL,
 								  smgr,
 								  smgr_persistence,
@@ -1370,13 +1359,11 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data)
  * Transitional support for code that would like to perform or skip reads
  * itself, without using the stream.  Returns, and consumes, the next block
  * number that would be read by the stream's look-ahead algorithm, or
- * InvalidBlockNumber if the end of the stream is reached.  Also reports the
- * strategy that would be used to read it.
+ * InvalidBlockNumber if the end of the stream is reached.
  */
 BlockNumber
-read_stream_next_block(ReadStream *stream, BufferAccessStrategy *strategy)
+read_stream_next_block(ReadStream *stream)
 {
-	*strategy = stream->ios[0].op.strategy;
 	return read_stream_get_block(stream, NULL);
 }
 
diff --git a/src/backend/storage/buffer/README b/src/backend/storage/buffer/README
index b332e00..58df6bd 100644
--- a/src/backend/storage/buffer/README
+++ b/src/backend/storage/buffer/README
@@ -203,50 +203,6 @@ have to give up and try another buffer.  This however is not a concern
 of the basic select-a-victim-buffer algorithm.)
 
 
-Buffer Ring Replacement Strategy
----------------------------------
-
-When running a query that needs to access a large number of pages just once,
-such as VACUUM or a large sequential scan, a different strategy is used.
-A page that has been touched only by such a scan is unlikely to be needed
-again soon, so instead of running the normal clock-sweep algorithm and
-blowing out the entire buffer cache, a small ring of buffers is allocated
-using the normal clock-sweep algorithm and those buffers are reused for the
-whole scan.  This also implies that much of the write traffic caused by such
-a statement will be done by the backend itself and not pushed off onto other
-processes.
-
-For sequential scans, a 256KB ring is used. That's small enough to fit in L2
-cache, which makes transferring pages from OS cache to shared buffer cache
-efficient.  Even less would often be enough, but the ring must be big enough
-to accommodate all pages in the scan that are pinned concurrently.  256KB
-should also be enough to leave a small cache trail for other backends to
-join in a synchronized seq scan.  If a ring buffer is dirtied and its LSN
-updated, we would normally have to write and flush WAL before we could
-re-use the buffer; in this case we instead discard the buffer from the ring
-and (later) choose a replacement using the normal clock-sweep algorithm.
-Hence this strategy works best for scans that are read-only (or at worst
-update hint bits).  In a scan that modifies every page in the scan, like a
-bulk UPDATE or DELETE, the buffers in the ring will always be dirtied and
-the ring strategy effectively degrades to the normal strategy.
-
-VACUUM uses a ring like sequential scans, however, the size of this ring is
-controlled by the vacuum_buffer_usage_limit GUC.  Dirty pages are not removed
-from the ring.  Instead, the WAL is flushed if needed to allow reuse of the
-buffers.  Before introducing the buffer ring strategy in 8.3, VACUUM's buffers
-were sent to the freelist, which was effectively a buffer ring of 1 buffer,
-resulting in excessive WAL flushing.
-
-Bulk writes work similarly to VACUUM.  Currently this applies only to
-COPY IN and CREATE TABLE AS SELECT.  (Might it be interesting to make
-seqscan UPDATE and DELETE use the bulkwrite strategy?)  For bulk writes
-we use a ring size of 16MB (but not more than 1/8th of shared_buffers).
-Smaller sizes have been shown to result in the COPY blocking too often
-for WAL flushes.  While it's okay for a background vacuum to be slowed by
-doing its own WAL flushing, we'd prefer that COPY not be subject to that,
-so we let it use up a bit more of the buffer arena.
-
-
 Background Writer's Processing
 ------------------------------
 
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 1f5a008..cc2fab9 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -612,10 +612,9 @@ ForgetPrivateRefCountEntry(PrivateRefCountEntry *ref)
 static Buffer ReadBuffer_common(Relation rel,
 								SMgrRelation smgr, char smgr_persistence,
 								ForkNumber forkNum, BlockNumber blockNum,
-								ReadBufferMode mode, BufferAccessStrategy strategy);
+								ReadBufferMode mode);
 static BlockNumber ExtendBufferedRelCommon(BufferManagerRelation bmr,
 										   ForkNumber fork,
-										   BufferAccessStrategy strategy,
 										   uint32 flags,
 										   uint32 extend_by,
 										   BlockNumber extend_upto,
@@ -623,13 +622,12 @@ static BlockNumber ExtendBufferedRelCommon(BufferManagerRelation bmr,
 										   uint32 *extended_by);
 static BlockNumber ExtendBufferedRelShared(BufferManagerRelation bmr,
 										   ForkNumber fork,
-										   BufferAccessStrategy strategy,
 										   uint32 flags,
 										   uint32 extend_by,
 										   BlockNumber extend_upto,
 										   Buffer *buffers,
 										   uint32 *extended_by);
-static bool PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy,
+static bool PinBuffer(BufferDesc *buf,
 					  bool skip_if_not_valid);
 static void PinBuffer_Locked(BufferDesc *buf);
 static void UnpinBuffer(BufferDesc *buf);
@@ -645,7 +643,6 @@ static inline BufferDesc *BufferAlloc(SMgrRelation smgr,
 									  char relpersistence,
 									  ForkNumber forkNum,
 									  BlockNumber blockNum,
-									  BufferAccessStrategy strategy,
 									  bool *foundPtr, IOContext io_context);
 static bool AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress);
 static void CheckReadBuffersOperation(ReadBuffersOperation *operation, bool is_complete);
@@ -654,7 +651,7 @@ static pg_always_inline void TrackBufferHit(IOObject io_object,
 											IOContext io_context,
 											Relation rel, char persistence, SMgrRelation smgr,
 											ForkNumber forknum, BlockNumber blocknum);
-static Buffer GetVictimBuffer(BufferAccessStrategy strategy, IOContext io_context);
+static Buffer GetVictimBuffer(IOContext io_context);
 static void FlushUnlockedBuffer(BufferDesc *buf, SMgrRelation reln,
 								IOObject io_object, IOContext io_context);
 static void FlushBuffer(BufferDesc *buf, SMgrRelation reln,
@@ -858,7 +855,7 @@ ReadRecentBuffer(RelFileLocator rlocator, ForkNumber forkNum, BlockNumber blockN
 		 * pin.
 		 */
 		if (BufferTagsEqual(&tag, &bufHdr->tag) &&
-			PinBuffer(bufHdr, NULL, true))
+			PinBuffer(bufHdr, true))
 		{
 			if (BufferTagsEqual(&tag, &bufHdr->tag))
 			{
@@ -874,12 +871,12 @@ ReadRecentBuffer(RelFileLocator rlocator, ForkNumber forkNum, BlockNumber blockN
 
 /*
  * ReadBuffer -- a shorthand for ReadBufferExtended, for reading from main
- *		fork with RBM_NORMAL mode and default strategy.
+ *		fork with RBM_NORMAL mode.
  */
 Buffer
 ReadBuffer(Relation reln, BlockNumber blockNum)
 {
-	return ReadBufferExtended(reln, MAIN_FORKNUM, blockNum, RBM_NORMAL, NULL);
+	return ReadBufferExtended(reln, MAIN_FORKNUM, blockNum, RBM_NORMAL);
 }
 
 /*
@@ -919,13 +916,10 @@ ReadBuffer(Relation reln, BlockNumber blockNum)
  * a cleanup-strength lock on the page.
  *
  * RBM_NORMAL_NO_LOG mode is treated the same as RBM_NORMAL here.
- *
- * If strategy is not NULL, a nondefault buffer access strategy is used.
- * See buffer/README for details.
  */
 inline Buffer
 ReadBufferExtended(Relation reln, ForkNumber forkNum, BlockNumber blockNum,
-				   ReadBufferMode mode, BufferAccessStrategy strategy)
+				   ReadBufferMode mode)
 {
 	Buffer		buf;
 
@@ -935,7 +929,7 @@ ReadBufferExtended(Relation reln, ForkNumber forkNum, BlockNumber blockNum,
 	 * ReadBuffer_common().
 	 */
 	buf = ReadBuffer_common(reln, RelationGetSmgr(reln), 0,
-							forkNum, blockNum, mode, strategy);
+							forkNum, blockNum, mode);
 
 	return buf;
 }
@@ -954,14 +948,14 @@ ReadBufferExtended(Relation reln, ForkNumber forkNum, BlockNumber blockNum,
 Buffer
 ReadBufferWithoutRelcache(RelFileLocator rlocator, ForkNumber forkNum,
 						  BlockNumber blockNum, ReadBufferMode mode,
-						  BufferAccessStrategy strategy, bool permanent)
+						  bool permanent)
 {
 	SMgrRelation smgr = smgropen(rlocator, INVALID_PROC_NUMBER);
 
 	return ReadBuffer_common(NULL, smgr,
 							 permanent ? RELPERSISTENCE_PERMANENT : RELPERSISTENCE_UNLOGGED,
 							 forkNum, blockNum,
-							 mode, strategy);
+							 mode);
 }
 
 /*
@@ -970,13 +964,12 @@ ReadBufferWithoutRelcache(RelFileLocator rlocator, ForkNumber forkNum,
 Buffer
 ExtendBufferedRel(BufferManagerRelation bmr,
 				  ForkNumber forkNum,
-				  BufferAccessStrategy strategy,
 				  uint32 flags)
 {
 	Buffer		buf;
 	uint32		extend_by = 1;
 
-	ExtendBufferedRelBy(bmr, forkNum, strategy, flags, extend_by,
+	ExtendBufferedRelBy(bmr, forkNum, flags, extend_by,
 						&buf, &extend_by);
 
 	return buf;
@@ -1002,7 +995,6 @@ ExtendBufferedRel(BufferManagerRelation bmr,
 BlockNumber
 ExtendBufferedRelBy(BufferManagerRelation bmr,
 					ForkNumber fork,
-					BufferAccessStrategy strategy,
 					uint32 flags,
 					uint32 extend_by,
 					Buffer *buffers,
@@ -1015,7 +1007,7 @@ ExtendBufferedRelBy(BufferManagerRelation bmr,
 	if (bmr.relpersistence == '\0')
 		bmr.relpersistence = bmr.rel->rd_rel->relpersistence;
 
-	return ExtendBufferedRelCommon(bmr, fork, strategy, flags,
+	return ExtendBufferedRelCommon(bmr, fork, flags,
 								   extend_by, InvalidBlockNumber,
 								   buffers, extended_by);
 }
@@ -1031,7 +1023,6 @@ ExtendBufferedRelBy(BufferManagerRelation bmr,
 Buffer
 ExtendBufferedRelTo(BufferManagerRelation bmr,
 					ForkNumber fork,
-					BufferAccessStrategy strategy,
 					uint32 flags,
 					BlockNumber extend_to,
 					ReadBufferMode mode)
@@ -1097,7 +1088,7 @@ ExtendBufferedRelTo(BufferManagerRelation bmr,
 		if ((uint64) current_size + num_pages > extend_to)
 			num_pages = extend_to - current_size;
 
-		first_block = ExtendBufferedRelCommon(bmr, fork, strategy, flags,
+		first_block = ExtendBufferedRelCommon(bmr, fork, flags,
 											  num_pages, extend_to,
 											  buffers, &extended_by);
 
@@ -1123,7 +1114,7 @@ ExtendBufferedRelTo(BufferManagerRelation bmr,
 	{
 		Assert(extended_by == 0);
 		buffer = ReadBuffer_common(bmr.rel, BMR_GET_SMGR(bmr), bmr.relpersistence,
-								   fork, extend_to - 1, mode, strategy);
+								   fork, extend_to - 1, mode);
 	}
 
 	return buffer;
@@ -1226,7 +1217,6 @@ PinBufferForBlock(Relation rel,
 				  char persistence,
 				  ForkNumber forkNum,
 				  BlockNumber blockNum,
-				  BufferAccessStrategy strategy,
 				  IOObject io_object,
 				  IOContext io_context,
 				  bool *foundPtr)
@@ -1250,7 +1240,7 @@ PinBufferForBlock(Relation rel,
 		bufHdr = LocalBufferAlloc(smgr, forkNum, blockNum, foundPtr);
 	else
 		bufHdr = BufferAlloc(smgr, persistence, forkNum, blockNum,
-							 strategy, foundPtr, io_context);
+							 foundPtr, io_context);
 
 	if (*foundPtr)
 		TrackBufferHit(io_object, io_context, rel, persistence, smgr, forkNum, blockNum);
@@ -1276,8 +1266,7 @@ PinBufferForBlock(Relation rel,
 static pg_always_inline Buffer
 ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
 				  ForkNumber forkNum,
-				  BlockNumber blockNum, ReadBufferMode mode,
-				  BufferAccessStrategy strategy)
+				  BlockNumber blockNum, ReadBufferMode mode)
 {
 	ReadBuffersOperation operation;
 	Buffer		buffer;
@@ -1313,7 +1302,7 @@ ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
 		if (mode == RBM_ZERO_AND_LOCK || mode == RBM_ZERO_AND_CLEANUP_LOCK)
 			flags |= EB_LOCK_FIRST;
 
-		return ExtendBufferedRel(BMR_REL(rel), forkNum, strategy, flags);
+		return ExtendBufferedRel(BMR_REL(rel), forkNum, flags);
 	}
 
 	if (rel)
@@ -1335,12 +1324,12 @@ ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
 		}
 		else
 		{
-			io_context = IOContextForStrategy(strategy);
+			io_context = IOCONTEXT_NORMAL;
 			io_object = IOOBJECT_RELATION;
 		}
 
 		buffer = PinBufferForBlock(rel, smgr, persistence,
-								   forkNum, blockNum, strategy,
+								   forkNum, blockNum,
 								   io_object, io_context, &found);
 		ZeroAndLockBuffer(buffer, mode, found);
 		return buffer;
@@ -1358,7 +1347,6 @@ ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
 	operation.rel = rel;
 	operation.persistence = persistence;
 	operation.forknum = forkNum;
-	operation.strategy = strategy;
 	if (StartReadBuffer(&operation,
 						&buffer,
 						blockNum,
@@ -1399,7 +1387,7 @@ StartReadBuffersImpl(ReadBuffersOperation *operation,
 	}
 	else
 	{
-		io_context = IOContextForStrategy(operation->strategy);
+		io_context = IOCONTEXT_NORMAL;
 		io_object = IOOBJECT_RELATION;
 	}
 
@@ -1450,7 +1438,6 @@ StartReadBuffersImpl(ReadBuffersOperation *operation,
 										   operation->persistence,
 										   operation->forknum,
 										   blockNum + i,
-										   operation->strategy,
 										   io_object, io_context,
 										   &found);
 		}
@@ -1771,7 +1758,7 @@ WaitReadBuffers(ReadBuffersOperation *operation)
 	}
 	else
 	{
-		io_context = IOContextForStrategy(operation->strategy);
+		io_context = IOCONTEXT_NORMAL;
 		io_object = IOOBJECT_RELATION;
 	}
 
@@ -1961,7 +1948,7 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress)
 	}
 	else
 	{
-		io_context = IOContextForStrategy(operation->strategy);
+		io_context = IOCONTEXT_NORMAL;
 		io_object = IOOBJECT_RELATION;
 	}
 
@@ -2180,24 +2167,18 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress)
  *		buffer.  If no buffer exists already, selects a replacement victim and
  *		evicts the old page, but does NOT read in new page.
  *
- * "strategy" can be a buffer replacement strategy object, or NULL for
- * the default strategy.  The selected buffer's usage_count is advanced when
- * using the default strategy, but otherwise possibly not (see PinBuffer).
- *
  * The returned buffer is pinned and is already marked as holding the
  * desired page.  If it already did have the desired page, *foundPtr is
  * set true.  Otherwise, *foundPtr is set false.
  *
- * io_context is passed as an output parameter to avoid calling
- * IOContextForStrategy() when there is a shared buffers hit and no IO
- * statistics need be captured.
+ * io_context is passed as an output parameter to avoid capturing IO
+ * statistics when there is a shared buffers hit and no IO occurs.
  *
  * No locks are held either at entry or exit.
  */
 static pg_always_inline BufferDesc *
 BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 			BlockNumber blockNum,
-			BufferAccessStrategy strategy,
 			bool *foundPtr, IOContext io_context)
 {
 	BufferTag	newTag;			/* identity of requested block */
@@ -2235,7 +2216,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 		 */
 		buf = GetBufferDescriptor(existing_buf_id);
 
-		valid = PinBuffer(buf, strategy, false);
+		valid = PinBuffer(buf, false);
 
 		/* Can release the mapping lock as soon as we've pinned it */
 		LWLockRelease(newPartitionLock);
@@ -2266,7 +2247,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	 * don't hold any conflicting locks. If so we'll have to undo our work
 	 * later.
 	 */
-	victim_buffer = GetVictimBuffer(strategy, io_context);
+	victim_buffer = GetVictimBuffer(io_context);
 	victim_buf_hdr = GetBufferDescriptor(victim_buffer - 1);
 
 	/*
@@ -2297,7 +2278,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 
 		existing_buf_hdr = GetBufferDescriptor(existing_buf_id);
 
-		valid = PinBuffer(existing_buf_hdr, strategy, false);
+		valid = PinBuffer(existing_buf_hdr, false);
 
 		/* Can release the mapping lock as soon as we've pinned it */
 		LWLockRelease(newPartitionLock);
@@ -2335,9 +2316,12 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	 * just like permanent relations.
 	 */
 	set_bits |= BM_TAG_VALID;
-	/* Admit the newly loaded page COOL (probation); a second access via
+
+	/*
+	 * Admit the newly loaded page COOL (probation); a second access via
 	 * PinBuffer promotes it to HOT.  This is what makes a one-touch scan
-	 * self-evicting -- see the cooling-state notes in buf_internals.h. */
+	 * self-evicting -- see the cooling-state notes in buf_internals.h.
+	 */
 	if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM)
 		set_bits |= BM_PERMANENT;
 
@@ -2549,12 +2533,11 @@ InvalidateVictimBuffer(BufferDesc *buf_hdr)
 }
 
 static Buffer
-GetVictimBuffer(BufferAccessStrategy strategy, IOContext io_context)
+GetVictimBuffer(IOContext io_context)
 {
 	BufferDesc *buf_hdr;
 	Buffer		buf;
 	uint64		buf_state;
-	bool		from_ring;
 
 	/*
 	 * Ensure, before we pin a victim buffer, that there's a free refcount
@@ -2570,7 +2553,7 @@ again:
 	 * Select a victim buffer.  The buffer is returned pinned and owned by
 	 * this backend.
 	 */
-	buf_hdr = StrategyGetBuffer(strategy, &buf_state, &from_ring);
+	buf_hdr = StrategyGetBuffer(&buf_state);
 	buf = BufferDescriptorGetBuffer(buf_hdr);
 
 	/*
@@ -2614,26 +2597,6 @@ again:
 			goto again;
 		}
 
-		/*
-		 * If using a nondefault strategy, and this victim came from the
-		 * strategy ring, let the strategy decide whether to reject it when
-		 * reusing it would require a WAL flush.  This only applies to
-		 * permanent buffers; unlogged buffers can have fake LSNs, so
-		 * XLogNeedsFlush() is not meaningful for them.
-		 *
-		 * We need to hold the content lock in at least share-exclusive mode
-		 * to safely inspect the page LSN, so this couldn't have been done
-		 * inside StrategyGetBuffer().
-		 */
-		if (strategy && from_ring &&
-			buf_state & BM_PERMANENT &&
-			XLogNeedsFlush(BufferGetLSN(buf_hdr)) &&
-			StrategyRejectBuffer(strategy, buf_hdr, from_ring))
-		{
-			UnlockReleaseBuffer(buf);
-			goto again;
-		}
-
 		/* OK, do the I/O */
 		FlushBuffer(buf_hdr, NULL, IOOBJECT_RELATION, io_context);
 		LockBuffer(buf, BUFFER_LOCK_UNLOCK);
@@ -2646,23 +2609,15 @@ again:
 	if (buf_state & BM_VALID)
 	{
 		/*
-		 * When a BufferAccessStrategy is in use, blocks evicted from shared
-		 * buffers are counted as IOOP_EVICT in the corresponding context
-		 * (e.g. IOCONTEXT_BULKWRITE). Shared buffers are evicted by a
-		 * strategy in two cases: 1) while initially claiming buffers for the
-		 * strategy ring 2) to replace an existing strategy ring buffer
-		 * because it is pinned or in use and cannot be reused.
+		 * Blocks evicted from shared buffers are counted as IOOP_EVICT.
 		 *
-		 * Blocks evicted from buffers already in the strategy ring are
-		 * counted as IOOP_REUSE in the corresponding strategy context.
-		 *
-		 * At this point, we can accurately count evictions and reuses,
-		 * because we have successfully claimed the valid buffer. Previously,
-		 * we may have been forced to release the buffer due to concurrent
-		 * pinners or erroring out.
+		 * At this point, we can accurately count evictions, because we have
+		 * successfully claimed the valid buffer. Previously, we may have been
+		 * forced to release the buffer due to concurrent pinners or erroring
+		 * out.
 		 */
 		pgstat_count_io_op(IOOBJECT_RELATION, io_context,
-						   from_ring ? IOOP_REUSE : IOOP_EVICT, 1, 0);
+						   IOOP_EVICT, 1, 0);
 	}
 
 	/*
@@ -2754,7 +2709,6 @@ LimitAdditionalPins(uint32 *additional_pins)
 static BlockNumber
 ExtendBufferedRelCommon(BufferManagerRelation bmr,
 						ForkNumber fork,
-						BufferAccessStrategy strategy,
 						uint32 flags,
 						uint32 extend_by,
 						BlockNumber extend_upto,
@@ -2789,7 +2743,7 @@ ExtendBufferedRelCommon(BufferManagerRelation bmr,
 											 buffers, &extend_by);
 	}
 	else
-		first_block = ExtendBufferedRelShared(bmr, fork, strategy, flags,
+		first_block = ExtendBufferedRelShared(bmr, fork, flags,
 											  extend_by, extend_upto,
 											  buffers, &extend_by);
 	*extended_by = extend_by;
@@ -2812,7 +2766,6 @@ ExtendBufferedRelCommon(BufferManagerRelation bmr,
 static BlockNumber
 ExtendBufferedRelShared(BufferManagerRelation bmr,
 						ForkNumber fork,
-						BufferAccessStrategy strategy,
 						uint32 flags,
 						uint32 extend_by,
 						BlockNumber extend_upto,
@@ -2820,7 +2773,7 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 						uint32 *extended_by)
 {
 	BlockNumber first_block;
-	IOContext	io_context = IOContextForStrategy(strategy);
+	IOContext	io_context = IOCONTEXT_NORMAL;
 	instr_time	io_start;
 
 	LimitAdditionalPins(&extend_by);
@@ -2839,7 +2792,7 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 	{
 		Block		buf_block;
 
-		buffers[i] = GetVictimBuffer(strategy, io_context);
+		buffers[i] = GetVictimBuffer(io_context);
 		buf_block = BufHdrGetBlock(GetBufferDescriptor(buffers[i] - 1));
 
 		/* new buffers are zero-filled */
@@ -2957,7 +2910,7 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 			 * Pin the existing buffer before releasing the partition lock,
 			 * preventing it from being evicted.
 			 */
-			valid = PinBuffer(existing_hdr, strategy, false);
+			valid = PinBuffer(existing_hdr, false);
 
 			LWLockRelease(partition_lock);
 			UnpinBuffer(victim_buf_hdr);
@@ -3007,8 +2960,11 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 			victim_buf_hdr->tag = tag;
 
 			set_bits |= BM_TAG_VALID;
-			/* Admit COOL (probation); see the comment at the other admission
-			 * site and the cooling-state notes in buf_internals.h. */
+
+			/*
+			 * Admit COOL (probation); see the comment at the other admission
+			 * site and the cooling-state notes in buf_internals.h.
+			 */
 			if (bmr.relpersistence == RELPERSISTENCE_PERMANENT || fork == INIT_FORKNUM)
 				set_bits |= BM_PERMANENT;
 
@@ -3275,13 +3231,8 @@ ReleaseAndReadBuffer(Buffer buffer,
 /*
  * PinBuffer -- make buffer unavailable for replacement.
  *
- * For the default access strategy, the buffer's usage_count is incremented
- * when we first pin it; for other strategies we just make sure the usage_count
- * isn't zero.  (The idea of the latter is that we don't want synchronized
- * heap scans to inflate the count, but we need it to not be zero to discourage
- * other backends from stealing buffers from our ring.  As long as we cycle
- * through the ring faster than the global clock-sweep cycles, buffers in
- * our ring won't be chosen as victims for replacement by other backends.)
+ * The buffer's cooling state is promoted to HOT (the 2Q rescue) when we pin
+ * it; see the cooling-state notes in buf_internals.h.
  *
  * This should be applied only to shared buffers, never local ones.
  *
@@ -3298,7 +3249,7 @@ ReleaseAndReadBuffer(Buffer buffer,
  * (recently) invalid and has not been pinned.
  */
 static bool
-PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy,
+PinBuffer(BufferDesc *buf,
 		  bool skip_if_not_valid)
 {
 	Buffer		b = BufferDescriptorGetBuffer(buf);
@@ -3396,7 +3347,7 @@ PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy,
  * ResourceOwnerEnlarge(CurrentResourceOwner);
  *
  * Currently, no callers of this function want to modify the buffer's
- * usage_count at all, so there's no need for a strategy parameter.
+ * cooling state at all.
  * Also we don't bother with a BM_VALID test (the caller could check that for
  * itself).
  *
@@ -4674,22 +4625,8 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object,
 			  false);
 
 	/*
-	 * When a strategy is in use, only flushes of dirty buffers already in the
-	 * strategy ring are counted as strategy writes (IOCONTEXT
-	 * [BULKREAD|BULKWRITE|VACUUM] IOOP_WRITE) for the purpose of IO
-	 * statistics tracking.
-	 *
-	 * If a shared buffer initially added to the ring must be flushed before
-	 * being used, this is counted as an IOCONTEXT_NORMAL IOOP_WRITE.
-	 *
-	 * If a shared buffer which was added to the ring later because the
-	 * current strategy buffer is pinned or in use or because all strategy
-	 * buffers were dirty and rejected (for BAS_BULKREAD operations only)
-	 * requires flushing, this is counted as an IOCONTEXT_NORMAL IOOP_WRITE
-	 * (from_ring will be false).
-	 *
-	 * When a strategy is not in use, the write can only be a "regular" write
-	 * of a dirty shared buffer (IOCONTEXT_NORMAL IOOP_WRITE).
+	 * All writes of a dirty shared buffer are counted as an IOCONTEXT_NORMAL
+	 * IOOP_WRITE for the purpose of IO statistics tracking.
 	 */
 	pgstat_count_io_op_time(io_object, io_context,
 							IOOP_WRITE, io_start, 1, BLCKSZ);
@@ -5450,8 +5387,6 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 	BlockNumber nblocks;
 	BlockNumber blkno;
 	PGIOAlignedBlock buf;
-	BufferAccessStrategy bstrategy_src;
-	BufferAccessStrategy bstrategy_dst;
 	BlockRangeReadStreamPrivate p;
 	ReadStream *src_stream;
 	SMgrRelation src_smgr;
@@ -5479,10 +5414,6 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 	smgrextend(smgropen(dstlocator, INVALID_PROC_NUMBER), forkNum, nblocks - 1,
 			   buf.data, true);
 
-	/* This is a bulk operation, so use buffer access strategies. */
-	bstrategy_src = GetAccessStrategy(BAS_BULKREAD);
-	bstrategy_dst = GetAccessStrategy(BAS_BULKWRITE);
-
 	/* Initialize streaming read */
 	p.current_blocknum = 0;
 	p.last_exclusive = nblocks;
@@ -5494,7 +5425,6 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 	 */
 	src_stream = read_stream_begin_smgr_relation(READ_STREAM_FULL |
 												 READ_STREAM_USE_BATCHING,
-												 bstrategy_src,
 												 src_smgr,
 												 permanent ? RELPERSISTENCE_PERMANENT : RELPERSISTENCE_UNLOGGED,
 												 forkNum,
@@ -5514,7 +5444,7 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 
 		dstBuf = ReadBufferWithoutRelcache(dstlocator, forkNum,
 										   BufferGetBlockNumber(srcBuf),
-										   RBM_ZERO_AND_LOCK, bstrategy_dst,
+										   RBM_ZERO_AND_LOCK,
 										   permanent);
 		dstPage = BufferGetPage(dstBuf);
 
@@ -5535,9 +5465,6 @@ RelationCopyStorageUsingBuffer(RelFileLocator srclocator,
 	}
 	Assert(read_stream_next_buffer(src_stream, NULL) == InvalidBuffer);
 	read_stream_end(src_stream);
-
-	FreeAccessStrategy(bstrategy_src);
-	FreeAccessStrategy(bstrategy_dst);
 }
 
 /* ---------------------------------------------------------------------
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index e8cf13f..ee62b33 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -77,40 +77,6 @@ const ShmemCallbacks StrategyCtlShmemCallbacks = {
 	.init_fn = StrategyCtlShmemInit,
 };
 
-/*
- * Private (non-shared) state for managing a ring of shared buffers to re-use.
- * This is currently the only kind of BufferAccessStrategy object, but someday
- * we might have more kinds.
- */
-typedef struct BufferAccessStrategyData
-{
-	/* Overall strategy type */
-	BufferAccessStrategyType btype;
-	/* Number of elements in buffers[] array */
-	int			nbuffers;
-
-	/*
-	 * Index of the "current" slot in the ring, ie, the one most recently
-	 * returned by GetBufferFromRing.
-	 */
-	int			current;
-
-	/*
-	 * Array of buffer numbers.  InvalidBuffer (that is, zero) indicates we
-	 * have not yet selected a buffer for this ring slot.  For allocation
-	 * simplicity this is palloc'd together with the fixed fields of the
-	 * struct.
-	 */
-	Buffer		buffers[FLEXIBLE_ARRAY_MEMBER];
-}			BufferAccessStrategyData;
-
-
-/* Prototypes for internal functions */
-static BufferDesc *GetBufferFromRing(BufferAccessStrategy strategy,
-									 uint64 *buf_state);
-static void AddBufferToRing(BufferAccessStrategy strategy,
-							BufferDesc *buf);
-
 /*
  * Per-backend state for the batched clock sweep.  Each backend claims a run
  * of consecutive clock-hand values with a single atomic fetch-add and then
@@ -157,8 +123,8 @@ ClockSweepTick(void)
 			 * must hold the spinlock so StrategySyncStart() can read
 			 * nextVictimBuffer and completePasses consistently.
 			 *
-			 * With batching, multiple backends may each land a fetch-add
-			 * that returns a value past NBuffers in the same pass.  After
+			 * With batching, multiple backends may each land a fetch-add that
+			 * returns a value past NBuffers in the same pass.  After
 			 * acquiring the spinlock we re-read the counter: if another
 			 * backend already wrapped it below NBuffers we are done.
 			 */
@@ -214,8 +180,6 @@ ClockSweepTick(void)
  *	GetVictimBuffer(). The only hard requirement GetVictimBuffer() has is that
  *	the selected buffer must not currently be pinned by anyone.
  *
- *	strategy is a BufferAccessStrategy object, or NULL for default strategy.
- *
  *	It is the callers responsibility to ensure the buffer ownership can be
  *	tracked via TrackNewBufferPin().
  *
@@ -223,29 +187,13 @@ ClockSweepTick(void)
  *	before returning.
  */
 BufferDesc *
-StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_ring)
+StrategyGetBuffer(uint64 *buf_state)
 {
 	BufferDesc *buf;
 	int			bgwprocno;
 	int			trycounter;
 	bool		force_cool;
 
-	*from_ring = false;
-
-	/*
-	 * If given a strategy object, see whether it can select a buffer. We
-	 * assume strategy objects don't need buffer_strategy_lock.
-	 */
-	if (strategy != NULL)
-	{
-		buf = GetBufferFromRing(strategy, buf_state);
-		if (buf != NULL)
-		{
-			*from_ring = true;
-			return buf;
-		}
-	}
-
 	/*
 	 * If asked, we need to waken the bgwriter. Since we don't want to rely on
 	 * a spinlock for this we force a read from shared memory once, and then
@@ -274,8 +222,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 
 	/*
 	 * We count buffer allocation requests so that the bgwriter can estimate
-	 * the rate of buffer consumption.  Note that buffers recycled by a
-	 * strategy object are intentionally not counted here.
+	 * the rate of buffer consumption.
 	 */
 	pg_atomic_fetch_add_u32(&StrategyControl->numBufferAllocs, 1);
 
@@ -347,7 +294,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 				if (!force_cool)
 				{
 					no_progress = true;
-					break;			/* advance the hand, look for COOL */
+					break;		/* advance the hand, look for COOL */
 				}
 
 				if (BUF_STATE_GET_REFBIT(local_buf_state))
@@ -387,8 +334,6 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 												   local_buf_state))
 				{
 					/* Found a usable buffer */
-					if (strategy != NULL)
-						AddBufferToRing(strategy, buf);
 					*buf_state = local_buf_state;
 
 					TrackNewBufferPin(BufferDescriptorGetBuffer(buf));
@@ -399,11 +344,12 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 		}
 
 		/*
-		 * A tick that made no progress toward a victim counts down trycounter.
-		 * A full unproductive pass escalates to force_cool (cool HOT buffers
-		 * into victims); a second unproductive full pass means everything is
-		 * pinned, so fail rather than spin forever.  (A failed CAS above is
-		 * neither progress nor a full miss: we simply retry the same buffer.)
+		 * A tick that made no progress toward a victim counts down
+		 * trycounter. A full unproductive pass escalates to force_cool (cool
+		 * HOT buffers into victims); a second unproductive full pass means
+		 * everything is pinned, so fail rather than spin forever.  (A failed
+		 * CAS above is neither progress nor a full miss: we simply retry the
+		 * same buffer.)
 		 */
 		if (no_progress && --trycounter == 0)
 		{
@@ -531,361 +477,3 @@ StrategyCtlShmemInit(void *arg)
 	else
 		StrategyControl->batchSize = 1;
 }
-
-
-/* ----------------------------------------------------------------
- *				Backend-private buffer ring management
- * ----------------------------------------------------------------
- */
-
-
-/*
- * GetAccessStrategy -- create a BufferAccessStrategy object
- *
- * The object is allocated in the current memory context.
- */
-BufferAccessStrategy
-GetAccessStrategy(BufferAccessStrategyType btype)
-{
-	int			ring_size_kb;
-
-	/*
-	 * Select ring size to use.  See buffer/README for rationales.
-	 *
-	 * Note: if you change the ring size for BAS_BULKREAD, see also
-	 * SYNC_SCAN_REPORT_INTERVAL in access/heap/syncscan.c.
-	 */
-	switch (btype)
-	{
-		case BAS_NORMAL:
-			/* if someone asks for NORMAL, just give 'em a "default" object */
-			return NULL;
-
-		case BAS_BULKREAD:
-			{
-				int			ring_max_kb;
-
-				/*
-				 * The ring always needs to be large enough to allow some
-				 * separation in time between providing a buffer to the user
-				 * of the strategy and that buffer being reused. Otherwise the
-				 * user's pin will prevent reuse of the buffer, even without
-				 * concurrent activity.
-				 *
-				 * We also need to ensure the ring always is large enough for
-				 * SYNC_SCAN_REPORT_INTERVAL, as noted above.
-				 *
-				 * Thus we start out a minimal size and increase the size
-				 * further if appropriate.
-				 */
-				ring_size_kb = 256;
-
-				/*
-				 * There's no point in a larger ring if we won't be allowed to
-				 * pin sufficiently many buffers.  But we never limit to less
-				 * than the minimal size above.
-				 */
-				ring_max_kb = GetPinLimit() * (BLCKSZ / 1024);
-				ring_max_kb = Max(ring_size_kb, ring_max_kb);
-
-				/*
-				 * We would like the ring to additionally have space for the
-				 * configured degree of IO concurrency. While being read in,
-				 * buffers can obviously not yet be reused.
-				 *
-				 * Each IO can be up to io_combine_limit blocks large, and we
-				 * want to start up to effective_io_concurrency IOs.
-				 *
-				 * Note that effective_io_concurrency may be 0, which disables
-				 * AIO.
-				 */
-				ring_size_kb += (BLCKSZ / 1024) *
-					io_combine_limit * effective_io_concurrency;
-
-				if (ring_size_kb > ring_max_kb)
-					ring_size_kb = ring_max_kb;
-				break;
-			}
-		case BAS_BULKWRITE:
-			ring_size_kb = 16 * 1024;
-			break;
-		case BAS_VACUUM:
-			ring_size_kb = 2048;
-			break;
-
-		default:
-			elog(ERROR, "unrecognized buffer access strategy: %d",
-				 (int) btype);
-			return NULL;		/* keep compiler quiet */
-	}
-
-	return GetAccessStrategyWithSize(btype, ring_size_kb);
-}
-
-/*
- * GetAccessStrategyWithSize -- create a BufferAccessStrategy object with a
- *		number of buffers equivalent to the passed in size.
- *
- * If the given ring size is 0, no BufferAccessStrategy will be created and
- * the function will return NULL.  ring_size_kb must not be negative.
- */
-BufferAccessStrategy
-GetAccessStrategyWithSize(BufferAccessStrategyType btype, int ring_size_kb)
-{
-	int			ring_buffers;
-	BufferAccessStrategy strategy;
-
-	Assert(ring_size_kb >= 0);
-
-	/* Figure out how many buffers ring_size_kb is */
-	ring_buffers = ring_size_kb / (BLCKSZ / 1024);
-
-	/* 0 means unlimited, so no BufferAccessStrategy required */
-	if (ring_buffers == 0)
-		return NULL;
-
-	/* Cap to 1/8th of shared_buffers */
-	ring_buffers = Min(NBuffers / 8, ring_buffers);
-
-	/* NBuffers should never be less than 16, so this shouldn't happen */
-	Assert(ring_buffers > 0);
-
-	/* Allocate the object and initialize all elements to zeroes */
-	strategy = (BufferAccessStrategy)
-		palloc0(offsetof(BufferAccessStrategyData, buffers) +
-				ring_buffers * sizeof(Buffer));
-
-	/* Set fields that don't start out zero */
-	strategy->btype = btype;
-	strategy->nbuffers = ring_buffers;
-
-	return strategy;
-}
-
-/*
- * GetAccessStrategyBufferCount -- an accessor for the number of buffers in
- *		the ring
- *
- * Returns 0 on NULL input to match behavior of GetAccessStrategyWithSize()
- * returning NULL with 0 size.
- */
-int
-GetAccessStrategyBufferCount(BufferAccessStrategy strategy)
-{
-	if (strategy == NULL)
-		return 0;
-
-	return strategy->nbuffers;
-}
-
-/*
- * GetAccessStrategyPinLimit -- get cap of number of buffers that should be pinned
- *
- * When pinning extra buffers to look ahead, users of a ring-based strategy are
- * in danger of pinning too much of the ring at once while performing look-ahead.
- * For some strategies, that means "escaping" from the ring, and in others it
- * means forcing dirty data to disk very frequently with associated WAL
- * flushing.  Since external code has no insight into any of that, allow
- * individual strategy types to expose a clamp that should be applied when
- * deciding on a maximum number of buffers to pin at once.
- *
- * Callers should combine this number with other relevant limits and take the
- * minimum.
- */
-int
-GetAccessStrategyPinLimit(BufferAccessStrategy strategy)
-{
-	if (strategy == NULL)
-		return NBuffers;
-
-	switch (strategy->btype)
-	{
-		case BAS_BULKREAD:
-
-			/*
-			 * Since BAS_BULKREAD uses StrategyRejectBuffer(), dirty buffers
-			 * shouldn't be a problem and the caller is free to pin up to the
-			 * entire ring at once.
-			 */
-			return strategy->nbuffers;
-
-		default:
-
-			/*
-			 * Tell caller not to pin more than half the buffers in the ring.
-			 * This is a trade-off between look ahead distance and deferring
-			 * writeback and associated WAL traffic.
-			 */
-			return strategy->nbuffers / 2;
-	}
-}
-
-/*
- * FreeAccessStrategy -- release a BufferAccessStrategy object
- *
- * A simple pfree would do at the moment, but we would prefer that callers
- * don't assume that much about the representation of BufferAccessStrategy.
- */
-void
-FreeAccessStrategy(BufferAccessStrategy strategy)
-{
-	/* don't crash if called on a "default" strategy */
-	if (strategy != NULL)
-		pfree(strategy);
-}
-
-/*
- * GetBufferFromRing -- returns a buffer from the ring, or NULL if the
- *		ring is empty / not usable.
- *
- * The buffer is pinned and marked as owned, using TrackNewBufferPin(), before
- * returning.
- */
-static BufferDesc *
-GetBufferFromRing(BufferAccessStrategy strategy, uint64 *buf_state)
-{
-	BufferDesc *buf;
-	Buffer		bufnum;
-	uint64		old_buf_state;
-	uint64		local_buf_state;	/* to avoid repeated (de-)referencing */
-
-
-	/* Advance to next ring slot */
-	if (++strategy->current >= strategy->nbuffers)
-		strategy->current = 0;
-
-	/*
-	 * If the slot hasn't been filled yet, tell the caller to allocate a new
-	 * buffer with the normal allocation strategy.  He will then fill this
-	 * slot by calling AddBufferToRing with the new buffer.
-	 */
-	bufnum = strategy->buffers[strategy->current];
-	if (bufnum == InvalidBuffer)
-		return NULL;
-
-	buf = GetBufferDescriptor(bufnum - 1);
-
-	/*
-	 * Check whether the buffer can be used and pin it if so. Do this using a
-	 * CAS loop, to avoid having to lock the buffer header.
-	 */
-	old_buf_state = pg_atomic_read_u64(&buf->state);
-	for (;;)
-	{
-		local_buf_state = old_buf_state;
-
-		/*
-		 * If the buffer is pinned we cannot use it under any circumstances.
-		 *
-		 * With the cooling-state replacement the field holds only COOL or HOT,
-		 * so the stock "usage_count > 1 means another backend touched it"
-		 * heuristic no longer applies: a ring element is reusable whenever it
-		 * is unpinned.  (The whole ring mechanism is removed in a later patch;
-		 * scan resistance is now intrinsic to the sweep.)
-		 */
-		if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0)
-			break;
-
-		/* See equivalent code in PinBuffer() */
-		if (unlikely(local_buf_state & BM_LOCKED))
-		{
-			old_buf_state = WaitBufHdrUnlocked(buf);
-			continue;
-		}
-
-		/* pin the buffer if the CAS succeeds */
-		local_buf_state += BUF_REFCOUNT_ONE;
-
-		if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
-										   local_buf_state))
-		{
-			*buf_state = local_buf_state;
-
-			TrackNewBufferPin(BufferDescriptorGetBuffer(buf));
-			return buf;
-		}
-	}
-
-	/*
-	 * Tell caller to allocate a new buffer with the normal allocation
-	 * strategy.  He'll then replace this ring element via AddBufferToRing.
-	 */
-	return NULL;
-}
-
-/*
- * AddBufferToRing -- add a buffer to the buffer ring
- *
- * Caller must hold the buffer header spinlock on the buffer.  Since this
- * is called with the spinlock held, it had better be quite cheap.
- */
-static void
-AddBufferToRing(BufferAccessStrategy strategy, BufferDesc *buf)
-{
-	strategy->buffers[strategy->current] = BufferDescriptorGetBuffer(buf);
-}
-
-/*
- * Utility function returning the IOContext of a given BufferAccessStrategy's
- * strategy ring.
- */
-IOContext
-IOContextForStrategy(BufferAccessStrategy strategy)
-{
-	if (!strategy)
-		return IOCONTEXT_NORMAL;
-
-	switch (strategy->btype)
-	{
-		case BAS_NORMAL:
-
-			/*
-			 * Currently, GetAccessStrategy() returns NULL for
-			 * BufferAccessStrategyType BAS_NORMAL, so this case is
-			 * unreachable.
-			 */
-			pg_unreachable();
-			return IOCONTEXT_NORMAL;
-		case BAS_BULKREAD:
-			return IOCONTEXT_BULKREAD;
-		case BAS_BULKWRITE:
-			return IOCONTEXT_BULKWRITE;
-		case BAS_VACUUM:
-			return IOCONTEXT_VACUUM;
-	}
-
-	elog(ERROR, "unrecognized BufferAccessStrategyType: %d", strategy->btype);
-	pg_unreachable();
-}
-
-/*
- * StrategyRejectBuffer -- consider rejecting a dirty buffer
- *
- * When a nondefault strategy is used, the buffer manager calls this function
- * when it turns out that the buffer selected by StrategyGetBuffer needs to
- * be written out and doing so would require flushing WAL too.  This gives us
- * a chance to choose a different victim.
- *
- * Returns true if buffer manager should ask for a new victim, and false
- * if this buffer should be written and re-used.
- */
-bool
-StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf, bool from_ring)
-{
-	/* We only do this in bulkread mode */
-	if (strategy->btype != BAS_BULKREAD)
-		return false;
-
-	/* Don't muck with behavior of normal buffer-replacement strategy */
-	if (!from_ring ||
-		strategy->buffers[strategy->current] != BufferDescriptorGetBuffer(buf))
-		return false;
-
-	/*
-	 * Remove the dirty buffer from the ring; necessary to prevent infinite
-	 * loop if all ring members are dirty.
-	 */
-	strategy->buffers[strategy->current] = InvalidBuffer;
-
-	return true;
-}
diff --git a/src/backend/storage/freespace/freespace.c b/src/backend/storage/freespace/freespace.c
index 006edab..29cb2d7 100644
--- a/src/backend/storage/freespace/freespace.c
+++ b/src/backend/storage/freespace/freespace.c
@@ -603,7 +603,7 @@ fsm_readbuf(Relation rel, FSMAddress addr, bool extend)
 			return InvalidBuffer;
 	}
 	else
-		buf = ReadBufferExtended(rel, FSM_FORKNUM, blkno, RBM_ZERO_ON_ERROR, NULL);
+		buf = ReadBufferExtended(rel, FSM_FORKNUM, blkno, RBM_ZERO_ON_ERROR);
 
 	/*
 	 * Initializing the page when needed is trickier than it looks, because of
@@ -638,7 +638,7 @@ fsm_readbuf(Relation rel, FSMAddress addr, bool extend)
 static Buffer
 fsm_extend(Relation rel, BlockNumber fsm_nblocks)
 {
-	return ExtendBufferedRelTo(BMR_REL(rel), FSM_FORKNUM, NULL,
+	return ExtendBufferedRelTo(BMR_REL(rel), FSM_FORKNUM,
 							   EB_CREATE_FORK_IF_NEEDED |
 							   EB_CLEAR_SIZE_CACHE,
 							   fsm_nblocks,
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 9f96d9c..28e2587 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -1540,15 +1540,8 @@ register_dirty_segment(SMgrRelation reln, ForkNumber forknum, MdfdVec *seg)
 							FilePathName(seg->mdfd_vfd))));
 
 		/*
-		 * We have no way of knowing if the current IOContext is
-		 * IOCONTEXT_NORMAL or IOCONTEXT_[BULKREAD, BULKWRITE, VACUUM] at this
-		 * point, so count the fsync as being in the IOCONTEXT_NORMAL
-		 * IOContext. This is probably okay, because the number of backend
-		 * fsyncs doesn't say anything about the efficacy of the
-		 * BufferAccessStrategy. And counting both fsyncs done in
-		 * IOCONTEXT_NORMAL and IOCONTEXT_[BULKREAD, BULKWRITE, VACUUM] under
-		 * IOCONTEXT_NORMAL is likely clearer when investigating the number of
-		 * backend fsyncs.
+		 * Count the fsync in the IOCONTEXT_NORMAL IOContext, the only
+		 * IOContext relations are read/written under.
 		 */
 		pgstat_count_io_op_time(IOOBJECT_RELATION, IOCONTEXT_NORMAL,
 								IOOP_FSYNC, io_start, 1, 0);
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 4f7a39a..2cf5740 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -241,16 +241,10 @@ pgstat_get_io_context_name(IOContext io_context)
 {
 	switch (io_context)
 	{
-		case IOCONTEXT_BULKREAD:
-			return "bulkread";
-		case IOCONTEXT_BULKWRITE:
-			return "bulkwrite";
 		case IOCONTEXT_INIT:
 			return "init";
 		case IOCONTEXT_NORMAL:
 			return "normal";
-		case IOCONTEXT_VACUUM:
-			return "vacuum";
 	}
 
 	elog(ERROR, "unrecognized IOContext value: %d", io_context);
@@ -451,45 +445,32 @@ pgstat_tracks_io_object(BackendType bktype, IOObject io_object,
 	 * IOContexts, and, while it may not be inherently incorrect for them to
 	 * do so, excluding those rows from the view makes the view easier to use.
 	 */
-	if ((bktype == B_CHECKPOINTER || bktype == B_BG_WRITER) &&
-		(io_context == IOCONTEXT_BULKREAD ||
-		 io_context == IOCONTEXT_BULKWRITE ||
-		 io_context == IOCONTEXT_VACUUM))
-		return false;
-
-	if (bktype == B_AUTOVAC_LAUNCHER && io_context == IOCONTEXT_VACUUM)
-		return false;
-
-	if ((bktype == B_AUTOVAC_WORKER || bktype == B_AUTOVAC_LAUNCHER) &&
-		io_context == IOCONTEXT_BULKWRITE)
-		return false;
 
 	/*
 	 * The data checksums launcher scans catalogs and emits WAL records for
-	 * checksum state changes. Catalog scans can use a bulkread strategy.
+	 * checksum state changes.  With the access-strategy rings removed, its
+	 * catalog scans are tracked under the normal context.
 	 */
 	if (bktype == B_DATACHECKSUMSWORKER_LAUNCHER)
 	{
 		if (io_object == IOOBJECT_WAL ||
 			(io_object == IOOBJECT_RELATION &&
-			 (io_context == IOCONTEXT_BULKREAD ||
-			  io_context == IOCONTEXT_NORMAL)))
+			 io_context == IOCONTEXT_NORMAL))
 			return true;
 
 		return false;
 	}
 
 	/*
-	 * The worker also scans catalogs, then processes relations using a vacuum
-	 * access strategy. Catalog scans can use a bulkread strategy.
+	 * The worker also scans catalogs, then processes relations.  With the
+	 * access-strategy rings removed, both are tracked under the normal
+	 * context.
 	 */
 	if (bktype == B_DATACHECKSUMSWORKER_WORKER)
 	{
 		if (io_object == IOOBJECT_WAL ||
 			(io_object == IOOBJECT_RELATION &&
-			 (io_context == IOCONTEXT_BULKREAD ||
-			  io_context == IOCONTEXT_NORMAL ||
-			  io_context == IOCONTEXT_VACUUM)))
+			 io_context == IOCONTEXT_NORMAL))
 			return true;
 
 		return false;
@@ -510,8 +491,6 @@ bool
 pgstat_tracks_io_op(BackendType bktype, IOObject io_object,
 					IOContext io_context, IOOp io_op)
 {
-	bool		strategy_io_context;
-
 	/* if (io_context, io_object) will never collect stats, we're done */
 	if (!pgstat_tracks_io_object(bktype, io_object, io_context))
 		return false;
@@ -554,17 +533,13 @@ pgstat_tracks_io_op(BackendType bktype, IOObject io_object,
 	/*
 	 * Some IOOps are not valid in certain IOContexts and some IOOps are only
 	 * valid in certain contexts.
+	 *
+	 * IOOP_REUSE was only relevant when a BufferAccessStrategy was in use.
+	 * Buffer access strategies (ring buffers) have been removed -- scan
+	 * resistance is now intrinsic to the cooling-stage clock sweep -- so
+	 * IOOP_REUSE never occurs.
 	 */
-	if (io_context == IOCONTEXT_BULKREAD && io_op == IOOP_EXTEND)
-		return false;
-
-	strategy_io_context = io_context == IOCONTEXT_BULKREAD ||
-		io_context == IOCONTEXT_BULKWRITE || io_context == IOCONTEXT_VACUUM;
-
-	/*
-	 * IOOP_REUSE is only relevant when a BufferAccessStrategy is in use.
-	 */
-	if (!strategy_io_context && io_op == IOOP_REUSE)
+	if (io_op == IOOP_REUSE)
 		return false;
 
 	/*
@@ -578,14 +553,5 @@ pgstat_tracks_io_op(BackendType bktype, IOObject io_object,
 		!(io_op == IOOP_WRITE || io_op == IOOP_READ || io_op == IOOP_FSYNC))
 		return false;
 
-	/*
-	 * IOOP_FSYNC IOOps done by a backend using a BufferAccessStrategy are
-	 * counted in the IOCONTEXT_NORMAL IOContext. See comment in
-	 * register_dirty_segment() for more details.
-	 */
-	if (strategy_io_context && io_op == IOOP_FSYNC)
-		return false;
-
-
 	return true;
 }
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index bbd28d1..555c042 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -149,7 +149,6 @@ int			autovacuum_max_parallel_workers = 0;
 int			MaxBackends = 0;
 
 /* GUC parameters for vacuum */
-int			VacuumBufferUsageLimit = 2048;
 
 int			VacuumCostPageHit = 1;
 int			VacuumCostPageMiss = 2;
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index d421cdb..b1ea81f 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -3326,16 +3326,6 @@
   boot_val => 'DEFAULT_UPDATE_PROCESS_TITLE',
 },
 
-{ name => 'vacuum_buffer_usage_limit', type => 'int', context => 'PGC_USERSET', group => 'RESOURCES_MEM',
-  short_desc => 'Sets the buffer pool size for VACUUM, ANALYZE, and autovacuum.',
-  flags => 'GUC_UNIT_KB',
-  variable => 'VacuumBufferUsageLimit',
-  boot_val => '2048',
-  min => '0',
-  max => 'MAX_BAS_VAC_RING_SIZE_KB',
-  check_hook => 'check_vacuum_buffer_usage_limit',
-},
-
 { name => 'vacuum_cost_delay', type => 'real', context => 'PGC_USERSET', group => 'VACUUM_COST_DELAY',
   short_desc => 'Vacuum cost delay in milliseconds.',
   flags => 'GUC_UNIT_MS',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 7958653..cb1eb8e 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -164,9 +164,6 @@
                                         #   mmap
                                         # (change requires restart)
 #min_dynamic_shared_memory = 0MB        # (change requires restart)
-#vacuum_buffer_usage_limit = 2MB        # size of vacuum and analyze buffer access strategy ring;
-                                        # 0 to disable vacuum buffer access strategy;
-                                        # range 128kB to 16GB
 
 # SLRU buffers (change requires restart)
 #commit_timestamp_buffers = 0           # memory for pg_commit_ts (0 = auto)
diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index f8158ca..f78e57f 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -57,7 +57,6 @@ main(int argc, char *argv[])
 		{"no-truncate", no_argument, NULL, 10},
 		{"no-process-toast", no_argument, NULL, 11},
 		{"no-process-main", no_argument, NULL, 12},
-		{"buffer-usage-limit", required_argument, NULL, 13},
 		{"missing-stats-only", no_argument, NULL, 14},
 		{"dry-run", no_argument, NULL, 15},
 		{NULL, 0, NULL, 0}
@@ -202,9 +201,6 @@ main(int argc, char *argv[])
 			case 12:
 				vacopts.process_main = false;
 				break;
-			case 13:
-				vacopts.buffer_usage_limit = escape_quotes(optarg);
-				break;
 			case 14:
 				vacopts.missing_stats_only = true;
 				break;
@@ -290,14 +286,6 @@ main(int argc, char *argv[])
 		pg_fatal("cannot use the \"%s\" option with the \"%s\" option",
 				 "no-index-cleanup", "force-index-cleanup");
 
-	/*
-	 * buffer-usage-limit is not allowed with VACUUM FULL unless ANALYZE is
-	 * included too.
-	 */
-	if (vacopts.buffer_usage_limit && vacopts.full && !vacopts.and_analyze)
-		pg_fatal("cannot use the \"%s\" option with the \"%s\" option",
-				 "buffer-usage-limit", "full");
-
 	/*
 	 * Prohibit --missing-stats-only without --analyze-only or
 	 * --analyze-in-stages.
@@ -352,7 +340,6 @@ help(const char *progname)
 	printf(_("  %s [OPTION]... [DBNAME]\n"), progname);
 	printf(_("\nOptions:\n"));
 	printf(_("  -a, --all                       vacuum all databases\n"));
-	printf(_("      --buffer-usage-limit=SIZE   size of ring buffer used for vacuum\n"));
 	printf(_("  -d, --dbname=DBNAME             database to vacuum\n"));
 	printf(_("      --disable-page-skipping     disable all page-skipping behavior\n"));
 	printf(_("      --dry-run                   show the commands that would be sent to the server\n"));
diff --git a/src/bin/scripts/vacuuming.c b/src/bin/scripts/vacuuming.c
index 855a575..078a3bb 100644
--- a/src/bin/scripts/vacuuming.c
+++ b/src/bin/scripts/vacuuming.c
@@ -264,13 +264,6 @@ vacuum_one_database(ConnParams *cparams,
 				 "--parallel", "13");
 	}
 
-	if (vacopts->buffer_usage_limit && PQserverVersion(conn) < 160000)
-	{
-		PQfinish(conn);
-		pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s",
-				 "--buffer-usage-limit", "16");
-	}
-
 	if (vacopts->missing_stats_only && PQserverVersion(conn) < 150000)
 	{
 		PQfinish(conn);
@@ -865,13 +858,6 @@ prepare_vacuum_command(PGconn *conn, PQExpBuffer sql,
 				appendPQExpBuffer(sql, "%sVERBOSE", sep);
 				sep = comma;
 			}
-			if (vacopts->buffer_usage_limit)
-			{
-				Assert(serverVersion >= 160000);
-				appendPQExpBuffer(sql, "%sBUFFER_USAGE_LIMIT '%s'", sep,
-								  vacopts->buffer_usage_limit);
-				sep = comma;
-			}
 			if (sep != paren)
 				appendPQExpBufferChar(sql, ')');
 		}
@@ -974,13 +960,6 @@ prepare_vacuum_command(PGconn *conn, PQExpBuffer sql,
 								  vacopts->parallel_workers);
 				sep = comma;
 			}
-			if (vacopts->buffer_usage_limit)
-			{
-				Assert(serverVersion >= 160000);
-				appendPQExpBuffer(sql, "%sBUFFER_USAGE_LIMIT '%s'", sep,
-								  vacopts->buffer_usage_limit);
-				sep = comma;
-			}
 			if (sep != paren)
 				appendPQExpBufferChar(sql, ')');
 		}
diff --git a/src/bin/scripts/vacuuming.h b/src/bin/scripts/vacuuming.h
index 5a491db..83a2469 100644
--- a/src/bin/scripts/vacuuming.h
+++ b/src/bin/scripts/vacuuming.h
@@ -49,7 +49,6 @@ typedef struct vacuumingOptions
 	bool		process_main;
 	bool		process_toast;
 	bool		skip_database_stats;
-	char	   *buffer_usage_limit;
 	bool		missing_stats_only;
 	bool		echo;
 	bool		quiet;
diff --git a/src/include/access/genam.h b/src/include/access/genam.h
index 68bfe40..bc87f6f 100644
--- a/src/include/access/genam.h
+++ b/src/include/access/genam.h
@@ -58,7 +58,6 @@ typedef struct IndexVacuumInfo
 	bool		estimated_count;	/* num_heap_tuples is an estimate */
 	int			message_level;	/* ereport level for progress messages */
 	double		num_heap_tuples;	/* tuples remaining in heap */
-	BufferAccessStrategy strategy;	/* access strategy for reads */
 } IndexVacuumInfo;
 
 /*
diff --git a/src/include/access/hash.h b/src/include/access/hash.h
index a8702f0..7c9268f 100644
--- a/src/include/access/hash.h
+++ b/src/include/access/hash.h
@@ -405,12 +405,11 @@ extern void _hash_pgaddmultitup(Relation rel, Buffer buf, IndexTuple *itups,
 extern Buffer _hash_addovflpage(Relation rel, Buffer metabuf, Buffer buf, bool retain_pin);
 extern BlockNumber _hash_freeovflpage(Relation rel, Buffer bucketbuf, Buffer ovflbuf,
 									  Buffer wbuf, IndexTuple *itups, OffsetNumber *itup_offsets,
-									  Size *tups_size, uint16 nitups, BufferAccessStrategy bstrategy);
+									  Size *tups_size, uint16 nitups);
 extern void _hash_initbitmapbuffer(Buffer buf, uint16 bmsize, bool initpage);
 extern void _hash_squeezebucket(Relation rel,
 								Bucket bucket, BlockNumber bucket_blkno,
-								Buffer bucket_buf,
-								BufferAccessStrategy bstrategy);
+								Buffer bucket_buf);
 extern uint32 _hash_ovflblkno_to_bitno(HashMetaPage metap, BlockNumber ovflblkno);
 
 /* hashpage.c */
@@ -428,9 +427,6 @@ extern void _hash_initbuf(Buffer buf, uint32 max_bucket, uint32 num_bucket,
 						  uint32 flag, bool initpage);
 extern Buffer _hash_getnewbuf(Relation rel, BlockNumber blkno,
 							  ForkNumber forkNum);
-extern Buffer _hash_getbuf_with_strategy(Relation rel, BlockNumber blkno,
-										 int access, int flags,
-										 BufferAccessStrategy bstrategy);
 extern void _hash_relbuf(Relation rel, Buffer buf);
 extern void _hash_dropbuf(Relation rel, Buffer buf);
 extern void _hash_dropscanbuf(Relation rel, HashScanOpaque so);
@@ -481,7 +477,6 @@ extern void _hash_kill_items(IndexScanDesc scan);
 /* hash.c */
 extern void hashbucketcleanup(Relation rel, Bucket cur_bucket,
 							  Buffer bucket_buf, BlockNumber bucket_blkno,
-							  BufferAccessStrategy bstrategy,
 							  uint32 maxbucket, uint32 highmask, uint32 lowmask,
 							  double *tuples_removed, double *num_index_tuples,
 							  bool split_cleanup,
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 5176478..0967b65 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -72,8 +72,6 @@ typedef struct HeapScanDescData
 	Buffer		rs_cbuf;		/* current buffer in scan, if any */
 	/* NB: if rs_cbuf is not InvalidBuffer, we hold a pin on that buffer */
 
-	BufferAccessStrategy rs_strategy;	/* access strategy for reads */
-
 	HeapTupleData rs_ctup;		/* current tuple in scan, if any */
 
 	/* For scans that stream reads */
@@ -466,7 +464,7 @@ extern void log_heap_prune_and_freeze(Relation relation, Buffer buffer,
 
 /* in heap/vacuumlazy.c */
 extern void heap_vacuum_rel(Relation rel,
-							const VacuumParams *params, BufferAccessStrategy bstrategy);
+							const VacuumParams *params);
 #ifdef USE_ASSERT_CHECKING
 extern bool heap_page_is_all_visible(Relation rel, Buffer buf,
 									 GlobalVisState *vistest,
diff --git a/src/include/access/hio.h b/src/include/access/hio.h
index 60cfc37..63bfcc8 100644
--- a/src/include/access/hio.h
+++ b/src/include/access/hio.h
@@ -28,7 +28,6 @@
  */
 typedef struct BulkInsertStateData
 {
-	BufferAccessStrategy strategy;	/* our BULKWRITE strategy object */
 	Buffer		current_buf;	/* current insertion target page */
 
 	/*
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index f2c3669..83ba0f3 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -689,8 +689,7 @@ typedef struct TableAmRoutine
 	 * integrate with autovacuum's scheduling.
 	 */
 	void		(*relation_vacuum) (Relation rel,
-									const VacuumParams *params,
-									BufferAccessStrategy bstrategy);
+									const VacuumParams *params);
 
 	/*
 	 * Prepare to analyze block `blockno` of `scan`. The scan has been started
@@ -1774,10 +1773,9 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
  * routine, even if (for ANALYZE) it is part of the same VACUUM command.
  */
 static inline void
-table_relation_vacuum(Relation rel, const VacuumParams *params,
-					  BufferAccessStrategy bstrategy)
+table_relation_vacuum(Relation rel, const VacuumParams *params)
 {
-	rel->rd_tableam->relation_vacuum(rel, params, bstrategy);
+	rel->rd_tableam->relation_vacuum(rel, params);
 }
 
 /*
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 956d9ce..14a9b53 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -363,7 +363,7 @@ extern PGDLLIMPORT int64 parallel_vacuum_worker_delay_ns;
 /* in commands/vacuum.c */
 extern void ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel);
 extern void vacuum(List *relations, const VacuumParams *params,
-				   BufferAccessStrategy bstrategy, MemoryContext vac_context,
+				   MemoryContext vac_context,
 				   bool isTopLevel);
 extern void vac_open_indexes(Relation relation, LOCKMODE lockmode,
 							 int *nindexes, Relation **Irel);
@@ -407,8 +407,7 @@ extern void VacuumUpdateCosts(void);
 /* in commands/vacuumparallel.c */
 extern ParallelVacuumState *parallel_vacuum_init(Relation rel, Relation *indrels,
 												 int nindexes, int nrequested_workers,
-												 int vac_work_mem, int elevel,
-												 BufferAccessStrategy bstrategy);
+												 int vac_work_mem, int elevel);
 extern void parallel_vacuum_end(ParallelVacuumState *pvs, IndexBulkDeleteResult **istats);
 extern TidStore *parallel_vacuum_get_dead_items(ParallelVacuumState *pvs,
 												VacDeadItemsInfo **dead_items_info_p);
@@ -428,8 +427,7 @@ extern void parallel_vacuum_main(dsm_segment *seg, shm_toc *toc);
 
 /* in commands/analyze.c */
 extern void analyze_rel(Oid relid, RangeVar *relation,
-						const VacuumParams *params, List *va_cols, bool in_outer_xact,
-						BufferAccessStrategy bstrategy);
+						const VacuumParams *params, List *va_cols, bool in_outer_xact);
 extern bool attribute_is_analyzable(Relation onerel, int attnum, Form_pg_attribute attr,
 									int *p_attstattarget);
 extern bool std_typanalyze(VacAttrStats *stats);
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 7170a4b..c5d582b 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -273,15 +273,6 @@ extern PGDLLIMPORT double hash_mem_multiplier;
 extern PGDLLIMPORT int maintenance_work_mem;
 extern PGDLLIMPORT int max_parallel_maintenance_workers;
 
-/*
- * Upper and lower hard limits for the buffer access strategy ring size
- * specified by the VacuumBufferUsageLimit GUC and BUFFER_USAGE_LIMIT option
- * to VACUUM and ANALYZE.
- */
-#define MIN_BAS_VAC_RING_SIZE_KB 128
-#define MAX_BAS_VAC_RING_SIZE_KB (16 * 1024 * 1024)
-
-extern PGDLLIMPORT int VacuumBufferUsageLimit;
 extern PGDLLIMPORT int VacuumCostPageHit;
 extern PGDLLIMPORT int VacuumCostPageMiss;
 extern PGDLLIMPORT int VacuumCostPageDirty;
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a4485..68b4e50 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -287,14 +287,11 @@ typedef enum IOObject
 
 typedef enum IOContext
 {
-	IOCONTEXT_BULKREAD,
-	IOCONTEXT_BULKWRITE,
 	IOCONTEXT_INIT,
 	IOCONTEXT_NORMAL,
-	IOCONTEXT_VACUUM,
 } IOContext;
 
-#define IOCONTEXT_NUM_TYPES (IOCONTEXT_VACUUM + 1)
+#define IOCONTEXT_NUM_TYPES (IOCONTEXT_NORMAL + 1)
 
 /*
  * Enumeration of IO operations.
diff --git a/src/include/storage/buf.h b/src/include/storage/buf.h
index b214455..fa942a6 100644
--- a/src/include/storage/buf.h
+++ b/src/include/storage/buf.h
@@ -36,11 +36,4 @@ typedef int Buffer;
  */
 #define BufferIsLocal(buffer)	((buffer) < 0)
 
-/*
- * Buffer access strategy objects.
- *
- * BufferAccessStrategyData is private to freelist.c
- */
-typedef struct BufferAccessStrategyData *BufferAccessStrategy;
-
 #endif							/* BUF_H */
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index 131df12..3b44ad3 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -632,11 +632,7 @@ extern void TerminateBufferIO(BufferDesc *buf, bool clear_dirty, uint64 set_flag
 
 
 /* freelist.c */
-extern IOContext IOContextForStrategy(BufferAccessStrategy strategy);
-extern BufferDesc *StrategyGetBuffer(BufferAccessStrategy strategy,
-									 uint64 *buf_state, bool *from_ring);
-extern bool StrategyRejectBuffer(BufferAccessStrategy strategy,
-								 BufferDesc *buf, bool from_ring);
+extern BufferDesc *StrategyGetBuffer(uint64 *buf_state);
 
 extern int	StrategySyncStart(uint32 *complete_passes, uint32 *num_buf_alloc);
 extern void StrategyNotifyBgWriter(int bgwprocno);
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 6837b35..3602379 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -25,21 +25,6 @@
 
 typedef void *Block;
 
-/*
- * Possible arguments for GetAccessStrategy().
- *
- * If adding a new BufferAccessStrategyType, also add a new IOContext so
- * IO statistics using this strategy are tracked.
- */
-typedef enum BufferAccessStrategyType
-{
-	BAS_NORMAL,					/* Normal random access */
-	BAS_BULKREAD,				/* Large read-only scan (hint bit updates are
-								 * ok) */
-	BAS_BULKWRITE,				/* Large multi-block write (e.g. COPY IN) */
-	BAS_VACUUM,					/* VACUUM */
-} BufferAccessStrategyType;
-
 /* Possible modes for ReadBufferExtended() */
 typedef enum
 {
@@ -135,7 +120,6 @@ struct ReadBuffersOperation
 	SMgrRelation smgr;
 	char		persistence;
 	ForkNumber	forknum;
-	BufferAccessStrategy strategy;
 
 	/*
 	 * The following private members are private state for communication
@@ -235,11 +219,10 @@ extern bool ReadRecentBuffer(RelFileLocator rlocator, ForkNumber forkNum,
 							 BlockNumber blockNum, Buffer recent_buffer);
 extern Buffer ReadBuffer(Relation reln, BlockNumber blockNum);
 extern Buffer ReadBufferExtended(Relation reln, ForkNumber forkNum,
-								 BlockNumber blockNum, ReadBufferMode mode,
-								 BufferAccessStrategy strategy);
+								 BlockNumber blockNum, ReadBufferMode mode);
 extern Buffer ReadBufferWithoutRelcache(RelFileLocator rlocator,
 										ForkNumber forkNum, BlockNumber blockNum,
-										ReadBufferMode mode, BufferAccessStrategy strategy,
+										ReadBufferMode mode,
 										bool permanent);
 
 extern bool StartReadBuffer(ReadBuffersOperation *operation,
@@ -266,18 +249,15 @@ extern Buffer ReleaseAndReadBuffer(Buffer buffer, Relation relation,
 
 extern Buffer ExtendBufferedRel(BufferManagerRelation bmr,
 								ForkNumber forkNum,
-								BufferAccessStrategy strategy,
 								uint32 flags);
 extern BlockNumber ExtendBufferedRelBy(BufferManagerRelation bmr,
 									   ForkNumber fork,
-									   BufferAccessStrategy strategy,
 									   uint32 flags,
 									   uint32 extend_by,
 									   Buffer *buffers,
 									   uint32 *extended_by);
 extern Buffer ExtendBufferedRelTo(BufferManagerRelation bmr,
 								  ForkNumber fork,
-								  BufferAccessStrategy strategy,
 								  uint32 flags,
 								  BlockNumber extend_to,
 								  ReadBufferMode mode);
@@ -376,14 +356,6 @@ extern void AtProcExit_LocalBuffers(void);
 
 /* in freelist.c */
 
-extern BufferAccessStrategy GetAccessStrategy(BufferAccessStrategyType btype);
-extern BufferAccessStrategy GetAccessStrategyWithSize(BufferAccessStrategyType btype,
-													  int ring_size_kb);
-extern int	GetAccessStrategyBufferCount(BufferAccessStrategy strategy);
-extern int	GetAccessStrategyPinLimit(BufferAccessStrategy strategy);
-
-extern void FreeAccessStrategy(BufferAccessStrategy strategy);
-
 
 /* inline functions */
 
diff --git a/src/include/storage/read_stream.h b/src/include/storage/read_stream.h
index 48995c6..9e5fbbe 100644
--- a/src/include/storage/read_stream.h
+++ b/src/include/storage/read_stream.h
@@ -83,17 +83,14 @@ extern BlockNumber block_range_read_stream_cb(ReadStream *stream,
 											  void *callback_private_data,
 											  void *per_buffer_data);
 extern ReadStream *read_stream_begin_relation(int flags,
-											  BufferAccessStrategy strategy,
 											  Relation rel,
 											  ForkNumber forknum,
 											  ReadStreamBlockNumberCB callback,
 											  void *callback_private_data,
 											  size_t per_buffer_data_size);
 extern Buffer read_stream_next_buffer(ReadStream *stream, void **per_buffer_data);
-extern BlockNumber read_stream_next_block(ReadStream *stream,
-										  BufferAccessStrategy *strategy);
+extern BlockNumber read_stream_next_block(ReadStream *stream);
 extern ReadStream *read_stream_begin_smgr_relation(int flags,
-												   BufferAccessStrategy strategy,
 												   SMgrRelation smgr,
 												   char smgr_persistence,
 												   ForkNumber forknum,
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 307f4fb..3f28558 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -31,8 +31,6 @@ extern void assign_application_name(const char *newval, void *extra);
 extern const char *show_archive_command(void);
 extern bool check_autovacuum_work_mem(int *newval, void **extra,
 									  GucSource source);
-extern bool check_vacuum_buffer_usage_limit(int *newval, void **extra,
-											GucSource source);
 extern bool check_backtrace_functions(char **newval, void **extra,
 									  GucSource source);
 extern void assign_backtrace_functions(const char *newval, void *extra);
diff --git a/src/test/modules/test_aio/test_aio.c b/src/test/modules/test_aio/test_aio.c
index 6270775..8d7b2a7 100644
--- a/src/test/modules/test_aio/test_aio.c
+++ b/src/test/modules/test_aio/test_aio.c
@@ -194,7 +194,6 @@ grow_rel(PG_FUNCTION_ARGS)
 
 		ExtendBufferedRelBy(BMR_REL(rel),
 							MAIN_FORKNUM,
-							NULL,
 							0,
 							extend_by_pages,
 							victim_buffers,
@@ -231,7 +230,7 @@ modify_rel_block(PG_FUNCTION_ARGS)
 	rel = relation_open(relid, AccessExclusiveLock);
 
 	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno,
-							 RBM_ZERO_ON_ERROR, NULL);
+							 RBM_ZERO_ON_ERROR);
 
 	LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
 
@@ -331,7 +330,7 @@ create_toy_buffer(Relation rel, BlockNumber blkno)
 	uint64		unset_bits = 0;
 
 	/* place buffer in shared buffers without erroring out */
-	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_ZERO_AND_LOCK, NULL);
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_ZERO_AND_LOCK);
 	LockBuffer(buf, BUFFER_LOCK_UNLOCK);
 
 	if (RelationUsesLocalBuffers(rel))
@@ -737,7 +736,6 @@ read_buffers(PG_FUNCTION_ARGS)
 		operation->rel = rel;
 		operation->smgr = smgr;
 		operation->persistence = rel->rd_rel->relpersistence;
-		operation->strategy = NULL;
 		operation->forknum = MAIN_FORKNUM;
 
 		io_reqds[nios] = StartReadBuffers(operation,
@@ -879,7 +877,6 @@ read_stream_for_blocks(PG_FUNCTION_ARGS)
 	rel = relation_open(relid, AccessShareLock);
 
 	stream = read_stream_begin_relation(READ_STREAM_FULL,
-										NULL,
 										rel,
 										MAIN_FORKNUM,
 										read_stream_for_blocks_cb,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index e230356..ac4ff4c 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -16,22 +16,16 @@ SHOW track_counts;  -- must be on
 SELECT backend_type, object, context FROM pg_stat_io
   ORDER BY backend_type COLLATE "C", object COLLATE "C", context COLLATE "C";
 backend_type|object|context
-autovacuum launcher|relation|bulkread
 autovacuum launcher|relation|init
 autovacuum launcher|relation|normal
 autovacuum launcher|wal|init
 autovacuum launcher|wal|normal
-autovacuum worker|relation|bulkread
 autovacuum worker|relation|init
 autovacuum worker|relation|normal
-autovacuum worker|relation|vacuum
 autovacuum worker|wal|init
 autovacuum worker|wal|normal
-background worker|relation|bulkread
-background worker|relation|bulkwrite
 background worker|relation|init
 background worker|relation|normal
-background worker|relation|vacuum
 background worker|temp relation|normal
 background worker|wal|init
 background worker|wal|normal
@@ -43,60 +37,39 @@ checkpointer|relation|init
 checkpointer|relation|normal
 checkpointer|wal|init
 checkpointer|wal|normal
-client backend|relation|bulkread
-client backend|relation|bulkwrite
 client backend|relation|init
 client backend|relation|normal
-client backend|relation|vacuum
 client backend|temp relation|normal
 client backend|wal|init
 client backend|wal|normal
-datachecksums launcher|relation|bulkread
 datachecksums launcher|relation|normal
 datachecksums launcher|wal|init
 datachecksums launcher|wal|normal
-datachecksums worker|relation|bulkread
 datachecksums worker|relation|normal
-datachecksums worker|relation|vacuum
 datachecksums worker|wal|init
 datachecksums worker|wal|normal
-io worker|relation|bulkread
-io worker|relation|bulkwrite
 io worker|relation|init
 io worker|relation|normal
-io worker|relation|vacuum
 io worker|temp relation|normal
 io worker|wal|init
 io worker|wal|normal
-slotsync worker|relation|bulkread
-slotsync worker|relation|bulkwrite
 slotsync worker|relation|init
 slotsync worker|relation|normal
-slotsync worker|relation|vacuum
 slotsync worker|temp relation|normal
 slotsync worker|wal|init
 slotsync worker|wal|normal
-standalone backend|relation|bulkread
-standalone backend|relation|bulkwrite
 standalone backend|relation|init
 standalone backend|relation|normal
-standalone backend|relation|vacuum
 standalone backend|wal|init
 standalone backend|wal|normal
-startup|relation|bulkread
-startup|relation|bulkwrite
 startup|relation|init
 startup|relation|normal
-startup|relation|vacuum
 startup|wal|init
 startup|wal|normal
 walreceiver|wal|init
 walreceiver|wal|normal
-walsender|relation|bulkread
-walsender|relation|bulkwrite
 walsender|relation|init
 walsender|relation|normal
-walsender|relation|vacuum
 walsender|temp relation|normal
 walsender|wal|init
 walsender|wal|normal
@@ -104,7 +77,7 @@ walsummarizer|wal|init
 walsummarizer|wal|normal
 walwriter|wal|init
 walwriter|wal|normal
-(88 rows)
+(61 rows)
 \a
 -- List of registered statistics kinds.
 SELECT id, name, fixed_amount,
@@ -1750,80 +1723,20 @@ SELECT :io_sum_local_new_tblspc_writes > :io_sum_local_after_writes;
 (1 row)
 
 RESET temp_buffers;
--- Test that reuse of strategy buffers and reads of blocks into these reused
--- buffers while VACUUMing are tracked in pg_stat_io. If there is sufficient
--- demand for shared buffers from concurrent queries, some buffers may be
--- pinned by other backends before they can be reused. In such cases, the
--- backend will evict a buffer from outside the ring and add it to the
--- ring. This is considered an eviction and not a reuse.
--- Set wal_skip_threshold smaller than the expected size of
--- test_io_vac_strategy so that, even if wal_level is minimal, VACUUM FULL will
--- fsync the newly rewritten test_io_vac_strategy instead of writing it to WAL.
--- Writing it to WAL will result in the newly written relation pages being in
--- shared buffers -- preventing us from testing BAS_VACUUM BufferAccessStrategy
--- reads.
-SET wal_skip_threshold = '1 kB';
-SELECT sum(reuses) AS reuses, sum(reads) AS reads, sum(evictions) AS evictions
-  FROM pg_stat_io WHERE context = 'vacuum' \gset io_sum_vac_strategy_before_
-CREATE TABLE test_io_vac_strategy(a int, b int) WITH (autovacuum_enabled = 'false');
-INSERT INTO test_io_vac_strategy SELECT i, i from generate_series(1, 4500)i;
--- Ensure that the next VACUUM will need to perform IO by rewriting the table
--- first with VACUUM (FULL).
-VACUUM (FULL) test_io_vac_strategy;
--- Use the minimum BUFFER_USAGE_LIMIT to cause reuses or evictions with the
--- smallest table possible.
-VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) test_io_vac_strategy;
-SELECT pg_stat_force_next_flush();
- pg_stat_force_next_flush 
---------------------------
- 
-(1 row)
-
-SELECT sum(reuses) AS reuses, sum(reads) AS reads, sum(evictions) AS evictions
-  FROM pg_stat_io WHERE context = 'vacuum' \gset io_sum_vac_strategy_after_
-SELECT :io_sum_vac_strategy_after_reads > :io_sum_vac_strategy_before_reads;
- ?column? 
-----------
- t
-(1 row)
-
-SELECT (:io_sum_vac_strategy_after_reuses + :io_sum_vac_strategy_after_evictions) >
-  (:io_sum_vac_strategy_before_reuses + :io_sum_vac_strategy_before_evictions);
- ?column? 
-----------
- t
-(1 row)
-
-RESET wal_skip_threshold;
--- Test that extends done by a CTAS, which uses a BAS_BULKWRITE
--- BufferAccessStrategy, are tracked in pg_stat_io.
-SELECT sum(extends) AS io_sum_bulkwrite_strategy_extends_before
-  FROM pg_stat_io WHERE context = 'bulkwrite' \gset
-CREATE TABLE test_io_bulkwrite_strategy AS SELECT i FROM generate_series(1,100)i;
-SELECT pg_stat_force_next_flush();
- pg_stat_force_next_flush 
---------------------------
- 
-(1 row)
-
-SELECT sum(extends) AS io_sum_bulkwrite_strategy_extends_after
-  FROM pg_stat_io WHERE context = 'bulkwrite' \gset
-SELECT :io_sum_bulkwrite_strategy_extends_after > :io_sum_bulkwrite_strategy_extends_before;
- ?column? 
-----------
- t
-(1 row)
-
 -- Test IO stats reset
+-- Note: reuses is intentionally excluded from these totals.  With the
+-- BufferAccessStrategy ring buffers removed, IOOP_REUSE is never tracked, so
+-- the reuses column is NULL for every row; summing it would make the whole
+-- total NULL.
 SELECT pg_stat_have_stats('io', 0, 0);
  pg_stat_have_stats 
 --------------------
  t
 (1 row)
 
-SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS io_stats_pre_reset
+SELECT sum(evictions) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS io_stats_pre_reset
   FROM pg_stat_io \gset
-SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_pre_reset
+SELECT sum(evictions) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_pre_reset
   FROM pg_stat_get_backend_io(pg_backend_pid()) \gset
 SELECT pg_stat_reset_shared('io');
  pg_stat_reset_shared 
@@ -1831,7 +1744,7 @@ SELECT pg_stat_reset_shared('io');
  
 (1 row)
 
-SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS io_stats_post_reset
+SELECT sum(evictions) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS io_stats_post_reset
   FROM pg_stat_io \gset
 SELECT :io_stats_post_reset < :io_stats_pre_reset;
  ?column? 
@@ -1839,7 +1752,7 @@ SELECT :io_stats_post_reset < :io_stats_pre_reset;
  t
 (1 row)
 
-SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_post_reset
+SELECT sum(evictions) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_post_reset
   FROM pg_stat_get_backend_io(pg_backend_pid()) \gset
 -- pg_stat_reset_shared() did not reset backend IO stats
 SELECT :my_io_stats_pre_reset <= :my_io_stats_post_reset;
@@ -1855,7 +1768,7 @@ SELECT pg_stat_reset_backend_stats(pg_backend_pid());
  
 (1 row)
 
-SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_post_backend_reset
+SELECT sum(evictions) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_post_backend_reset
   FROM pg_stat_get_backend_io(pg_backend_pid()) \gset
 SELECT :my_io_stats_pre_reset > :my_io_stats_post_backend_reset;
  ?column? 
diff --git a/src/test/regress/expected/vacuum.out b/src/test/regress/expected/vacuum.out
index d4696bc..85d0690 100644
--- a/src/test/regress/expected/vacuum.out
+++ b/src/test/regress/expected/vacuum.out
@@ -526,25 +526,6 @@ SELECT t.relfilenode = :toast_filenode AS is_same_toast_filenode
  f
 (1 row)
 
--- BUFFER_USAGE_LIMIT option
-VACUUM (BUFFER_USAGE_LIMIT '512 kB') vac_option_tab;
-ANALYZE (BUFFER_USAGE_LIMIT '512 kB') vac_option_tab;
--- try disabling the buffer usage limit
-VACUUM (BUFFER_USAGE_LIMIT 0) vac_option_tab;
-ANALYZE (BUFFER_USAGE_LIMIT 0) vac_option_tab;
--- value exceeds max size error
-VACUUM (BUFFER_USAGE_LIMIT 16777220) vac_option_tab;
-ERROR:  BUFFER_USAGE_LIMIT option must be 0 or between 128 kB and 16777216 kB
--- value is less than min size error
-VACUUM (BUFFER_USAGE_LIMIT 120) vac_option_tab;
-ERROR:  BUFFER_USAGE_LIMIT option must be 0 or between 128 kB and 16777216 kB
--- integer overflow error
-VACUUM (BUFFER_USAGE_LIMIT 10000000000) vac_option_tab;
-ERROR:  BUFFER_USAGE_LIMIT option must be 0 or between 128 kB and 16777216 kB
-HINT:  Value exceeds integer range.
--- incompatible with VACUUM FULL error
-VACUUM (BUFFER_USAGE_LIMIT '512 kB', FULL) vac_option_tab;
-ERROR:  BUFFER_USAGE_LIMIT cannot be specified for VACUUM FULL
 -- SKIP_DATABASE_STATS option
 VACUUM (SKIP_DATABASE_STATS) vactst;
 -- ONLY_DATABASE_STATS option
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index 4c265d1..4eb4811 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -814,65 +814,27 @@ SELECT sum(writes) AS io_sum_local_new_tblspc_writes
 SELECT :io_sum_local_new_tblspc_writes > :io_sum_local_after_writes;
 RESET temp_buffers;
 
--- Test that reuse of strategy buffers and reads of blocks into these reused
--- buffers while VACUUMing are tracked in pg_stat_io. If there is sufficient
--- demand for shared buffers from concurrent queries, some buffers may be
--- pinned by other backends before they can be reused. In such cases, the
--- backend will evict a buffer from outside the ring and add it to the
--- ring. This is considered an eviction and not a reuse.
-
--- Set wal_skip_threshold smaller than the expected size of
--- test_io_vac_strategy so that, even if wal_level is minimal, VACUUM FULL will
--- fsync the newly rewritten test_io_vac_strategy instead of writing it to WAL.
--- Writing it to WAL will result in the newly written relation pages being in
--- shared buffers -- preventing us from testing BAS_VACUUM BufferAccessStrategy
--- reads.
-SET wal_skip_threshold = '1 kB';
-SELECT sum(reuses) AS reuses, sum(reads) AS reads, sum(evictions) AS evictions
-  FROM pg_stat_io WHERE context = 'vacuum' \gset io_sum_vac_strategy_before_
-CREATE TABLE test_io_vac_strategy(a int, b int) WITH (autovacuum_enabled = 'false');
-INSERT INTO test_io_vac_strategy SELECT i, i from generate_series(1, 4500)i;
--- Ensure that the next VACUUM will need to perform IO by rewriting the table
--- first with VACUUM (FULL).
-VACUUM (FULL) test_io_vac_strategy;
--- Use the minimum BUFFER_USAGE_LIMIT to cause reuses or evictions with the
--- smallest table possible.
-VACUUM (PARALLEL 0, BUFFER_USAGE_LIMIT 128) test_io_vac_strategy;
-SELECT pg_stat_force_next_flush();
-SELECT sum(reuses) AS reuses, sum(reads) AS reads, sum(evictions) AS evictions
-  FROM pg_stat_io WHERE context = 'vacuum' \gset io_sum_vac_strategy_after_
-SELECT :io_sum_vac_strategy_after_reads > :io_sum_vac_strategy_before_reads;
-SELECT (:io_sum_vac_strategy_after_reuses + :io_sum_vac_strategy_after_evictions) >
-  (:io_sum_vac_strategy_before_reuses + :io_sum_vac_strategy_before_evictions);
-RESET wal_skip_threshold;
-
--- Test that extends done by a CTAS, which uses a BAS_BULKWRITE
--- BufferAccessStrategy, are tracked in pg_stat_io.
-SELECT sum(extends) AS io_sum_bulkwrite_strategy_extends_before
-  FROM pg_stat_io WHERE context = 'bulkwrite' \gset
-CREATE TABLE test_io_bulkwrite_strategy AS SELECT i FROM generate_series(1,100)i;
-SELECT pg_stat_force_next_flush();
-SELECT sum(extends) AS io_sum_bulkwrite_strategy_extends_after
-  FROM pg_stat_io WHERE context = 'bulkwrite' \gset
-SELECT :io_sum_bulkwrite_strategy_extends_after > :io_sum_bulkwrite_strategy_extends_before;
-
 -- Test IO stats reset
+-- Note: reuses is intentionally excluded from these totals.  With the
+-- BufferAccessStrategy ring buffers removed, IOOP_REUSE is never tracked, so
+-- the reuses column is NULL for every row; summing it would make the whole
+-- total NULL.
 SELECT pg_stat_have_stats('io', 0, 0);
-SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS io_stats_pre_reset
+SELECT sum(evictions) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS io_stats_pre_reset
   FROM pg_stat_io \gset
-SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_pre_reset
+SELECT sum(evictions) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_pre_reset
   FROM pg_stat_get_backend_io(pg_backend_pid()) \gset
 SELECT pg_stat_reset_shared('io');
-SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS io_stats_post_reset
+SELECT sum(evictions) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS io_stats_post_reset
   FROM pg_stat_io \gset
 SELECT :io_stats_post_reset < :io_stats_pre_reset;
-SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_post_reset
+SELECT sum(evictions) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_post_reset
   FROM pg_stat_get_backend_io(pg_backend_pid()) \gset
 -- pg_stat_reset_shared() did not reset backend IO stats
 SELECT :my_io_stats_pre_reset <= :my_io_stats_post_reset;
 -- but pg_stat_reset_backend_stats() does
 SELECT pg_stat_reset_backend_stats(pg_backend_pid());
-SELECT sum(evictions) + sum(reuses) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_post_backend_reset
+SELECT sum(evictions) + sum(extends) + sum(fsyncs) + sum(reads) + sum(writes) + sum(writebacks) + sum(hits) AS my_io_stats_post_backend_reset
   FROM pg_stat_get_backend_io(pg_backend_pid()) \gset
 SELECT :my_io_stats_pre_reset > :my_io_stats_post_backend_reset;
 
diff --git a/src/test/regress/sql/vacuum.sql b/src/test/regress/sql/vacuum.sql
index 247b8e2..506c438 100644
--- a/src/test/regress/sql/vacuum.sql
+++ b/src/test/regress/sql/vacuum.sql
@@ -390,21 +390,6 @@ SELECT t.relfilenode = :toast_filenode AS is_same_toast_filenode
   FROM pg_class c, pg_class t
   WHERE c.reltoastrelid = t.oid AND c.relname = 'vac_option_tab';
 
--- BUFFER_USAGE_LIMIT option
-VACUUM (BUFFER_USAGE_LIMIT '512 kB') vac_option_tab;
-ANALYZE (BUFFER_USAGE_LIMIT '512 kB') vac_option_tab;
--- try disabling the buffer usage limit
-VACUUM (BUFFER_USAGE_LIMIT 0) vac_option_tab;
-ANALYZE (BUFFER_USAGE_LIMIT 0) vac_option_tab;
--- value exceeds max size error
-VACUUM (BUFFER_USAGE_LIMIT 16777220) vac_option_tab;
--- value is less than min size error
-VACUUM (BUFFER_USAGE_LIMIT 120) vac_option_tab;
--- integer overflow error
-VACUUM (BUFFER_USAGE_LIMIT 10000000000) vac_option_tab;
--- incompatible with VACUUM FULL error
-VACUUM (BUFFER_USAGE_LIMIT '512 kB', FULL) vac_option_tab;
-
 -- SKIP_DATABASE_STATS option
 VACUUM (SKIP_DATABASE_STATS) vactst;
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 56c1f99..064a987 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -355,8 +355,6 @@ BtreeLevel
 Bucket
 BufFile
 Buffer
-BufferAccessStrategy
-BufferAccessStrategyType
 BufferCacheOsPagesContext
 BufferCacheOsPagesRec
 BufferDesc
-- 
2.50.1



^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention
  2026-04-25 20:08 [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-04-27 14:15 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-04-28 14:30   ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 14:00     ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 18:07       ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-11 15:54         ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-14 17:53           ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
@ 2026-07-22 14:52             ` Ants Aasma <ants.aasma@cybertec.at>
  2026-07-22 15:29               ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-07-25 10:35               ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2 siblings, 2 replies; 21+ messages in thread

From: Ants Aasma @ 2026-07-22 14:52 UTC (permalink / raw)
  To: Greg Burd <greg@burd.me>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Tomas Vondra <tomas@vondra.me>; Nathan Bossart <nathandbossart@gmail.com>; Andres Freund <andres@anarazel.de>

On Tue, 14 Jul 2026 at 20:53, Greg Burd <greg@burd.me> wrote:
> The buffer cache ring strategy didn't only bound cache pollution, it also
> deferred writeback. A backend running a big COPY, bulk UPDATE or VACUUM
> reused a small ring and, via StrategyRejectBuffer, pushed dirty victims back
> for the bgwriter and checkpointer to write instead of writing (and
> WAL-flushing) them inline on its own allocation path.  Pull the rings (as in
> 0003) and, done naively, that deferral goes too.
...
> One caveat up front: (2) is INSERT...SELECT, not the COPY command.  It hits
> the same relation-extend + WAL-logged-write path BAS_BULKWRITE served, which
> is what I wanted to stress, but if that distinction matters to anyone's
> conclusion I'll re-run with COPY FROM.

That is not quite right. First of all, bulk update and insert..select
do not use a bulkwrite strategy. COPY and CREATE TABLE AS do.
Secondly, you have it the wrong way around, the idea of bulkwrite is
to have the backend itself handle the writes. It reduces cache
pollution, but it also forces the bulk writer to handle the work it
created, so unrelated backends don't get slowed down doing the work
that the bulk writer created. StrategyRejectBuffer is only for bulk
reads. A seq scan based update would be using BAS_BULKREAD, but as you
noted, after 256kB it will fall back to normal allocation.

If we are to get rid of the ring buffer then I think we need some
other way to apply backpressure on bulk writers. Just making the
bgwriter more aggressive doesn't help when writing out the modified is
the bottleneck. It will just fall behind until every allocation is
forced to do a flush.

So the differences observed in the bulk insert and update workloads
isn't explained by getting rid of the ring buffers. It must be related
to differing eviction patterns. I think that should be easy to
demonstrate by also benchmarking only the first two patches.

> This idea started with the question of, "I wonder if we really need a 5
> count on each buffer in the clock-sweep eviction path or if that is simply
> overhead and carries no signal at all for eviction or information useful to
> the bgwriter?"  FWIW, I feel that the COOL/HOT clock is simplier and that
> tests show it is better than the 0..5 clock.  I'm not thrilled by *any*
> regression, especially not one that induces more WAL traffic, so I'm open to
> thoughts on this.

I think the initial results are a promising indication that G-CLOCKs
counters are not providing much value. But to have confidence in not
regressing we need to cover a much wider gamut of access patterns.
Another design dimension is the pure miss rate, which is relevant for
direct I/O based systems and miss rate when combined with kernel page
cache. Seems to me that a more systematic way to generate different
access pattern mixes is needed to identify potential regressions.

Btw. on the surface the algorithm looks like CLOCK-Pro [1]. The
literature might offer some interesting tuning tips.

Regards,
Ants

[1] https://www.usenix.org/legacy/event/usenix05/tech/general/full_papers/jiang/jiang.pdf





^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention
  2026-04-25 20:08 [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-04-27 14:15 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-04-28 14:30   ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 14:00     ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 18:07       ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-11 15:54         ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-14 17:53           ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-22 14:52             ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Ants Aasma <ants.aasma@cybertec.at>
@ 2026-07-22 15:29               ` Andres Freund <andres@anarazel.de>
  2026-07-22 16:09                 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Ants Aasma <ants.aasma@cybertec.at>
  1 sibling, 1 reply; 21+ messages in thread

From: Andres Freund @ 2026-07-22 15:29 UTC (permalink / raw)
  To: Ants Aasma <ants.aasma@cybertec.at>; +Cc: Greg Burd <greg@burd.me>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Tomas Vondra <tomas@vondra.me>; Nathan Bossart <nathandbossart@gmail.com>

Hi,

FWIW, I think it's quite unhelpful to combine a thread to make a relatively
local optimization (batched clock sweep) with huge changes to how buffer
replacement works.  I don't see how those things are related.


On 2026-07-22 17:52:02 +0300, Ants Aasma wrote:
> I think the initial results are a promising indication that G-CLOCKs
> counters are not providing much value.

I am doubtful that the benchmarks in this thread actually shows anything
particularly interesting wrt that. There's just not enough interesting IO in
pgbench -S:

- The indexes are too small compared to the table data to be interesting, even
  index leaf pages are unlikely to be evicted.

- The fact that realistically there's at max two misses for a single query
  (one index leaf page and the heap page) rather substantially limits the max
  impact the wrong buffer replacement triggers.

- I don't think the hit ratio is an all that interesting way to evaluate the
  cache replacement, particularly for something like pgbench -S. It really
  matters *which* buffers are evicted.

- pgbench -S has about the same penalty for an index miss as for a heap miss
  (since both trigger one synchronous IO wait and since there's 1 leaf page
  and one heap access per query). That's not true for many other workloads.


Greetings,

Andres Freund





^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention
  2026-04-25 20:08 [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-04-27 14:15 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-04-28 14:30   ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 14:00     ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 18:07       ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-11 15:54         ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-14 17:53           ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-22 14:52             ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Ants Aasma <ants.aasma@cybertec.at>
  2026-07-22 15:29               ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
@ 2026-07-22 16:09                 ` Ants Aasma <ants.aasma@cybertec.at>
  2026-07-22 16:24                   ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  0 siblings, 1 reply; 21+ messages in thread

From: Ants Aasma @ 2026-07-22 16:09 UTC (permalink / raw)
  To: Andres Freund <andres@anarazel.de>; +Cc: Greg Burd <greg@burd.me>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Tomas Vondra <tomas@vondra.me>; Nathan Bossart <nathandbossart@gmail.com>

Generally agreed, but I wanted to understand one point a bit better.

On Wed, 22 Jul 2026 at 18:29, Andres Freund <andres@anarazel.de> wrote:
> - I don't think the hit ratio is an all that interesting way to evaluate the
>   cache replacement, particularly for something like pgbench -S. It really
>   matters *which* buffers are evicted.

Is that only because a single bad eviction could cause multiple
backends to wait for an I/O? To me, a high probability of multiple
accesses within one disk latency would imply that the buffer is really
hot and the eviction algorithm would have to be really terrible to
evict it. In other words, I have a hard time imagining when it would
matter in practice.

Of course with prefetching, distinguisghing prefetched misses from
synchronous misses is very much important.

Ants






^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention
  2026-04-25 20:08 [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-04-27 14:15 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-04-28 14:30   ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 14:00     ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 18:07       ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-11 15:54         ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-14 17:53           ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-22 14:52             ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Ants Aasma <ants.aasma@cybertec.at>
  2026-07-22 15:29               ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-07-22 16:09                 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Ants Aasma <ants.aasma@cybertec.at>
@ 2026-07-22 16:24                   ` Andres Freund <andres@anarazel.de>
  0 siblings, 0 replies; 21+ messages in thread

From: Andres Freund @ 2026-07-22 16:24 UTC (permalink / raw)
  To: Ants Aasma <ants.aasma@cybertec.at>; +Cc: Greg Burd <greg@burd.me>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Tomas Vondra <tomas@vondra.me>; Nathan Bossart <nathandbossart@gmail.com>

Hi,

On 2026-07-22 19:09:18 +0300, Ants Aasma wrote:
> Generally agreed, but I wanted to understand one point a bit better.
> 
> On Wed, 22 Jul 2026 at 18:29, Andres Freund <andres@anarazel.de> wrote:
> > - I don't think the hit ratio is an all that interesting way to evaluate the
> >   cache replacement, particularly for something like pgbench -S. It really
> >   matters *which* buffers are evicted.
> 
> Is that only because a single bad eviction could cause multiple
> backends to wait for an I/O?

Mainly that, yea. If you evict, in the most extreme case, an inner index page,
it's quite likely that you cause multiple backends to wait once you have a
decent amount of concurrency. Even with index leaf pages that's not
unlikely. Whereas it's rather unlikely for a heap page, just due to the larger
number of those.

It's probably less of an issue with local NVMe latencies, but once you have
cloud storage involved...


> To me, a high probability of multiple accesses within one disk latency would
> imply that the buffer is really hot and the eviction algorithm would have to
> be really terrible to evict it.

I think it won't happen that often if the working set is close to s_b, because
the replacement rate is relatively low. But if the working set is considerably
larger than s_b + kernel page cache and you have a non-uniform access model
(so there's a decent number of accesses even to a small number of heap pages),
it's not hard to have a replacement algorithm that can't distinguish between
the importance of the index pages vs heap pages.



> Of course with prefetching, distinguisghing prefetched misses from
> synchronous misses is very much important.

Indeed.  One thing, in the context of the current buffer replacement model, I
have been wondering about, is cabining how far async reads can increase the
usage count. Differentiating that would make it more likely that synchronously
needed buffers survive for longer.

Greetings,

Andres Freund






^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention
  2026-04-25 20:08 [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-04-27 14:15 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-04-28 14:30   ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 14:00     ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 18:07       ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-11 15:54         ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-14 17:53           ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-22 14:52             ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Ants Aasma <ants.aasma@cybertec.at>
@ 2026-07-25 10:35               ` Greg Burd <greg@burd.me>
  1 sibling, 0 replies; 21+ messages in thread

From: Greg Burd @ 2026-07-25 10:35 UTC (permalink / raw)
  To: Ants Aasma <ants.aasma@cybertec.at>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Tomas Vondra <tomas@vondra.me>; Nathan Bossart <nathandbossart@gmail.com>; Andres Freund <andres@anarazel.de>


On Wed, Jul 22, 2026, at 10:52 AM, Ants Aasma wrote:
> On Tue, 14 Jul 2026 at 20:53, Greg Burd <greg@burd.me> wrote:
>> The buffer cache ring strategy didn't only bound cache pollution, it also
>> deferred writeback. A backend running a big COPY, bulk UPDATE or VACUUM
>> reused a small ring and, via StrategyRejectBuffer, pushed dirty victims back
>> for the bgwriter and checkpointer to write instead of writing (and
>> WAL-flushing) them inline on its own allocation path.  Pull the rings (as in
>> 0003) and, done naively, that deferral goes too.
> ...

Hi Ants,

Thanks -- this was the most useful kind of review, because you caught a place
where I had the mechanism plain wrong, and the correction changes the story
for the better.

>> One caveat up front: (2) is INSERT...SELECT, not the COPY command.  It hits
>> the same relation-extend + WAL-logged-write path BAS_BULKWRITE served, which
>> is what I wanted to stress, but if that distinction matters to anyone's
>> conclusion I'll re-run with COPY FROM.
>
> That is not quite right. First of all, bulk update and insert..select
> do not use a bulkwrite strategy. COPY and CREATE TABLE AS do.
> Secondly, you have it the wrong way around, the idea of bulkwrite is
> to have the backend itself handle the writes. It reduces cache
> pollution, but it also forces the bulk writer to handle the work it
> created, so unrelated backends don't get slowed down doing the work
> that the bulk writer created. StrategyRejectBuffer is only for bulk
> reads. A seq scan based update would be using BAS_BULKREAD, but as you
> noted, after 256kB it will fall back to normal allocation.

You're right on both counts, and I went and re-read the code to be sure I
understood rather than just nodding.  GetBulkInsertState() is the only thing
that sets BAS_BULKWRITE, and its callers are COPY (copyfrom.c) and CREATE
TABLE AS / matview refresh (createas.c).  nodeModifyTable has no bulk-insert
state, so a plain INSERT..SELECT or UPDATE runs through the normal allocation
path with no strategy at all.  And StrategyRejectBuffer() bails immediately
unless the strategy is BAS_BULKREAD.  So my "the rings deferred writeback to
the bgwriter" framing was simply wrong: BAS_BULKWRITE keeps the writes *on*
the backend that made them.  Thank you for the correction; I've dropped that
explanation entirely.

> If we are to get rid of the ring buffer then I think we need some
> other way to apply backpressure on bulk writers. Just making the
> bgwriter more aggressive doesn't help when writing out the modified is
> the bottleneck. It will just fall behind until every allocation is
> forced to do a flush.

This is the real blocker and I think you're correct.  I tested it directly: a
sustained bulk-dirtying workload with the bgwriter driven as hard as it will
go, and the backend's share of relation writes still climbs to ~100% over
time -- the bgwriter simply falls behind and every allocation ends up flushing
inline, precisely as you describe.  So "make the bgwriter keep up" is not a
backpressure mechanism, and I'm no longer proposing it as one.

The consequence is that I'm splitting the series and dropping the
BufferAccessStrategy removal (old 0003) from what I'm asking anyone to
consider now.  Removing the rings needs a real replacement for the bulk-writer
backpressure BAS_BULKWRITE provides, and I don't have one I'm happy with yet;
that's its own project.  What's left is two independent pieces:

  - the batched clock sweep (contention), and
  - the cooling-stage evictor (replacing the 0..5 usage_count),
  - with the rings left in place.

which I think is also the right thing to do per Andres's point that these
shouldn't ride in one thread.

> So the differences observed in the bulk insert and update workloads
> isn't explained by getting rid of the ring buffers. It must be related
> to differing eviction patterns. I think that should be easy to
> demonstrate by also benchmarking only the first two patches.

That is exactly what it turned out to be, and your suggested test is the one
that showed it.  Benchmarking 0001+0002 with the rings still in place (0003
not applied), the bulk-UPDATE difference is already present -- so it is an
eviction/writeback-path effect from the cooling change, not from removing the
strategies.  With that isolation done and a corrected, real-COPY dirty-bulk
run, the "regression" I reported in the earlier mail largely does not survive:
the bulk-UPDATE delta was inside run-to-run noise once I stopped conflating
INSERT..SELECT with COPY and stopped letting a stale checkpoint bleed into the
timing.  I'm not going to defend the earlier numbers; they were measuring the
wrong thing.

>> This idea started with the question of, "I wonder if we really need a 5
>> count on each buffer in the clock-sweep eviction path or if that is simply
>> overhead and carries no signal at all for eviction or information useful to
>> the bgwriter?"  FWIW, I feel that the COOL/HOT clock is simplier and that
>> tests show it is better than the 0..5 clock.  I'm not thrilled by *any*
>> regression, especially not one that induces more WAL traffic, so I'm open to
>> thoughts on this.
>
> I think the initial results are a promising indication that G-CLOCKs
> counters are not providing much value. But to have confidence in not
> regressing we need to cover a much wider gamut of access patterns.
> Another design dimension is the pure miss rate, which is relevant for
> direct I/O based systems and miss rate when combined with kernel page
> cache. Seems to me that a more systematic way to generate different
> access pattern mixes is needed to identify potential regressions.

Agreed, and this is where I've put the measurement effort since.  The clearest
result so far is on the *cost* of the 0..5 counter rather than its cache-hit
quality: on a large shared_buffers pool with huge_pages off, when the pool is
hot and eviction is continuous, the stock clock hand has to decrement a hot
buffer up to five times to make it evictable, and it resets its scan counter
on every decrement -- so it does on the order of 5*NBuffers descriptor visits
per victim, each a likely dTLB miss across the (multi-GB) descriptor array.  I
instrumented ticks-per-victim: on a 384-vCPU box at a residency fraction where
the pool stays hot, stock runs ~18 clock-hand visits per victim while the
1-bit HOT/COOL sweep runs ~1.6 -- roughly an order of magnitude less
descriptor-scanning work per eviction, and it doesn't grow the way the 0..5
grind does.  Under enough concurrent eviction pressure that turns into a
throughput cliff / multi-second p99 for stock that the cooling sweep doesn't
have.  I'm writing this up properly (methodology, the residency-fraction model
that predicts where it bites, huge_pages on/off, and where a connection pooler
mitigates it) and will post it as its own thing rather than bury it here.

On the systematic access-pattern point: yes.  What I've been driving is a
residency model -- touches-per-buffer before eviction is f/(1-f) for residency
fraction f = shared_buffers/working_set -- which predicts the regime where the
0..5 counter can climb enough to matter (f roughly 0.8-0.9) versus where it
can't (f ~ 0.5, every buffer dies at usage ~1 and the counter is pure
overhead).  I'd like to turn that into the "generate a matrix of access
patterns" harness you're describing: {residency fraction} x {uniform, skewed}
x {buffered vs direct IO} x {index-heavy vs heap-heavy}, reporting per-page-kind
eviction (an inner index page evicted stalls many backends; a heap page,
fewer) rather than a single hit ratio.  If you have a workload set you consider
representative I'd rather start from that than invent my own.

> Btw. on the surface the algorithm looks like CLOCK-Pro [1]. The
> literature might offer some interesting tuning tips.

Thanks for the pointer -- I'd been thinking of it in LeanStore / 2Q terms
(demand-loaded pages admitted COOL/probationary, promoted to HOT on a second
touch), but the CLOCK-Pro framing of hot/cold with a test period is close and
I'll read it properly again before I make any claims about the admission policy.
If the literature has a better-justified promotion rule than "second touch,"
I'd rather adopt it than defend an ad-hoc one.

>
> Regards,
> Ants
>
> [1] 
> https://www.usenix.org/legacy/event/usenix05/tech/general/full_papers/jiang/jiang.pdf

Thanks again for the careful read.

best.

-greg





^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention
  2026-04-25 20:08 [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-04-27 14:15 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-04-28 14:30   ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 14:00     ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 18:07       ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-11 15:54         ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-14 17:53           ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
@ 2026-07-22 15:10             ` Andres Freund <andres@anarazel.de>
  2026-07-25 10:45               ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2 siblings, 1 reply; 21+ messages in thread

From: Andres Freund @ 2026-07-22 15:10 UTC (permalink / raw)
  To: Greg Burd <greg@burd.me>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Tomas Vondra <tomas@vondra.me>; Nathan Bossart <nathandbossart@gmail.com>

Hi,

On 2026-07-14 13:53:06 -0400, Greg Burd wrote:
> The buffer cache ring strategy didn't only bound cache pollution, it also
> deferred writeback. A backend running a big COPY, bulk UPDATE or VACUUM
> reused a small ring and, via StrategyRejectBuffer, pushed dirty victims back
> for the bgwriter and checkpointer to write instead of writing (and
> WAL-flushing) them inline on its own allocation path.  Pull the rings (as in
> 0003) and, done naively, that deferral goes too.

That's not right, StrategyRejectBuffer does that only for a bulkread
strat. And because of the limited number of buffers used for bulkwrite, they
don't actually tend to involve bgwriter a lot.

The effect is literally the opposite for bulk copies. Without the strategy
there's basically no back pressure against a backend dirtying a lot during
bulk writes, with the strategy there's a *lot* (perhaps too much)
backpressure.

There is no such handing off.


>     CREATE TABLE bulk (id int, pad text) WITH (fillfactor=90);
>     INSERT INTO bulk SELECT g, repeat('x',180) FROM generate_series(1,2e7) g;
>     VACUUM (FREEZE) bulk; CHECKPOINT;
> 
>     -- (1) UPDATE, dirties every page
>     UPDATE bulk SET id = id + 1;
> 
>     -- (2) bulk INSERT into an empty table
>     CREATE TABLE ins (id int, pad text);
>     INSERT INTO ins SELECT g, repeat('y',180) FROM generate_series(1,2e7) g;
> 
>     -- (3) VACUUM after dirtying half the pages
>     UPDATE bulk SET pad = repeat('z',180) WHERE id % 2 = 0; CHECKPOINT;
>     VACUUM bulk;
> 
> One caveat up front: (2) is INSERT...SELECT, not the COPY command.

INSERT ... SELECT does not use the bulkwrite strategy, so there can't be an
effect from removing the strategies until the VACUUM.


> It hits the same relation-extend + WAL-logged-write path BAS_BULKWRITE
> served, which is what I wanted to stress, but if that distinction matters to
> anyone's conclusion I'll re-run with COPY FROM.

It's not at all the "same relation-extend + WAL-logged-write path". INSERT
SELECT uses individual heap_inserts, COPY uses heap_multi_insert. The latter
extends does a lot less WAL logging and extends the relation much more
aggressively (heap_insert() only bulk extends if there's contention).


> For each statement I record time_s (wall seconds), bwrites (buffers the
> backend wrote itself -- sum(writes) from pg_stat_io where object='relation'
> after a forced flush, i.e. the writes the rings used to hand off), and
> wal_gb (LSN delta, just a check the two builds did the same work).  Full
> series, Option B included:
> 
>     workload      metric     stock       patched     impact of patches
>     ------------  ---------  ----------  ----------  ------------------
>     bulk UPDATE   time_s         26.0        27.3     5% slower
>                   bwrites       90754      158165     74% more
>                   wal_gb          5.83        5.90    +1%
>     bulk INSERT   time_s         14.2        15.6     10% slower
>                   bwrites           0           0     none; extend path
>                   wal_gb          5.00        5.11    +2%
>     VACUUM        time_s         11.4         9.3     19% faster
>                   bwrites      996255      390945     61% fewer
>                   wal_gb          6.80        6.77    -1%

Af course VACUUM is faster if you allow it to fill up all of shared buffers
with dirty buffers. FWIW, you can get that effect today using
  VACUUM (BUFFER_USAGE_LIMIT 0)


Since a bulk update doesn't use a strategy, I don't see how strategy related
changes can trigger 74% more writes via bgwriter.

Greetings,

Andres Freund






^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention
  2026-04-25 20:08 [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-04-27 14:15 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-04-28 14:30   ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 14:00     ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 18:07       ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-11 15:54         ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-14 17:53           ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-22 15:10             ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
@ 2026-07-25 10:45               ` Greg Burd <greg@burd.me>
  2026-07-29 17:56                 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  0 siblings, 1 reply; 21+ messages in thread

From: Greg Burd @ 2026-07-25 10:45 UTC (permalink / raw)
  To: Andres Freund <andres@anarazel.de>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Tomas Vondra <tomas@vondra.me>; Nathan Bossart <nathandbossart@gmail.com>


On Wed, Jul 22, 2026, at 11:10 AM, Andres Freund wrote:
> Hi,

Hi Andres,

You and Ants landed on the same core correction, so I've owned the
StrategyRejectBuffer / BAS_BULKWRITE mistake in my reply to him rather than
repeat it here -- short version: you're both right, BAS_BULKWRITE keeps the
writes on the backend, there is no hand-off, and I've dropped that framing.
Let me answer the points that were specific to your mail.

> On 2026-07-14 13:53:06 -0400, Greg Burd wrote:
>> The buffer cache ring strategy didn't only bound cache pollution, it also
>> deferred writeback. A backend running a big COPY, bulk UPDATE or VACUUM
>> reused a small ring and, via StrategyRejectBuffer, pushed dirty victims back
>> for the bgwriter and checkpointer to write instead of writing (and
>> WAL-flushing) them inline on its own allocation path.  Pull the rings (as in
>> 0003) and, done naively, that deferral goes too.
>
> That's not right, StrategyRejectBuffer does that only for a bulkread
> strat. And because of the limited number of buffers used for bulkwrite, they
> don't actually tend to involve bgwriter a lot.
>
> The effect is literally the opposite for bulk copies. Without the strategy
> there's basically no back pressure against a backend dirtying a lot during
> bulk writes, with the strategy there's a *lot* (perhaps too much)
> backpressure.
>
> There is no such handing off.

Right, and this reframes the whole thing for me.  I had been treating the
ring's writeback behavior as a cost to preserve; it's actually backpressure to
preserve.  I confirmed the "no backpressure without it" direction directly: a
sustained bulk-dirtying workload with the bgwriter pushed as hard as it goes
still ends with the backend doing ~100% of the relation writes itself -- the
bgwriter just falls behind and every allocation flushes inline.  So removing
the rings removes real backpressure and "make the bgwriter keep up" does not
replace it.

The consequence is that I'm dropping the BufferAccessStrategy removal (old
0003) from what I'm putting forward.  It needs a genuine replacement for the
bulk-writer backpressure BAS_BULKWRITE provides, which is its own project, and
your "perhaps too much" aside is interesting on its own -- if the current
backpressure is heavier than it needs to be, the right move might be to tune
or rethink it rather than either keep it as-is or delete it.  But that's not
this patch.  What I'm left proposing is two independent things: the batched
sweep, and the cooling-stage evictor with the rings left in place.

>>     CREATE TABLE bulk (id int, pad text) WITH (fillfactor=90);
>>     INSERT INTO bulk SELECT g, repeat('x',180) FROM generate_series(1,2e7) g;
>>     VACUUM (FREEZE) bulk; CHECKPOINT;
>> 
>>     -- (1) UPDATE, dirties every page
>>     UPDATE bulk SET id = id + 1;
>> 
>>     -- (2) bulk INSERT into an empty table
>>     CREATE TABLE ins (id int, pad text);
>>     INSERT INTO ins SELECT g, repeat('y',180) FROM generate_series(1,2e7) g;
>> 
>>     -- (3) VACUUM after dirtying half the pages
>>     UPDATE bulk SET pad = repeat('z',180) WHERE id % 2 = 0; CHECKPOINT;
>>     VACUUM bulk;
>> 
>> One caveat up front: (2) is INSERT...SELECT, not the COPY command.
>
> INSERT ... SELECT does not use the bulkwrite strategy, so there can't be an
> effect from removing the strategies until the VACUUM.
>
>
>> It hits the same relation-extend + WAL-logged-write path BAS_BULKWRITE
>> served, which is what I wanted to stress, but if that distinction matters to
>> anyone's conclusion I'll re-run with COPY FROM.
>
> It's not at all the "same relation-extend + WAL-logged-write path". INSERT
> SELECT uses individual heap_inserts, COPY uses heap_multi_insert. The latter
> extends does a lot less WAL logging and extends the relation much more
> aggressively (heap_insert() only bulk extends if there's contention).

RelationGetBufferForTuple/RelationAddBlocks extends by num_pages scaled by the
extension-lock waiter count, so a single-tuple heap_insert extends one page
unless it's contended, while heap_multi_insert asks for many; and
heap_multi_insert emits one XLOG_HEAP2_MULTI_INSERT per batch versus one record
per row.  So (2) in my script was not exercising the BAS_BULKWRITE path at all,
and it was not the "same" write/extend path as COPY.  Both criticisms are
correct and the workload was mislabeled.

>> For each statement I record time_s (wall seconds), bwrites (buffers the
>> backend wrote itself -- sum(writes) from pg_stat_io where object='relation'
>> after a forced flush, i.e. the writes the rings used to hand off), and
>> wal_gb (LSN delta, just a check the two builds did the same work).  Full
>> series, Option B included:
>> 
>>     workload      metric     stock       patched     impact of patches
>>     ------------  ---------  ----------  ----------  ------------------
>>     bulk UPDATE   time_s         26.0        27.3     5% slower
>>                   bwrites       90754      158165     74% more
>>                   wal_gb          5.83        5.90    +1%
>>     bulk INSERT   time_s         14.2        15.6     10% slower
>>                   bwrites           0           0     none; extend path
>>                   wal_gb          5.00        5.11    +2%
>>     VACUUM        time_s         11.4         9.3     19% faster
>>                   bwrites      996255      390945     61% fewer
>>                   wal_gb          6.80        6.77    -1%
>
> Af course VACUUM is faster if you allow it to fill up all of shared buffers
> with dirty buffers. FWIW, you can get that effect today using
>   VACUUM (BUFFER_USAGE_LIMIT 0)

Yes.  I re-ran this and you're right: the VACUUM "win" is just the removal of
the BAS_VACUUM self-throttle, not anything my change does.  With a corrected
run the patched VACUUM time is ~the same as stock, and stock with
BUFFER_USAGE_LIMIT 0 gets essentially the same effect.  I've withdrawn the
VACUUM-speedup claim; it was measuring the throttle, not the evictor.

> Since a bulk update doesn't use a strategy, I don't see how strategy related
> changes can trigger 74% more writes via bgwriter.

They can't, and that was the tell that my explanation was wrong.  The +74%
was not a strategy effect -- a bulk UPDATE uses no strategy in either build.
Isolating it (benchmarking 0001+0002 with the rings still present, which is
also the test Ants suggested) shows the write-shifting comes from the cooling
change in 0002 -- specifically an over-eager bgwriter tweak I had folded in
there ("Option B") that pushed writes off the backend onto the bgwriter.  That
was an eviction-path change riding in 0002, not ring removal, and given the
backpressure discussion above it's the wrong behavior anyway, so I'm removing
it.  Once it's out, the bulk-UPDATE delta between stock and the cooling evictor
sits inside run-to-run noise; I won't be presenting it as a win or a
regression.

> Greetings,
>
> Andres Freund

Net: the honest residue of that whole benchmark is "I mismeasured."  Where I
think there is a real, defensible result is on the *cost of the 0..5
usage_count itself* under a hot, continuously-evicting large pool with
huge_pages off -- the clock hand doing ~5*NBuffers dTLB-missing descriptor
visits per victim because it decrements a hot buffer five times and resets its
scan counter on every decrement.  I've been measuring that (ticks-per-victim,
and the residency-fraction regime where it bites) and will bring it as its own
thread with a methodology built to your "real IO, not all-in-page-cache,
measure which buffers get evicted" bar rather than tacking it onto this one.

Thanks for the corrections and insights.

best.

-greg






^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention
  2026-04-25 20:08 [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-04-27 14:15 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-04-28 14:30   ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 14:00     ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 18:07       ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-11 15:54         ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-14 17:53           ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-22 15:10             ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-07-25 10:45               ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
@ 2026-07-29 17:56                 ` Greg Burd <greg@burd.me>
  2026-07-29 20:10                   ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  0 siblings, 1 reply; 21+ messages in thread

From: Greg Burd @ 2026-07-29 17:56 UTC (permalink / raw)
  To: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Tomas Vondra <tomas@vondra.me>; Andres Freund <andres@anarazel.de>; Ants Aasma <ants.aasma@cybertec.at>; +Cc: Nathan Bossart <nathandbossart@gmail.com>

Hello again.

Well, I've been using the past few days to dig into the
HOT/COOL+batching model and I think I've found the corners and have
data to support it.  Attached find v8 (on 9fa2c1e) and a tar.gz with
my benchmark.

TL;DR:
 Empirically-grounded data shows that HOT/COOL+batching:
 - (a) provides scan resistance which is net new outside of the
       ring buffer
 - (b) is simpler (1-bit vs 0..5) <- that's a lot fewer bits, lol!
 - (c) I've found no evidence of any signal provided by the 0..5
       clock-sweep counter and direct evidence that it can, in
       limited but not-unimaginable cases, become the root of
       pathologically bad performance
 - (d) batching de-contends nextVictimBuffer on NUMA +68-75% when
       the sweep is hot, no regression otherwise.  Not "always
       faster," but "targeted de-contention that pays off exactly
       when the sweep saturates, and never hurts when pooled.

I've tested with pgbench, HammerDB (TPROC-C/H), and custom code to
model and elicit the worst/best cases and the pay-off cases I could
imagine.  I've not done this on local (non-cloud) hardware (I'm
building that out now!) but on EC2 metal instances which I'll argue
are close enough and all I had available (for now).

I may still be benchmarking in ways that you feel don't elicit the
best, most accurate results.  Feel free to point that out and I'll
adapt and re-run the tests.

AI NOTICE:
Yes, this patch was co-developed/refined/tested/benchmarked using
AI/LLMs and even this email started off as LLM content.  All the work
is steered by me and reviewed before submission by me, I stand behind
it warts and all. (gulp) ;-P

DETAILS:
I've done quite a bit of measurement.  This reply is long because
it needs to be, legitimate requests for real data required thought
and time to execute, analyze, etc.  Short version of what changed and
what I found:

  - Two patches now, not three.  I dropped the BufferAccessStrategy
    removal; it removed real bulk-writer backpressure with no
    replacement (Ants and Andres were both right), so it's out until
    that's solved separately.
  - I withdrew the v6/7 dirty-bulk "regression" and VACUUM "win" as
    both were mismeasurements, detailed in my earlier replies.
  - The batched sweep (0001) and the cooling evictor (0002) are
    independent and I'm treating them as such; 0001 stands alone if 0002
    is contentious, but used together is the real win.
  - I built a sweep-bound microbenchmark that isolates the
    nextVictimBuffer atomic, and it settles the batching question with
    perf c2c evidence (below).  It also shows honestly where batching
    does *not* move the needle.

The series:

  0001  Batch the clock sweep to reduce nextVictimBuffer atomic contention
        (Jim Mlodgenski's idea; co-authored)
  0002  Replace the 0..5 usage_count clock sweep with a 1-bit HOT/COOL
        cooling-stage evictor (LeanStore/2Q-A1), rings kept

All numbers below are on AWS bare metal, working set sized to fit RAM so
misses are OS-page-cache-cheap (this is a CPU/TLB study, not a storage
one -- I'll say explicitly where that matters).  Instrumentation (a
throwaway ticks-per-victim counter and a block-thrash function) is not
part of the patches; the repro kit is attached as repro.tgz_ (underscore
to avoid being picked up by CI I hope).


0001 -- batched sweep: does it help NUMA, and does it regress?
========================================================================

Andres, in the first reply on this thread you doubted that batching
independent of contention/usage-rate would help, and suspected it might
hurt. I set out to prove or disprove that, and the answer is: it depends
entirely on whether the clock sweep is the bottleneck, and I can now
show exactly when that is the case.

First, the fair-throughput question.  Earlier I got confusing results
because I was running 2000-8000 backends against a 384-vCPU box -- 5-20x
oversubscribed -- which is a scheduler-storm test, not a batching test,
and there batching *did* look like it hurt (~-35%).  With a connection
count at or below core count (the pooled configuration anyone runs in
production), on a 6-NUMA r8i.metal (384 vCPU), uniform pgbench-style
point lookups, stock vs batch{1,16,64} are all within ~1-2% at every
level -- batching neither helps nor hurts.  So: no regression when
you're not oversubscribed; the -35% was an artifact of oversubscription,
which a connection pooler removes.

Why neutral there?  Because in a normal point-lookup the executor +
protocol + tuple deform dwarf the one atomic; even stock only did ~2.9
clock ticks per victim, so nextVictimBuffer just wasn't hot enough to
matter.

To find out whether the *mechanism* is real at all, I built a
sweep-bound microbenchmark: a function that pins+releases random blocks
of a table 1.7x shared_buffers with the normal (non-ring) strategy, so
StrategyGetBuffer is essentially the only work.  360 concurrent loops (=
cores on the instance), 64GB shared_buffers, 6 NUMA nodes:

    build          block-evictions/sec   vs stock
    stock                  2,096,314       --
    bcs batch=1            2,138,485       +2%   (single fetch-add: parity)
    bcs batch=16           3,531,021      +68%
    bcs batch=64           3,664,458      +75%

and the mechanism, straight from perf c2c (cross-node HITM on the
hottest line):

    stock:        StrategyGetBuffer's fetch-add on nextVictimBuffer is the #1
                  cross-node HITM line at 38.5%, spread across all 6 nodes.
    batch=16/64:  that same line drops to ~1-2% HITM.

So the batched claim does exactly what it claims: nextVictimBuffer is
touched ~1/N as often, its cross-socket bounce collapses, and when the
sweep is the bottleneck throughput rises 68-75%.  batch=1 == stock
confirms the win is purely the batched atomic, not the evictor.

The honest framing, which I'd rather state than have pulled out of me:
this is a microbenchmark that isolates the atomic.  It proves the
mechanism is real and bounds the ceiling; it does not claim a 68% win on
your OLTP box.  On real mixed workloads at backends<=cores the sweep is
a small share of each query, so end-to-end it's neutral -- no
regression, no headline speedup -- until the sweep actually saturates
(large hot pool, heavy eviction, many NUMA nodes, which is the reported
field pathology).  0001 is a targeted de-contention that pays off
exactly when the hand is hot and costs nothing when it isn't; on a
single socket it compiles to the stock path (batch size 1).

I did also run HammerDB TPROC-C at backends<=cores; it's
write/lock/WAL-bound, the sweep is not the bottleneck, and batch=64 was
~5% *below* stock (the cooling bookkeeping isn't free when the sweep
isn't the cost).  Reporting that too -- it's not a batching workload,
and I'm not going to pretend it is.


0002 -- cooling evictor: why the 0..5 counter, and the failure mode
========================================================================

Ants, you said the initial results were a promising indication that
G-CLOCK's 0..5 counter isn't carrying much value, and asked for a wider
access-pattern gamut.  Here's the strongest thing I found.

The 0..5 counter has a pathological failure mode that the 1-bit HOT/COOL
clock does not.  Stock must decrement a buffer from 5 to 0 before it can
evict it -- up to five visits per victim -- and it resets its
bounded-scan counter on every decrement, so under a hot,
continuously-evicting pool it never gives up early.  With a large
shared_buffers and huge_pages off (a multi-GB BufferDescriptors array on
4KB pages), those repeated visits are dTLB misses. I measured
ticks-per-victim climbing to ~9 on stock under load where the 1-bit
clock stays flat (it demotes HOT->COOL in one pass; a victim is produced
in at most ~3 ticks, and typically 1-2).  In the extreme this turns into
a throughput cliff / multi-second p99 for stock -- the field reports of
"raise shared_buffers and TPS falls off" on large no-huge-pages
machines.  The 1-bit clock has no such cliff because it does bounded
work per victim by construction.

On scan resistance, which is the reason the 0..5 counter's replacement
has to be careful: a demand-loaded page is admitted COOL (probationary)
and only promoted to HOT on a genuine second touch, so a one-touch
sequential scan fills and drains the COOL stage without displacing the
HOT working set.  I tested this adversarially -- an OLTP hot set that
fits in shared_buffers, hit by point lookups, while a concurrent seqscan
streams a table 4x shared_buffers, on local NVMe (no EBS/IOPS cap
masking the eviction rate):

    OLTP hit ratio under the scan:   stock 99.999%   bcs 99.998%
    OLTP resident set:               flat in both

i.e. the cooling evictor's scan resistance is at parity with what the
BAS_BULKREAD ring gives today.  (An earlier version of this patch also
removed that read ring on the theory that COOL admission made it
redundant; local-NVMe testing showed it wasn't quite -- the ring is
measurably tighter under concurrent scan -- so I kept the ring.  That's
the dropped 0003.)

On the literature: you pointed at CLOCK-Pro, thanks -- the admission
model here (probationary COOL, promote on reuse) is the 2Q-A1 /
test-period idea and I'd rather adopt a better-justified promotion rule
from that line than defend "second touch" if the list prefers one.  I
tried a stricter "promote only on a repeat reference" variant; it was
neutral on OLTP and didn't improve scan resistance, so I reverted it to
keep 0002 minimal, but it's an easy knob.

On backpressure: I'm not removing the rings, precisely because
BAS_BULKWRITE is backpressure, not just a pollution guard.  If we ever
do want the rings gone, that backpressure needs a real replacement
first; I don't have one worth proposing, so it's out of scope here.


Relationship to the NUMA partitioned sweep
========================================================================

Tomas, this overlaps your NUMA series and I want to be clear it's not a
competitor.  0001 attacks the same nextVictimBuffer cross-node
contention your partitioned sweep does, but as a minimal,
placement-agnostic change: one fetch-add per batch instead of per tick,
no per-node partitioning, no buffer-placement changes, batch-size-1
fallback so it's a no-op off NUMA.  The c2c data above is the same
contention your partitioning targets, measured in isolation.

I think these compose rather than conflict: a partitioned sweep still
advances a hand within each partition, and batching that per-partition
hand should give the same cross-node relief inside a partition that it
gives the global hand here.  If your series lands, 0001 is a small
change on top (or moot, if the partition hands are already node-local
and uncontended).  If it's useful I'm happy to test batching layered on
your patches -- I just didn't want to fold placement/partitioning into
this, since the evictor and the atomic de-contention stand on their own
and are far smaller.


What I'm claiming, and what I'm not
========================================================================

  - 0002 is simpler (1 bit vs a 0..5 counter), has intrinsic scan
    resistance at parity with the BAS_BULKREAD ring, and removes a real
    large-pool/no-huge-pages cliff that the 0..5 counter can hit. It is
    throughput-neutral on ordinary OLTP (within noise), which is the bar
    for a replacement-policy change.
  - 0001 de-contends nextVictimBuffer on NUMA: +68-75% when the sweep is
    the bottleneck (c2c-proven mechanism), neutral otherwise, no-op off
    NUMA, no regression when connections are kept at/under core count.
    One idea would be to lower batch size to 1 when the number of
    backends > cores available on the host to avoid the regression, but
    that isn't in v8 nor have I tested it in practice.
  - I am NOT claiming a general TPS win; on write/lock-bound or
    non-sweep-bound workloads there isn't one, and I've shown that.

Repro kit attached (methodology, the microbenchmark, the ticks/victim
and block-thrash instruments, the residency-fraction model that predicts
when the 0..5 cliff appears, and the huge_pages on/off and
confound-discrimination notes).  It's built to be re-run and attacked;
if any of this doesn't reproduce for you, that's the feedback I want.

Thanks for the reviews, they made the patch set smaller and the claims
honest.

best.

-greg

Attachments:

  [text/x-patch] v8-0001-Batch-the-clock-sweep-to-reduce-nextVictimBuffer-.patch (9.7K, ../../8bd912c8-22bb-4667-a656-3be843709be2@app.fastmail.com/2-v8-0001-Batch-the-clock-sweep-to-reduce-nextVictimBuffer-.patch)
  download | inline diff:
From 40679bd604a4f55ebc460cdd4337fc3d370ec8ec Mon Sep 17 00:00:00 2001
From: Greg Burd <greg@burd.me>
Date: Mon, 6 Jul 2026 17:25:00 -0400
Subject: [PATCH v8 1/2] Batch the clock sweep to reduce nextVictimBuffer
 atomic contention

StrategyGetBuffer() advances the shared clock hand, nextVictimBuffer, with a
pg_atomic_fetch_add_u32(..., 1) on every tick.  On a multi-socket system the
cache line holding that counter has to travel over the interconnect on each
operation, pushing a sweep tick from ~20ns (same-socket, line warm in L1/L2)
into the ~100-200ns range.  Under eviction pressure with hundreds of backends
in StrategyGetBuffer() concurrently, that single cache line becomes the
dominant cost of the sweep, visible as elevated bus-cycles and cache-misses in
a perf profile.

Have each backend claim a run of consecutive buffer IDs from the shared hand
with a single fetch-add and then iterate through them privately.  The sweep
still advances through the pool in order, each buffer is still visited exactly
once per complete pass, and the meaning of the clock state is unchanged; only
the temporal ordering of visits within a pass changes, which the algorithm
does not depend on.  The contended atomic now fires roughly once per batch
rather than once per buffer.

The batch is one cache line's worth of clock-hand values --
PG_CACHE_LINE_SIZE / sizeof(uint32) -- capped at NBuffers so a claim can never
wrap the pool more than once.  Batching only helps when the counter's cache
line actually bounces between sockets, so it is enabled only on multi-node
NUMA hardware (pg_numa_get_max_node() >= 1); on a single socket, or where
libnuma is unavailable, the batch size stays 1 and the code path is
byte-identical to the stock clock sweep.

Wraparound handling is adjusted: with batching, several backends can each see
a fetch-add return a value past NBuffers within the same pass.  Any such
backend takes buffer_strategy_lock, re-reads the counter, and if it is still
out of range wraps it with a single CAS and increments completePasses.
StrategySyncStart() continues to see a consistent (nextVictimBuffer,
completePasses) pair.

This is the batched-clock-sweep idea from Jim Mlodgenski's pgsql-hackers
thread, adapted to derive the batch size from the platform cache-line size.

Co-authored-by: Jim Mlodgenski <mlodj@amazon.com>
---
 src/backend/storage/buffer/freelist.c | 152 ++++++++++++++++++--------
 1 file changed, 109 insertions(+), 43 deletions(-)

diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index fdb5bad..46104b3 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -22,6 +22,7 @@
 #include "storage/proc.h"
 #include "storage/shmem.h"
 #include "storage/subsystems.h"
+#include "port/pg_numa.h"
 
 #define INT_ACCESS_ONCE(var)	((int)(*((volatile int *)&(var))))
 
@@ -100,68 +101,110 @@ static BufferDesc *GetBufferFromRing(BufferAccessStrategy strategy,
 static void AddBufferToRing(BufferAccessStrategy strategy,
 							BufferDesc *buf);
 
+/*
+ * Per-backend state for the batched clock sweep.  Each backend claims a run
+ * of consecutive clock-hand values with a single atomic fetch-add and then
+ * iterates through them privately, so the contended nextVictimBuffer cache
+ * line is touched roughly 1/batch as often.  MyBatchPos is the next hand
+ * value to hand out; MyBatchEnd is one past the end of the claimed run.  Both
+ * are absolute (monotonically increasing) hand values; the buffer id is the
+ * value modulo NBuffers.
+ */
+static uint32 MyBatchPos = 0;
+static uint32 MyBatchEnd = 0;
+
+/*
+ * Number of clock-hand values a backend claims per atomic fetch-add,
+ * computed once at startup (see StrategyCtlShmemInit).  When batching is
+ * enabled it is one cache line's worth of hand advance, so concurrent
+ * backends sweep non-overlapping, cache-line-sized runs of the pool; the
+ * global sweep order is preserved (each buffer is still visited exactly once
+ * per pass).  Batching is enabled only on multi-node NUMA hardware; otherwise
+ * this stays 1 and the sweep is byte-identical to the stock clock.
+ */
+static uint32 ClockSweepBatchSize = 1;
+
 /*
  * ClockSweepTick - Helper routine for StrategyGetBuffer()
  *
- * Move the clock hand one buffer ahead of its current position and return the
- * id of the buffer now under the hand.
+ * Return the next buffer to consider for eviction.  Backends claim batches of
+ * consecutive buffer IDs from the shared clock hand, then iterate through
+ * them locally without further atomic operations.  This preserves the global
+ * sweep order while reducing contention on the shared counter.
  */
 static inline uint32
 ClockSweepTick(void)
 {
 	uint32		victim;
 
-	/*
-	 * Atomically move hand ahead one buffer - if there's several processes
-	 * doing this, this can lead to buffers being returned slightly out of
-	 * apparent order.
-	 */
-	victim =
-		pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer, 1);
-
-	if (victim >= NBuffers)
+	if (MyBatchPos >= MyBatchEnd)
 	{
-		uint32		originalVictim = victim;
-
-		/* always wrap what we look up in BufferDescriptors */
-		victim = victim % NBuffers;
-
 		/*
-		 * If we're the one that just caused a wraparound, force
-		 * completePasses to be incremented while holding the spinlock. We
-		 * need the spinlock so StrategySyncStart() can return a consistent
-		 * value consisting of nextVictimBuffer and completePasses.
+		 * Claim a fresh batch from the shared clock hand.  This is the only
+		 * atomic operation per batch, reducing contention by the batch size.
 		 */
-		if (victim == 0)
-		{
-			uint32		expected;
-			uint32		wrapped;
-			bool		success = false;
+		uint32		start;
+		uint32		batch_size = ClockSweepBatchSize;
 
-			expected = originalVictim + 1;
+		start = pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer,
+										batch_size);
 
-			while (!success)
-			{
-				/*
-				 * Acquire the spinlock while increasing completePasses. That
-				 * allows other readers to read nextVictimBuffer and
-				 * completePasses in a consistent manner which is required for
-				 * StrategySyncStart().  In theory delaying the increment
-				 * could lead to an overflow of nextVictimBuffers, but that's
-				 * highly unlikely and wouldn't be particularly harmful.
-				 */
-				SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
+		if (start >= (uint32) NBuffers)
+		{
+			start = start % NBuffers;
 
-				wrapped = expected % NBuffers;
+			/*
+			 * The counter has grown past NBuffers; try to wrap it back.  We
+			 * must hold the spinlock so StrategySyncStart() can read
+			 * nextVictimBuffer and completePasses consistently.
+			 *
+			 * With batching, multiple backends may each land a fetch-add
+			 * that returns a value past NBuffers in the same pass.  After
+			 * acquiring the spinlock we re-read the counter: if another
+			 * backend already wrapped it below NBuffers we are done.
+			 */
+			SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
+			{
+				uint32		current;
+				uint32		wrapped;
 
-				success = pg_atomic_compare_exchange_u32(&StrategyControl->nextVictimBuffer,
-														 &expected, wrapped);
-				if (success)
-					StrategyControl->completePasses++;
-				SpinLockRelease(&StrategyControl->buffer_strategy_lock);
+				current = pg_atomic_read_u32(&StrategyControl->nextVictimBuffer);
+				if (current >= (uint32) NBuffers)
+				{
+					wrapped = current % NBuffers;
+					if (pg_atomic_compare_exchange_u32(&StrategyControl->nextVictimBuffer,
+													   &current, wrapped))
+						StrategyControl->completePasses++;
+				}
 			}
+			SpinLockRelease(&StrategyControl->buffer_strategy_lock);
+		}
+		else if (start + batch_size > (uint32) NBuffers)
+		{
+			/*
+			 * The fetch-add returned a value below NBuffers, but this batch
+			 * spans the wrap point (start .. NBuffers-1 then 0 .. k are
+			 * iterated locally via "% NBuffers").  That crossing is a
+			 * complete pass too, but the wrap-and-increment path above only
+			 * fires when the fetch-add return is itself >= NBuffers, so
+			 * without this it would go uncounted and completePasses would
+			 * drift low (it feeds bgwriter pacing / StrategySyncStart, not
+			 * correctness).  The batch is capped at NBuffers, so a batch
+			 * spans the wrap at most once; count it under the spinlock for a
+			 * consistent (nextVictimBuffer, completePasses) pair.
+			 */
+			SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
+			StrategyControl->completePasses++;
+			SpinLockRelease(&StrategyControl->buffer_strategy_lock);
 		}
+
+		MyBatchPos = start;
+		MyBatchEnd = start + batch_size;
 	}
+
+	victim = MyBatchPos % NBuffers;
+	MyBatchPos++;
+
 	return victim;
 }
 
@@ -408,6 +451,29 @@ StrategyCtlShmemInit(void *arg)
 
 	/* No pending notification */
 	StrategyControl->bgwprocno = -1;
+
+	/*
+	 * Decide whether to batch the clock sweep.
+	 *
+	 * Batching claims a run of consecutive buffer IDs per atomic fetch-add so
+	 * concurrent backends touch the shared nextVictimBuffer cache line ~1/batch
+	 * as often -- a win only when that line actually bounces across sockets,
+	 * i.e. on multi-node NUMA hardware.  On a single socket the atomic is
+	 * already node-local and batching would only make backends skip ahead for
+	 * no benefit, so we fall back to batch size 1 there (byte-identical to the
+	 * stock one-buffer-at-a-time clock sweep).
+	 *
+	 * Batch (> 1) only when libnuma reports more than one node
+	 * (pg_numa_get_max_node() >= 1); pg_numa_init() returns -1 when NUMA is
+	 * unavailable.  (Benchmarking showed the win holds with or without huge
+	 * pages, so huge-page availability is intentionally not part of the gate.)
+	 */
+	if (pg_numa_init() != -1 &&
+		pg_numa_get_max_node() >= 1)
+		ClockSweepBatchSize = Min(PG_CACHE_LINE_SIZE / (uint32) sizeof(uint32),
+								  (uint32) NBuffers);
+	else
+		ClockSweepBatchSize = 1;
 }
 
 
-- 
2.50.1



  [text/x-patch] v8-0002-Replace-the-usage_count-clock-sweep-with-a-coolin.patch (29.4K, ../../8bd912c8-22bb-4667-a656-3be843709be2@app.fastmail.com/3-v8-0002-Replace-the-usage_count-clock-sweep-with-a-coolin.patch)
  download | inline diff:
From 898e100f5e2c42d0804b0515310f51c357166fec Mon Sep 17 00:00:00 2001
From: Greg Burd <greg@burd.me>
Date: Mon, 6 Jul 2026 19:22:45 -0400
Subject: [PATCH v8 2/2] Replace the usage_count clock sweep with a
 cooling-stage evictor

Replace the 0..5 usage_count buffer-replacement policy with a cooling-stage
clock (the LeanStore / 2Q-A1 model): a buffer is simply HOT (recently used)
or COOL (an eviction candidate), with "pinned" being the existing refcount.
There is no per-buffer access counter.

  - A demand-loaded page is admitted COOL (probationary), not HOT.  A second
    access via PinBuffer promotes it COOL -> HOT (the rescue).  So a page
    touched once -- a sequential scan -- fills and drains the COOL stage and
    is evicted from it without ever displacing the HOT working set.  Scan
    resistance is intrinsic to the replacement algorithm, which is what lets a
    later commit remove the BufferAccessStrategy ring buffers entirely.

  - The foreground sweep in StrategyGetBuffer() reclaims an already-COOL,
    unpinned buffer, pinning it with a CAS so a racing PinBuffer always wins.
    Promotion (COOL -> HOT) and demotion (HOT -> COOL) are single-bit
    transitions; only the eviction claim is a CAS.

  - The background writer maintains the supply of COOL victims.  As its LRU
    scan runs ahead of the clock hand it demotes HOT buffers to COOL, so the
    foreground finds a victim in a single pass rather than having to cool
    buffers itself.  The demotion is demand-driven -- bounded by the predicted
    allocation for the next cycle -- so it stages just enough COOL buffers
    without cooling the whole pool, and it is done under the buffer header
    lock the scan already holds.  A single second-chance reference bit
    (set by PinBuffer, cleared on the bgwriter's first pass over a HOT buffer)
    spares a recently-accessed buffer one cooling pass, keeping the genuinely
    hot set out of the COOL stage under scan pressure.  BgBufferSync also
    tracks the reusable-buffer density it directly observes on a shorter
    smoothing window, so a burst of probationary/scan COOL pages is followed
    promptly rather than averaged away.  Under a bulk-dirtying workload the
    per-cycle clean-write cap is raised from bgwriter_lru_maxpages to predicted
    demand so the bgwriter keeps supplying clean victims, rather than the
    foreground sweep having to flush dirty victims inline; normal workloads,
    where demand is below the cap, are unaffected.

The 4-bit usage_count field is reinterpreted in place: bit 0 is the HOT/COOL
state (BUF_COOLSTATE_ONE), bit 1 the reference bit (BUF_REFBIT).  The 64-bit
buffer-state layout -- refcount, flag and lock offsets and their StaticAsserts
-- is unchanged; only the meaning of the field and the instructions that touch
it change.  A StaticAssert requires the field to be at least 2 bits wide so a
future width change cannot push the reference bit into the flag bits.
BM_MAX_USAGE_COUNT becomes BUF_COOLSTATE_HOT (1), so the pin fast path
saturates at HOT.  Local (temp-table) buffers get the same two-state
treatment; being single-backend they need no background cooler.

Because the reference bit shares the field, the full 4-bit value can be 0..3;
readers that mean "the cooling state" must use BUF_STATE_GET_COOLSTATE(), which
masks to bit 0 and returns only 0 (COOL) or 1 (HOT), never the raw field.
contrib/pg_buffercache is updated accordingly: its usagecount column and the
pg_buffercache_summary average report the cooling state (0 = COOL, 1 = HOT),
and pg_buffercache_usage_counts() buckets on it.  Using the raw 4-bit getter
there would index its BM_MAX_USAGE_COUNT+1 = 2-element arrays with values up
to 3 and overrun the stack; masking to the cooling bit keeps the index in
range.  The reference bit is deliberately not exposed as usagecount.

Depends on the batched clock sweep from the previous commit.
---
 contrib/pg_buffercache/pg_buffercache_pages.c |   6 +-
 src/backend/storage/buffer/bufmgr.c           | 130 ++++++++++++++----
 src/backend/storage/buffer/freelist.c         | 114 +++++++++------
 src/backend/storage/buffer/localbuf.c         |  13 +-
 src/include/storage/buf_internals.h           |  63 +++++++--
 5 files changed, 239 insertions(+), 87 deletions(-)

diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index 5104559..b94e6f6 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -161,7 +161,7 @@ pg_buffercache_pages(PG_FUNCTION_ARGS)
 		reldatabase = bufHdr->tag.dbOid;
 		forknum = BufTagGetForkNum(&bufHdr->tag);
 		blocknum = bufHdr->tag.blockNum;
-		usagecount = BUF_STATE_GET_USAGECOUNT(buf_state);
+		usagecount = BUF_STATE_GET_COOLSTATE(buf_state);
 		pinning_backends = BUF_STATE_GET_REFCOUNT(buf_state);
 
 		if (buf_state & BM_DIRTY)
@@ -605,7 +605,7 @@ pg_buffercache_summary(PG_FUNCTION_ARGS)
 		if (buf_state & BM_VALID)
 		{
 			buffers_used++;
-			usagecount_total += BUF_STATE_GET_USAGECOUNT(buf_state);
+			usagecount_total += BUF_STATE_GET_COOLSTATE(buf_state);
 
 			if (buf_state & BM_DIRTY)
 				buffers_dirty++;
@@ -655,7 +655,7 @@ pg_buffercache_usage_counts(PG_FUNCTION_ARGS)
 
 		CHECK_FOR_INTERRUPTS();
 
-		usage_count = BUF_STATE_GET_USAGECOUNT(buf_state);
+		usage_count = BUF_STATE_GET_COOLSTATE(buf_state);
 		usage_counts[usage_count]++;
 
 		if (buf_state & BM_DIRTY)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 3908529..8689266 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -83,6 +83,7 @@
 /* Bits in SyncOneBuffer's return value */
 #define BUF_WRITTEN				0x01
 #define BUF_REUSABLE			0x02
+#define BUF_COOLED				0x04
 
 #define RELS_BSEARCH_THRESHOLD		20
 
@@ -634,7 +635,7 @@ static void PinBuffer_Locked(BufferDesc *buf);
 static void UnpinBuffer(BufferDesc *buf);
 static void UnpinBufferNoOwner(BufferDesc *buf);
 static void BufferSync(int flags);
-static int	SyncOneBuffer(int buf_id, bool skip_recently_used,
+static int	SyncOneBuffer(int buf_id, bool skip_recently_used, bool cool_if_hot,
 						  WritebackContext *wb_context);
 static void WaitIO(BufferDesc *buf);
 static void AbortBufferIO(Buffer buffer);
@@ -2333,7 +2334,10 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	 * checkpoints, except for their "init" forks, which need to be treated
 	 * just like permanent relations.
 	 */
-	set_bits |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
+	set_bits |= BM_TAG_VALID;
+	/* Admit the newly loaded page COOL (probation); a second access via
+	 * PinBuffer promotes it to HOT.  This is what makes a one-touch scan
+	 * self-evicting -- see the cooling-state notes in buf_internals.h. */
 	if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM)
 		set_bits |= BM_PERMANENT;
 
@@ -3002,7 +3006,9 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 
 			victim_buf_hdr->tag = tag;
 
-			set_bits |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
+			set_bits |= BM_TAG_VALID;
+			/* Admit COOL (probation); see the comment at the other admission
+			 * site and the cooling-state notes in buf_internals.h. */
 			if (bmr.relpersistence == RELPERSISTENCE_PERMANENT || fork == INIT_FORKNUM)
 				set_bits |= BM_PERMANENT;
 
@@ -3332,21 +3338,17 @@ PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy,
 			/* increase refcount */
 			buf_state += BUF_REFCOUNT_ONE;
 
-			if (strategy == NULL)
-			{
-				/* Default case: increase usagecount unless already max. */
-				if (BUF_STATE_GET_USAGECOUNT(buf_state) < BM_MAX_USAGE_COUNT)
-					buf_state += BUF_USAGECOUNT_ONE;
-			}
-			else
-			{
-				/*
-				 * Ring buffers shouldn't evict others from pool.  Thus we
-				 * don't make usagecount more than 1.
-				 */
-				if (BUF_STATE_GET_USAGECOUNT(buf_state) == 0)
-					buf_state += BUF_USAGECOUNT_ONE;
-			}
+			/*
+			 * Accessing a resident buffer promotes it to HOT (the 2Q rescue):
+			 * a page loaded COOL on probation becomes part of the hot working
+			 * set on its second touch.  BM_MAX_USAGE_COUNT is
+			 * BUF_COOLSTATE_HOT (1), so this saturates at HOT and never
+			 * overflows the field.  We also set the second-chance ref bit so
+			 * the bgwriter's next cooling pass spares this recently-used buffer.
+			 */
+			if (BUF_STATE_GET_COOLSTATE(buf_state) < BUF_COOLSTATE_HOT)
+				buf_state += BUF_COOLSTATE_ONE;
+			buf_state |= BUF_REFBIT;
 
 			if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
 											   buf_state))
@@ -3785,7 +3787,7 @@ BufferSync(int flags)
 		 */
 		if (pg_atomic_read_u64(&bufHdr->state) & BM_CHECKPOINT_NEEDED)
 		{
-			if (SyncOneBuffer(buf_id, false, &wb_context) & BUF_WRITTEN)
+			if (SyncOneBuffer(buf_id, false, false, &wb_context) & BUF_WRITTEN)
 			{
 				TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
 				PendingCheckpointerStats.buffers_written++;
@@ -3876,6 +3878,19 @@ BgBufferSync(WritebackContext *wb_context)
 	float		smoothing_samples = 16;
 	float		scan_whole_pool_milliseconds = 120000.0;
 
+	/*
+	 * The cleaner scan directly observes the reusable (COOL, unpinned) buffer
+	 * density over the region it walks, which -- with the cooling-stage
+	 * evictor -- is exactly the sweep's victim predicate.  That observation is
+	 * ground truth for the buffers about to be reused, whereas the strategy
+	 * scan's positional proxy (strategy_delta/recent_alloc) blurs a pool whose
+	 * COOL population is spatially clustered (a scan burst leaves whole regions
+	 * COOL, hot OLTP regions not).  So we let the cleaner's own sample adapt on
+	 * a shorter window than the strategy proxy, tracking a burst of
+	 * probationary/scan COOL pages within a cycle or two instead of lagging it.
+	 */
+	float		cleaner_smoothing_samples = 4;
+
 	/* Used to compute how far we scan ahead */
 	long		strategy_delta;
 	int			bufs_to_lap;
@@ -3889,6 +3904,7 @@ BgBufferSync(WritebackContext *wb_context)
 	int			num_to_scan;
 	int			num_written;
 	int			reusable_buffers;
+	int			write_limit;
 
 	/* Variables for final smoothed_density update */
 	long		new_strategy_delta;
@@ -4063,8 +4079,22 @@ BgBufferSync(WritebackContext *wb_context)
 	 * Now write out dirty reusable buffers, working forward from the
 	 * next_to_clean point, until we have lapped the strategy scan, or cleaned
 	 * enough buffers to match our estimate of the next cycle's allocation
-	 * requirements, or hit the bgwriter_lru_maxpages limit.
-	 */
+	 * requirements, or hit the write limit.
+	 *
+	 * The per-cycle write cap is normally bgwriter_lru_maxpages.  But under a
+	 * bulk-dirtying workload (COPY, bulk UPDATE, VACUUM) the pool fills with
+	 * dirty COOL buffers faster than that fixed cap can clean, so the
+	 * foreground clock sweep is forced to flush dirty victims inline -- the
+	 * very cost the cooling-stage evictor is meant to keep off the critical
+	 * path.  When predicted demand (upcoming_alloc_est) exceeds the fixed cap,
+	 * raise the limit to meet demand so the bgwriter stays ahead and supplies
+	 * clean victims.  This stays bounded (by demand and by lapping the
+	 * strategy point), so it cannot run away, and normal workloads are
+	 * unaffected because there upcoming_alloc_est <= bgwriter_lru_maxpages.
+	 */
+	write_limit = bgwriter_lru_maxpages;
+	if (upcoming_alloc_est > write_limit)
+		write_limit = upcoming_alloc_est;
 
 	num_to_scan = bufs_to_lap;
 	num_written = 0;
@@ -4073,7 +4103,13 @@ BgBufferSync(WritebackContext *wb_context)
 	/* Execute the LRU scan */
 	while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
 	{
-		int			sync_state = SyncOneBuffer(next_to_clean, true,
+		/*
+		 * cool_if_hot = false: with the cool-in-place foreground sweep, the
+		 * clock hand demotes HOT buffers on every tick, so the bgwriter does
+		 * writeback only and leaves all cooling to the sweep (no background
+		 * pre-cooling, no double-cooling).
+		 */
+		int			sync_state = SyncOneBuffer(next_to_clean, true, false,
 											   wb_context);
 
 		if (++next_to_clean >= NBuffers)
@@ -4086,7 +4122,7 @@ BgBufferSync(WritebackContext *wb_context)
 		if (sync_state & BUF_WRITTEN)
 		{
 			reusable_buffers++;
-			if (++num_written >= bgwriter_lru_maxpages)
+			if (++num_written >= write_limit)
 			{
 				PendingBgWriterStats.maxwritten_clean++;
 				break;
@@ -4121,7 +4157,7 @@ BgBufferSync(WritebackContext *wb_context)
 	{
 		scans_per_alloc = (float) new_strategy_delta / (float) new_recent_alloc;
 		smoothed_density += (scans_per_alloc - smoothed_density) /
-			smoothing_samples;
+			cleaner_smoothing_samples;
 
 #ifdef BGW_DEBUG
 		elog(DEBUG2, "bgwriter: cleaner density alloc=%u scan=%ld density=%.2f new smoothed=%.2f",
@@ -4140,16 +4176,26 @@ BgBufferSync(WritebackContext *wb_context)
  * If skip_recently_used is true, we don't write currently-pinned buffers, nor
  * buffers marked recently used, as these are not replacement candidates.
  *
+ * If cool_if_hot is true (the bgwriter's LRU scan), an unpinned HOT buffer is
+ * demoted HOT -> COOL as we pass it, pre-staging eviction candidates so the
+ * foreground clock sweep finds a COOL victim in a single pass instead of
+ * having to cool buffers itself (force_cool).  The demotion is done under the
+ * buffer header lock we already hold, so it needs no CAS and cannot race a
+ * concurrent demotion.  A concurrent PinBuffer promotes it back to HOT, which
+ * is the intended 2Q behavior (a re-accessed buffer is rescued).
+ *
  * Returns a bitmask containing the following flag bits:
  *	BUF_WRITTEN: we wrote the buffer.
  *	BUF_REUSABLE: buffer is available for replacement, ie, it has
- *		pin count 0 and usage count 0.
+ *		pin count 0 and is COOL (an eviction candidate).
+ *	BUF_COOLED: we demoted this buffer HOT -> COOL this call.
  *
  * (BUF_WRITTEN could be set in error if FlushBuffer finds the buffer clean
  * after locking it, but we don't care all that much.)
  */
 static int
-SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
+SyncOneBuffer(int buf_id, bool skip_recently_used, bool cool_if_hot,
+			  WritebackContext *wb_context)
 {
 	BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
 	int			result = 0;
@@ -4171,8 +4217,38 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 	 */
 	buf_state = LockBufHdr(bufHdr);
 
+	/*
+	 * Pre-cool with a second chance: if asked, act on an unpinned HOT buffer.
+	 * If its ref bit is set (accessed since our last pass), clear the ref bit
+	 * and leave it HOT -- a recently-used buffer earns one reprieve, keeping
+	 * the hot working set out of the COOL stage under scan pressure.  Only a
+	 * HOT buffer whose ref bit is already clear is demoted HOT -> COOL,
+	 * pre-staging it as an eviction candidate for the foreground sweep.  We
+	 * hold the header lock, so each transition is a plain masked store applied
+	 * atomically by UnlockBufHdrExt; we re-lock to continue the dirty-write
+	 * inspection below.
+	 */
+	if (cool_if_hot &&
+		BUF_STATE_GET_REFCOUNT(buf_state) == 0 &&
+		BUF_STATE_GET_COOLSTATE(buf_state) != BUF_COOLSTATE_COOL)
+	{
+		if (BUF_STATE_GET_REFBIT(buf_state))
+		{
+			/* second chance: consume the ref bit, stay HOT */
+			UnlockBufHdrExt(bufHdr, buf_state, 0, BUF_REFBIT, 0);
+			buf_state = LockBufHdr(bufHdr);
+		}
+		else
+		{
+			/* not re-accessed since last pass: demote to COOL */
+			UnlockBufHdrExt(bufHdr, buf_state, 0, BUF_USAGECOUNT_MASK, 0);
+			buf_state = LockBufHdr(bufHdr);
+			result |= BUF_COOLED;
+		}
+	}
+
 	if (BUF_STATE_GET_REFCOUNT(buf_state) == 0 &&
-		BUF_STATE_GET_USAGECOUNT(buf_state) == 0)
+		BUF_STATE_GET_COOLSTATE(buf_state) == BUF_COOLSTATE_COOL)
 	{
 		result |= BUF_REUSABLE;
 	}
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index 46104b3..808f133 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -49,6 +49,16 @@ typedef struct
 	uint32		completePasses; /* Complete cycles of the clock-sweep */
 	pg_atomic_uint32 numBufferAllocs;	/* Buffers allocated since last reset */
 
+	/*
+	 * Number of clock-hand values a backend claims per atomic fetch-add.
+	 * Computed once at startup (see StrategyCtlShmemInit).  Kept in shared
+	 * memory rather than a backend-local static so that EXEC_BACKEND children
+	 * (which do not inherit the postmaster's statics) see the same value; a
+	 * backend-local copy would silently reset to 1 there, disabling batching
+	 * on Windows.
+	 */
+	uint32		batchSize;
+
 	/*
 	 * Bgworker process to be notified upon activity or -1 if none. See
 	 * StrategyNotifyBgWriter.
@@ -113,17 +123,6 @@ static void AddBufferToRing(BufferAccessStrategy strategy,
 static uint32 MyBatchPos = 0;
 static uint32 MyBatchEnd = 0;
 
-/*
- * Number of clock-hand values a backend claims per atomic fetch-add,
- * computed once at startup (see StrategyCtlShmemInit).  When batching is
- * enabled it is one cache line's worth of hand advance, so concurrent
- * backends sweep non-overlapping, cache-line-sized runs of the pool; the
- * global sweep order is preserved (each buffer is still visited exactly once
- * per pass).  Batching is enabled only on multi-node NUMA hardware; otherwise
- * this stays 1 and the sweep is byte-identical to the stock clock.
- */
-static uint32 ClockSweepBatchSize = 1;
-
 /*
  * ClockSweepTick - Helper routine for StrategyGetBuffer()
  *
@@ -144,7 +143,7 @@ ClockSweepTick(void)
 		 * atomic operation per batch, reducing contention by the batch size.
 		 */
 		uint32		start;
-		uint32		batch_size = ClockSweepBatchSize;
+		uint32		batch_size = StrategyControl->batchSize;
 
 		start = pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer,
 										batch_size);
@@ -279,12 +278,30 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 	 */
 	pg_atomic_fetch_add_u32(&StrategyControl->numBufferAllocs, 1);
 
-	/* Use the "clock sweep" algorithm to find a free buffer */
+	/*
+	 * Use the cooling-stage clock sweep to find a victim.
+	 *
+	 * A buffer is HOT (recently used) or COOL (an eviction candidate).  This
+	 * is a COOL-IN-PLACE sweep: every tick past a HOT buffer demotes it one
+	 * step toward COOL (respecting a single second-chance reference bit), and
+	 * a COOL, unpinned buffer is claimed as the victim.  Because every tick
+	 * makes cooling progress, the sweep is self-staging: it never needs a
+	 * background pre-cooler to keep COOL victims available, and it cannot get
+	 * stuck in a "no COOL victim anywhere" state that forces a foreground
+	 * escalation pass.  Newly loaded pages are admitted COOL (see
+	 * BufferAlloc), so scan resistance falls out of the algorithm.
+	 *
+	 * trycounter bounds the search: only a pinned buffer is "no progress".
+	 * Demoting or reclaiming is progress and resets it.  A full NBuffers pass
+	 * that finds every buffer pinned fails, matching the stock "no unpinned
+	 * buffers available" contract.
+	 */
 	trycounter = NBuffers;
 	for (;;)
 	{
 		uint64		old_buf_state;
 		uint64		local_buf_state;
+		bool		no_progress = false;
 
 		buf = GetBufferDescriptor(ClockSweepTick());
 
@@ -297,25 +314,10 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 		{
 			local_buf_state = old_buf_state;
 
-			/*
-			 * If the buffer is pinned or has a nonzero usage_count, we cannot
-			 * use it; decrement the usage_count (unless pinned) and keep
-			 * scanning.
-			 */
-
+			/* If the buffer is pinned we cannot use it; keep scanning. */
 			if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0)
 			{
-				if (--trycounter == 0)
-				{
-					/*
-					 * We've scanned all the buffers without making any state
-					 * changes, so all the buffers are pinned (or were when we
-					 * looked at them). We could hope that someone will free
-					 * one eventually, but it's probably better to fail than
-					 * to risk getting stuck in an infinite loop.
-					 */
-					elog(ERROR, "no unpinned buffers available");
-				}
+				no_progress = true;
 				break;
 			}
 
@@ -326,9 +328,23 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 				continue;
 			}
 
-			if (BUF_STATE_GET_USAGECOUNT(local_buf_state) != 0)
+			if (BUF_STATE_GET_COOLSTATE(local_buf_state) != BUF_COOLSTATE_COOL)
 			{
-				local_buf_state -= BUF_USAGECOUNT_ONE;
+				/*
+				 * HOT buffer: cool it in place this tick.  Apply a single
+				 * second-chance reference bit: a HOT buffer whose ref bit is
+				 * set (touched since it was last passed) has the ref bit
+				 * cleared and stays HOT; only a HOT buffer whose ref bit is
+				 * already clear is demoted HOT -> COOL.  Either transition is
+				 * progress toward a victim, so reset trycounter.  We do NOT
+				 * claim the buffer this tick -- a demoted buffer becomes a
+				 * candidate for a later tick, giving it one more full sweep of
+				 * grace before eviction.
+				 */
+				if (BUF_STATE_GET_REFBIT(local_buf_state))
+					local_buf_state &= ~BUF_REFBIT;			/* second chance: clear ref, stay HOT */
+				else
+					local_buf_state &= ~BUF_USAGECOUNT_MASK;	/* HOT -> COOL, clear ref bit */
 
 				if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
 												   local_buf_state))
@@ -339,7 +355,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 			}
 			else
 			{
-				/* pin the buffer if the CAS succeeds */
+				/* COOL and unpinned: claim it.  Pin if the CAS succeeds. */
 				local_buf_state += BUF_REFCOUNT_ONE;
 
 				if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
@@ -356,6 +372,15 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 				}
 			}
 		}
+
+		/*
+		 * Only a pinned buffer is no progress.  A full NBuffers pass that
+		 * makes no progress means every buffer is pinned, so fail rather than
+		 * spin forever.  (A failed CAS above is neither progress nor a full
+		 * miss: we simply retry the same buffer.)
+		 */
+		if (no_progress && --trycounter == 0)
+			elog(ERROR, "no unpinned buffers available");
 	}
 }
 
@@ -470,10 +495,10 @@ StrategyCtlShmemInit(void *arg)
 	 */
 	if (pg_numa_init() != -1 &&
 		pg_numa_get_max_node() >= 1)
-		ClockSweepBatchSize = Min(PG_CACHE_LINE_SIZE / (uint32) sizeof(uint32),
-								  (uint32) NBuffers);
+		StrategyControl->batchSize = Min(PG_CACHE_LINE_SIZE / (uint32) sizeof(uint32),
+										 (uint32) NBuffers);
 	else
-		ClockSweepBatchSize = 1;
+		StrategyControl->batchSize = 1;
 }
 
 
@@ -721,14 +746,17 @@ GetBufferFromRing(BufferAccessStrategy strategy, uint64 *buf_state)
 		/*
 		 * If the buffer is pinned we cannot use it under any circumstances.
 		 *
-		 * If usage_count is 0 or 1 then the buffer is fair game (we expect 1,
-		 * since our own previous usage of the ring element would have left it
-		 * there, but it might've been decremented by clock-sweep since then).
-		 * A higher usage_count indicates someone else has touched the buffer,
-		 * so we shouldn't re-use it.
+		 * If it is unpinned but has been promoted to HOT, another backend
+		 * touched it since we last cycled past this ring slot, so it has
+		 * joined the working set and we must not recycle it -- tell the caller
+		 * to get a fresh victim from the sweep instead.  This is the 1-bit
+		 * cooling-state analog of the stock ring's "usage_count > 1 means
+		 * someone else touched it" test: a slot the ring keeps reusing and
+		 * nobody else pins stays COOL, and an out-of-ring PinBuffer() is
+		 * exactly what promotes it to HOT.
 		 */
-		if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0
-			|| BUF_STATE_GET_USAGECOUNT(local_buf_state) > 1)
+		if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0 ||
+			BUF_STATE_GET_COOLSTATE(local_buf_state) != BUF_COOLSTATE_COOL)
 			break;
 
 		/* See equivalent code in PinBuffer() */
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 4870c8e..21c0809 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -167,7 +167,7 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
 
 		buf_state = pg_atomic_read_u64(&bufHdr->state);
 		buf_state &= ~(BUF_FLAG_MASK | BUF_USAGECOUNT_MASK);
-		buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
+		buf_state |= BM_TAG_VALID;	/* admit COOL (probation) */
 		pg_atomic_unlocked_write_u64(&bufHdr->state, buf_state);
 
 		*foundPtr = false;
@@ -248,9 +248,10 @@ GetLocalVictimBuffer(void)
 		{
 			uint64		buf_state = pg_atomic_read_u64(&bufHdr->state);
 
-			if (BUF_STATE_GET_USAGECOUNT(buf_state) > 0)
+			if (BUF_STATE_GET_COOLSTATE(buf_state) != BUF_COOLSTATE_COOL)
 			{
-				buf_state -= BUF_USAGECOUNT_ONE;
+				/* HOT: give it a second chance, cool it and keep scanning. */
+				buf_state -= BUF_COOLSTATE_ONE;
 				pg_atomic_unlocked_write_u64(&bufHdr->state, buf_state);
 				trycounter = NLocBuffer;
 			}
@@ -454,7 +455,7 @@ ExtendBufferedRelLocal(BufferManagerRelation bmr,
 
 			victim_buf_hdr->tag = tag;
 
-			buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
+			buf_state |= BM_TAG_VALID;	/* admit COOL (probation) */
 
 			pg_atomic_unlocked_write_u64(&victim_buf_hdr->state, buf_state);
 
@@ -839,9 +840,9 @@ PinLocalBuffer(BufferDesc *buf_hdr, bool adjust_usagecount)
 		NLocalPinnedBuffers++;
 		buf_state += BUF_REFCOUNT_ONE;
 		if (adjust_usagecount &&
-			BUF_STATE_GET_USAGECOUNT(buf_state) < BM_MAX_USAGE_COUNT)
+			BUF_STATE_GET_COOLSTATE(buf_state) < BUF_COOLSTATE_HOT)
 		{
-			buf_state += BUF_USAGECOUNT_ONE;
+			buf_state += BUF_COOLSTATE_ONE;
 		}
 		pg_atomic_unlocked_write_u64(&buf_hdr->state, buf_state);
 
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index e4ff561..131df12 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -67,6 +67,50 @@ StaticAssertDecl(BUF_REFCOUNT_BITS + BUF_USAGECOUNT_BITS + BUF_FLAG_BITS + BUF_L
 #define BUF_USAGECOUNT_ONE \
 	(UINT64CONST(1) << BUF_REFCOUNT_BITS)
 
+/*
+ * Cooling state (LeanStore / 2Q-A1 cooling-stage clock sweep).
+ *
+ * The field historically used for the 0..5 usage_count now holds a single
+ * cooling-state bit: HOT (recently accessed, not an eviction candidate) or
+ * COOL (an eviction candidate).  We reuse BUF_USAGECOUNT_ONE as the unit so
+ * the buffer-state bit geography -- refcount, flag, and lock offsets, and the
+ * 64-bit StaticAsserts -- is unchanged; only the meaning of the field and the
+ * instructions that touch it change.
+ *
+ * A demand-loaded page is admitted COOL (probation); a second access promotes
+ * it to HOT (the rescue).  The sweep prefers COOL victims and demotes HOT to
+ * COOL only when a full pass finds no COOL victim.  So a one-touch scan fills
+ * and drains the COOL stage without displacing the HOT working set -- scan
+ * resistance intrinsic to the replacement algorithm.
+ */
+#define BUF_COOLSTATE_COOL	0
+#define BUF_COOLSTATE_HOT	1
+#define BUF_COOLSTATE_ONE	BUF_USAGECOUNT_ONE
+
+/*
+ * Second-chance reference bit, bit 1 of the (former usagecount) field, one
+ * position above the cooling-state bit.  PinBuffer sets it on every access.
+ * The bgwriter's pre-cooling gives a HOT buffer a second chance: the first
+ * time it passes a HOT buffer whose ref bit is set, it clears the ref bit and
+ * leaves the buffer HOT; only a HOT buffer whose ref bit is already clear (not
+ * re-accessed since the previous bgwriter pass) is demoted to COOL.  This
+ * keeps genuinely-hot pages out of the COOL stage (protecting the working set
+ * from being cooled under scan pressure) while leaving the foreground sweep a
+ * single-pass search over the pre-staged COOL buffers.  A separate bit (not a
+ * count) so it stays a plain masked store under the header lock.
+ */
+#define BUF_REFBIT			(UINT64CONST(2) << BUF_REFCOUNT_BITS)
+#define BUF_STATE_GET_REFBIT(state)	(((state) & BUF_REFBIT) != 0)
+
+/*
+ * The cooling state occupies bit 0 and the reference bit occupies bit 1 of
+ * the (former usagecount) field, so the field must be at least 2 bits wide.
+ * Assert it here so a future change to BUF_USAGECOUNT_BITS cannot silently
+ * push BUF_REFBIT up into the flag bits.
+ */
+StaticAssertDecl(BUF_USAGECOUNT_BITS >= 2,
+				 "cooling state + reference bit need at least 2 bits in the usagecount field");
+
 /* flags related definitions */
 #define BUF_FLAG_SHIFT \
 	(BUF_REFCOUNT_BITS + BUF_USAGECOUNT_BITS)
@@ -92,6 +136,11 @@ StaticAssertDecl(BUF_REFCOUNT_BITS + BUF_USAGECOUNT_BITS + BUF_FLAG_BITS + BUF_L
 #define BUF_STATE_GET_USAGECOUNT(state) \
 	((uint32)(((state) & BUF_USAGECOUNT_MASK) >> BUF_USAGECOUNT_SHIFT))
 
+/* Cooling state (HOT/COOL) from buffer state -- bit 0 of the field only, so
+ * the second-chance ref bit (bit 1) does not perturb the HOT/COOL test. */
+#define BUF_STATE_GET_COOLSTATE(state) \
+	((uint32) (((state) >> BUF_USAGECOUNT_SHIFT) & 1))
+
 /*
  * Flags for buffer descriptors
  *
@@ -134,17 +183,15 @@ StaticAssertDecl(MAX_BACKENDS_BITS <= (BUF_LOCK_BITS - 2),
 
 
 /*
- * The maximum allowed value of usage_count represents a tradeoff between
- * accuracy and speed of the clock-sweep buffer management algorithm.  A
- * large value (comparable to NBuffers) would approximate LRU semantics.
- * But it can take as many as BM_MAX_USAGE_COUNT+1 complete cycles of the
- * clock-sweep hand to find a free buffer, so in practice we don't want the
- * value to be very large.
+ * The cooling state is a single bit (HOT/COOL); the maximum value stored in
+ * the field is therefore BUF_COOLSTATE_HOT.  Retained under the historical
+ * name BM_MAX_USAGE_COUNT so the pin fast path ("promote unless already at
+ * max") reads naturally.
  */
-#define BM_MAX_USAGE_COUNT	5
+#define BM_MAX_USAGE_COUNT	BUF_COOLSTATE_HOT
 
 StaticAssertDecl(BM_MAX_USAGE_COUNT < (UINT64CONST(1) << BUF_USAGECOUNT_BITS),
-				 "BM_MAX_USAGE_COUNT doesn't fit in BUF_USAGECOUNT_BITS bits");
+				 "cooling state doesn't fit in BUF_USAGECOUNT_BITS bits");
 
 /*
  * Buffer tag identifies which disk block the buffer contains.
-- 
2.50.1



  [application/octet-stream] repro.tgz_ (15.2K, ../../8bd912c8-22bb-4667-a656-3be843709be2@app.fastmail.com/4-repro.tgz_)
  download

^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention
  2026-04-25 20:08 [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-04-27 14:15 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-04-28 14:30   ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 14:00     ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 18:07       ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-11 15:54         ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-14 17:53           ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-22 15:10             ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-07-25 10:45               ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-29 17:56                 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
@ 2026-07-29 20:10                   ` Andres Freund <andres@anarazel.de>
  2026-07-30 16:45                     ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  0 siblings, 1 reply; 21+ messages in thread

From: Andres Freund @ 2026-07-29 20:10 UTC (permalink / raw)
  To: Greg Burd <greg@burd.me>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Tomas Vondra <tomas@vondra.me>; Ants Aasma <ants.aasma@cybertec.at>; Nathan Bossart <nathandbossart@gmail.com>

Hi,

On 2026-07-29 13:56:41 -0400, Greg Burd wrote:
> AI NOTICE:
> Yes, this patch was co-developed/refined/tested/benchmarked using
> AI/LLMs and even this email started off as LLM content.  All the work
> is steered by me and reviewed before submission by me, I stand behind
> it warts and all. (gulp) ;-P

Not a fan. I'm ok with some closely supervised AI written code, but AI written
emails I tend to just put to the bottom of my pile (which I rarely reach). I
can't tell how much vetting goes into them, and I have too much to do to be an
AI sanity checker.



> To find out whether the *mechanism* is real at all, I built a
> sweep-bound microbenchmark: a function that pins+releases random blocks
> of a table 1.7x shared_buffers with the normal (non-ring) strategy, so
> StrategyGetBuffer is essentially the only work.  360 concurrent loops (=
> cores on the instance), 64GB shared_buffers, 6 NUMA nodes:
>
>     build          block-evictions/sec   vs stock
>     stock                  2,096,314       --
>     bcs batch=1            2,138,485       +2%   (single fetch-add: parity)
>     bcs batch=16           3,531,021      +68%
>     bcs batch=64           3,664,458      +75%

Seems pretty doubtful this is measuring something super interesting, given
that you'd a) use a lot of memory bandwidth for that much IO b) there's a lot
of other contention points for that much IO (e.g. buffer table partition
locks).

A 75% win actually seems somewhat disappointing for such a microbenchmark? I'd
bet that you'd see WAY bigger gains with properly partitioning this (since the
batching still leaves you with a lot of c2c accesses).


I still don't see what the point of pursuing batching instead of helping out
with Tomas' partitioned sweep patch is.


>
> 0002 -- cooling evictor: why the 0..5 counter, and the failure mode
> ========================================================================
>
> Ants, you said the initial results were a promising indication that
> G-CLOCK's 0..5 counter isn't carrying much value, and asked for a wider
> access-pattern gamut.  Here's the strongest thing I found.

Bla.


> The 0..5 counter has a pathological failure mode that the 1-bit HOT/COOL
> clock does not.  Stock must decrement a buffer from 5 to 0 before it can
> evict it -- up to five visits per victim -- and it resets its
> bounded-scan counter on every decrement, so under a hot,
> continuously-evicting pool it never gives up early.  With a large
> shared_buffers and huge_pages off (a multi-GB BufferDescriptors array on
> 4KB pages), those repeated visits are dTLB misses. I measured
> ticks-per-victim climbing to ~9 on stock under load where the 1-bit
> clock stays flat (it demotes HOT->COOL in one pass; a victim is produced
> in at most ~3 ticks, and typically 1-2).  In the extreme this turns into
> a throughput cliff / multi-second p99 for stock -- the field reports of
> "raise shared_buffers and TPS falls off" on large no-huge-pages
> machines.  The 1-bit clock has no such cliff because it does bounded
> work per victim by construction.

That's a lot of words with no actual evidence what the real world cost of that
is.


> On scan resistance, which is the reason the 0..5 counter's replacement
> has to be careful: a demand-loaded page is admitted COOL (probationary)
> and only promoted to HOT on a genuine second touch, so a one-touch
> sequential scan fills and drains the COOL stage without displacing the
> HOT working set.  I tested this adversarially -- an OLTP hot set that
> fits in shared_buffers, hit by point lookups, while a concurrent seqscan
> streams a table 4x shared_buffers, on local NVMe (no EBS/IOPS cap
> masking the eviction rate):
>
>     OLTP hit ratio under the scan:   stock 99.999%   bcs 99.998%
>     OLTP resident set:               flat in both

I don't think it makes much sense to measure this when you can achieve close
to 100 hit rate, since that's so easy to achieve. There's dozens of ways to
achieve scan resistance that work well in that scenario but that fall entirely
completely flat when you have actual cache pressure.

I think this algorithm will *trivially* fail in a lot of cases without
strategies. Once you have sufficient cache pressure the likelihood for
cold->hot promotion ends up being too low to happen reliably before cold pages
are evicted. And because this doesn't have something like a ghost directory,
it'll not even have a chance to detect and fix that the next time the page is
read in.

I don't believe this algorithm will actually work well even without concurrent
bulk access, I suspect it'll fail to work well even with something like
pgbench -S where the index fits comfortably in s_b, but the table data does
not.  We already aren't good in that kind of workload, but I don't see how
your algorithm will actually achieve *any* meaningful protection for anything
but the inner index pages, the likelihood of repeat accesses to the same index
page won't be high enough to keep those pages warm.


> On the literature: you pointed at CLOCK-Pro, thanks -- the admission model
> here (probationary COOL, promote on reuse) is the 2Q-A1 / test-period idea
> and I'd rather adopt a better-justified promotion rule from that line than
> defend "second touch" if the list prefers one.  I tried a stricter "promote
> only on a repeat reference" variant; it was neutral on OLTP and didn't
> improve scan resistance, so I reverted it to keep 0002 minimal, but it's an
> easy knob.

FWIW, I really doubt that any of the scan resistant algorithms actually will
allow us to get the read side strategies entirely, even with a ghost
directory. All of the scan resistant algorithms only are scan resistant if
repeated accesses (that should be cached) that occur during a concurrent bulk
scan are actually within something like the size of the buffer pool, but that
doesnt' really seem good enough.

That said, the current logic of strategies, where we actually evict the pages
with a ringbuffer, seems rather wrong, because it takes approximately forever
to fill the buffer pool even if there *are* repeat accesses.


I suspect we should go for something like an extended version of
S3-FIFO. S3-FIFO, has a FIFO for cold pages, taking up a certain percentage of
the buffer pool (10% IIRC) and a ghost directory to detect repeat accesses
that don't happen while the page is still in the cold fifo queue. That's scan
resistant as long as repeat accesses are frequent enough (1.10x the buffer
pool, I think).

To avoid bulk reads from pushing repeat accesses beyond the window in which
repeat accesses are recognized, I think we could "just" make sure the ghost
directory has eviction logic that will not allow strategy reads to take up
more than a certain percentage of the ghost directory.


Greetings,

Andres Freund






^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention
  2026-04-25 20:08 [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-04-27 14:15 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-04-28 14:30   ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 14:00     ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 18:07       ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-11 15:54         ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-14 17:53           ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-22 15:10             ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-07-25 10:45               ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-29 17:56                 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-29 20:10                   ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
@ 2026-07-30 16:45                     ` Greg Burd <greg@burd.me>
  2026-07-30 21:17                       ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  0 siblings, 1 reply; 21+ messages in thread

From: Greg Burd @ 2026-07-30 16:45 UTC (permalink / raw)
  To: Andres Freund <andres@anarazel.de>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Tomas Vondra <tomas@vondra.me>; Ants Aasma <ants.aasma@cybertec.at>; Nathan Bossart <nathandbossart@gmail.com>


On Wed, Jul 29, 2026, at 4:10 PM, Andres Freund wrote:
> Hi,
>
> On 2026-07-29 13:56:41 -0400, Greg Burd wrote:
>> AI NOTICE:
>> Yes, this patch was co-developed/refined/tested/benchmarked using
>> AI/LLMs and even this email started off as LLM content.  All the work
>> is steered by me and reviewed before submission by me, I stand behind
>> it warts and all. (gulp) ;-P
>
> Not a fan. I'm ok with some closely supervised AI written code, but AI written
> emails I tend to just put to the bottom of my pile (which I rarely reach). I
> can't tell how much vetting goes into them, and I have too much to do to be an
> AI sanity checker.

Fair, and I'll stop using AI/LLMs in that way.  I'd rather you judge the work than wonder how much I vetted it.

The last email was vetted by me.  Was it too wordy - yes, was it a bit robot-voice - yes.  I get it, human voice is "authentic" and that investment has meaning.

>> To find out whether the *mechanism* is real at all, I built a
>> sweep-bound microbenchmark: a function that pins+releases random blocks
>> of a table 1.7x shared_buffers with the normal (non-ring) strategy, so
>> StrategyGetBuffer is essentially the only work.  360 concurrent loops (=
>> cores on the instance), 64GB shared_buffers, 6 NUMA nodes:
>>
>>     build          block-evictions/sec   vs stock
>>     stock                  2,096,314       --
>>     bcs batch=1            2,138,485       +2%   (single fetch-add: parity)
>>     bcs batch=16           3,531,021      +68%
>>     bcs batch=64           3,664,458      +75%
>
> Seems pretty doubtful this is measuring something super interesting, given
> that you'd a) use a lot of memory bandwidth for that much IO b) there's a lot
> of other contention points for that much IO (e.g. buffer table partition
> locks).

It is super-interesting because this condition does happen in practice with customer instances and has caused at least one cloud provider to create an internal work-around for it.  Yes, there are other points where this workload has contention but right now I'm focused on this one.

> A 75% win actually seems somewhat disappointing for such a microbenchmark? I'd
> bet that you'd see WAY bigger gains with properly partitioning this (since the
> batching still leaves you with a lot of c2c accesses).

Maybe, but 75% isn't bad especially when the p99 during the overloaded state for clock-sweep is seconds to minutes and for HOT/COOL+batching is as steady as in any state.  That's a real win IMO.

> I still don't see what the point of pursuing batching instead of helping out
> with Tomas' partitioned sweep patch is.

I'll do both, I appreciate the serious consideration of this algorithm.  There's no reason the two ideas can't be combined.  A partitioned HOT/COOL might be a solid improvement on its own, without partitioning.


>> 0002 -- cooling evictor: why the 0..5 counter, and the failure mode
>> ========================================================================
>>
>> Ants, you said the initial results were a promising indication that
>> G-CLOCK's 0..5 counter isn't carrying much value, and asked for a wider
>> access-pattern gamut.  Here's the strongest thing I found.
>
> Bla.

Gesundheit.

>> The 0..5 counter has a pathological failure mode that the 1-bit HOT/COOL
>> clock does not.  Stock must decrement a buffer from 5 to 0 before it can
>> evict it -- up to five visits per victim -- and it resets its
>> bounded-scan counter on every decrement, so under a hot,
>> continuously-evicting pool it never gives up early.  With a large
>> shared_buffers and huge_pages off (a multi-GB BufferDescriptors array on
>> 4KB pages), those repeated visits are dTLB misses. I measured
>> ticks-per-victim climbing to ~9 on stock under load where the 1-bit
>> clock stays flat (it demotes HOT->COOL in one pass; a victim is produced
>> in at most ~3 ticks, and typically 1-2).  In the extreme this turns into
>> a throughput cliff / multi-second p99 for stock -- the field reports of
>> "raise shared_buffers and TPS falls off" on large no-huge-pages
>> machines.  The 1-bit clock has no such cliff because it does bounded
>> work per victim by construction.
>
> That's a lot of words with no actual evidence what the real world cost of that
> is.

This is the definition of the pathological case observed in production that Jimbo looked into when he was first trying out ideas that eventually led to the batching proposal.

The ticks-per-victim is a reasonable metric for "ability to make progress" and ~9 vs 3 can be meaningful in practice.  The "real-world cost" is that your p99 latency spikes to seconds/minutes ore more in practice with the clock-sweep/0..5 algorithm but does not with HOT/COOL+batched.

>> On scan resistance, which is the reason the 0..5 counter's replacement
>> has to be careful: a demand-loaded page is admitted COOL (probationary)
>> and only promoted to HOT on a genuine second touch, so a one-touch
>> sequential scan fills and drains the COOL stage without displacing the
>> HOT working set.  I tested this adversarially -- an OLTP hot set that
>> fits in shared_buffers, hit by point lookups, while a concurrent seqscan
>> streams a table 4x shared_buffers, on local NVMe (no EBS/IOPS cap
>> masking the eviction rate):
>>
>>     OLTP hit ratio under the scan:   stock 99.999%   bcs 99.998%
>>     OLTP resident set:               flat in both
>
> I don't think it makes much sense to measure this when you can achieve close
> to 100 hit rate, since that's so easy to achieve. There's dozens of ways to
> achieve scan resistance that work well in that scenario but that fall entirely
> completely flat when you have actual cache pressure.

You're right that 99.99% hit rate is the easy regime and not the interesting one.  The interesting question is testing your prediction, which I'll do.  I'll see which page either algorithm protects/preserves in cache.

> I think this algorithm will *trivially* fail in a lot of cases without
> strategies. Once you have sufficient cache pressure the likelihood for
> cold->hot promotion ends up being too low to happen reliably before cold pages
> are evicted. And because this doesn't have something like a ghost directory,
> it'll not even have a chance to detect and fix that the next time the page is
> read in.
>
> I don't believe this algorithm will actually work well even without concurrent
> bulk access, I suspect it'll fail to work well even with something like
> pgbench -S where the index fits comfortably in s_b, but the table data does
> not.  We already aren't good in that kind of workload, but I don't see how
> your algorithm will actually achieve *any* meaningful protection for anything
> but the inner index pages, the likelihood of repeat accesses to the same index
> page won't be high enough to keep those pages warm.
>
>
>> On the literature: you pointed at CLOCK-Pro, thanks -- the admission model
>> here (probationary COOL, promote on reuse) is the 2Q-A1 / test-period idea
>> and I'd rather adopt a better-justified promotion rule from that line than
>> defend "second touch" if the list prefers one.  I tried a stricter "promote
>> only on a repeat reference" variant; it was neutral on OLTP and didn't
>> improve scan resistance, so I reverted it to keep 0002 minimal, but it's an
>> easy knob.
>
> FWIW, I really doubt that any of the scan resistant algorithms actually will
> allow us to get the read side strategies entirely, even with a ghost
> directory. All of the scan resistant algorithms only are scan resistant if
> repeated accesses (that should be cached) that occur during a concurrent bulk
> scan are actually within something like the size of the buffer pool, but that
> doesnt' really seem good enough.

Only one way to find out, I'll test this.

> That said, the current logic of strategies, where we actually evict the pages
> with a ringbuffer, seems rather wrong, because it takes approximately forever
> to fill the buffer pool even if there *are* repeat accesses.

Agreed.

> I suspect we should go for something like an extended version of
> S3-FIFO. S3-FIFO, has a FIFO for cold pages, taking up a certain percentage of
> the buffer pool (10% IIRC) and a ghost directory to detect repeat accesses
> that don't happen while the page is still in the cold fifo queue. That's scan
> resistant as long as repeat accesses are frequent enough (1.10x the buffer
> pool, I think).

That should be easy enough to test.

> To avoid bulk reads from pushing repeat accesses beyond the window in which
> repeat accesses are recognized, I think we could "just" make sure the ghost
> directory has eviction logic that will not allow strategy reads to take up
> more than a certain percentage of the ghost directory.

First, I appreciate your time reviewing this.  I think it's time well spent and I'll try to make sure to voice my replies myself, warts and all.

Second, I'll review and add voice to Tomas's thread.  This is a valid algorithm and I think they compose.  I'll consider efficient ways to manage a ghost directory.

> Greetings,
>
> Andres Freund

best.

-greg






^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention
  2026-04-25 20:08 [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-04-27 14:15 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-04-28 14:30   ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 14:00     ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 18:07       ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-11 15:54         ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-14 17:53           ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-22 15:10             ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-07-25 10:45               ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-29 17:56                 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-29 20:10                   ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-07-30 16:45                     ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
@ 2026-07-30 21:17                       ` Greg Burd <greg@burd.me>
  2026-09-11 14:47                         ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  0 siblings, 1 reply; 21+ messages in thread

From: Greg Burd @ 2026-07-30 21:17 UTC (permalink / raw)
  To: Andres Freund <andres@anarazel.de>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Tomas Vondra <tomas@vondra.me>; Ants Aasma <ants.aasma@cybertec.at>; Nathan Bossart <nathandbossart@gmail.com>


On 2026-07-29, Andres Freund wrote:
> I don't believe this algorithm will actually work well even without
> concurrent bulk access [...] the likelihood of repeat accesses to the
> same index page won't be high enough to keep those pages warm.

Andres,

Me again.  I like testable predictions, so I tested it. :)

I'll keep this to just this one question: under real cache pressure, with the index resident and the heap much larger than shared_buffers, does the HOT/COOL+batching fail to protect index pages relative to today's 0..5 clock?

I hope I got that objection correctly framed, otherwise my tests might be invalid.  I'm sure you'll (bluntly) let me know if I have.  ;-)


Setup:
  - i4i.metal (128 vCPU, 2 NUMA nodes), data on local NVMe RAID0.
  - shared_buffers = 4GB, huge_pages on.
  - pgbench scale 2000: pgbench_accounts heap = 25GB (6.25x s_b),
    primary key index = 4.3GB (~1.1x s_b). So the index does not fit
    with room to spare; its leaf pages genuinely compete for the pool
    and do get evicted.
    As you pointed out, a tiny always-resident index would prove
    nothing.
  - 96 clients, 10-minute measured runs, autovacuum off.
  - Two access distributions:
     a) uniform pgbench -S (the case you named)
     b) a zipfian point lookup (s=1.2) so the hot subset of rows is
        re-read often.
  - I compare stock master against the two-patch series, both built
    identically, with a small throwaway instrument patch that counts
    evictions per relfilenode.
  - Using that I classified each evicted relation as heap or index
    via pg_class. The metric is the share of evictions that fall on
    index pages. If the HOT/COOL+batch protects index pages worse
    than stock, it should evict them at a higher share (right?).


Results (I had an LLM format this table):
                              index      heap-evict     index-evict
  workload            build  evict %    (buffers)      (buffers)
  ------------------  -----  -------    -----------    -----------
  uniform -S          stock   38.41%    616,467,240    384,451,330
  uniform -S          bcs     35.06%    639,240,582    345,178,854
  zipfian s=1.2       stock   30.17%     12,266,976      5,299,490
  zipfian s=1.2       bcs     29.28%     11,665,582      4,828,986

  Change in index-eviction share (bcs - stock):
    uniform  -3.35 pt
    zipfian  -0.89 pt

  Throughput, same runs:
    uniform  stock 1.126M tps -> bcs 1.309M tps  (+16%)
    zipfian  stock 1.153M tps -> bcs 1.305M tps  (+13%)

So the proposed new algorithm (HOT/COOL+batch) evicts index pages at a slightly lower rate than the existing one (0..5 clock) not a higher one which refutes your claim in this particular benchmark.  That results in higher throughput with these two patches for that workload, I'll take that win.

I'm guessing that a primary-key leaf page backs on the order of a few hundred heap rows, so any given leaf is re-touched far more often per-unit time than any single heap page. Under COOL admission that reuse arrives well before the leaf drains out of the COOL stage, so the leaf promotes to HOT and survives.

I don't disagree with the promotion race you described, I think it is real.  However, in this workload the index-leaf reuse rate simply clears the bar.

I've not added any form of "ghost directory" which could potentially improve this even more, to your point.  A page whose second access lands after it has already been evicted from COOL is gone with no way to learn/remember that.  Or, it may turn out that the maintenance of a ghost directory could outweigh the benefits I can't say from this one benchmark.

I don't know if S3-FIFO is a better approach than the one I propose, no data yet.  To say it is "trivially going to fail" is a statement worth testing and my next target for the next email on this thread.  My bet is that it won't, but the only way to find out (and convince you and others that this approach is worth consideration) is to try it out and post numbers.

If I missed something or you'd like the test adjusted let me know and I'll give it a whirl.

best.

-greg






^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention
  2026-04-25 20:08 [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-04-27 14:15 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-04-28 14:30   ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 14:00     ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-10 18:07       ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-11 15:54         ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-14 17:53           ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-22 15:10             ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-07-25 10:45               ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-29 17:56                 ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-29 20:10                   ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Andres Freund <andres@anarazel.de>
  2026-07-30 16:45                     ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
  2026-07-30 21:17                       ` Re: [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
@ 2026-09-11 14:47                         ` Greg Burd <greg@burd.me>
  0 siblings, 0 replies; 21+ messages in thread

From: Greg Burd @ 2026-09-11 14:47 UTC (permalink / raw)
  To: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; +Cc: Jim Mlodgenski <mlodj@amazon.com>

Hello,

I'm withdrawing this series, and I want to leave the reasons on the
record rather than just let the thread go quiet.

Over the last week I built Vondra's NUMA partitioning patchset
(v20260605 0001-0006 plus Wartak's 0008) and benchmarked it against both
of my patches, using Jakub Wartak's harness from the "Adding basic NUMA
awareness" thread -- his seqconcurrscans.sql and pgbenchS.sql unchanged,
scale 1000 against shared_buffers=8GB, huge_pages on, 20s runs, 5 runs
per point with the arm order rotated, datadir on a real filesystem. Two
boxes: i4i.metal (2 NUMA nodes, 128 threads) and r8i.metal-96xl (6
nodes, 384 threads). Four arms, with each arm's feature combination
verified at runtime before measuring -- partition count from
pg_buffercache_partitions, and usagecount values to confirm which
evictor was live ({1,2,3,4,5} for stock, {0,1} for HOT/COOL.)

The partitioning works, better than expected and better than my batched
approach.

Partitioning alone, versus master, on seqconcurrscans:
  2 nodes: -12% at c=1, then +12/+6/+8% at c=8/32/64
  6 nodes: -49% at c=1, then +23/+46/+47% at c=8/32/64

Then, with partitioning on in both arms, changing only the evictor from
the 0..5 clocksweep to 1-bit HOT/COOL. Ratios are per-run and paired,
which matters because the run-to-run spread here is 8-23%:

  2 nodes, seqconcurrscans   median ratio   HOT/COOL faster in
    c=1                          1.058          3/5 runs
    c=8                          1.012          4/5 runs
    c=32                         1.008          3/5 runs
    c=64                         1.030          5/5 runs
  6 nodes, seqconcurrscans
    c=1                          0.987          2/5 runs
    c=8                          1.140          4/5 runs
    c=32                         0.976          1/5 runs
    c=64                         1.036          3/5 runs

pgbenchS is flat for every arm on both boxes.

Read honestly, that is a wash. The only point I'd call real is 2 nodes
at c=64, where HOT/COOL is faster in 5 of 5 runs for about +3%;
everything else straddles 1.0 and flips sign between adjacent client
counts on the same box, which is what noise looks like. The apparent
+14% at 6 nodes / c=8 has individual ratios from 0.94 to 1.15, so I
don't believe it.

I'm dropping the batched clock sweep entirely. It solves a problem that
partitioning solves better and more generally, and I see no argument for
carrying a second mechanism for the same cache line.

Continuing to advocate for HOT/COOL, but on the NUMA thread rather than
here, and as a simplification rather than a performance patch. The case
isn't throughput, the numbers above show there isn't any once
partitioned. The case is that it replaces a 0..5 counter and its magic
constant with a single bit, it makes scan resistance a property of the
replacement algorithm instead of something the ring buffers have to bolt
on, and it removes a failure mode I measured earlier in this thread
where the 0..5 counter can spend up to five dTLB-missing visits per
victim on a large pool with huge pages off. Andres is right that I have
not shown a real-world cost for that cliff, and that remains open. But
if the buffer manager is being restructured for NUMA anyway, doing it
with one bit of replacement state rather than three seems worth arguing
for while the code is already being touched.

Thanks to Andres and Ants for the reviews on this thread. They were
blunt and they were correct, and between them they killed two of my
three ideas (the strategy removal and the ghost directory) before they
cost anyone else any time. That's the thread working.  They also pushed
for more collaboration on the Vondra thread, so here I come. :)

Marking the commitfest entry withdrawn too.

best.

-greg






^ permalink  raw  reply  [nested|flat] 21+ messages in thread


end of thread, other threads:[~2026-09-11 14:47 UTC | newest]

Thread overview: 21+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-04-25 20:08 [PATCH] Batched clock sweep to reduce cross-socket atomic contention Greg Burd <greg@burd.me>
2026-04-27 13:26 ` Greg Burd <greg@burd.me>
2026-04-27 14:15 ` Andres Freund <andres@anarazel.de>
2026-04-28 14:30   ` Greg Burd <greg@burd.me>
2026-07-10 14:00     ` Greg Burd <greg@burd.me>
2026-07-10 18:07       ` Greg Burd <greg@burd.me>
2026-07-11 15:54         ` Greg Burd <greg@burd.me>
2026-07-14 17:53           ` Greg Burd <greg@burd.me>
2026-07-20 15:36             ` Greg Burd <greg@burd.me>
2026-07-22 14:52             ` Ants Aasma <ants.aasma@cybertec.at>
2026-07-22 15:29               ` Andres Freund <andres@anarazel.de>
2026-07-22 16:09                 ` Ants Aasma <ants.aasma@cybertec.at>
2026-07-22 16:24                   ` Andres Freund <andres@anarazel.de>
2026-07-25 10:35               ` Greg Burd <greg@burd.me>
2026-07-22 15:10             ` Andres Freund <andres@anarazel.de>
2026-07-25 10:45               ` Greg Burd <greg@burd.me>
2026-07-29 17:56                 ` Greg Burd <greg@burd.me>
2026-07-29 20:10                   ` Andres Freund <andres@anarazel.de>
2026-07-30 16:45                     ` Greg Burd <greg@burd.me>
2026-07-30 21:17                       ` Greg Burd <greg@burd.me>
2026-09-11 14:47                         ` Greg Burd <greg@burd.me>

This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox