Re: index prefetching
Andres Freund <andres@anarazel.de>
Hi, On 2026-03-24 21:34:51 -0400, Peter Geoghegan wrote: > On Tue, Mar 24, 2026 at 1:27 PM Andres Freund <andres@anarazel.de> wrote: > > > But that means that it won't be triggered when we don't enter the "if > > > (hscan->xs_blk != ItemPointerGetBlockNumber(tid))" block that contains > > > all this code. Besides, it just doesn't seem possible that > > > heap_page_prune_opt would release its caller's pin. > > > > I was more concerned about read_stream_next_buffer() returning the wrong > > block, due to prefetching somehow "desynchronizing" with the scan position and > > catching that when it's clear that we just read a new block, rather than in a > > place where it could be either the continuation of a scan on the same page or > > a new page. > > Then I don't follow. The existing assertions will catch that (I should > know, they've failed enough times during development). > > Basically, I don't get the concern about heap_page_prune_opt releasing > its caller's pin. Even if that happened, the existing assertions would > still catch it. My point wasn't that heap_page_prune_opt() would release the pin or such, but that an assertion failure in the "new block" case will tell you that it was definitely prefetching that resulted in you looking at the wrong block, rather than some state confusion leading to continuing on the last page when it wouldn't be right. An assertion that's after the if (changed block) doesn't tell you which of those two went wrong. But it really doesn't matter enough to continue discussing :) > > After replacing the pause with an error I found that it's surprisingly easy to > > hit on slow storage (or on fast storage if you set needed_wait=true in > > read_stream_next_buffer()). I've not done any performance validation on > > whether that means the limit is too low. > > It's been a while since I last validated performance to justify the > current maximum number of batches. I used buffered I/O for that. I'm > sure that a higher maximum with very slow storage and a very high > effective_io_concurrency will provide some benefit. But perfectly > handling that isn't essential for the first committed version of index > prefetching. Agreed. > 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) Of course that's overly simplistic, as typically the latency increases the more IO you issue, increasing the required QD to actually fully utilize the available IOs. But it's a good approximation. IIRC the upper end of "SSD like" cloud storage latency is around 3.5ms and tends to top out around 20k IOPS. That's unfortunately volume limits, on larger instance you can stripe multiple disks to get higher IOPS. Faster tiers are ~0.25ms and capped at ~250k-400k IOPS. I think those are typically instance limits, but I'm not sure about that. So on the upper end of the cheaper tiers you need a queue depth of about 70, and on the upper end of the (very expensive) tiers you need about 100. That's to utilize all IOPS of course, so if you have multiple processes doing IO... For local NVMe SSDs latencies are around 8-30us and IOPS tops out at about 1.5M (although the latter is extremely hard to sustain, it's definitely only possible with DIO etc). That leaves you with QDs < 50 or so. 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. 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. 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? Just testing a read stream of random 4kB IO I can fully saturate all the SSDs I have, up to ~700k IOPS of random IO. IIRC I tried this with a few striped SSDs in the past and got a bit higher than that. Greetings, Andres Freund
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