Re: index prefetching
Peter Geoghegan <pg@bowt.ie>
On Thu, Mar 26, 2026 at 5:47 PM Andres Freund <andres@anarazel.de> wrote:> > I must admit I'm unsure how to evaluate the maximum number of batches. > > It can make sense to pursue diminishing returns. But up to what point, > > and according to what principle? > > I think the theoretical amount of required IO concurrency can be calculated > based on the storage latency and IOPS. IIRC it is > > iops_qd1 = (1000 / latency_ms) > queue_depth = IOPS / iops_qd1 > queue_depth = IOPS / (1000 / latency_ms) > So, to be able to fully utilize current hardware with one query, we need to be > able to reach queue depth in the low hundreds, in the case of striped cheap > cloud SSDs. That's when a backend *just* does IO, nothing else. That sounds like a useful starting point. But empirically, as far as I can tell, the relationship between query latency and how close you are to fully saturating I/O is not linear, or anything like it. Maybe it's an S-curve? It seems that a fully I/O-bound query isn't remotely close to twice as fast as the same query when it is restricted to using only half the number of batches (half the number required to reach that saturation point). OTOH, using more batches than strictly necessary usually isn't much of a problem. So I don't think that we can rely on a precise formula, even if we're willing to make fixed assumptions about data layout (which we're not). If there were a good enough reason for index prefetching to use an unbounded number of batches, we could surely figure out a way to support that requirement. It'd be messy, and relatively hard to test. And I'd worry a bit about there being zero backstop for index-only scans. But it can be done. If we did things that way (which doesn't seem like a good idea right now), we wouldn't have to model I/O saturation at all. Which tbh makes me wonder if that kind of modelling has much practical use either way. > Something like an index scan, will have its own limit to how much it can > process in a second. If we can only do 100k IOPS while searching the index, > fetching the heap tuples and processing them, we don't need to support the > queue depths to support doing 1M IOPS within one backend. > > That's something that can presumably be quite easily experimentally > ballparked: > > A fully cached, completely uncorrelated, index scan seems to be able to fetch > about 1.5M page fetches on my ~6 YO server CPU with turbo boost disabled, when > never looking at the results (i.e. using OFFSET) or immediately filtering away > the row. > So I'd guess the limit on newer CPUs in SKUs optimized for clock > speed and boost enabled, is north of 2.5M pages/sec, higher than I'd have > thought! That's without doing any IO though. We've done good work on nbtree's ability to avoid provably unnecessary work in recent years; see _bt_set_startikey. What that means is that the majority of the index scans used to test the patch probably have _bt_readpage calls that spend most of their time simply collecting all of the TIDs from the leaf page, without any scan key overhead (barring an initial precheck within _bt_set_startikey once per _bt_readpage, to prove that the optimization is safe). With large posting list tuples, we'll do even less work, since they're just an array of ItemPointerData. > With correlated scans the limit is much lower, maybe 150k, just because > there's so many more tuples per page (and processing them trivially becomes > the bottleneck). > > > So, to support actually utilizing the full IO IO capability, we need to allow > for enough batches to keep a few hundred IOs in flight at the very extreme > end. I'd assume you have a much better idea to how many batches that > translates to? I can give you a range. The problem is that it's a range starting from "absurdly optimistic" through to "absurdly pessimistic". Neither extreme is very unlikely (there's a wide natural variation in workloads), and it's hard to argue usefully about what will be true in most cases. In short, I can tell you plenty, but nothing that seems particularly useful for determining how many batches we should cap the ring buffer at. I don't think there's anything fundamentally objectionable about our deriving the current maximum of 64 through trial and error. I assume that INDEX_SCAN_MAX_BATCHES must be constrained to a low-ish power of two so that the ring buffer maintenance routines avoid DIV instructions (from the use of a modulo operator that the compiler cannot optimize into a bitwise AND). There just aren't that many integers that even qualify as candidates! I'm pretty sure that 32 is likely too low (though it's hard to tell with buffered I/O on a fast local SSD). 128 might still be too low in extreme corner cases involving high latency and few matches per batch (though I doubt it). 256 seems too implausibly high to ever make sense (but I've been wrong before). -- Peter Geoghegan
Commits
-
read stream: Split decision about look ahead for AIO and combining
- 8ca147d582a5 19 (unreleased) landed
-
read_stream: Only increase read-ahead distance when waiting for IO
- f63ca3379025 19 (unreleased) landed
-
aio: io_uring: Trigger async processing for large IOs
- a9ee66881744 19 (unreleased) landed
-
heapam: Keep buffer pins across index scan resets.
- 2d3490dd99f0 19 (unreleased) landed
-
heapam: Track heap block in IndexFetchHeapData.
- c7d09595e46f 19 (unreleased) landed
-
Move heapam_handler.c index scan code to new file.
- a29fdd6c8d81 19 (unreleased) landed
-
Rename heapam_index_fetch_tuple argument for clarity.
- 1adff1a0c558 19 (unreleased) landed
-
Optimize fast-path FK checks with batched index probes
- b7b27eb41a5c 19 (unreleased) cited
-
read_stream: Prevent distance from decaying too quickly
- 6e36930f9aaf 19 (unreleased) landed
-
read_stream: Issue IO synchronously while in fast path
- cceb1bf45e3a 19 (unreleased) landed
-
bufmgr: Return whether WaitReadBuffers() needed to wait
- 513374a47a71 19 (unreleased) landed
-
aio: io_uring: Allow IO methods to check if IO completed in the background
- 6e648e353fa0 19 (unreleased) landed
-
bufmgr: Make UnlockReleaseBuffer() more efficient
- f39cb8c01106 19 (unreleased) cited
-
Add fake LSN support to hash index AM.
- e5836f7b7d9a 19 (unreleased) landed
-
Make IndexScanInstrumentation a pointer in executor scan nodes.
- f026fbf059f2 19 (unreleased) landed
-
Use fake LSNs to improve nbtree dropPin behavior.
- 8a879119a1d1 19 (unreleased) landed
-
Move fake LSN infrastructure out of GiST.
- d774072f0040 19 (unreleased) landed
-
Use simplehash for backend-private buffer pin refcounts.
- a367c433ad01 19 (unreleased) landed
-
nbtree: Avoid allocating _bt_search stack.
- d071e1cfec23 19 (unreleased) landed
-
bufmgr: Fix use of wrong variable in GetPrivateRefCountEntrySlow()
- 6322a028fa43 19 (unreleased) landed
-
Conditional locking in pgaio_worker_submit_internal
- 29a0fb215779 19 (unreleased) landed
-
Reduce ExecSeqScan* code size using pg_assume()
- b227b0bb4e03 19 (unreleased) cited
-
Fix rare bug in read_stream.c's split IO handling.
- b421223172a2 19 (unreleased) cited
-
Remove HeapBitmapScan's skip_fetch optimization
- 459e7bf8e2f8 18.0 cited
-
Optimize nbtree backwards scans.
- 1bd4bc85cac2 18.0 cited
-
Fix multiranges to behave more like dependent types.
- 3e8235ba4f9c 17.0 cited
-
Add EXPLAIN (MEMORY) to report planner memory consumption
- 5de890e3610d 17.0 cited
-
Optimize nbtree backward scan boundary cases.
- c9c0589fda0e 17.0 cited
-
Increment xactCompletionCount during subtransaction abort.
- 90c885cdab8b 14.0 cited
-
Add nbtree Valgrind buffer lock checks.
- 4a70f829d86c 14.0 cited
-
Add nbtree high key "continuescan" optimization.
- 29b64d1de7c7 12.0 cited
-
Reduce pinning and buffer content locking for btree scans.
- 2ed5b87f96d4 9.5.0 cited
-
Teach btree to handle ScalarArrayOpExpr quals natively.
- 9e8da0f75731 9.2.0 cited