Thread
Commits
GET /api/v1/messages/:b64id/commits
the thread's linked commits as JSON, with link sources.
API reference →
-
bufmgr: Return whether WaitReadBuffers() needed to wait
- 513374a47a71 19 (unreleased) landed
-
EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-03-15T19:49:17Z
Hi, Here's a patch adding prefetch/read-ahead info about IO to EXPLAIN. This was initially developed as part of the index prefetch patches, but it became clear it can be useful for other nodes using ReadStream. That includes seqscan and bitmap heap scans, and (hopefully) will include index scan in PG19. The 0001 patch is "prerequisite" adjusting WaitReadBuffers to indicate if the buffer read had to wait for the I/O. 0002 is the part this thread is really about. In EXPLAIN, this new info is enabled by a new option "IO", which adds two new lines to the output: -------------------- EXPLAIN (ANALYZE, COSTS off, TIMING off, IO) SELECT * FROM t WHERE a < 100000; QUERY PLAN ------------------------------------------------- Seq Scan on t (actual rows=999996.00 loops=1) Filter: (a < 100000) Rows Removed by Filter: 4 Prefetch: avg=262.629 max=271 capacity=272 I/O: stalls=653 size=15.983 inprogress=15.934 Buffers: shared read=55556 Planning: Buffers: shared hit=50 read=23 Planning Time: 7.358 ms Execution Time: 358.214 ms (10 rows) -------------------- The first line "Prefetch" tracks the look-ahead distance, i.e. how many blocks ahead the ReadStream is requesting. The second line "I/O" is about the I/O requests actually issued - how many times we had to wait for the block (when we get to process it), average size of a request (in BLCKSZ blocks), and average number of in-progress requests. The above example shows we've been reading ~262 blocks ahead average, with theoretical maximum of 272 (and at some point we read 271 blocks). The average is pretty close to the maximum, so we've been reading ~2MB ahead, which seems pretty good. The I/O stats says we've been reading in 16-block requests, so 128kB chunks (which aligns with io_combine_limit=16). And when issuing a new I/O, there were ~16 requests in-progress already (which aligns with the maximum distance, because 16*17=272). We could track much more information. The WIP patches tracked histograms of various metrics (distances, sizes, ...), and it was quite handy during development. For regular EXPLAIN we chose to select only the most useful subset, to make it meaningful and also cheap to collect. And also generic enough to work for other table AM implementations. Regarding the basic design, and a couple topics for review questions: 1) The information is collected by ReadStream, unconditionally. I experimented with having a flag to "request" collecting these stats (so that it can be done only for EXPLAIN ANALYZE), but that turned out pretty useless. The stats are super cheap, so the extra flag did not make a meaningful difference. And passing the flag to the scan/stream was way too invasive. So all ReadStreams track it. There's a ReadStreamInstrumentation defined in instrumet_node.h, but I'm not convinced that's the right place. Maybe it should be defined in read_stream.h instead. 2) This is inherently a TAM implementation detail. The ReadStream is "inside" the TAM, and some TAMs may not even use a ReadStream to do I/O. So EXPLAIN can't just inspect the stream directly, that'd violate the layering. It needs to do it through the TAM API. So this introduces a new TAM callback "scan_stats" which gets a scan descriptor, and extracts stats from the stream (if any). The index prefetching will need a second callback for IndexScanDesc. It also introduces a "generic" TableScanStatsData struct, so that it does not work with ReadStreamInstrumentation (because what if the TAM does not use a ReadStream). Of course, for heap AM it's 1:1. For heap AM the callback is pretty simple, it just collects the read stream stats and "translates" it to the TableScanStats instance. 3) Adds shared instrumentation to SeqScan workers. The patch also had to improve a parallel SeqScan to collect per-worker instrumentation, similarly to a BitmapHeapScan. Until now this was not needed, but it's needed for tracking per-worker prefetch stats. Here's a couple questions I'm asking myself about this patch: - Is the selected information really the minimal meaningful set of stats we could track? Is it sufficiently clear what we're tracking, and/or should we track something else? - Is the TableScanStatsData generic enough? Can other TAMs (not using ReadStream) use this to show meaningful stats? - If the separation between TAM and the low-level instrumentation clear enough? Or is the ReadStreamInstrumentation "leaking" somewhere? For example, is it OK it's in SeqScanInstrumentation? But I'm sure there are other questions I haven't thought of. regards -- Tomas Vondra -
Re: EXPLAIN: showing ReadStream / prefetch stats
Lukas Fittl <lukas@fittl.com> — 2026-03-16T07:07:54Z
Hi Tomas, On Sun, Mar 15, 2026 at 12:49 PM Tomas Vondra <tomas@vondra.me> wrote: > Here's a patch adding prefetch/read-ahead info about IO to EXPLAIN. This > was initially developed as part of the index prefetch patches, but it > became clear it can be useful for other nodes using ReadStream. That > includes seqscan and bitmap heap scans, and (hopefully) will include > index scan in PG19. I think surfacing this to users makes a lot of sense - read streams are hard to understand in detail, and this will make it easier to tie back the I/O performance of a query to what happened in terms of pre-fetching and stalls. Big +1 on the concept of making this visible. > In EXPLAIN, this new info is enabled by a new option "IO", which adds > two new lines to the output: I'm 50/50 if hiding this behind a new option really makes sense - if its cheap enough to always capture, why not always show it? e.g. we could consider doing with this what we did with BUFFERS recently, which is to enable it by default. If someone finds that too visually busy, they could still do IO OFF. This also does make me wonder a bit what we should do about I/O timings. Conceptually they'd belong closer to IO now than BUFFERS.. > > -------------------- > EXPLAIN (ANALYZE, COSTS off, TIMING off, IO) > SELECT * FROM t WHERE a < 100000; > > QUERY PLAN > ------------------------------------------------- > Seq Scan on t (actual rows=999996.00 loops=1) > Filter: (a < 100000) > Rows Removed by Filter: 4 > Prefetch: avg=262.629 max=271 capacity=272 > I/O: stalls=653 size=15.983 inprogress=15.934 > Buffers: shared read=55556 > Planning: > Buffers: shared hit=50 read=23 > Planning Time: 7.358 ms > Execution Time: 358.214 ms > (10 rows) > -------------------- > > The first line "Prefetch" tracks the look-ahead distance, i.e. how many > blocks ahead the ReadStream is requesting. > > The second line "I/O" is about the I/O requests actually issued - how > many times we had to wait for the block (when we get to process it), > average size of a request (in BLCKSZ blocks), and average number of > in-progress requests. I wonder if we could somehow consolidate this into one line for the text format? (specifically, moving prefetch into "I/O" at the end?) I'm also not sure if "max" is really that useful, vs capacity? > 1) The information is collected by ReadStream, unconditionally. > > I experimented with having a flag to "request" collecting these stats > (so that it can be done only for EXPLAIN ANALYZE), but that turned out > pretty useless. The stats are super cheap, so the extra flag did not > make a meaningful difference. And passing the flag to the scan/stream > was way too invasive. So all ReadStreams track it. Makes sense. I did some testing with your patch against master, and couldn't find any meaningful difference - its logical that this would be cheap enough to always do. > 2) This is inherently a TAM implementation detail. > > ... > > 3) Adds shared instrumentation to SeqScan workers. > > The patch also had to improve a parallel SeqScan to collect per-worker > instrumentation, similarly to a BitmapHeapScan. Until now this was not > needed, but it's needed for tracking per-worker prefetch stats. I feel like something is off about the complexity of having each node type ferry back the information. e.g. when you're implementing the support for index prefetching, it'll require a bunch more changes. In my mind, there is a reason we have a related problem that we solved with the current pgBufferUsage, instead of dealing with that on a per-node basis. I really feel we should have a more generic way of dealing with this. I'm saying that not being completely unbiased, because I think this would be a great fit for the stack-based instrumentation I've been discussing with Andres and Zsolt over at [0]. Andres at least expressed some potential interest in getting that into 19, though its definitely not a trivial patch set. If we were to get stack-based instrumentation in, we could easily add a new "IOUsage" to the Instrumentation struct, avoid any modification to the TAM interface, and align it with how we treat BufferUsage and WALUsage. I've attached a prototype of how that could look like (apply the other patch set first, v8, see commit fest entry [1] - also attached a preparatory refactoring of using "Instrumentation" for parallel query reporting, which avoids having individual structs there). Performance is actually better for "EXPLAIN (ANALYZE, IO, BUFFERS OFF, TIMING OFF)" with that patch set on a quick COUNT(*) test, but that's mostly due to the overhead of ExecProcNode dispatching that I fixed in the other patch set (v8/0006). I think otherwise this would be similar in performance thanks to the stack avoiding any "accum diff" logic. Its also worth noting that this would make it trivial to output this information for utility commands that have read stream support, or show aggregate statistics in pg_stat_statements/etc. Let me know if that's at all of interest and happy to pair up on this to make it work. Thanks, Lukas [0]: https://www.postgresql.org/message-id/flat/CAP53PkzdBK8VJ1fS4AZ481LgMN8f9mJiC39ZRHqkFUSYq6KWmg%40mail.gmail.com [1]: https://commitfest.postgresql.org/patch/6023/ -- Lukas Fittl
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Melanie Plageman <melanieplageman@gmail.com> — 2026-03-16T17:14:15Z
On Sun, Mar 15, 2026 at 3:49 PM Tomas Vondra <tomas@vondra.me> wrote: > > - If the separation between TAM and the low-level instrumentation clear > enough? Or is the ReadStreamInstrumentation "leaking" somewhere? For > example, is it OK it's in SeqScanInstrumentation? Personally, I don't like having both structs (ReadStreamInstrumentation and TableScanStatsData). The executor nodes (SeqScanState, BitmapHeapScanState) already embed ReadStreamInstrumentation directly in their instrumentation structs, so we already have a reference to the read stream in table AM-agnostic code. Having a second identical struct means maintaining two definitions without any actual benefit. > But I'm sure there are other questions I haven't thought of. I see a couple more issues with the counting in read_stream.c. You are double-counting stalls for synchronous IO. You increment stalls in read_stream_next_buffer() but we actually execute synchronous IO in WaitReadBuffers and return needed_wait as true, which will count a stall again. You are not counting fast path IOs because those don't go through read_stream_start_pending_read() and instead are started directly by StartReadBuffer() in read_stream_next_buffer(). Simple diff attached should fix this. Also, per worker stats are not displayed when BUFFERS false is passed even with IO true because of a small oversight. I fixed it in the attached diff. - Melanie
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Melanie Plageman <melanieplageman@gmail.com> — 2026-03-16T17:29:21Z
On Mon, Mar 16, 2026 at 3:08 AM Lukas Fittl <lukas@fittl.com> wrote: > > I'm 50/50 if hiding this behind a new option really makes sense - if > its cheap enough to always capture, why not always show it? > > e.g. we could consider doing with this what we did with BUFFERS > recently, which is to enable it by default. If someone finds that too > visually busy, they could still do IO OFF. I like this idea. > This also does make me wonder a bit what we should do about I/O > timings. Conceptually they'd belong closer to IO now than BUFFERS.. I could see moving the timings to the I/O line. > > The second line "I/O" is about the I/O requests actually issued - how > > many times we had to wait for the block (when we get to process it), > > average size of a request (in BLCKSZ blocks), and average number of > > in-progress requests. > > I wonder if we could somehow consolidate this into one line for the > text format? (specifically, moving prefetch into "I/O" at the end?) Next release, I'm hopeful we'll get write combining in and then want to include write IO details here. Not a reason to avoid making it one line now, though. However, I think it will be a very long line... > I'm also not sure if "max" is really that useful, vs capacity? I find it very helpful. If you keep increasing effective_io_concurrency and io_combine_limit and don't see the max increasing, I think it is helpful to know what the configured possible limit would be vs what the read stream actually got you up to. And because you can set effective_io_concurrency and io_combine_limit per query, it is nice to know what this value was at the time the query was run. > I feel like something is off about the complexity of having each node > type ferry back the information. e.g. when you're implementing the > support for index prefetching, it'll require a bunch more changes. In > my mind, there is a reason we have a related problem that we solved > with the current pgBufferUsage, instead of dealing with that on a > per-node basis. I really feel we should have a more generic way of > dealing with this. <--snip--> > I've attached a prototype of how that could look like (apply the other > patch set first, v8, see commit fest entry [1] - also attached a > preparatory refactoring of using "Instrumentation" for parallel query > reporting, which avoids having individual structs there). The patch footprint is _much_ nicer with your stack-based instrumentation. Very cool. I'll leave it to Tomas whether he wants to create a dependency on your big project a few weeks before feature freeze, though. > Its also worth noting that this would make it trivial to output this > information for utility commands that have read stream support, or > show aggregate statistics in pg_stat_statements/etc. Yes this would be a major bonus. Even for currently possible users, this hasn't been extended to TID Range Scan -- which involves more LOC and boilerplate. - Melanie
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-03-16T17:49:06Z
On 3/16/26 18:14, Melanie Plageman wrote: > On Sun, Mar 15, 2026 at 3:49 PM Tomas Vondra <tomas@vondra.me> wrote: >> >> - If the separation between TAM and the low-level instrumentation clear >> enough? Or is the ReadStreamInstrumentation "leaking" somewhere? For >> example, is it OK it's in SeqScanInstrumentation? > > Personally, I don't like having both structs > (ReadStreamInstrumentation and TableScanStatsData). > > The executor nodes (SeqScanState, BitmapHeapScanState) already embed > ReadStreamInstrumentation directly in their instrumentation structs, > so we already have a reference to the read stream in table AM-agnostic > code. Having a second identical struct means maintaining two > definitions without any actual benefit. > That's a good point. The executor nodes should not embed anything readstream-specific, not even in the instrumentation part. AFAICS the cleanest way would be to replace this with the TableScanStatsData. However, ReadStreamInstrumentation is not really ReadStream specific, except for the name. I'm actually wondering if we should have just one struct, that'd be enough. >> But I'm sure there are other questions I haven't thought of. > > I see a couple more issues with the counting in read_stream.c. > > You are double-counting stalls for synchronous IO. You increment > stalls in read_stream_next_buffer() but we actually execute > synchronous IO in WaitReadBuffers and return needed_wait as true, > which will count a stall again. > > You are not counting fast path IOs because those don't go through > read_stream_start_pending_read() and instead are started directly by > StartReadBuffer() in read_stream_next_buffer(). Simple diff attached > should fix this. > > Also, per worker stats are not displayed when BUFFERS false is passed > even with IO true because of a small oversight. I fixed it in the > attached diff. > Thanks! -- Tomas Vondra
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-03-16T18:26:09Z
On 3/16/26 18:29, Melanie Plageman wrote: > On Mon, Mar 16, 2026 at 3:08 AM Lukas Fittl <lukas@fittl.com> wrote: >> >> I'm 50/50 if hiding this behind a new option really makes sense - if >> its cheap enough to always capture, why not always show it? >> >> e.g. we could consider doing with this what we did with BUFFERS >> recently, which is to enable it by default. If someone finds that too >> visually busy, they could still do IO OFF. > > I like this idea. > Maybe. I think it's important not to overwhelm the users with too much information, and so my intuition was to keep it OFF by default. But if the agreement is that it should be opt-out, I can live with that. >> This also does make me wonder a bit what we should do about I/O >> timings. Conceptually they'd belong closer to IO now than BUFFERS.. > > I could see moving the timings to the I/O line. > Moving the I/O timings to the "IO" line seems reasonable to me. >>> The second line "I/O" is about the I/O requests actually issued - how >>> many times we had to wait for the block (when we get to process it), >>> average size of a request (in BLCKSZ blocks), and average number of >>> in-progress requests. >> >> I wonder if we could somehow consolidate this into one line for the >> text format? (specifically, moving prefetch into "I/O" at the end?) > > Next release, I'm hopeful we'll get write combining in and then want > to include write IO details here. Not a reason to avoid making it one > line now, though. However, I think it will be a very long line... > Let's not make the lines too long. The distance information is not really about I/O, it's about an internal queue / heuristics. FWIW I assume we'd want to track some of the metrics (average IO size, number of IOs, ...) separate for reads and writes, so I'm not sure those will go to the same line too. >> I'm also not sure if "max" is really that useful, vs capacity? > > I find it very helpful. If you keep increasing > effective_io_concurrency and io_combine_limit and don't see the max > increasing, I think it is helpful to know what the configured possible > limit would be vs what the read stream actually got you up to. And > because you can set effective_io_concurrency and io_combine_limit per > query, it is nice to know what this value was at the time the query > was run. > I'm not sure. Shouldn't the average distance give us about the same thing? Sure, it'll never be exactly the maximum, but if the prefetch behavior is reasonably stable, it should not be too far. It'll be different if the prefetch distance varies a lot (e.g. because parts of the table have vastly different cache hit ratios). But then it's hard to interpret the "max" too, I think. So maybe just average + capacity might suffice? >> I feel like something is off about the complexity of having each node >> type ferry back the information. e.g. when you're implementing the >> support for index prefetching, it'll require a bunch more changes. In >> my mind, there is a reason we have a related problem that we solved >> with the current pgBufferUsage, instead of dealing with that on a >> per-node basis. I really feel we should have a more generic way of >> dealing with this. > <--snip--> >> I've attached a prototype of how that could look like (apply the other >> patch set first, v8, see commit fest entry [1] - also attached a >> preparatory refactoring of using "Instrumentation" for parallel query >> reporting, which avoids having individual structs there). > > The patch footprint is _much_ nicer with your stack-based > instrumentation. Very cool. I'll leave it to Tomas whether he wants to > create a dependency on your big project a few weeks before feature > freeze, though. > I don't know. I have not looked at the stack-based instrumentation yet, so I have no idea how complex or how close to committable it is. >> Its also worth noting that this would make it trivial to output this >> information for utility commands that have read stream support, or >> show aggregate statistics in pg_stat_statements/etc. > > Yes this would be a major bonus. Even for currently possible users, > this hasn't been extended to TID Range Scan -- which involves more LOC > and boilerplate. > Oh, I forgot about the TID scan ... I agree doing this in a way that does not require changes to TAM interface would be nice. regards -- Tomas Vondra
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-03-17T00:15:23Z
On 3/16/26 19:26, Tomas Vondra wrote: > On 3/16/26 18:29, Melanie Plageman wrote: >> On Mon, Mar 16, 2026 at 3:08 AM Lukas Fittl <lukas@fittl.com> wrote: > ... > >>> I feel like something is off about the complexity of having each node >>> type ferry back the information. e.g. when you're implementing the >>> support for index prefetching, it'll require a bunch more changes. In >>> my mind, there is a reason we have a related problem that we solved >>> with the current pgBufferUsage, instead of dealing with that on a >>> per-node basis. I really feel we should have a more generic way of >>> dealing with this. >> <--snip--> >>> I've attached a prototype of how that could look like (apply the other >>> patch set first, v8, see commit fest entry [1] - also attached a >>> preparatory refactoring of using "Instrumentation" for parallel query >>> reporting, which avoids having individual structs there). >> It looks reasonable, and it's nice to not have to worry about passing the data through the TAM interface etc. Having to do stuff like this: - if (instr->instr.need_bufusage || instr->instr.need_walusage) + if (instr->instr.need_bufusage || instr->instr.need_walusage || instr->instr.need_iousage) in so many places is not great. It'd really benefit from some macro that says "if any of the pieces is needed ...". >> The patch footprint is _much_ nicer with your stack-based >> instrumentation. Very cool. I'll leave it to Tomas whether he wants to >> create a dependency on your big project a few weeks before feature >> freeze, though. >> > > I don't know. I have not looked at the stack-based instrumentation yet, > so I have no idea how complex or how close to committable it is. > I've started looking at the stack tracking patch only today, and it looks in pretty good shape. I have no clear idea how close to being committed it is, so I'm hesitant to make this patch depend on it. AFAICS the stack-based tracking is meant for resources with just one instance. E.g. there's only one WAL, or one buffer pool, so when a node says "update WAL usage" or "increment buffer hit/read", there's one counter to update. But will that necessarily apply for read streams? AFAIK existing scans use only a single read stream, but that's only because none of the built-in index AMs uses a read stream. If we get index prefetching in PG19, or if any index AM (e.g. pgvector) chooses to use a read stream internally, how will that work? Will we "merge" the stats from all read streams used by the node, or what will happen? I realize this is a similar issue the 0007 part of the stack-based tracing patch deals with - tracking buffers for index/table separately. And it does that by making nodeIndexscan.c quite a bit more complex by expanding index_getnext_slot(). Doesn't seem great, but not sure. I suppose nodeIndexonlyscan.c will have to do something similar. regards -- Tomas Vondra
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-03-17T17:51:15Z
On 3/16/26 18:49, Tomas Vondra wrote: > On 3/16/26 18:14, Melanie Plageman wrote: >> ... >> >> I see a couple more issues with the counting in read_stream.c. >> >> You are double-counting stalls for synchronous IO. You increment >> stalls in read_stream_next_buffer() but we actually execute >> synchronous IO in WaitReadBuffers and return needed_wait as true, >> which will count a stall again. >> >> You are not counting fast path IOs because those don't go through >> read_stream_start_pending_read() and instead are started directly by >> StartReadBuffer() in read_stream_next_buffer(). Simple diff attached >> should fix this. >> >> Also, per worker stats are not displayed when BUFFERS false is passed >> even with IO true because of a small oversight. I fixed it in the >> attached diff. >> > > Thanks! > Here's a v2 with your fixes merged. No other changes. regards -- Tomas Vondra
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Andres Freund <andres@anarazel.de> — 2026-03-17T18:40:09Z
Hi, On 2026-03-17 18:51:15 +0100, Tomas Vondra wrote: > Subject: [PATCH v2 2/2] explain: show prefetch stats in EXPLAIN (ANALYZE, > VERBOSE) > > This adds details about AIO / prefetch for a number of executor nodes > using the ReadStream, notably: > > - SeqScan > - BitmapHeapScan > > The statistics is tracked by the ReadStream, and then propagated up > through the table AM interface. > > The ReadStream tracks the statistics unconditionally, i.e. even outside > EXPLAIN ANALYZE etc. The amount of statistics is trivial (a handful of > integer counters), it's not worth gating this by a flag. > > The TAM gets one new callback "scan_stats", to collect stats from the > scan (which fetch tuples from the TAM). There is also a new struct > TableScanStatsData/TableScanStats to separate the statistics from the > actual TAM implementation. It's not really clear to me that this need to go through the tableam interface. All the accesses already happen within AM code, it seems we could just populate fields in TableScanDesc TableScanDesc->rs_flags indicates that that is desired. That seems like it might be nicer, because a) there could be multiple read stream instances within a scan b) we will need one such callback for each different type of scan - so we could just as well do that within the scan datastructure Showing any such detail in explain will always be a bit of a piercing of abstraction layers, I'm not particularly concerned with having optionally fillable information in *ScanDesc that some AM might not want in that shape. It seems unlikely that a lot of other architectural decisions would be based on that, so we can just evolve this in the future. > + case T_SeqScan: > + { > + SharedSeqScanInstrumentation *sinstrument > + = ((SeqScanState *) planstate)->sinstrument; > + > + /* get the sum of the counters set within each and every process */ > + if (sinstrument) > + { > + for (int i = 0; i < sinstrument->num_workers; ++i) > + { > + SeqScanInstrumentation *winstrument = &sinstrument->sinstrument[i]; > + > + stats.prefetch_count += winstrument->stream.prefetch_count; > + stats.distance_sum += winstrument->stream.distance_sum; > + if (winstrument->stream.distance_max > stats.distance_max) > + stats.distance_max = winstrument->stream.distance_max; > + if (winstrument->stream.distance_capacity > stats.distance_capacity) > + stats.distance_capacity = winstrument->stream.distance_capacity; > + stats.stall_count += winstrument->stream.stall_count; > + stats.io_count += winstrument->stream.io_count; > + stats.io_nblocks += winstrument->stream.io_nblocks; > + stats.io_in_progress += winstrument->stream.io_in_progress; > + } > + } > + > + break; > + } > + case T_BitmapHeapScan: > + { > + SharedBitmapHeapInstrumentation *sinstrument > + = ((BitmapHeapScanState *) planstate)->sinstrument; > + > + /* get the sum of the counters set within each and every process */ > + if (sinstrument) > + { > + for (int i = 0; i < sinstrument->num_workers; ++i) > + { > + BitmapHeapScanInstrumentation *winstrument = &sinstrument->sinstrument[i]; > + > + stats.prefetch_count += winstrument->stream.prefetch_count; > + stats.distance_sum += winstrument->stream.distance_sum; > + if (winstrument->stream.distance_max > stats.distance_max) > + stats.distance_max = winstrument->stream.distance_max; > + if (winstrument->stream.distance_capacity > stats.distance_capacity) > + stats.distance_capacity = winstrument->stream.distance_capacity; > + stats.stall_count += winstrument->stream.stall_count; > + stats.io_count += winstrument->stream.io_count; > + stats.io_nblocks += winstrument->stream.io_nblocks; > + stats.io_in_progress += winstrument->stream.io_in_progress; It doesn't seem good to duplicate these fairly large blocks. Can't that be in a helper? > + if (es->format == EXPLAIN_FORMAT_TEXT) > + { > + /* prefetch distance info */ > + ExplainIndentText(es); > + appendStringInfo(es->str, "Prefetch: avg=%.3f max=%d capacity=%d", > + (stats.distance_sum * 1.0 / stats.prefetch_count), > + stats.distance_max, > + stats.distance_capacity); > + appendStringInfoChar(es->str, '\n'); > + > + /* prefetch I/O info (only if there were actual I/Os) */ > + if (stats.stall_count > 0 || stats.io_count > 0) > + { Cann there be stalls without IO? Why check both? > + ExplainIndentText(es); > + appendStringInfo(es->str, "I/O: stalls=%" PRIu64, > + stats.stall_count); > + > + if (stats.io_count > 0) > + { > + appendStringInfo(es->str, " size=%.3f inprogress=%.3f", > + (stats.io_nblocks * 1.0 / stats.io_count), > + (stats.io_in_progress * 1.0 / stats.io_count)); > + } > + > + appendStringInfoChar(es->str, '\n'); > + } > + } > + else > + { > + ExplainOpenGroup("Prefetch", "I/O", true, es); > + > + ExplainPropertyFloat("Average Distance", NULL, > + (stats.distance_sum * 1.0 / stats.prefetch_count), 3, es); > + ExplainPropertyInteger("Max Distance", NULL, > + stats.distance_max, es); > + ExplainPropertyInteger("Capacity", NULL, > + stats.distance_capacity, es); > + ExplainPropertyUInteger("Stalls", NULL, > + stats.stall_count, es); > + > + if (stats.io_count > 0) > + { > + ExplainPropertyFloat("Average IO Size", NULL, > + (stats.io_nblocks * 1.0 / stats.io_count), 3, es); > + ExplainPropertyFloat("Average IOs In Progress", NULL, > + (stats.io_in_progress * 1.0 / stats.io_count), 3, es); > + } > + > + ExplainCloseGroup("Prefetch", "I/O", true, es); > + } > + } > +} > + > +/* > + * show_io_worker_info > + * show info about prefetching for a single worker > + * > + * Shows prefetching stats for a parallel scan worker. > + */ > +static void > +show_worker_io_info(PlanState *planstate, ExplainState *es, int worker) > +{ > + Plan *plan = planstate->plan; > + ReadStreamInstrumentation *stats = NULL; > + > + if (!es->io) > + return; > + > + /* get instrumentation for the given worker */ > + switch (nodeTag(plan)) > + { > + case T_BitmapHeapScan: > + { > + BitmapHeapScanState *state = ((BitmapHeapScanState *) planstate); > + SharedBitmapHeapInstrumentation *sinstrument = state->sinstrument; > + BitmapHeapScanInstrumentation *instrument = &sinstrument->sinstrument[worker]; > + > + stats = &instrument->stream; > + > + break; > + } > + case T_SeqScan: > + { > + SeqScanState *state = ((SeqScanState *) planstate); > + SharedSeqScanInstrumentation *sinstrument = state->sinstrument; > + SeqScanInstrumentation *instrument = &sinstrument->sinstrument[worker]; > + > + stats = &instrument->stream; > + > + break; > + } > + default: > + /* ignore other plans */ > + return; > + } > + > + /* don't print stats if there's nothing to report */ > + if (stats->prefetch_count > 0) > + { > + if (es->format == EXPLAIN_FORMAT_TEXT) > + { > + /* prefetch distance info */ > + ExplainIndentText(es); > + appendStringInfo(es->str, "Prefetch: avg=%.3f max=%d capacity=%d", > + (stats->distance_sum * 1.0 / stats->prefetch_count), > + stats->distance_max, > + stats->distance_capacity); > + appendStringInfoChar(es->str, '\n'); > + > + /* prefetch I/O info (only if there were actual I/Os) */ > + if (stats->stall_count > 0 || stats->io_count > 0) > + { > + ExplainIndentText(es); > + appendStringInfo(es->str, "I/O: stalls=%" PRIu64, > + stats->stall_count); > + > + if (stats->io_count > 0) > + { > + appendStringInfo(es->str, " size=%.3f inprogress=%.3f", > + (stats->io_nblocks * 1.0 / stats->io_count), > + (stats->io_in_progress * 1.0 / stats->io_count)); > + } > + > + appendStringInfoChar(es->str, '\n'); > + } > + } > + else > + { > + ExplainOpenGroup("Prefetch", "I/O", true, es); > + > + ExplainPropertyFloat("Average Distance", NULL, > + (stats->distance_sum * 1.0 / stats->prefetch_count), 3, es); > + ExplainPropertyInteger("Max Distance", NULL, > + stats->distance_max, es); > + ExplainPropertyInteger("Capacity", NULL, > + stats->distance_capacity, es); > + ExplainPropertyUInteger("Stalls", NULL, > + stats->stall_count, es); > + > + if (stats->io_count > 0) > + { > + ExplainPropertyFloat("Average IO Size", NULL, > + (stats->io_nblocks * 1.0 / stats->io_count), 3, es); > + ExplainPropertyFloat("Average IOs In Progress", NULL, > + (stats->io_in_progress * 1.0 / stats->io_count), 3, es); > + } > + > + ExplainCloseGroup("Prefetch", "I/O", true, es); > + } > + } > +} Lots of duplication between show_scan_io_info() and show_worker_io_info(). I assume that's just due to prototype state? > +/* > + * read_stream_update_stats_prefetch > + * update read_stream stats with current pinned buffer depth Other stuff in read stream does not do this absurd thing of restating the function name in a comment and indenting the "title" of the function. Don't do it here. > + * > + * Called once per buffer returned to the consumer in read_stream_next_buffer(). > + * Records the number of pinned buffers at that moment, so we can compute the > + * average look-ahead depth. > + */ > +static inline void > +read_stream_update_stats_prefetch(ReadStream *stream) > +{ > + stream->stats.prefetch_count++; > + stream->stats.distance_sum += stream->pinned_buffers; > + if (stream->pinned_buffers > stream->stats.distance_max) > + stream->stats.distance_max = stream->pinned_buffers; > +} > +/* > + * read_stream_update_stats_io > + * update read_stream stats about size of I/O requests > + * > + * We count the number of I/O requests, size of requests (counted in blocks) > + * and number of in-progress I/Os. > + */ > +static inline void > +read_stream_update_stats_io(ReadStream *stream, int nblocks, int in_progress) > +{ > + stream->stats.io_count++; > + stream->stats.io_nblocks += nblocks; > + stream->stats.io_in_progress += in_progress; > +} What's the point of the caller passing in stream->ios_in_progress? Are you expecting some callers to pass in different values? If so, why? Seems a bit confusing to have both stream->stats.io_in_progress and streams->ios_in_progress. It's also not clear that stats.io_in_progress is a sum from the name. > @@ -851,6 +894,7 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data) > flags))) > { > /* Fast return. */ > + read_stream_update_stats_prefetch(stream); > return buffer; > } > > @@ -860,6 +904,9 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data) > stream->ios_in_progress = 1; > stream->ios[0].buffer_index = oldest_buffer_index; > stream->seq_blocknum = next_blocknum + 1; > + > + /* update I/O stats */ > + read_stream_update_stats_io(stream, 1, stream->ios_in_progress); > } > else > { > @@ -871,6 +918,7 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data) > } > > stream->fast_path = false; > + read_stream_update_stats_prefetch(stream); > return buffer; > } > #endif Should read_stream_update_stats_prefetch() actually be called when we reached the end of the stream? > @@ -916,12 +964,17 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data) > { > int16 io_index = stream->oldest_io_index; > int32 distance; /* wider temporary value, clamped below */ > + bool needed_wait; > > /* Sanity check that we still agree on the buffers. */ > Assert(stream->ios[io_index].op.buffers == > &stream->buffers[oldest_buffer_index]); > > - WaitReadBuffers(&stream->ios[io_index].op); > + needed_wait = WaitReadBuffers(&stream->ios[io_index].op); > + > + /* Count it as a stall if we need to wait for IO */ > + if (needed_wait) > + stream->stats.stall_count += 1; > > Assert(stream->ios_in_progress > 0); > stream->ios_in_progress--; I'd probably put the stalls_count++ in a helper too, that makes it easier to test compiling out the stats support and similar things. > @@ -328,6 +329,21 @@ ExecEndBitmapHeapScan(BitmapHeapScanState *node) > */ > si->exact_pages += node->stats.exact_pages; > si->lossy_pages += node->stats.lossy_pages; > + > + /* collect prefetch info for this process from the read_stream */ > + if ((stats = table_scan_stats(node->ss.ss_currentScanDesc)) != NULL) > + { > + si->stream.prefetch_count += stats->prefetch_count; > + si->stream.distance_sum += stats->distance_sum; > + if (stats->distance_max > si->stream.distance_max) > + si->stream.distance_max = stats->distance_max; > + if (stats->distance_capacity > si->stream.distance_capacity) > + si->stream.distance_capacity = stats->distance_capacity; > + si->stream.stall_count += stats->stall_count; > + si->stream.io_count += stats->io_count; > + si->stream.io_nblocks += stats->io_nblocks; > + si->stream.io_in_progress += stats->io_in_progress; > + } > } Could this be wrapped in a helper function? > +/* > + * Generic prefetch stats for table scans. > + */ > +typedef struct TableScanStatsData > +{ > + /* number of buffers returned to consumer (for averaging distance) */ > + uint64 prefetch_count; > + > + /* sum of pinned_buffers sampled at each buffer return */ > + uint64 distance_sum; > + > + /* maximum actual pinned_buffers observed during the scan */ > + int16 distance_max; > + > + /* maximum possible look-ahead distance (max_pinned_buffers) */ > + int16 distance_capacity; > + > + /* number of stalled reads (waiting for I/O) */ > + uint64 stall_count; > + > + /* I/O stats */ > + uint64 io_count; /* number of I/Os */ > + uint64 io_nblocks; /* sum of blocks for all I/Os */ > + uint64 io_in_progress; /* sum of in-progress I/Os */ > +} TableScanStatsData; > +typedef struct TableScanStatsData *TableScanStats; > --- a/src/include/executor/instrument_node.h > +++ b/src/include/executor/instrument_node.h > @@ -41,9 +41,51 @@ typedef struct SharedAggInfo > > > /* --------------------- > - * Instrumentation information for indexscans (amgettuple and amgetbitmap) > + * Instrumentation information about read streams > * --------------------- > */ > +typedef struct ReadStreamInstrumentation > +{ > + /* number of buffers returned to consumer (for averaging distance) */ > + uint64 prefetch_count; > + > + /* sum of pinned_buffers sampled at each buffer return */ > + uint64 distance_sum; > + > + /* maximum actual pinned_buffers observed during the scan */ > + int16 distance_max; > + > + /* maximum possible look-ahead distance (max_pinned_buffers) */ > + int16 distance_capacity; > + > + /* number of stalled reads (waiting for I/O) */ > + uint64 stall_count; > + > + /* I/O stats */ > + uint64 io_count; /* number of I/Os */ > + uint64 io_nblocks; /* sum of blocks for all I/Os */ > + uint64 io_in_progress; /* sum of in-progress I/Os */ > +} ReadStreamInstrumentation; I don't get what we gain by having this twice, once as TableScanStatsData and once as ReadStreamInstrumentation. Greetings, Andres Freund -
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-03-17T19:32:44Z
On 3/17/26 19:40, Andres Freund wrote: > Hi, > > On 2026-03-17 18:51:15 +0100, Tomas Vondra wrote: >> Subject: [PATCH v2 2/2] explain: show prefetch stats in EXPLAIN (ANALYZE, >> VERBOSE) >> >> This adds details about AIO / prefetch for a number of executor nodes >> using the ReadStream, notably: >> >> - SeqScan >> - BitmapHeapScan >> >> The statistics is tracked by the ReadStream, and then propagated up >> through the table AM interface. >> >> The ReadStream tracks the statistics unconditionally, i.e. even outside >> EXPLAIN ANALYZE etc. The amount of statistics is trivial (a handful of >> integer counters), it's not worth gating this by a flag. >> >> The TAM gets one new callback "scan_stats", to collect stats from the >> scan (which fetch tuples from the TAM). There is also a new struct >> TableScanStatsData/TableScanStats to separate the statistics from the >> actual TAM implementation. > > It's not really clear to me that this need to go through the tableam > interface. All the accesses already happen within AM code, it seems we could > just populate fields in TableScanDesc TableScanDesc->rs_flags indicates that > that is desired. > Interesting idea to pass this data through the scan descriptor ... > That seems like it might be nicer, because > > a) there could be multiple read stream instances within a scan > b) we will need one such callback for each different type of scan - so we > could just as well do that within the scan datastructure > AFAICS (a) is very similar to the argument I made regarding the stack-based instrumentation earlier, as it wasn't clear to me how would that work for scans with multiple streams. It didn't occur to me it's an issue for the current solution with TAM callbacks too. However, does passing the info through the scan descriptor address this? I don't see how could it, if there's just a single field per scan. Also, what do you mean by "one callback for each different type of scan"? Right now we need two callbacks - one for TableScanDesc and one for IndexScanDesc. Do we expect to get more? I don't see much difference between an optional TAM callback for each scan type, and an optional field in each scan descriptor. We can base this on rs_flags, but wouldn't it be required in all table_beginscan variants anyway? I have initially planned to pass a flag indicating we're in EXPLAIN (ANALYZE), but rs_flags is not that. It's hard-coded in tableam. > > Showing any such detail in explain will always be a bit of a piercing of > abstraction layers, I'm not particularly concerned with having optionally > fillable information in *ScanDesc that some AM might not want in that > shape. It seems unlikely that a lot of other architectural decisions would be > based on that, so we can just evolve this in the future. > > >> + case T_SeqScan: >> + { >> + SharedSeqScanInstrumentation *sinstrument >> + = ((SeqScanState *) planstate)->sinstrument; >> + >> + /* get the sum of the counters set within each and every process */ >> + if (sinstrument) >> + { >> + for (int i = 0; i < sinstrument->num_workers; ++i) >> + { >> + SeqScanInstrumentation *winstrument = &sinstrument->sinstrument[i]; >> + >> + stats.prefetch_count += winstrument->stream.prefetch_count; >> + stats.distance_sum += winstrument->stream.distance_sum; >> + if (winstrument->stream.distance_max > stats.distance_max) >> + stats.distance_max = winstrument->stream.distance_max; >> + if (winstrument->stream.distance_capacity > stats.distance_capacity) >> + stats.distance_capacity = winstrument->stream.distance_capacity; >> + stats.stall_count += winstrument->stream.stall_count; >> + stats.io_count += winstrument->stream.io_count; >> + stats.io_nblocks += winstrument->stream.io_nblocks; >> + stats.io_in_progress += winstrument->stream.io_in_progress; >> + } >> + } >> + >> + break; >> + } >> + case T_BitmapHeapScan: >> + { >> + SharedBitmapHeapInstrumentation *sinstrument >> + = ((BitmapHeapScanState *) planstate)->sinstrument; >> + >> + /* get the sum of the counters set within each and every process */ >> + if (sinstrument) >> + { >> + for (int i = 0; i < sinstrument->num_workers; ++i) >> + { >> + BitmapHeapScanInstrumentation *winstrument = &sinstrument->sinstrument[i]; >> + >> + stats.prefetch_count += winstrument->stream.prefetch_count; >> + stats.distance_sum += winstrument->stream.distance_sum; >> + if (winstrument->stream.distance_max > stats.distance_max) >> + stats.distance_max = winstrument->stream.distance_max; >> + if (winstrument->stream.distance_capacity > stats.distance_capacity) >> + stats.distance_capacity = winstrument->stream.distance_capacity; >> + stats.stall_count += winstrument->stream.stall_count; >> + stats.io_count += winstrument->stream.io_count; >> + stats.io_nblocks += winstrument->stream.io_nblocks; >> + stats.io_in_progress += winstrument->stream.io_in_progress; > > It doesn't seem good to duplicate these fairly large blocks. Can't that be in > a helper? > Yes, good point. > >> + if (es->format == EXPLAIN_FORMAT_TEXT) >> + { >> + /* prefetch distance info */ >> + ExplainIndentText(es); >> + appendStringInfo(es->str, "Prefetch: avg=%.3f max=%d capacity=%d", >> + (stats.distance_sum * 1.0 / stats.prefetch_count), >> + stats.distance_max, >> + stats.distance_capacity); >> + appendStringInfoChar(es->str, '\n'); >> + >> + /* prefetch I/O info (only if there were actual I/Os) */ >> + if (stats.stall_count > 0 || stats.io_count > 0) >> + { > > Cann there be stalls without IO? Why check both? > I don't recall. I think I originally checked only (io_count > 0), but then at some point added the other condition. I should have documented the reason better, I'll check. > >> + ExplainIndentText(es); >> + appendStringInfo(es->str, "I/O: stalls=%" PRIu64, >> + stats.stall_count); >> + >> + if (stats.io_count > 0) >> + { >> + appendStringInfo(es->str, " size=%.3f inprogress=%.3f", >> + (stats.io_nblocks * 1.0 / stats.io_count), >> + (stats.io_in_progress * 1.0 / stats.io_count)); >> + } >> + >> + appendStringInfoChar(es->str, '\n'); >> + } >> + } >> + else >> + { >> + ExplainOpenGroup("Prefetch", "I/O", true, es); >> + >> + ExplainPropertyFloat("Average Distance", NULL, >> + (stats.distance_sum * 1.0 / stats.prefetch_count), 3, es); >> + ExplainPropertyInteger("Max Distance", NULL, >> + stats.distance_max, es); >> + ExplainPropertyInteger("Capacity", NULL, >> + stats.distance_capacity, es); >> + ExplainPropertyUInteger("Stalls", NULL, >> + stats.stall_count, es); >> + >> + if (stats.io_count > 0) >> + { >> + ExplainPropertyFloat("Average IO Size", NULL, >> + (stats.io_nblocks * 1.0 / stats.io_count), 3, es); >> + ExplainPropertyFloat("Average IOs In Progress", NULL, >> + (stats.io_in_progress * 1.0 / stats.io_count), 3, es); >> + } >> + >> + ExplainCloseGroup("Prefetch", "I/O", true, es); >> + } >> + } >> +} >> + >> +/* >> + * show_io_worker_info >> + * show info about prefetching for a single worker >> + * >> + * Shows prefetching stats for a parallel scan worker. >> + */ >> +static void >> +show_worker_io_info(PlanState *planstate, ExplainState *es, int worker) >> +{ >> + Plan *plan = planstate->plan; >> + ReadStreamInstrumentation *stats = NULL; >> + >> + if (!es->io) >> + return; >> + >> + /* get instrumentation for the given worker */ >> + switch (nodeTag(plan)) >> + { >> + case T_BitmapHeapScan: >> + { >> + BitmapHeapScanState *state = ((BitmapHeapScanState *) planstate); >> + SharedBitmapHeapInstrumentation *sinstrument = state->sinstrument; >> + BitmapHeapScanInstrumentation *instrument = &sinstrument->sinstrument[worker]; >> + >> + stats = &instrument->stream; >> + >> + break; >> + } >> + case T_SeqScan: >> + { >> + SeqScanState *state = ((SeqScanState *) planstate); >> + SharedSeqScanInstrumentation *sinstrument = state->sinstrument; >> + SeqScanInstrumentation *instrument = &sinstrument->sinstrument[worker]; >> + >> + stats = &instrument->stream; >> + >> + break; >> + } >> + default: >> + /* ignore other plans */ >> + return; >> + } >> + >> + /* don't print stats if there's nothing to report */ >> + if (stats->prefetch_count > 0) >> + { >> + if (es->format == EXPLAIN_FORMAT_TEXT) >> + { >> + /* prefetch distance info */ >> + ExplainIndentText(es); >> + appendStringInfo(es->str, "Prefetch: avg=%.3f max=%d capacity=%d", >> + (stats->distance_sum * 1.0 / stats->prefetch_count), >> + stats->distance_max, >> + stats->distance_capacity); >> + appendStringInfoChar(es->str, '\n'); >> + >> + /* prefetch I/O info (only if there were actual I/Os) */ >> + if (stats->stall_count > 0 || stats->io_count > 0) >> + { >> + ExplainIndentText(es); >> + appendStringInfo(es->str, "I/O: stalls=%" PRIu64, >> + stats->stall_count); >> + >> + if (stats->io_count > 0) >> + { >> + appendStringInfo(es->str, " size=%.3f inprogress=%.3f", >> + (stats->io_nblocks * 1.0 / stats->io_count), >> + (stats->io_in_progress * 1.0 / stats->io_count)); >> + } >> + >> + appendStringInfoChar(es->str, '\n'); >> + } >> + } >> + else >> + { >> + ExplainOpenGroup("Prefetch", "I/O", true, es); >> + >> + ExplainPropertyFloat("Average Distance", NULL, >> + (stats->distance_sum * 1.0 / stats->prefetch_count), 3, es); >> + ExplainPropertyInteger("Max Distance", NULL, >> + stats->distance_max, es); >> + ExplainPropertyInteger("Capacity", NULL, >> + stats->distance_capacity, es); >> + ExplainPropertyUInteger("Stalls", NULL, >> + stats->stall_count, es); >> + >> + if (stats->io_count > 0) >> + { >> + ExplainPropertyFloat("Average IO Size", NULL, >> + (stats->io_nblocks * 1.0 / stats->io_count), 3, es); >> + ExplainPropertyFloat("Average IOs In Progress", NULL, >> + (stats->io_in_progress * 1.0 / stats->io_count), 3, es); >> + } >> + >> + ExplainCloseGroup("Prefetch", "I/O", true, es); >> + } >> + } >> +} > > Lots of duplication between show_scan_io_info() and show_worker_io_info(). I > assume that's just due to prototype state? > Yes, I think it needs helpers as mentioned earlier, etc. > >> +/* >> + * read_stream_update_stats_prefetch >> + * update read_stream stats with current pinned buffer depth > > Other stuff in read stream does not do this absurd thing of restating the > function name in a comment and indenting the "title" of the function. Don't do > it here. > OK > >> + * >> + * Called once per buffer returned to the consumer in read_stream_next_buffer(). >> + * Records the number of pinned buffers at that moment, so we can compute the >> + * average look-ahead depth. >> + */ >> +static inline void >> +read_stream_update_stats_prefetch(ReadStream *stream) >> +{ >> + stream->stats.prefetch_count++; >> + stream->stats.distance_sum += stream->pinned_buffers; >> + if (stream->pinned_buffers > stream->stats.distance_max) >> + stream->stats.distance_max = stream->pinned_buffers; >> +} > > >> +/* >> + * read_stream_update_stats_io >> + * update read_stream stats about size of I/O requests >> + * >> + * We count the number of I/O requests, size of requests (counted in blocks) >> + * and number of in-progress I/Os. >> + */ >> +static inline void >> +read_stream_update_stats_io(ReadStream *stream, int nblocks, int in_progress) >> +{ >> + stream->stats.io_count++; >> + stream->stats.io_nblocks += nblocks; >> + stream->stats.io_in_progress += in_progress; >> +} > > What's the point of the caller passing in stream->ios_in_progress? Are you > expecting some callers to pass in different values? If so, why? > Are you suggesting we remove the argument and just use stream->ios_in_progress directly? Yes, we can do do that. An earlier version of the patch did pass (stream->ios_in_progress+1) from one place, but after that was removed we don't need the argument. > Seems a bit confusing to have both stream->stats.io_in_progress and > streams->ios_in_progress. It's also not clear that stats.io_in_progress is a > sum from the name. > I don't think it's all that confusing - those fields are in different structs. But I agree stream->stats.io_in_progress should indicate it's a sum of values. > > >> @@ -851,6 +894,7 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data) >> flags))) >> { >> /* Fast return. */ >> + read_stream_update_stats_prefetch(stream); >> return buffer; >> } >> >> @@ -860,6 +904,9 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data) >> stream->ios_in_progress = 1; >> stream->ios[0].buffer_index = oldest_buffer_index; >> stream->seq_blocknum = next_blocknum + 1; >> + >> + /* update I/O stats */ >> + read_stream_update_stats_io(stream, 1, stream->ios_in_progress); >> } >> else >> { >> @@ -871,6 +918,7 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data) >> } >> >> stream->fast_path = false; >> + read_stream_update_stats_prefetch(stream); >> return buffer; >> } >> #endif > > Should read_stream_update_stats_prefetch() actually be called when we reached > the end of the stream? > Hmmm, maybe we should not update the stats if (next_blocknum == InvalidBlockNumber) > > >> @@ -916,12 +964,17 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data) >> { >> int16 io_index = stream->oldest_io_index; >> int32 distance; /* wider temporary value, clamped below */ >> + bool needed_wait; >> >> /* Sanity check that we still agree on the buffers. */ >> Assert(stream->ios[io_index].op.buffers == >> &stream->buffers[oldest_buffer_index]); >> >> - WaitReadBuffers(&stream->ios[io_index].op); >> + needed_wait = WaitReadBuffers(&stream->ios[io_index].op); >> + >> + /* Count it as a stall if we need to wait for IO */ >> + if (needed_wait) >> + stream->stats.stall_count += 1; >> >> Assert(stream->ios_in_progress > 0); >> stream->ios_in_progress--; > > I'd probably put the stalls_count++ in a helper too, that makes it easier to > test compiling out the stats support and similar things. > OK, makes sense > > >> @@ -328,6 +329,21 @@ ExecEndBitmapHeapScan(BitmapHeapScanState *node) >> */ >> si->exact_pages += node->stats.exact_pages; >> si->lossy_pages += node->stats.lossy_pages; >> + >> + /* collect prefetch info for this process from the read_stream */ >> + if ((stats = table_scan_stats(node->ss.ss_currentScanDesc)) != NULL) >> + { >> + si->stream.prefetch_count += stats->prefetch_count; >> + si->stream.distance_sum += stats->distance_sum; >> + if (stats->distance_max > si->stream.distance_max) >> + si->stream.distance_max = stats->distance_max; >> + if (stats->distance_capacity > si->stream.distance_capacity) >> + si->stream.distance_capacity = stats->distance_capacity; >> + si->stream.stall_count += stats->stall_count; >> + si->stream.io_count += stats->io_count; >> + si->stream.io_nblocks += stats->io_nblocks; >> + si->stream.io_in_progress += stats->io_in_progress; >> + } >> } > > Could this be wrapped in a helper function? > Yes, similarly to the earlier comment. > >> +/* >> + * Generic prefetch stats for table scans. >> + */ >> +typedef struct TableScanStatsData >> +{ >> + /* number of buffers returned to consumer (for averaging distance) */ >> + uint64 prefetch_count; >> + >> + /* sum of pinned_buffers sampled at each buffer return */ >> + uint64 distance_sum; >> + >> + /* maximum actual pinned_buffers observed during the scan */ >> + int16 distance_max; >> + >> + /* maximum possible look-ahead distance (max_pinned_buffers) */ >> + int16 distance_capacity; >> + >> + /* number of stalled reads (waiting for I/O) */ >> + uint64 stall_count; >> + >> + /* I/O stats */ >> + uint64 io_count; /* number of I/Os */ >> + uint64 io_nblocks; /* sum of blocks for all I/Os */ >> + uint64 io_in_progress; /* sum of in-progress I/Os */ >> +} TableScanStatsData; >> +typedef struct TableScanStatsData *TableScanStats; > > >> --- a/src/include/executor/instrument_node.h >> +++ b/src/include/executor/instrument_node.h >> @@ -41,9 +41,51 @@ typedef struct SharedAggInfo >> >> >> /* --------------------- >> - * Instrumentation information for indexscans (amgettuple and amgetbitmap) >> + * Instrumentation information about read streams >> * --------------------- >> */ >> +typedef struct ReadStreamInstrumentation >> +{ >> + /* number of buffers returned to consumer (for averaging distance) */ >> + uint64 prefetch_count; >> + >> + /* sum of pinned_buffers sampled at each buffer return */ >> + uint64 distance_sum; >> + >> + /* maximum actual pinned_buffers observed during the scan */ >> + int16 distance_max; >> + >> + /* maximum possible look-ahead distance (max_pinned_buffers) */ >> + int16 distance_capacity; >> + >> + /* number of stalled reads (waiting for I/O) */ >> + uint64 stall_count; >> + >> + /* I/O stats */ >> + uint64 io_count; /* number of I/Os */ >> + uint64 io_nblocks; /* sum of blocks for all I/Os */ >> + uint64 io_in_progress; /* sum of in-progress I/Os */ >> +} ReadStreamInstrumentation; > > I don't get what we gain by having this twice, once as TableScanStatsData and > once as ReadStreamInstrumentation. > Nothing, really. I already suggested we should have just one struct yesterday. Thanks! -- Tomas Vondra -
Re: EXPLAIN: showing ReadStream / prefetch stats
Andres Freund <andres@anarazel.de> — 2026-03-17T20:49:12Z
Hi, On 2026-03-17 20:32:44 +0100, Tomas Vondra wrote: > On 3/17/26 19:40, Andres Freund wrote: > > On 2026-03-17 18:51:15 +0100, Tomas Vondra wrote: > >> Subject: [PATCH v2 2/2] explain: show prefetch stats in EXPLAIN (ANALYZE, > >> VERBOSE) > >> > >> This adds details about AIO / prefetch for a number of executor nodes > >> using the ReadStream, notably: > >> > >> - SeqScan > >> - BitmapHeapScan > >> > >> The statistics is tracked by the ReadStream, and then propagated up > >> through the table AM interface. > >> > >> The ReadStream tracks the statistics unconditionally, i.e. even outside > >> EXPLAIN ANALYZE etc. The amount of statistics is trivial (a handful of > >> integer counters), it's not worth gating this by a flag. > >> > >> The TAM gets one new callback "scan_stats", to collect stats from the > >> scan (which fetch tuples from the TAM). There is also a new struct > >> TableScanStatsData/TableScanStats to separate the statistics from the > >> actual TAM implementation. > > > > It's not really clear to me that this need to go through the tableam > > interface. All the accesses already happen within AM code, it seems we could > > just populate fields in TableScanDesc TableScanDesc->rs_flags indicates that > > that is desired. > > > > Interesting idea to pass this data through the scan descriptor ... > > > That seems like it might be nicer, because > > > > a) there could be multiple read stream instances within a scan > > b) we will need one such callback for each different type of scan - so we > > could just as well do that within the scan datastructure > > > > AFAICS (a) is very similar to the argument I made regarding the > stack-based instrumentation earlier, as it wasn't clear to me how would > that work for scans with multiple streams. It didn't occur to me it's an > issue for the current solution with TAM callbacks too. > > However, does passing the info through the scan descriptor address this? > I don't see how could it, if there's just a single field per scan. I'd expect the AM to merge the stats internally. > Also, what do you mean by "one callback for each different type of > scan"? Right now we need two callbacks - one for TableScanDesc and one > for IndexScanDesc. Those two were what I was referring to. The only reason we don't need more is that we already are combining different scan types (seq, bitmap, tid, sample) via TableScanDesc, so we really already are quite strongly associating the stats with the *ScanDesc structures. Regardless of how we structure it, since we already combine stats for streams in the parallel query stats, perhaps we should add a field to track for how many streams the stats are? > Do we expect to get more? I think we eventually might want to use read streams for things like getting the source tuple to update/delete in a bulk update and for detoasting during expression evaluation (which is surprisingly often IO bound). These would be bound to an AM but currently we don't have a real descriptor (however, the need for that has come up repeatedly, e.g. to be able to use multi_insert for INSERT statements). I also suspect that there are some executor nodes that could benefit from doing streaming IO internally, e.g. Sort, Materialize, spilling Hash Agg, Hash joins... But that'd not be abstracted behind an AM or such, so it's not really orthogonal. For things like bulk inserts it would be quite nice if we could use AIO for reading in index pages with fewer synchronous waits by doing a loop over the to-be-inserted entries and doing an index lookup that stops at the first miss and start reading that in. Before thinking about detoasting I had a bit of a hard time seeing a clear advantage of using the stack based infra - thinking we always should have very clear association between the executor node and the stream (in contrast to the buffer stats which are collected without any possibility to make such an association). However, if we wanted to e.g. track streams that are done as part of expression evaluation - I am not at all sure whether we do - that'd dissolve that clear association, and would be much easier to implement with the stack based approach. > I don't see much difference between an optional TAM callback for each > scan type, and an optional field in each scan descriptor. It's not entirely different, I agree. The scan descriptor approach seems a tad nicer because it means that a) the read stream can be terminated once it's not needed before we get to the stats collection phase and b) it allows to combine the stats for multiple streams, without the AM having to store the stats in the AM specific part of the descriptor, in a field that is then just returned by the AM. > We can base this on rs_flags, but wouldn't it be required in all > table_beginscan variants anyway? I have initially planned to pass a flag > indicating we're in EXPLAIN (ANALYZE), but rs_flags is not that. It's > hard-coded in tableam. I think Melanie has a patch that changes that :) Until then you could just add a helper that ORs in the instrumentation flag. > >> +/* > >> + * read_stream_update_stats_io > >> + * update read_stream stats about size of I/O requests > >> + * > >> + * We count the number of I/O requests, size of requests (counted in blocks) > >> + * and number of in-progress I/Os. > >> + */ > >> +static inline void > >> +read_stream_update_stats_io(ReadStream *stream, int nblocks, int in_progress) > >> +{ > >> + stream->stats.io_count++; > >> + stream->stats.io_nblocks += nblocks; > >> + stream->stats.io_in_progress += in_progress; > >> +} > > > > What's the point of the caller passing in stream->ios_in_progress? Are you > > expecting some callers to pass in different values? If so, why? > > > > Are you suggesting we remove the argument and just use > stream->ios_in_progress directly? Yes, that's what I am suggesting. Greetings, Andres Freund -
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-03-17T21:43:26Z
On 3/17/26 21:49, Andres Freund wrote: > Hi, > > On 2026-03-17 20:32:44 +0100, Tomas Vondra wrote: >> On 3/17/26 19:40, Andres Freund wrote: >>> On 2026-03-17 18:51:15 +0100, Tomas Vondra wrote: >>>> Subject: [PATCH v2 2/2] explain: show prefetch stats in EXPLAIN (ANALYZE, >>>> VERBOSE) >>>> >>>> This adds details about AIO / prefetch for a number of executor nodes >>>> using the ReadStream, notably: >>>> >>>> - SeqScan >>>> - BitmapHeapScan >>>> >>>> The statistics is tracked by the ReadStream, and then propagated up >>>> through the table AM interface. >>>> >>>> The ReadStream tracks the statistics unconditionally, i.e. even outside >>>> EXPLAIN ANALYZE etc. The amount of statistics is trivial (a handful of >>>> integer counters), it's not worth gating this by a flag. >>>> >>>> The TAM gets one new callback "scan_stats", to collect stats from the >>>> scan (which fetch tuples from the TAM). There is also a new struct >>>> TableScanStatsData/TableScanStats to separate the statistics from the >>>> actual TAM implementation. >>> >>> It's not really clear to me that this need to go through the tableam >>> interface. All the accesses already happen within AM code, it seems we could >>> just populate fields in TableScanDesc TableScanDesc->rs_flags indicates that >>> that is desired. >>> >> >> Interesting idea to pass this data through the scan descriptor ... >> >>> That seems like it might be nicer, because >>> >>> a) there could be multiple read stream instances within a scan >>> b) we will need one such callback for each different type of scan - so we >>> could just as well do that within the scan datastructure >>> >> >> AFAICS (a) is very similar to the argument I made regarding the >> stack-based instrumentation earlier, as it wasn't clear to me how would >> that work for scans with multiple streams. It didn't occur to me it's an >> issue for the current solution with TAM callbacks too. >> >> However, does passing the info through the scan descriptor address this? >> I don't see how could it, if there's just a single field per scan. > > I'd expect the AM to merge the stats internally. > Hmmm, I'm not sure that'll be good enough. What if the two streams are fundamentally different? For example, in the future we may end up using two streams in index scans. One stream for the table (which is what the prefetching is doing), and one for the index itself. Does it really make sense to merge stats for the streams? Furthermore, which place would be responsible for collecting and merging the stats? AFAICS it needs to happen at the end of the scan, but it can't happen in endscan, because that frees the descriptor (so we can't store the result there). Wouldn't we need a new callback anyway? > >> Also, what do you mean by "one callback for each different type of >> scan"? Right now we need two callbacks - one for TableScanDesc and one >> for IndexScanDesc. > > Those two were what I was referring to. The only reason we don't need more is > that we already are combining different scan types (seq, bitmap, tid, sample) > via TableScanDesc, so we really already are quite strongly associating the > stats with the *ScanDesc structures. > > Regardless of how we structure it, since we already combine stats for streams > in the parallel query stats, perhaps we should add a field to track for how > many streams the stats are? > We could. But does it make sense to combine stats from distinct streams? > >> Do we expect to get more? > > I think we eventually might want to use read streams for things like getting > the source tuple to update/delete in a bulk update and for detoasting during > expression evaluation (which is surprisingly often IO bound). These would be > bound to an AM but currently we don't have a real descriptor (however, the > need for that has come up repeatedly, e.g. to be able to use multi_insert for > INSERT statements). > True. But then again - does it make sense to "merge" stats form all these streams used for very different purposes? OTOH I'm not sure it works with the TAM approach either, we don't want a separate callback for each individual stream. > I also suspect that there are some executor nodes that could benefit from > doing streaming IO internally, e.g. Sort, Materialize, spilling Hash Agg, Hash > joins... But that'd not be abstracted behind an AM or such, so it's not > really orthogonal. > Agreed. > > For things like bulk inserts it would be quite nice if we could use AIO for > reading in index pages with fewer synchronous waits by doing a loop over the > to-be-inserted entries and doing an index lookup that stops at the first miss > and start reading that in. > Yes. I had a very ugly PoC last year to prefetch leaf pages for bulk inserts, and it was a huge benefit. > > Before thinking about detoasting I had a bit of a hard time seeing a clear > advantage of using the stack based infra - thinking we always should have very > clear association between the executor node and the stream (in contrast to the > buffer stats which are collected without any possibility to make such an > association). However, if we wanted to e.g. track streams that are done as > part of expression evaluation - I am not at all sure whether we do - that'd > dissolve that clear association, and would be much easier to implement with > the stack based approach. > Right. In some cases the association to streams may not be quite clear. Would it be possible for scans to "define" a list of streams? For example, an index scan have one stream for the index, one stream for table, one stream for TOAST. And we'd have an array of stats, with one slot for each stream. With the current approach, we'd collect stats from each of all those streams into the correct "slot" in the instrumentation. With the stack-based instrumentation we might "tell" each stream which slot to use. So ReadStream would get a field "stats_index" and it'd be passed to INSTR_IOUSAGE_INCR() to update the correct slot. I haven't tried any of this, and AFAIK we don't even have a scan using two read streams yet. > >> I don't see much difference between an optional TAM callback for each >> scan type, and an optional field in each scan descriptor. > > It's not entirely different, I agree. > > The scan descriptor approach seems a tad nicer because it means that a) the > read stream can be terminated once it's not needed before we get to the stats > collection phase and b) it allows to combine the stats for multiple streams, > without the AM having to store the stats in the AM specific part of the > descriptor, in a field that is then just returned by the AM. > > >> We can base this on rs_flags, but wouldn't it be required in all >> table_beginscan variants anyway? I have initially planned to pass a flag >> indicating we're in EXPLAIN (ANALYZE), but rs_flags is not that. It's >> hard-coded in tableam. > > I think Melanie has a patch that changes that :) > > Until then you could just add a helper that ORs in the instrumentation flag. > OK. > > >>>> +/* >>>> + * read_stream_update_stats_io >>>> + * update read_stream stats about size of I/O requests >>>> + * >>>> + * We count the number of I/O requests, size of requests (counted in blocks) >>>> + * and number of in-progress I/Os. >>>> + */ >>>> +static inline void >>>> +read_stream_update_stats_io(ReadStream *stream, int nblocks, int in_progress) >>>> +{ >>>> + stream->stats.io_count++; >>>> + stream->stats.io_nblocks += nblocks; >>>> + stream->stats.io_in_progress += in_progress; >>>> +} >>> >>> What's the point of the caller passing in stream->ios_in_progress? Are you >>> expecting some callers to pass in different values? If so, why? >>> >> >> Are you suggesting we remove the argument and just use >> stream->ios_in_progress directly? > > Yes, that's what I am suggesting. > OK, will do. regards -- Tomas Vondra -
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-03-18T22:41:13Z
Hi! Here's a v3 of the patch, adopting the idea of passing the data through a field in the scan descriptor. I've kept it broken into multiple smaller pieces 0002-0006 to show the incremental changes, but in the end this should be all merged. 0001 - a prerequisite patch 0002 - v2 of the patch 0003 - basic cleanup / helpers to reduce code duplication, various smaller fixes and improvements, ... 0004 - changes the design to use a new field in TableScanDesc 0005 - minor cleanup of 0004 0006 - support for TidRangeScan (the last scan using ReadStream), requires changes similar to SeqScan in v2 Overall, I think it looks reasonable. There are probably ways to simplify it further (e.g. by introducing a new read_stream_begin_ helper, so that it's not necessary to update every caller of read_stream_begin_relation by adding a NULL argument). The 0003 also changes the EXPLAIN to enable IO by default, just like we do for BUFFERS. It seems like a reasonable precedent to me. 0004 reworks the tracking to use a field in TableScanDesc, instead of adding a table AM callback. I agree it seems simpler / less disruptive. 0005 is a minor cleanup for 0004 0006 adds EXPLAIN support for TidRangeScan, similarly to how 0002 adds support for SeqScan (but that still uses the TAM interface) regards -- Tomas Vondra
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Lukas Fittl <lukas@fittl.com> — 2026-03-19T18:11:16Z
On Wed, Mar 18, 2026 at 3:41 PM Tomas Vondra <tomas@vondra.me> wrote: > The 0003 also changes the EXPLAIN to enable IO by default, just like we > do for BUFFERS. It seems like a reasonable precedent to me. One side effect of that is that the tests now fail for me locally, because the specific values are system-dependent. Attached a patch (nocfbot-0002) that fixed that for me. There is one detail maybe calling out specifically on JSON output: Currently Postgres always emits all fields in JSON output, even if they are zero. The code that you have in v3 skips the "I/O" group when the value is zero, which doesn't work well with how current regression tests are written. I'm definitely not a fan of the unnecessary verbosity of JSON EXPLAIN output, but I'd suggest we don't break with the tradition here, and instead always output the "I/O" group in non-text formats. Also attached a patch for that (nocfbot-0001). Overall I think the abstraction here seems reasonable if we're primarily focused on getting the per-node instrumentation taken care of. That said, two thoughts on an example EXPLAIN output I just ran: 1) I do wonder if its a bit confusing that we propagate I/O timings up the EXPLAIN tree, but not the "I/O" information - I realize fixing that would be a bit involved though, e.g. we'd have to invent accumulation logic in explain.c. It'd also maybe make people thing this covers things like temporary file reads/etc. 2) The ordering of "I/O Timings" in relation to "I/O" feels off to me (since they're not next to each other) - maybe we should re-order I/O Timings to come before Buffers in show_buffer_usage to address that? EXPLAIN (ANALYZE) SELECT COUNT(*) FROM t; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------- Aggregate (cost=218331.00..218331.01 rows=1 width=8) (actual time=563.437..563.437 rows=1.00 loops=1) Buffers: shared hit=15806 read=41274 I/O Timings: shared read=1.180 -> Seq Scan on t (cost=0.00..186080.80 rows=12900080 width=0) (actual time=0.335..306.737 rows=12900005.00 loops=1) Prefetch: avg=61.517 max=91 capacity=94 I/O: stalls=7 size=14.825 inprogress=5.321 Buffers: shared hit=15806 read=41274 I/O Timings: shared read=1.180 Planning Time: 0.101 ms Execution Time: 563.471 ms (10 rows) Thanks, Lukas -- Lukas Fittl -
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-03-19T20:57:32Z
Hi Lukas, Thanks for the review. I've been working on cleaning up the patch, reordering the changes a bit. I ran into most of the issues you mentioned, and the attached v4 should address most of them. I got rid of the "old" approach entirely, and just do everything through the scan descriptor. I reorganized the patch series to make more sense - it's split into three pieces (0001 is a prerequisite patch, not really a part of the changes): * 0002 - Basic changes, adding the info to a single scan node that already uses shared instrumentation (=BitmapHeapScan). It adds the counting to read_stream, and printing to explain. It also adjusts all the failing tests checking query plans (usually by disabling I/O). * 0003 - Adds support for SeqScan (has to setup the instrumentation for parallel workers, ...). * 0004 - Adds support for TidRangeScan (has to setup the instrumentation for parallel workers, ...). I think it'd make sense to commit these parts separately. The 0002 part could be made smaller by introducing a new read_stream helper, so that we don't need to add a parameter to all the places calling read_stream_begin_relation(). But that's a detail. More comments inline. On 3/19/26 19:11, Lukas Fittl wrote: > On Wed, Mar 18, 2026 at 3:41 PM Tomas Vondra <tomas@vondra.me> wrote: >> The 0003 also changes the EXPLAIN to enable IO by default, just like we >> do for BUFFERS. It seems like a reasonable precedent to me. > > One side effect of that is that the tests now fail for me locally, > because the specific values are system-dependent. Attached a patch > (nocfbot-0002) that fixed that for me. > Right. I haven't addressed that in v3, but with v4 the tests should be passing (after each patch). > There is one detail maybe calling out specifically on JSON output: > Currently Postgres always emits all fields in JSON output, even if > they are zero. The code that you have in v3 skips the "I/O" group when > the value is zero, which doesn't work well with how current regression > tests are written. I'm definitely not a fan of the unnecessary > verbosity of JSON EXPLAIN output, but I'd suggest we don't break with > the tradition here, and instead always output the "I/O" group in > non-text formats. Also attached a patch for that (nocfbot-0001). > Yeah, I noticed that too, but I wasn't sure what to do about it. The I/O stats are conditional on (io_count > 0) because it calculates average I/O size etc. If there are no I/Os, we could probably print 0.0. Another thing I noticed in the non-text formats is that this is the only case with explicit groups, which adds nesting. It looks weird, so I plan to remove that, and will add some "prefix" to the items. > Overall I think the abstraction here seems reasonable if we're > primarily focused on getting the per-node instrumentation taken care > of. > OK, good. I think passing the stats through a scan descriptor works OK. I still have a strange feeling about it - we have the TAM interface and yet we choose to pass data in other ways. It's better than global variables, of course. But it's not the only piece of data returned through the scan descriptor, so at least there's a precedent. > That said, two thoughts on an example EXPLAIN output I just ran: > > 1) I do wonder if its a bit confusing that we propagate I/O timings up > the EXPLAIN tree, but not the "I/O" information - I realize fixing > that would be a bit involved though, e.g. we'd have to invent > accumulation logic in explain.c. It'd also maybe make people thing > this covers things like temporary file reads/etc. > I'm not sure it makes sense to combine this information from multiple nodes. If you have multiple read streams with very different average I/O size and/or prefetch distances, what does the "global average" mean? I also don't think it's very useful - the information about distance and/or I/O size is most useful when analyzing individual nodes. What would it tell me for multiple nodes combined? So I don't think it's something we need, and we can add it later. > 2) The ordering of "I/O Timings" in relation to "I/O" feels off to me > (since they're not next to each other) - maybe we should re-order I/O > Timings to come before Buffers in show_buffer_usage to address that? > Yeah, maybe. I don't think we have any explicit order, but why not. Thanks! -- Tomas Vondra
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-03-26T00:35:01Z
Hi, Here's a slightly improved v5, with a couple minor changes over v4. Patches 0001 and 0002 are prerequisites from other threads. 0001 was already included in v4. 0002 is new and comes from [1], and it adds "flags" to TAM beginscan() methods. This allows 0003 to pass a new SO_SCAN_INSTRUMENT flag, so that e.g. heap_beginscan() can initialize the instrumentation only when executed in EXPLAIN ANALYZE. In v4 it had to be done every time, which did not seem quite great. This mimics what we do for index scans in the index prefetching, which only allocates the instrumentation when needed. 0004 and 0005 are adjusted to do this for SeqScan and TidRangeScans too. 0006 is a minor adjustment of the explain output, based on the earlier discussion about non-text formats. It removes the explain groups (to not add nested output), and instead makes the labels more descriptive. It also prints the items every time, even with io_count==0. The last change is that I added the number of I/Os, instead of printing just the number of stalls and average I/O size. Without the count it seemed a bit difficult to interpret. It might be possible to deduce some of this from buffers reads, but I found it a bit unclear. 0006 should be split and merged into the earlier parts, I mostly kept it to make those changes obvious. regards https://www.postgresql.org/message-id/itvgqc6vncbjsjfmrptfvkkeg5vqzhalaguya2z77t6c6ctpc3%40wsdrgbn4bxaa -- Tomas Vondra
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Andres Freund <andres@anarazel.de> — 2026-03-30T17:28:41Z
Hi, > Subject: [PATCH v5 1/6] bufmgr: Return whether WaitReadBuffers() needed to > wait > > In a subsequent commit read_stream.c will use this as an input to the read > ahead distance. Note that this actually depends on a prior commit to work for io_uring... > From 410eaaebe7b814ac9f44c080e153f4ec1d6d6b86 Mon Sep 17 00:00:00 2001 > From: Tomas Vondra <tomas@vondra.me> > Date: Thu, 19 Mar 2026 22:25:09 +0100 > Subject: [PATCH v5 3/6] explain: show prefetch stats in EXPLAIN (ANALYZE) > > This adds details about AIO / prefetch for executor nodes using a > ReadStream. Right now this applies only to BitmapHeapScan, because > that's the only scan node using a ReadStream and collecting > instrumentation from workers. I don't understand why that means it should be done as part of this commit, whereas seqscans shouldn't? > The stats are collected by the ReadStream unconditionally, i.e. without > EXPLAIN ANALYZE etc. The amount of statistics is trivial (a handful of > integer counters), it's not worth gating this by a flag. I think this is not true anymore. > --- a/doc/src/sgml/ref/explain.sgml > +++ b/doc/src/sgml/ref/explain.sgml > @@ -46,6 +46,7 @@ EXPLAIN [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] <rep > TIMING [ <replaceable class="parameter">boolean</replaceable> ] > SUMMARY [ <replaceable class="parameter">boolean</replaceable> ] > MEMORY [ <replaceable class="parameter">boolean</replaceable> ] > + IO [ <replaceable class="parameter">boolean</replaceable> ] > FORMAT { TEXT | XML | JSON | YAML } > </synopsis> > </refsynopsisdiv> > @@ -298,6 +299,17 @@ ROLLBACK; > </listitem> > </varlistentry> > > + <varlistentry> > + <term><literal>IO</literal></term> > + <listitem> > + <para> > + Include information on I/O performed by each node. > + This parameter may only be used when <literal>ANALYZE</literal> is also > + enabled. It defaults to <literal>TRUE</literal>. > + </para> > + </listitem> > + </varlistentry> > + > <varlistentry> > <term><literal>FORMAT</literal></term> > <listitem> I wonder if it's sane to default this to true. The amount of adjustments in the regression tests required in the later commits makes me a bit wary. But I guess just going around and adding a few dozen OPTION OFF is what we've lately been doing, so perhaps it's ok. > @@ -1198,6 +1198,7 @@ heap_beginscan(Relation relation, Snapshot snapshot, > scan->rs_base.rs_nkeys = nkeys; > 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; > > @@ -1267,6 +1268,12 @@ heap_beginscan(Relation relation, Snapshot snapshot, > > scan->rs_read_stream = NULL; > > + /* allocate instrumentation */ > + if (flags & SO_SCAN_INSTRUMENT) > + scan->rs_base.rs_instrument = palloc0_object(TableScanInstrumentation); > + else > + scan->rs_base.rs_instrument = NULL; > + > /* > * Set up a read stream for sequential scans and TID range scans. This > * should be done after initscan() because initscan() allocates the > @@ -1296,7 +1303,8 @@ heap_beginscan(Relation relation, Snapshot snapshot, > MAIN_FORKNUM, > cb, > scan, > - 0); > + 0, > + &scan->rs_base.rs_instrument->io); > } > else if (scan->rs_base.rs_flags & SO_TYPE_BITMAPSCAN) > { > @@ -1307,7 +1315,8 @@ heap_beginscan(Relation relation, Snapshot snapshot, > MAIN_FORKNUM, > bitmapheap_stream_read_next, > scan, > - sizeof(TBMIterateResult)); > + sizeof(TBMIterateResult), > + &scan->rs_base.rs_instrument->io); > } > > scan->rs_vmbuffer = InvalidBuffer; It doesn't seem great to dereference rs_base.rs_instrument if it's NULL, even if that works because you then just compute the address of that. I think you'll end up passing the offset of io in TableScanInstrumentation this way. > +static void > +print_io_usage(ExplainState *es, IOStats *stats) > +{ > + /* don't print stats if there's nothing to report */ > + if (stats->prefetch_count > 0) > + { > + if (es->format == EXPLAIN_FORMAT_TEXT) > + { > + /* prefetch distance info */ > + ExplainIndentText(es); > + appendStringInfo(es->str, "Prefetch: avg=%.3f max=%d capacity=%d", > + (stats->distance_sum * 1.0 / stats->prefetch_count), > + stats->distance_max, > + stats->distance_capacity); > + appendStringInfoChar(es->str, '\n'); > + > + /* prefetch I/O info (only if there were actual I/Os) */ > + if (stats->io_count > 0) > + { > + ExplainIndentText(es); > + appendStringInfo(es->str, "I/O: stalls=%" PRIu64, > + stats->stall_count); > + > + appendStringInfo(es->str, " size=%.3f inprogress=%.3f", > + (stats->io_nblocks * 1.0 / stats->io_count), > + (stats->io_in_progress * 1.0 / stats->io_count)); > + > + appendStringInfoChar(es->str, '\n'); > + } Why have these separate appendStringInfoChar's if you just could just include the \n in the prior appendStringInfo? Separately, is %.3f really the right thing? When is that degree of precision useful here? > diff --git a/src/backend/executor/nodeBitmapHeapscan.c b/src/backend/executor/nodeBitmapHeapscan.c > index 7e2c1b7467b..d181fd7b8ff 100644 > --- a/src/backend/executor/nodeBitmapHeapscan.c > +++ b/src/backend/executor/nodeBitmapHeapscan.c > @@ -104,6 +104,7 @@ BitmapTableScanSetup(BitmapHeapScanState *node) > TBMIterator tbmiterator = {0}; > ParallelBitmapHeapState *pstate = node->pstate; > dsa_area *dsa = node->ss.ps.state->es_query_dsa; > + EState *estate = node->ss.ps.state; > > if (!pstate) > { > @@ -146,7 +147,7 @@ BitmapTableScanSetup(BitmapHeapScanState *node) > { > node->ss.ss_currentScanDesc = > table_beginscan_bm(node->ss.ss_currentRelation, > - 0, > + (estate->es_instrument) ? SO_SCAN_INSTRUMENT : 0, > node->ss.ps.state->es_snapshot, > 0, > NULL); > @@ -329,6 +330,9 @@ ExecEndBitmapHeapScan(BitmapHeapScanState *node) > */ > si->exact_pages += node->stats.exact_pages; > si->lossy_pages += node->stats.lossy_pages; > + > + /* collect prefetch info for this process from the read_stream */ > + ACCUMULATE_IO_STATS(&si->stats.io, &node->ss.ss_currentScanDesc->rs_instrument->io); > } > > /* I see no point in having ACCUMULATE_IO_STATS be a macro instead of a static inline? > +/* > + * Update stream stats with current pinned buffer depth. > + * > + * Called once per buffer returned to the consumer in read_stream_next_buffer(). > + * Records the number of pinned buffers at that moment, so we can compute the > + * average look-ahead depth. > + */ > +static inline void > +read_stream_update_stats_prefetch(ReadStream *stream) > +{ > + IOStats *stats = stream->stats; > + > + if (stats == NULL) > + return; > + > + stats->prefetch_count++; > + stats->distance_sum += stream->pinned_buffers; > + if (stream->pinned_buffers > stats->distance_max) > + stats->distance_max = stream->pinned_buffers; > +} > + > +/* > + * Update stream stats about size of I/O requests. > + * > + * We count the number of I/O requests, size of requests (counted in blocks) > + * and number of in-progress I/Os. > + */ > +static inline void > +read_stream_update_stats_io(ReadStream *stream, int nblocks, int in_progress) > +{ > + IOStats *stats = stream->stats; > + > + if (stats == NULL) > + return; > + > + stats->io_count++; > + stats->io_nblocks += nblocks; > + stats->io_in_progress += in_progress; > +} > + > +static inline void > +read_stream_update_stats_stall(ReadStream *stream) > +{ Wonder if we should rename "stall" to "wait", seems like it might be easier to understand for a more general audience. > /* ---------------------------------------------------------------- > @@ -404,9 +471,50 @@ void > ExecSeqScanInitializeWorker(SeqScanState *node, > ParallelWorkerContext *pwcxt) > { > + EState *estate = node->ss.ps.state; > ParallelTableScanDesc pscan; > + char *ptr; > + Size size; > > pscan = shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id, false); > node->ss.ss_currentScanDesc = > - table_beginscan_parallel(node->ss.ss_currentRelation, 0, pscan); > + table_beginscan_parallel(node->ss.ss_currentRelation, > + (estate->es_instrument) ? SO_SCAN_INSTRUMENT : 0, > + pscan); > + > + /* > + * Workers don't get the pscan_len value in scan descriptor, so use the > + * TAM callback again. The result has to match the earlier result in > + * ExecSeqScanEstimate. > + */ > + size = table_parallelscan_estimate(node->ss.ss_currentRelation, > + estate->es_snapshot); > + That seems quite grotty... > + ptr = (char *) pscan; > + ptr += MAXALIGN(size); > + > + if (node->ss.ps.instrument) > + node->sinstrument = (SharedSeqScanInstrumentation *) ptr; Seems like the appropriate instrumentation pointers should be computed by the leader, not in the workers. > diff --git a/src/test/regress/expected/explain.out b/src/test/regress/expected/explain.out > index 97cb7206676..3fa6d838545 100644 > --- a/src/test/regress/expected/explain.out > +++ b/src/test/regress/expected/explain.out > @@ -93,112 +93,126 @@ select explain_filter('explain (analyze, buffers, io off, format text) select * > Execution Time: N.N ms > (3 rows) > > -select explain_filter('explain (analyze, buffers, io off, format xml) select * from int8_tbl i8'); > - explain_filter > --------------------------------------------------------- > - <explain xmlns="http://www.postgresql.org/N/explain"> + ... > + <explain xmlns="http://www.postgresql.org/N/explain"> + ... Seems these tests should really use unaligned output, to avoid this stupid "every line changes despite not really anything changing" type of diff. Greetings, Andres Freund -
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-03-30T18:21:29Z
On 3/30/26 19:28, Andres Freund wrote: > > Hi, > >> Subject: [PATCH v5 1/6] bufmgr: Return whether WaitReadBuffers() needed to >> wait >> >> In a subsequent commit read_stream.c will use this as an input to the read >> ahead distance. > > Note that this actually depends on a prior commit to work for io_uring... > Good point. > >> From 410eaaebe7b814ac9f44c080e153f4ec1d6d6b86 Mon Sep 17 00:00:00 2001 >> From: Tomas Vondra <tomas@vondra.me> >> Date: Thu, 19 Mar 2026 22:25:09 +0100 >> Subject: [PATCH v5 3/6] explain: show prefetch stats in EXPLAIN (ANALYZE) >> >> This adds details about AIO / prefetch for executor nodes using a >> ReadStream. Right now this applies only to BitmapHeapScan, because >> that's the only scan node using a ReadStream and collecting >> instrumentation from workers. > > I don't understand why that means it should be done as part of this commit, > whereas seqscans shouldn't? > Are you suggesting the commit adds support for all those scans (BHS, SeqScan and TidRangeScan) or none of them? To me it seems better to have at least some scan because of testing. But SeqScan/TidRangeScan don't have the instrumentation infrastructure for parallel queries, and I don't want to do that in the main patch - it seems rather unrelated. And I also don't want to add it before the main patch. > >> The stats are collected by the ReadStream unconditionally, i.e. without >> EXPLAIN ANALYZE etc. The amount of statistics is trivial (a handful of >> integer counters), it's not worth gating this by a flag. > > I think this is not true anymore. > Yes, that's a stale comment. Will fix. > >> --- a/doc/src/sgml/ref/explain.sgml >> +++ b/doc/src/sgml/ref/explain.sgml >> @@ -46,6 +46,7 @@ EXPLAIN [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] <rep >> TIMING [ <replaceable class="parameter">boolean</replaceable> ] >> SUMMARY [ <replaceable class="parameter">boolean</replaceable> ] >> MEMORY [ <replaceable class="parameter">boolean</replaceable> ] >> + IO [ <replaceable class="parameter">boolean</replaceable> ] >> FORMAT { TEXT | XML | JSON | YAML } >> </synopsis> >> </refsynopsisdiv> >> @@ -298,6 +299,17 @@ ROLLBACK; >> </listitem> >> </varlistentry> >> >> + <varlistentry> >> + <term><literal>IO</literal></term> >> + <listitem> >> + <para> >> + Include information on I/O performed by each node. >> + This parameter may only be used when <literal>ANALYZE</literal> is also >> + enabled. It defaults to <literal>TRUE</literal>. >> + </para> >> + </listitem> >> + </varlistentry> >> + >> <varlistentry> >> <term><literal>FORMAT</literal></term> >> <listitem> > > I wonder if it's sane to default this to true. The amount of adjustments in > the regression tests required in the later commits makes me a bit wary. But I > guess just going around and adding a few dozen OPTION OFF is what we've lately > been doing, so perhaps it's ok. > I'm not sure either. I initially added it as default OFF but it's true it's similar to "buffers". > > >> @@ -1198,6 +1198,7 @@ heap_beginscan(Relation relation, Snapshot snapshot, >> scan->rs_base.rs_nkeys = nkeys; >> 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; >> >> @@ -1267,6 +1268,12 @@ heap_beginscan(Relation relation, Snapshot snapshot, >> >> scan->rs_read_stream = NULL; >> >> + /* allocate instrumentation */ >> + if (flags & SO_SCAN_INSTRUMENT) >> + scan->rs_base.rs_instrument = palloc0_object(TableScanInstrumentation); >> + else >> + scan->rs_base.rs_instrument = NULL; >> + >> /* >> * Set up a read stream for sequential scans and TID range scans. This >> * should be done after initscan() because initscan() allocates the >> @@ -1296,7 +1303,8 @@ heap_beginscan(Relation relation, Snapshot snapshot, >> MAIN_FORKNUM, >> cb, >> scan, >> - 0); >> + 0, >> + &scan->rs_base.rs_instrument->io); >> } >> else if (scan->rs_base.rs_flags & SO_TYPE_BITMAPSCAN) >> { >> @@ -1307,7 +1315,8 @@ heap_beginscan(Relation relation, Snapshot snapshot, >> MAIN_FORKNUM, >> bitmapheap_stream_read_next, >> scan, >> - sizeof(TBMIterateResult)); >> + sizeof(TBMIterateResult), >> + &scan->rs_base.rs_instrument->io); >> } >> >> scan->rs_vmbuffer = InvalidBuffer; > > It doesn't seem great to dereference rs_base.rs_instrument if it's NULL, even > if that works because you then just compute the address of that. I think > you'll end up passing the offset of io in TableScanInstrumentation this way. > Yes, I have already noticed / fixed this in my development branch. > > >> +static void >> +print_io_usage(ExplainState *es, IOStats *stats) >> +{ >> + /* don't print stats if there's nothing to report */ >> + if (stats->prefetch_count > 0) >> + { >> + if (es->format == EXPLAIN_FORMAT_TEXT) >> + { >> + /* prefetch distance info */ >> + ExplainIndentText(es); >> + appendStringInfo(es->str, "Prefetch: avg=%.3f max=%d capacity=%d", >> + (stats->distance_sum * 1.0 / stats->prefetch_count), >> + stats->distance_max, >> + stats->distance_capacity); >> + appendStringInfoChar(es->str, '\n'); >> + >> + /* prefetch I/O info (only if there were actual I/Os) */ >> + if (stats->io_count > 0) >> + { >> + ExplainIndentText(es); >> + appendStringInfo(es->str, "I/O: stalls=%" PRIu64, >> + stats->stall_count); >> + >> + appendStringInfo(es->str, " size=%.3f inprogress=%.3f", >> + (stats->io_nblocks * 1.0 / stats->io_count), >> + (stats->io_in_progress * 1.0 / stats->io_count)); >> + >> + appendStringInfoChar(es->str, '\n'); >> + } > > Why have these separate appendStringInfoChar's if you just could just include > the \n in the prior appendStringInfo? > Not sure, IIRC it went through multiple versions and at some point the lines were far too long (esp. with histograms etc.). > Separately, is %.3f really the right thing? When is that degree of precision > useful here? > No idea. I think %.1f would be enough. We use %.2f in a couple places, but that's not a huge difference. > >> diff --git a/src/backend/executor/nodeBitmapHeapscan.c b/src/backend/executor/nodeBitmapHeapscan.c >> index 7e2c1b7467b..d181fd7b8ff 100644 >> --- a/src/backend/executor/nodeBitmapHeapscan.c >> +++ b/src/backend/executor/nodeBitmapHeapscan.c >> @@ -104,6 +104,7 @@ BitmapTableScanSetup(BitmapHeapScanState *node) >> TBMIterator tbmiterator = {0}; >> ParallelBitmapHeapState *pstate = node->pstate; >> dsa_area *dsa = node->ss.ps.state->es_query_dsa; >> + EState *estate = node->ss.ps.state; >> >> if (!pstate) >> { >> @@ -146,7 +147,7 @@ BitmapTableScanSetup(BitmapHeapScanState *node) >> { >> node->ss.ss_currentScanDesc = >> table_beginscan_bm(node->ss.ss_currentRelation, >> - 0, >> + (estate->es_instrument) ? SO_SCAN_INSTRUMENT : 0, >> node->ss.ps.state->es_snapshot, >> 0, >> NULL); >> @@ -329,6 +330,9 @@ ExecEndBitmapHeapScan(BitmapHeapScanState *node) >> */ >> si->exact_pages += node->stats.exact_pages; >> si->lossy_pages += node->stats.lossy_pages; >> + >> + /* collect prefetch info for this process from the read_stream */ >> + ACCUMULATE_IO_STATS(&si->stats.io, &node->ss.ss_currentScanDesc->rs_instrument->io); >> } >> >> /* > > I see no point in having ACCUMULATE_IO_STATS be a macro instead of a static > inline? > Both a macro and static inline would work for me. > >> +/* >> + * Update stream stats with current pinned buffer depth. >> + * >> + * Called once per buffer returned to the consumer in read_stream_next_buffer(). >> + * Records the number of pinned buffers at that moment, so we can compute the >> + * average look-ahead depth. >> + */ >> +static inline void >> +read_stream_update_stats_prefetch(ReadStream *stream) >> +{ >> + IOStats *stats = stream->stats; >> + >> + if (stats == NULL) >> + return; >> + >> + stats->prefetch_count++; >> + stats->distance_sum += stream->pinned_buffers; >> + if (stream->pinned_buffers > stats->distance_max) >> + stats->distance_max = stream->pinned_buffers; >> +} >> + >> +/* >> + * Update stream stats about size of I/O requests. >> + * >> + * We count the number of I/O requests, size of requests (counted in blocks) >> + * and number of in-progress I/Os. >> + */ >> +static inline void >> +read_stream_update_stats_io(ReadStream *stream, int nblocks, int in_progress) >> +{ >> + IOStats *stats = stream->stats; >> + >> + if (stats == NULL) >> + return; >> + >> + stats->io_count++; >> + stats->io_nblocks += nblocks; >> + stats->io_in_progress += in_progress; >> +} >> + >> +static inline void >> +read_stream_update_stats_stall(ReadStream *stream) >> +{ > > Wonder if we should rename "stall" to "wait", seems like it might be easier to > understand for a more general audience. > I agree "wait" sounds clearer. > >> /* ---------------------------------------------------------------- >> @@ -404,9 +471,50 @@ void >> ExecSeqScanInitializeWorker(SeqScanState *node, >> ParallelWorkerContext *pwcxt) >> { >> + EState *estate = node->ss.ps.state; >> ParallelTableScanDesc pscan; >> + char *ptr; >> + Size size; >> >> pscan = shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id, false); >> node->ss.ss_currentScanDesc = >> - table_beginscan_parallel(node->ss.ss_currentRelation, 0, pscan); >> + table_beginscan_parallel(node->ss.ss_currentRelation, >> + (estate->es_instrument) ? SO_SCAN_INSTRUMENT : 0, >> + pscan); >> + >> + /* >> + * Workers don't get the pscan_len value in scan descriptor, so use the >> + * TAM callback again. The result has to match the earlier result in >> + * ExecSeqScanEstimate. >> + */ >> + size = table_parallelscan_estimate(node->ss.ss_currentRelation, >> + estate->es_snapshot); >> + > > That seems quite grotty... > Yes. But what's a better solution? > >> + ptr = (char *) pscan; >> + ptr += MAXALIGN(size); >> + >> + if (node->ss.ps.instrument) >> + node->sinstrument = (SharedSeqScanInstrumentation *) ptr; > > Seems like the appropriate instrumentation pointers should be computed by the > leader, not in the workers. > This is how BitmapHeapScan does it. And how would the leader do that, when the node is private in the worker? > > >> diff --git a/src/test/regress/expected/explain.out b/src/test/regress/expected/explain.out >> index 97cb7206676..3fa6d838545 100644 >> --- a/src/test/regress/expected/explain.out >> +++ b/src/test/regress/expected/explain.out >> @@ -93,112 +93,126 @@ select explain_filter('explain (analyze, buffers, io off, format text) select * >> Execution Time: N.N ms >> (3 rows) >> >> -select explain_filter('explain (analyze, buffers, io off, format xml) select * from int8_tbl i8'); >> - explain_filter >> --------------------------------------------------------- >> - <explain xmlns="http://www.postgresql.org/N/explain"> + > ... >> + <explain xmlns="http://www.postgresql.org/N/explain"> + > ... > > Seems these tests should really use unaligned output, to avoid this stupid > "every line changes despite not really anything changing" type of diff. > Probably. I can adjust that in an initial patch. regards -- Tomas Vondra -
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-03-31T13:16:57Z
Here's v6, addressing most of the review comments. Most of this is pretty simple / non-contentious. The remaining questions are in the last two items (phs_len field, initializing the instrumentation pointers): - 0001 tweaks WaitReadBuffers to return if a wait was needed. As pointed out, this does not include some changes needed for io_uring, I don't want to make this larger. So test with "worker". - 0002 switches a couple explain tests to unaligned output. This makes it easier to understand output changes with yaml/json/xml output. - I kept the three scans in separate changes. It makes it easier to review etc. and to me it seems easier to review. I'd keep it like this for commit, but if someone thinks we should squash it into a single commit that's possible too. - I kept the IO option enabled by default. I think it makes sense to keep this aligned with BUFFERS, but on the other hand the explain should not be overwhelming. Various other explain options are similarly helpful, and yet we left them OFF by default. - Cleanup of the explain formatting code (merging appendString lines), and so on. One question is whether to keep printing the information only when (stats->prefetch_count > 0), especially in the non-text cases. With the three scans this is not an issue, because the read stream is initialized right away. But with index scans we delay that a bit, so I wonder if this might cause some test instability. Probably not. - I switched to %.2f format for printing the averages etc. instead of %.3f. Not a huge change, we could probably go even to %.1f ... - Replaced the ACCUMULATE_IO_STATS macro with a static inline. - Renamed "stalls" to "waits". - Fixed a couple obsolete comments, and the bug in dereferencing NULL when the instrumentation was not initialized. - Added the pscan_len length (returned by table_parallelscan_estimate) to ParallelTableScanDesc, so that the workers can use this to calculate the pointer of shared instrumentation without calling the _estimate again (which I agree seems wrong and grotty). This only matters for SeqScan and TidRangeScan, and it makes it quite a bit cleaner. I don't see a better way. Right now the nodes set the new field right after calling shm_toc_allocate, but I suppose it'd be more correct to pass it to table_parallelscan_initialize() and set it from there. Barring objections I'll adjust this for v7. - I still don't understand how could the leader calculate the instrumentation pointers for workers, so it's done the same way as in BHS (except that we need to use the new phs_len field to calculate the initial offset, of course). regards -- Tomas Vondra
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Andres Freund <andres@anarazel.de> — 2026-03-31T17:41:18Z
Hi, On 2026-03-30 20:21:29 +0200, Tomas Vondra wrote: > >> From 410eaaebe7b814ac9f44c080e153f4ec1d6d6b86 Mon Sep 17 00:00:00 2001 > >> From: Tomas Vondra <tomas@vondra.me> > >> Date: Thu, 19 Mar 2026 22:25:09 +0100 > >> Subject: [PATCH v5 3/6] explain: show prefetch stats in EXPLAIN (ANALYZE) > >> > >> This adds details about AIO / prefetch for executor nodes using a > >> ReadStream. Right now this applies only to BitmapHeapScan, because > >> that's the only scan node using a ReadStream and collecting > >> instrumentation from workers. > > > > I don't understand why that means it should be done as part of this commit, > > whereas seqscans shouldn't? > > > > Are you suggesting the commit adds support for all those scans (BHS, > SeqScan and TidRangeScan) or none of them? I guess I mostly just didn't quite understand what differentiates bitmap scans from the other scans, based on this explanation. > To me it seems better to have at least some scan because of testing. But > SeqScan/TidRangeScan don't have the instrumentation infrastructure for > parallel queries, and I don't want to do that in the main patch - it seems > rather unrelated. And I also don't want to add it before the main patch. I'd probably do the latter, i.e. add it before the main patch. Or at least separately from the change to show read stream instrumentation. > > Separately, is %.3f really the right thing? When is that degree of precision > > useful here? > > > > No idea. I think %.1f would be enough. We use %.2f in a couple places, > but that's not a huge difference. I'd probably use .1f here, I don't think you'd ever need more precision. Showing that it's not a round number is useful, but more than that... ? > >> /* ---------------------------------------------------------------- > >> @@ -404,9 +471,50 @@ void > >> ExecSeqScanInitializeWorker(SeqScanState *node, > >> ParallelWorkerContext *pwcxt) > >> { > >> + EState *estate = node->ss.ps.state; > >> ParallelTableScanDesc pscan; > >> + char *ptr; > >> + Size size; > >> > >> pscan = shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id, false); > >> node->ss.ss_currentScanDesc = > >> - table_beginscan_parallel(node->ss.ss_currentRelation, 0, pscan); > >> + table_beginscan_parallel(node->ss.ss_currentRelation, > >> + (estate->es_instrument) ? SO_SCAN_INSTRUMENT : 0, > >> + pscan); > >> + > >> + /* > >> + * Workers don't get the pscan_len value in scan descriptor, so use the > >> + * TAM callback again. The result has to match the earlier result in > >> + * ExecSeqScanEstimate. > >> + */ > >> + size = table_parallelscan_estimate(node->ss.ss_currentRelation, > >> + estate->es_snapshot); > >> + > > > > That seems quite grotty... > > > > Yes. But what's a better solution? Think the way you've done it in v6 is better. Greetings, Andres Freund -
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-03-31T18:03:02Z
On 3/31/26 19:41, Andres Freund wrote: > Hi, > > On 2026-03-30 20:21:29 +0200, Tomas Vondra wrote: >>>> From 410eaaebe7b814ac9f44c080e153f4ec1d6d6b86 Mon Sep 17 00:00:00 2001 >>>> From: Tomas Vondra <tomas@vondra.me> >>>> Date: Thu, 19 Mar 2026 22:25:09 +0100 >>>> Subject: [PATCH v5 3/6] explain: show prefetch stats in EXPLAIN (ANALYZE) >>>> >>>> This adds details about AIO / prefetch for executor nodes using a >>>> ReadStream. Right now this applies only to BitmapHeapScan, because >>>> that's the only scan node using a ReadStream and collecting >>>> instrumentation from workers. >>> >>> I don't understand why that means it should be done as part of this commit, >>> whereas seqscans shouldn't? >>> >> >> Are you suggesting the commit adds support for all those scans (BHS, >> SeqScan and TidRangeScan) or none of them? > > I guess I mostly just didn't quite understand what differentiates bitmap scans > from the other scans, based on this explanation. > > >> To me it seems better to have at least some scan because of testing. But >> SeqScan/TidRangeScan don't have the instrumentation infrastructure for >> parallel queries, and I don't want to do that in the main patch - it seems >> rather unrelated. And I also don't want to add it before the main patch. > > I'd probably do the latter, i.e. add it before the main patch. Or at least > separately from the change to show read stream instrumentation. > Sorry, I'm confused. Which "latter" option you mean? > >>> Separately, is %.3f really the right thing? When is that degree of precision >>> useful here? >>> >> >> No idea. I think %.1f would be enough. We use %.2f in a couple places, >> but that's not a huge difference. > > I'd probably use .1f here, I don't think you'd ever need more > precision. Showing that it's not a round number is useful, but more than > that... ? > OK, .1f WFM > >>>> /* ---------------------------------------------------------------- >>>> @@ -404,9 +471,50 @@ void >>>> ExecSeqScanInitializeWorker(SeqScanState *node, >>>> ParallelWorkerContext *pwcxt) >>>> { >>>> + EState *estate = node->ss.ps.state; >>>> ParallelTableScanDesc pscan; >>>> + char *ptr; >>>> + Size size; >>>> >>>> pscan = shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id, false); >>>> node->ss.ss_currentScanDesc = >>>> - table_beginscan_parallel(node->ss.ss_currentRelation, 0, pscan); >>>> + table_beginscan_parallel(node->ss.ss_currentRelation, >>>> + (estate->es_instrument) ? SO_SCAN_INSTRUMENT : 0, >>>> + pscan); >>>> + >>>> + /* >>>> + * Workers don't get the pscan_len value in scan descriptor, so use the >>>> + * TAM callback again. The result has to match the earlier result in >>>> + * ExecSeqScanEstimate. >>>> + */ >>>> + size = table_parallelscan_estimate(node->ss.ss_currentRelation, >>>> + estate->es_snapshot); >>>> + >>> >>> That seems quite grotty... >>> >> >> Yes. But what's a better solution? > > Think the way you've done it in v6 is better. > Thanks, I agree. I'll adjust the table_parallelscan_initialize to take the length as an extra argument. regards -- Tomas Vondra -
Re: EXPLAIN: showing ReadStream / prefetch stats
Andres Freund <andres@anarazel.de> — 2026-03-31T18:10:56Z
Hi, On 2026-03-31 15:16:57 +0200, Tomas Vondra wrote: > From 1bd80f66019aea773c2b6f46212172de592efb60 Mon Sep 17 00:00:00 2001 > From: Tomas Vondra <tomas@vondra.me> > Date: Tue, 31 Mar 2026 13:44:23 +0200 > Subject: [PATCH v6 2/5] switch explain to unaligned for json/xml/yaml LGTM. > From cd5437f5f49e84b8ae3f8cb8f0df4e605c1b5592 Mon Sep 17 00:00:00 2001 > From: Tomas Vondra <tomas@vondra.me> > Date: Tue, 31 Mar 2026 13:47:04 +0200 > Subject: [PATCH v6 3/5] explain: show prefetch stats in EXPLAIN (ANALYZE) > > This adds details about AIO / prefetch for executor nodes using a > ReadStream. Right now this applies only to BitmapHeapScan, because > that's the only scan node using a ReadStream and collecting > instrumentation from workers. s/Right now/As of this commit ... support for ... will be added in subsequent commits/ Not for now, but I wonder if we ought to introduce a REPRODUCIBLE option for EXPLAIN [ANALYZE] that sets BUFFERS, COSTS, IO, ... the right way for the regression tests, instead having to go through and change a gazillion tests every time. > diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c > index 20ff58aa782..5cfe8e24615 100644 > --- a/contrib/amcheck/verify_heapam.c > +++ b/contrib/amcheck/verify_heapam.c > @@ -477,7 +477,8 @@ verify_heapam(PG_FUNCTION_ARGS) > MAIN_FORKNUM, > stream_cb, > stream_data, > - 0); > + 0, > + NULL); > > while ((ctx.buffer = read_stream_next_buffer(stream, NULL)) != InvalidBuffer) > { Kinda wondering if we, instead of adding a NULL to a lot of places, should instead add a read_stream_enable_stats(stream, stats) or such. > --- a/src/backend/commands/explain.c > +++ b/src/backend/commands/explain.c > @@ -13,6 +13,8 @@ > */ > #include "postgres.h" > > +#include "access/genam.h" Why genam.h? > +#include "access/tableam.h" I'd probably include access/relscan.h instead. > +/* > + * show_scan_io_usage > + * show info about prefetching for a seq/bitmap scan > + * > + * Shows summary of stats for leader and workers (if any). > + */ > +static void > +show_scan_io_usage(ScanState *planstate, ExplainState *es) > +{ > + Plan *plan = planstate->ps.plan; > + IOStats stats; > + > + if (!es->io) > + return; > + > + /* scan not started or no prefetch stats */ > + if (!(planstate && > + planstate->ss_currentScanDesc && > + planstate->ss_currentScanDesc->rs_instrument)) > + return; > + > + /* Initialize counters with stats from the local process first */ > + switch (nodeTag(plan)) > + { That "local node first" comment looks a bit odd here, given that at that level it's for a block that does both? > + case T_BitmapHeapScan: > + { > + SharedBitmapHeapInstrumentation *sinstrument > + = ((BitmapHeapScanState *) planstate)->sinstrument; > + > + /* collect prefetch statistics from the read stream */ > + stats = planstate->ss_currentScanDesc->rs_instrument->io; > + > + /* > + * get the sum of the counters set within each and every > + * process > + */ > + if (sinstrument) > + { > + for (int i = 0; i < sinstrument->num_workers; ++i) > + { > + BitmapHeapScanInstrumentation *winstrument = &sinstrument->sinstrument[i]; > + > + AccumulateIOStats(&stats, &winstrument->stats.io); > + } I'm not entirely sure how useful it is to accumulate the stats from worker into the leader's stats. Doesn't that mean that the leader's stats can't ever be viewed in isolation? I'm also not really clear that it's very useful to just smush the stats of the different streams together. > +/* > + * show_io_usage > + * show info about I/O prefetching for a single worker > + * > + * Shows prefetching stats for a parallel scan worker. > + */ > +static void > +show_io_usage(PlanState *planstate, ExplainState *es, int worker) s/show_io_usage/show_worker_io_usage/? Yes, the other functions called in the per-worker block don't have _worker_ in them, but afaict they aren't specific to be used in the context of a worker. > +/* > + * Update stream stats with current pinned buffer depth. > + * > + * Called once per buffer returned to the consumer in read_stream_next_buffer(). > + * Records the number of pinned buffers at that moment, so we can compute the > + * average look-ahead depth. > + */ > +static inline void > +read_stream_count_prefetch(ReadStream *stream) > +{ > + IOStats *stats = stream->stats; > + > + if (stats == NULL) > + return; > + > + stats->prefetch_count++; > + stats->distance_sum += stream->pinned_buffers; > + if (stream->pinned_buffers > stats->distance_max) > + stats->distance_max = stream->pinned_buffers; I'd probably use Max(). > @@ -380,6 +435,9 @@ read_stream_start_pending_read(ReadStream *stream) > Assert(stream->ios_in_progress < stream->max_ios); > stream->ios_in_progress++; > stream->seq_blocknum = stream->pending_read_blocknum + nblocks; > + > + /* update I/O stats */ > + read_stream_count_io(stream, nblocks, stream->ios_in_progress); > } > > /* > @@ -544,7 +602,8 @@ read_stream_begin_impl(int flags, > ForkNumber forknum, > ReadStreamBlockNumberCB callback, > void *callback_private_data, > - size_t per_buffer_data_size) > + size_t per_buffer_data_size, > + IOStats *stats) > { > ReadStream *stream; > size_t size; > @@ -703,6 +762,11 @@ read_stream_begin_impl(int flags, > stream->seq_until_processed = InvalidBlockNumber; > stream->temporary = SmgrIsTemp(smgr); > > + /* set capacity */ > + stream->stats = stats; > + if (stream->stats) > + stream->stats->distance_capacity = max_pinned_buffers; > + The comments here don't seem to add much to me. > +/* --------------------- > + * Instrumentation information about read streams > + * --------------------- > + */ > +typedef struct IOStats > +{ > + /* number of buffers returned to consumer (for averaging distance) */ > + uint64 prefetch_count; > + > + /* sum of pinned_buffers sampled at each buffer return */ > + uint64 distance_sum; > + > + /* maximum actual pinned_buffers observed during the scan */ > + int16 distance_max; > + > + /* maximum possible look-ahead distance (max_pinned_buffers) */ > + int16 distance_capacity; > + > + /* number of waits for a read (for the I/O) */ > + uint64 wait_count; > + > + /* I/O stats */ > + uint64 io_count; /* number of I/Os */ > + uint64 io_nblocks; /* sum of blocks for all I/Os */ > + uint64 io_in_progress; /* sum of in-progress I/Os */ IO stats in a struct named IO stats ;) > /* --------------------- > * Instrumentation information for indexscans (amgettuple and amgetbitmap) > * --------------------- > */ > + > typedef struct IndexScanInstrumentation > { > /* Index search count (incremented with pgstat_count_index_scan call) */ Spurious new newline? > @@ -4068,6 +4068,27 @@ show_scan_io_usage(ScanState *planstate, ExplainState *es) > /* Initialize counters with stats from the local process first */ > switch (nodeTag(plan)) > { > + case T_SeqScan: > + { > + SharedSeqScanInstrumentation *sinstrument > + = ((SeqScanState *) planstate)->sinstrument; > + > + /* collect prefetch statistics from the read stream */ > + stats = planstate->ss_currentScanDesc->rs_instrument->io; > + > + /* get the sum of the counters set within each and every process */ > + if (sinstrument) > + { > + for (int i = 0; i < sinstrument->num_workers; ++i) > + { > + SeqScanInstrumentation *winstrument = &sinstrument->sinstrument[i]; > + > + AccumulateIOStats(&stats, &winstrument->stats.io); > + } > + } > + > + break; > + } > case T_BitmapHeapScan: > { > SharedBitmapHeapInstrumentation *sinstrument It's a bit sad how much repetition there is between the per-node support for each node type. Not sure there's a great solution though. Perhaps it'd be nicer if we instead sum up the stats in ExecSeqScanRetrieveInstrumentation() etc? > @@ -119,6 +119,13 @@ explain_filter > <Actual-Rows>N.N</Actual-Rows> > <Actual-Loops>N</Actual-Loops> > <Disabled>false</Disabled> > + <Average-Prefetch-Distance>N.N</Average-Prefetch-Distance> > + <Max-Prefetch-Distance>N</Max-Prefetch-Distance> > + <Prefetch-Capacity>N</Prefetch-Capacity> > + <I-O-Count>N</I-O-Count> > + <I-O-Waits>N</I-O-Waits> > + <Average-I-O-Size>N.N</Average-I-O-Size> > + <Average-I-Os-In-Progress>N.N</Average-I-Os-In-Progress> > <Shared-Hit-Blocks>N</Shared-Hit-Blocks> > <Shared-Read-Blocks>N</Shared-Read-Blocks> > <Shared-Dirtied-Blocks>N</Shared-Dirtied-Blocks> The I-O looks a bit ridiculous. But since people here seem to like I/O much better than IO (which I don't personally get), it's perhaps the right choice :(. I guess we also already have that precedent due to track_io_timing. Greetings, Andres Freund -
Re: EXPLAIN: showing ReadStream / prefetch stats
Andres Freund <andres@anarazel.de> — 2026-03-31T18:12:20Z
Hi, On 2026-03-31 20:03:02 +0200, Tomas Vondra wrote: > On 3/31/26 19:41, Andres Freund wrote: > > Hi, > > > > On 2026-03-30 20:21:29 +0200, Tomas Vondra wrote: > >>>> From 410eaaebe7b814ac9f44c080e153f4ec1d6d6b86 Mon Sep 17 00:00:00 2001 > >>>> From: Tomas Vondra <tomas@vondra.me> > >>>> Date: Thu, 19 Mar 2026 22:25:09 +0100 > >>>> Subject: [PATCH v5 3/6] explain: show prefetch stats in EXPLAIN (ANALYZE) > >>>> > >>>> This adds details about AIO / prefetch for executor nodes using a > >>>> ReadStream. Right now this applies only to BitmapHeapScan, because > >>>> that's the only scan node using a ReadStream and collecting > >>>> instrumentation from workers. > >>> > >>> I don't understand why that means it should be done as part of this commit, > >>> whereas seqscans shouldn't? > >>> > >> > >> Are you suggesting the commit adds support for all those scans (BHS, > >> SeqScan and TidRangeScan) or none of them? > > > > I guess I mostly just didn't quite understand what differentiates bitmap scans > > from the other scans, based on this explanation. > > > > > >> To me it seems better to have at least some scan because of testing. But > >> SeqScan/TidRangeScan don't have the instrumentation infrastructure for > >> parallel queries, and I don't want to do that in the main patch - it seems > >> rather unrelated. And I also don't want to add it before the main patch. > > > > I'd probably do the latter, i.e. add it before the main patch. Or at least > > separately from the change to show read stream instrumentation. > > > > Sorry, I'm confused. Which "latter" option you mean? Adding the instrumentation infrastructure for seqscan, tidscan as a separate patch before the main commit. Greetings, Andres Freund
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-03-31T18:33:15Z
On 3/31/26 20:12, Andres Freund wrote: > Hi, > > On 2026-03-31 20:03:02 +0200, Tomas Vondra wrote: >> On 3/31/26 19:41, Andres Freund wrote: >>> Hi, >>> >>> On 2026-03-30 20:21:29 +0200, Tomas Vondra wrote: >>>>>> From 410eaaebe7b814ac9f44c080e153f4ec1d6d6b86 Mon Sep 17 00:00:00 2001 >>>>>> From: Tomas Vondra <tomas@vondra.me> >>>>>> Date: Thu, 19 Mar 2026 22:25:09 +0100 >>>>>> Subject: [PATCH v5 3/6] explain: show prefetch stats in EXPLAIN (ANALYZE) >>>>>> >>>>>> This adds details about AIO / prefetch for executor nodes using a >>>>>> ReadStream. Right now this applies only to BitmapHeapScan, because >>>>>> that's the only scan node using a ReadStream and collecting >>>>>> instrumentation from workers. >>>>> >>>>> I don't understand why that means it should be done as part of this commit, >>>>> whereas seqscans shouldn't? >>>>> >>>> >>>> Are you suggesting the commit adds support for all those scans (BHS, >>>> SeqScan and TidRangeScan) or none of them? >>> >>> I guess I mostly just didn't quite understand what differentiates bitmap scans >>> from the other scans, based on this explanation. >>> >>> >>>> To me it seems better to have at least some scan because of testing. But >>>> SeqScan/TidRangeScan don't have the instrumentation infrastructure for >>>> parallel queries, and I don't want to do that in the main patch - it seems >>>> rather unrelated. And I also don't want to add it before the main patch. >>> >>> I'd probably do the latter, i.e. add it before the main patch. Or at least >>> separately from the change to show read stream instrumentation. >>> >> >> Sorry, I'm confused. Which "latter" option you mean? > > Adding the instrumentation infrastructure for seqscan, tidscan as a separate > patch before the main commit. > I don't like that, because then we're adding instrumentation that doesn't actually contain anything. How would you even know it does the right thing? I think it's better to add that after the main commit. regards -- Tomas Vondra
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-03-31T18:57:17Z
On 3/31/26 20:10, Andres Freund wrote: > Hi, > > On 2026-03-31 15:16:57 +0200, Tomas Vondra wrote: >> From 1bd80f66019aea773c2b6f46212172de592efb60 Mon Sep 17 00:00:00 2001 >> From: Tomas Vondra <tomas@vondra.me> >> Date: Tue, 31 Mar 2026 13:44:23 +0200 >> Subject: [PATCH v6 2/5] switch explain to unaligned for json/xml/yaml > > LGTM. > > >> From cd5437f5f49e84b8ae3f8cb8f0df4e605c1b5592 Mon Sep 17 00:00:00 2001 >> From: Tomas Vondra <tomas@vondra.me> >> Date: Tue, 31 Mar 2026 13:47:04 +0200 >> Subject: [PATCH v6 3/5] explain: show prefetch stats in EXPLAIN (ANALYZE) >> >> This adds details about AIO / prefetch for executor nodes using a >> ReadStream. Right now this applies only to BitmapHeapScan, because >> that's the only scan node using a ReadStream and collecting >> instrumentation from workers. > > s/Right now/As of this commit ... support for ... will be added in subsequent commits/ > OK > > Not for now, but I wonder if we ought to introduce a REPRODUCIBLE option for > EXPLAIN [ANALYZE] that sets BUFFERS, COSTS, IO, ... the right way for the > regression tests, instead having to go through and change a gazillion tests > every time. > Yes, having an alias for options that we know are stable sounds useful. > > >> diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c >> index 20ff58aa782..5cfe8e24615 100644 >> --- a/contrib/amcheck/verify_heapam.c >> +++ b/contrib/amcheck/verify_heapam.c >> @@ -477,7 +477,8 @@ verify_heapam(PG_FUNCTION_ARGS) >> MAIN_FORKNUM, >> stream_cb, >> stream_data, >> - 0); >> + 0, >> + NULL); >> >> while ((ctx.buffer = read_stream_next_buffer(stream, NULL)) != InvalidBuffer) >> { > > Kinda wondering if we, instead of adding a NULL to a lot of places, should > instead add a > read_stream_enable_stats(stream, stats) > or such. > I thought about that too, and I'll probably do it that way. I didn't do that because having too many helpers is not great, but it seems reasonable in this case. > >> --- a/src/backend/commands/explain.c >> +++ b/src/backend/commands/explain.c >> @@ -13,6 +13,8 @@ >> */ >> #include "postgres.h" >> >> +#include "gaccess/genam.h" > > Why genam.h? > > >> +#include "access/tableam.h" > > I'd probably include access/relscan.h instead. > Ooops, this is a residue of the initial approach of doing this through the table AM callbacks. As you say, relscan.h seems enough. > > >> +/* >> + * show_scan_io_usage >> + * show info about prefetching for a seq/bitmap scan >> + * >> + * Shows summary of stats for leader and workers (if any). >> + */ >> +static void >> +show_scan_io_usage(ScanState *planstate, ExplainState *es) >> +{ >> + Plan *plan = planstate->ps.plan; >> + IOStats stats; >> + >> + if (!es->io) >> + return; >> + >> + /* scan not started or no prefetch stats */ >> + if (!(planstate && >> + planstate->ss_currentScanDesc && >> + planstate->ss_currentScanDesc->rs_instrument)) >> + return; >> + >> + /* Initialize counters with stats from the local process first */ >> + switch (nodeTag(plan)) >> + { > > That "local node first" comment looks a bit odd here, given that at that level > it's for a block that does both? > Hmmm, yeah. That comment is a bit misplaced / incomplete with the current code. Will reword. > >> + case T_BitmapHeapScan: >> + { >> + SharedBitmapHeapInstrumentation *sinstrument >> + = ((BitmapHeapScanState *) planstate)->sinstrument; >> + >> + /* collect prefetch statistics from the read stream */ >> + stats = planstate->ss_currentScanDesc->rs_instrument->io; >> + >> + /* >> + * get the sum of the counters set within each and every >> + * process >> + */ >> + if (sinstrument) >> + { >> + for (int i = 0; i < sinstrument->num_workers; ++i) >> + { >> + BitmapHeapScanInstrumentation *winstrument = &sinstrument->sinstrument[i]; >> + >> + AccumulateIOStats(&stats, &winstrument->stats.io); >> + } > > I'm not entirely sure how useful it is to accumulate the stats from worker > into the leader's stats. Doesn't that mean that the leader's stats can't ever > be viewed in isolation? I'm also not really clear that it's very useful to > just smush the stats of the different streams together. > This is modeled after show_indexscan_info, which does it like that. Maybe it's not what we should do for read streams, ofc. You're right it means the leader stats can't be viewed in isolation, but we also don't show the worker stats without VERBOSE, so if we didn't accumulate the stats like this we'd not get the worker I/O stats at all. I think merging the stats like this is needed for the same reason. Maybe we should accumulate only without VERBOSE? Then we'd show the complete data for EXPLAIN (ANALYZE), and per-process data (both for the leader and workers) with EXPLAIN (ANALYZE, VERBOSE)? >> +/* --------------------- >> + * Instrumentation information about read streams >> + * --------------------- >> + */ >> +typedef struct IOStats >> +{ >> + /* number of buffers returned to consumer (for averaging distance) */ >> + uint64 prefetch_count; >> + >> + /* sum of pinned_buffers sampled at each buffer return */ >> + uint64 distance_sum; >> + >> + /* maximum actual pinned_buffers observed during the scan */ >> + int16 distance_max; >> + >> + /* maximum possible look-ahead distance (max_pinned_buffers) */ >> + int16 distance_capacity; >> + >> + /* number of waits for a read (for the I/O) */ >> + uint64 wait_count; >> + >> + /* I/O stats */ >> + uint64 io_count; /* number of I/Os */ >> + uint64 io_nblocks; /* sum of blocks for all I/Os */ >> + uint64 io_in_progress; /* sum of in-progress I/Os */ > > IO stats in a struct named IO stats ;) > Old MacDonald had a farm, IO IO IO ... > > >> @@ -4068,6 +4068,27 @@ show_scan_io_usage(ScanState *planstate, ExplainState *es) >> /* Initialize counters with stats from the local process first */ >> switch (nodeTag(plan)) >> { >> + case T_SeqScan: >> + { >> + SharedSeqScanInstrumentation *sinstrument >> + = ((SeqScanState *) planstate)->sinstrument; >> + >> + /* collect prefetch statistics from the read stream */ >> + stats = planstate->ss_currentScanDesc->rs_instrument->io; >> + >> + /* get the sum of the counters set within each and every process */ >> + if (sinstrument) >> + { >> + for (int i = 0; i < sinstrument->num_workers; ++i) >> + { >> + SeqScanInstrumentation *winstrument = &sinstrument->sinstrument[i]; >> + >> + AccumulateIOStats(&stats, &winstrument->stats.io); >> + } >> + } >> + >> + break; >> + } >> case T_BitmapHeapScan: >> { >> SharedBitmapHeapInstrumentation *sinstrument > > It's a bit sad how much repetition there is between the per-node support for > each node type. Not sure there's a great solution though. > > Perhaps it'd be nicer if we instead sum up the stats in > ExecSeqScanRetrieveInstrumentation() etc? > The duplication is not great, but wouldn't doing it in ExecSeqScanRetrieveInstrumentation just move the duplicated code elsewhere? I don't see how that'd be an improvement. >> @@ -119,6 +119,13 @@ explain_filter >> <Actual-Rows>N.N</Actual-Rows> >> <Actual-Loops>N</Actual-Loops> >> <Disabled>false</Disabled> >> + <Average-Prefetch-Distance>N.N</Average-Prefetch-Distance> >> + <Max-Prefetch-Distance>N</Max-Prefetch-Distance> >> + <Prefetch-Capacity>N</Prefetch-Capacity> >> + <I-O-Count>N</I-O-Count> >> + <I-O-Waits>N</I-O-Waits> >> + <Average-I-O-Size>N.N</Average-I-O-Size> >> + <Average-I-Os-In-Progress>N.N</Average-I-Os-In-Progress> >> <Shared-Hit-Blocks>N</Shared-Hit-Blocks> >> <Shared-Read-Blocks>N</Shared-Read-Blocks> >> <Shared-Dirtied-Blocks>N</Shared-Dirtied-Blocks> > > The I-O looks a bit ridiculous. But since people here seem to like I/O much > better than IO (which I don't personally get), it's perhaps the right choice > :(. I guess we also already have that precedent due to track_io_timing. > Uh, I didn't realize the XML transforms it like this. I'm not all that attached to using "I/O" (even though it's the correct spelling!), but as you say, track_io_timing already has the same issue. regards -- Tomas Vondra -
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-04-02T15:05:34Z
Attached is v7, with a couple adjustments based on the reviews. The preliminary patches (beginscan flags and WaitReadStream returning if it had to wait) were already committed, so there are no dependencies for this patch series. - The main change is an introduction of read_stream_enable_stats(), which installs the IOStats pointer into an existing stream. That way we don't need a new argument for read_stream_begin_relation(), which makes the patch quite a bit smaller. I like this. - Reworded a couple comments, the commit message, etc. I also noticed a new problem. The CI reported failures on FreeBSD, showing a weird difference in the explain output. For a while I was really confused, because it seemed like some sort of flakiness. But it turned out to be because of debug_parallel_query=regress (which the FreeBSD has enabled, unlike the other configs). That makes all queries to run as if under a Gather node, but with parallel_aware=false. But in this case the nodes may not get the instrumentation initialized, because execParallel.c checks plan->parallel_aware before initializing the DSM and worker. So there's no instrumentation, and in that case explain does not show the stats at all. This is what caused the regression failure on FreeBSD, because the queries outputing XML/JSON/YAML use a seqscan. Fixing this for seqscan/tidrangescan sees simple enough - we need to call the function even with parallel_aware=false, and then do check from the functions to terminate early if (!parallel_aware). This is how indexscans already do that, so I modeled it after that. I left it in separate commits 0004 and 0006, to make it more obvious. This works, but it's a bit ugly. At least for me it makes the code much less readable, unfortunately. I'm not sure how else to do this, though. There's another issue, though. BitmapHeapScan has the same issue, and needs a similar fix, but I failed to make that work. Apparently there are some dependencies on having the DSA that I don't understand, which means I get segfaults if I just "copy" the indexscan approach. FWIW this is not a new issue, it affects BitmapHeapScan already. Not the crashes, but not showing the stats sometimes. If the queries in explain.sql used BHS instead of a seqscan, it'd fail on CI in almost the same way. For a while I thought it only affects debug_parallel_query=regress, in which case maybe the best solution would be to special-case that somehow. But that's not really true. With a parallel query, the outer side of the plan will have parallel_aware=false, and so will be missing the instrumentation. Attached is a simple reproducer, forcing a parallel join with a BHS on both sides. Notice that the explain.log (from master) shows "Heap Blocks" only for the inner side, while the outer side has nothing. I suppose we should fix that, but considering no one complained about it so far (at least I'm not aware of any reports) ... regards -- Tomas Vondra
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-04-03T19:01:05Z
Here's v8, with a couple improvements: 1) stats for non-parallel-aware scans It correctly shows stats for scans that are not parallel-aware. I'd argue for BitmapHeapScan this is a live bug in 5a1e6df3b84c, so I kept it in a separate commit 0002. It probably should be backpatched to PG18, even if no one complained about it. For SeqScan/TidRangeScan I've merged the changes into 0004 and 0005. I have to admit I'm not a huge fan of the changes required to fix this. The logic with all these instument/parallel_aware conditions seems much less readable to me, but I don't know how else to do this. The approach is copied from nodeIndexscan.c, which already did that in 0fbceae841cb. 2) worker stats While working on fixing the BHS, I realized it prints the worker stats using ExplainOpenWorker/ExplainCloseWorker, which puts-aside the worker stats. Which is better than the original approach - it does not require a separate function (with a lot of duplication). The bad thing is this gives us even less control over the order of items in the explain output. I'm mostly happy with this, except for the decreased readability due to the various checks of instrument/parallel_aware. I'm also attaching SQL scripts for the three scans, with plans that include them in places with parallel_aware=false. All plans should have the relevant explain stats for all the scans. regards -- Tomas Vondra
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Melanie Plageman <melanieplageman@gmail.com> — 2026-04-05T22:18:28Z
On Fri, Apr 3, 2026 at 3:01 PM Tomas Vondra <tomas@vondra.me> wrote: > > 1) stats for non-parallel-aware scans > > It correctly shows stats for scans that are not parallel-aware. <--snip--> > For SeqScan/TidRangeScan I've merged the changes into 0004 and 0005. > > I have to admit I'm not a huge fan of the changes required to fix this. > The logic with all these instument/parallel_aware conditions seems much > less readable to me, but I don't know how else to do this. The approach > is copied from nodeIndexscan.c, which already did that in 0fbceae841cb. While this is the opposite direction of what I suggested to fix BHS in [1], what if we allocated the instrumentation and parallel-aware state separately and accessed them with their own keys? It's a little janky because what key could we use besides the plan_node_id, but if we add a key-sized offset to the plan node id, we can functionally have two separate keys. We could do this for all nodes that have both shared instrumentation and parallel-aware shared state. It introduces some boilerplate functions, but I do find it easier to understand. I've attached patches that implement this to make the idea more clear. 0002 does the two allocations for the existing index[-only] scan nodes. 0003 uses this method to fix BHS. 0004 is your patch to add prefetch stuff to explain and show it for BHS. And 0005 and 0006 are using the two allocation approach for seqscan and tidrange scan. There were some test changes you had that didn't seem required to make tests pass in the commits that I stuffed into 0007. I used an LLM to do some of the boilerplate and rebasing, so there might be some weirdness in there. If we don't do the above, then I think your current approach is the only other realistic option. We can't do what I suggested for BHS in [1] and always allocate the parallel-aware state because that state is much larger for sequential scans and TID range scans. - Melanie [1] https://www.postgresql.org/message-id/CAAKRu_a_c8HAtJ8Ynz-dU%3DJb2PzheW0zWME6A1BB9jQ62DMZBg%40mail.gmail.com
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-04-05T22:46:04Z
On 4/6/26 00:18, Melanie Plageman wrote: > On Fri, Apr 3, 2026 at 3:01 PM Tomas Vondra <tomas@vondra.me> wrote: >> >> 1) stats for non-parallel-aware scans >> >> It correctly shows stats for scans that are not parallel-aware. > <--snip--> >> For SeqScan/TidRangeScan I've merged the changes into 0004 and 0005. >> >> I have to admit I'm not a huge fan of the changes required to fix this. >> The logic with all these instument/parallel_aware conditions seems much >> less readable to me, but I don't know how else to do this. The approach >> is copied from nodeIndexscan.c, which already did that in 0fbceae841cb. > > While this is the opposite direction of what I suggested to fix BHS in > [1], what if we allocated the instrumentation and parallel-aware state > separately and accessed them with their own keys? It's a little janky > because what key could we use besides the plan_node_id, but if we add > a key-sized offset to the plan node id, we can functionally have two > separate keys. > Interesting idea! That'd mean we don't need to mess with the instrument/parallel_aware flags in the functions. That seems like an improvement, it made the code ... not nice. Presumably, we'd only do this in master? It seems way too invasive to backpatch (and for index scans it'd even be an ABI break, so we can't do that). Moreover, for index scans it's not even a bug, and it does not seem great to do it one way for BHS and a completely different way in index scans. So we'd either not fix BHS in backbranches, or do it the "ugly" way. > We could do this for all nodes that have both shared instrumentation > and parallel-aware shared state. It introduces some boilerplate > functions, but I do find it easier to understand. > > I've attached patches that implement this to make the idea more clear. > 0002 does the two allocations for the existing index[-only] scan > nodes. 0003 uses this method to fix BHS. 0004 is your patch to add > prefetch stuff to explain and show it for BHS. And 0005 and 0006 are > using the two allocation approach for seqscan and tidrange scan. There > were some test changes you had that didn't seem required to make tests > pass in the commits that I stuffed into 0007. I used an LLM to do some > of the boilerplate and rebasing, so there might be some weirdness in > there. Thanks! It's a bit late for me to experiment with this, but I'll take a closer look tomorrow. We're getting pretty clear to feature freeze, and these seem like big changes ... > > If we don't do the above, then I think your current approach is the > only other realistic option. We can't do what I suggested for BHS in > [1] and always allocate the parallel-aware state because that state is > much larger for sequential scans and TID range scans. > Yeah. I was wondering about these costs when you proposed to allocate the BHS parallel state always. I concluded it does not matter for BHS, but for scans with larger states it might be different. regards -- Tomas Vondra
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Melanie Plageman <melanieplageman@gmail.com> — 2026-04-05T22:50:25Z
On Sun, Apr 5, 2026 at 6:46 PM Tomas Vondra <tomas@vondra.me> wrote: > > On 4/6/26 00:18, Melanie Plageman wrote: > > > While this is the opposite direction of what I suggested to fix BHS in > > [1], what if we allocated the instrumentation and parallel-aware state > > separately and accessed them with their own keys? It's a little janky > > because what key could we use besides the plan_node_id, but if we add > > a key-sized offset to the plan node id, we can functionally have two > > separate keys. > > Presumably, we'd only do this in master? It seems way too invasive to > backpatch (and for index scans it'd even be an ABI break, so we can't do > that). Moreover, for index scans it's not even a bug, and it does not > seem great to do it one way for BHS and a completely different way in > index scans. > > So we'd either not fix BHS in backbranches, or do it the "ugly" way. I think we could backpatch the BHS patch I posted in the other thread that always allocated pstate. It's a very small diff and seems quite low risk to me. Though that struct changed a lot in 17 I think (courtesy of me), so backpatching might be a little bit of a headache in earlier versions. > > If we don't do the above, then I think your current approach is the > > only other realistic option. We can't do what I suggested for BHS in > > [1] and always allocate the parallel-aware state because that state is > > much larger for sequential scans and TID range scans. > > Yeah. I was wondering about these costs when you proposed to allocate > the BHS parallel state always. I concluded it does not matter for BHS, > but for scans with larger states it might be different. Yea, I think ParallelBitmapHeapState is like 48 bytes or something - Melanie
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-04-06T09:14:58Z
On 4/6/26 00:50, Melanie Plageman wrote: > On Sun, Apr 5, 2026 at 6:46 PM Tomas Vondra <tomas@vondra.me> wrote: >> >> On 4/6/26 00:18, Melanie Plageman wrote: >> >>> While this is the opposite direction of what I suggested to fix BHS in >>> [1], what if we allocated the instrumentation and parallel-aware state >>> separately and accessed them with their own keys? It's a little janky >>> because what key could we use besides the plan_node_id, but if we add >>> a key-sized offset to the plan node id, we can functionally have two >>> separate keys. >> >> Presumably, we'd only do this in master? It seems way too invasive to >> backpatch (and for index scans it'd even be an ABI break, so we can't do >> that). Moreover, for index scans it's not even a bug, and it does not >> seem great to do it one way for BHS and a completely different way in >> index scans. >> >> So we'd either not fix BHS in backbranches, or do it the "ugly" way. > > I think we could backpatch the BHS patch I posted in the other thread > that always allocated pstate. It's a very small diff and seems quite > low risk to me. Though that struct changed a lot in 17 I think > (courtesy of me), so backpatching might be a little bit of a headache > in earlier versions. > Makes sense. >>> If we don't do the above, then I think your current approach is the >>> only other realistic option. We can't do what I suggested for BHS in >>> [1] and always allocate the parallel-aware state because that state is >>> much larger for sequential scans and TID range scans. >> >> Yeah. I was wondering about these costs when you proposed to allocate >> the BHS parallel state always. I concluded it does not matter for BHS, >> but for scans with larger states it might be different. > > Yea, I think ParallelBitmapHeapState is like 48 bytes or something > Right. TBH after thinking about it I can't imagine this being a serious issue even for larger states, considering how expensive it is to setup parallel workers etc. That'd just dwarf this cost, I think. Anyway, that doesn't matter much, because the more I look at the approach with having a separate chunk of shared memory, the more I like it. It seems much simpler, more elegant, etc. I really disliked how unreadable the code got with the parallel_aware/instrument checks in multiple places, and this just gets rid of that. I like that. I was worried it might be adding a non-trivial amount of overhead, but after refreshing my memory of how the shm works, I think there's no change at all. Is execParallel.h the right place to define the offset? It means the various nodes (like nodeBitmapHeapScan) now have to include this header, and it seems a bit suspicious. I can't think of a better .h file, and maybe I'm wrong and it's perfectly fine. Regarding plan_node_id - I think the offset works fine for now. It effectively gives us 2 IDs per node. The only alternative I can think of is having nodes "request" how many IDs will be needed - most nodes would say "1", nodes with instrumentation would say "2", etc. In the future we might get a node that needs 3+ shm chunks. I don't think we need to do all that now. regards -- Tomas Vondra
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Melanie Plageman <melanieplageman@gmail.com> — 2026-04-06T14:01:47Z
On Mon, Apr 6, 2026 at 5:15 AM Tomas Vondra <tomas@vondra.me> wrote: > > Anyway, that doesn't matter much, because the more I look at the > approach with having a separate chunk of shared memory, the more I like > it. It seems much simpler, more elegant, etc. I really disliked how > unreadable the code got with the parallel_aware/instrument checks in > multiple places, and this just gets rid of that. I like that. > <--snip--> > > Is execParallel.h the right place to define the offset? It means the > various nodes (like nodeBitmapHeapScan) now have to include this header, > and it seems a bit suspicious. I can't think of a better .h file, and > maybe I'm wrong and it's perfectly fine. It could go in src/include/executor is instrument_node.h. It's where the structs for the shared instrumentation go. It's probably cheaper to include also because it doesn't include anything. > Regarding plan_node_id - I think the offset works fine for now. It > effectively gives us 2 IDs per node. The only alternative I can think of > is having nodes "request" how many IDs will be needed - most nodes would > say "1", nodes with instrumentation would say "2", etc. In the future we > might get a node that needs 3+ shm chunks. I don't think we need to do > all that now. Makes sense to me. - Melanie
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Melanie Plageman <melanieplageman@gmail.com> — 2026-04-06T16:50:58Z
On Mon, Apr 6, 2026 at 10:01 AM Melanie Plageman <melanieplageman@gmail.com> wrote: > > On Mon, Apr 6, 2026 at 5:15 AM Tomas Vondra <tomas@vondra.me> wrote: > > > > Anyway, that doesn't matter much, because the more I look at the > > approach with having a separate chunk of shared memory, the more I like > > it. It seems much simpler, more elegant, etc. I really disliked how > > unreadable the code got with the parallel_aware/instrument checks in > > multiple places, and this just gets rid of that. I like that. > > > <--snip--> > > > > Is execParallel.h the right place to define the offset? It means the > > various nodes (like nodeBitmapHeapScan) now have to include this header, > > and it seems a bit suspicious. I can't think of a better .h file, and > > maybe I'm wrong and it's perfectly fine. > > It could go in src/include/executor is instrument_node.h. It's where > the structs for the shared instrumentation go. It's probably cheaper > to include also because it doesn't include anything. I cleaned up the first patch in the set that refactors index-only and index scan to use this pattern and realized that I wasn't sure what to do about the duplication between index-only and index scans for these functions. ExecIndexScanInstrumentEstimate() and ExecIndexOnlyScanInstrumentEstimate() are the same code except they take different node types to do the check for intsrumentation if (!node->ss.ps.instrument || pcxt->nworkers == 0) return; The size estimation for SharedIndexScanIntrumentation could be common to the two but it is only three lines and doesn't seem worth another function call. ExecIndex[Only]ScanEstimate() use a common helper index_parallelscan_estimate() but that is more code. We could just have a common helper and only call it if node->ss.ps.instrument && pcxt->nworkers > 0 but I don't see anybody else in ExecParallelEstimate() checking for instrumentation being enabled. Besides that, the function names are very long already but don't include the word parallel (though ExecIndexScanEstimate() is for parallel scans only and doesn't include "parallel"). ExecIndex[Only]ScanInstrumentInitDSM() is the same between the two except the node type it takes but three different members of node are used here, so I'm not sure how much sense a common helper makes here. I have a feeling the above stuff isn't okay how it is now, but I'm not sure which direction to go. I think ExecIndex[Only]ScanInstrumentInitWorker() is fine the way it is because it is only 1 line and requires a different node for index/index-only. - Melanie -
Re: EXPLAIN: showing ReadStream / prefetch stats
Melanie Plageman <melanieplageman@gmail.com> — 2026-04-06T20:33:16Z
On Mon, Apr 6, 2026 at 12:50 PM Melanie Plageman <melanieplageman@gmail.com> wrote: > > I cleaned up the first patch in the set that refactors index-only and > index scan to use this pattern and realized that I wasn't sure what to > do about the duplication between index-only and index scans for these > functions. I changed my mind about the duplication: I think it's fine because there just aren't that many lines of code. Attached v10 has a preparatory commit that adds an assert-only check to shm_toc_insert() to make sure the key hasn't already been inserted. I thought it would be helpful since many of the magic numbers for parallel executor communication are spread around. - Melanie
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-04-06T20:39:08Z
On 4/6/26 18:50, Melanie Plageman wrote: > On Mon, Apr 6, 2026 at 10:01 AM Melanie Plageman > <melanieplageman@gmail.com> wrote: >> >> On Mon, Apr 6, 2026 at 5:15 AM Tomas Vondra <tomas@vondra.me> wrote: >>> >>> Anyway, that doesn't matter much, because the more I look at the >>> approach with having a separate chunk of shared memory, the more I like >>> it. It seems much simpler, more elegant, etc. I really disliked how >>> unreadable the code got with the parallel_aware/instrument checks in >>> multiple places, and this just gets rid of that. I like that. >>> >> <--snip--> >>> >>> Is execParallel.h the right place to define the offset? It means the >>> various nodes (like nodeBitmapHeapScan) now have to include this header, >>> and it seems a bit suspicious. I can't think of a better .h file, and >>> maybe I'm wrong and it's perfectly fine. >> >> It could go in src/include/executor is instrument_node.h. It's where >> the structs for the shared instrumentation go. It's probably cheaper >> to include also because it doesn't include anything. > > I cleaned up the first patch in the set that refactors index-only and > index scan to use this pattern and realized that I wasn't sure what to > do about the duplication between index-only and index scans for these > functions. > Thanks! > ExecIndexScanInstrumentEstimate() and > ExecIndexOnlyScanInstrumentEstimate() are the same code except they > take different node types to do the check for intsrumentation > if (!node->ss.ps.instrument || pcxt->nworkers == 0) > return; > The size estimation for SharedIndexScanIntrumentation could be common > to the two but it is only three lines and doesn't seem worth another > function call. ExecIndex[Only]ScanEstimate() use a common helper > index_parallelscan_estimate() but that is more code. Seems like a reasonable trade off to me. It's way more readable without the conditions, even if there's a bit more code than before. To me this seems fine. > We could just have a common helper and only call it if > node->ss.ps.instrument && pcxt->nworkers > 0 but I don't see anybody > else in ExecParallelEstimate() checking for instrumentation being > enabled. > Not sure I understand what helper you mean? In execParallel.c? AFAICS now we have 3 scans that check this (index, index-only, BHS). With the other patches there will be SeqScan and TidRangeScan too. > Besides that, the function names are very long already but don't > include the word parallel (though ExecIndexScanEstimate() is for > parallel scans only and doesn't include "parallel"). > > ExecIndex[Only]ScanInstrumentInitDSM() is the same between the two > except the node type it takes but three different members of node are > used here, so I'm not sure how much sense a common helper makes here. > > I have a feeling the above stuff isn't okay how it is now, but I'm not > sure which direction to go. > > I think ExecIndex[Only]ScanInstrumentInitWorker() is fine the way it > is because it is only 1 line and requires a different node for > index/index-only. > I think it's OK. Maybe there is a scheme that makes this work both for plain index scans and index-only scans, but I doubt it's much simpler in the end. Doesn't seem worth it to me. I think this is ready to go, with a tiny amount of polishing. 1) "amount of DSM" sounds a bit strange to me. The wording "amount of space in ..." from the other nodes seems better to me. Or maybe I'm just used to it, not sure. 2) I wonder if maybe PARALLEL_KEY_SCAN_INSTRUMENT_OFFSET should be placed in plannodes.h, because that's where Plan->plan_node_id is defined. instrument_node.h works too, but the places accessing the plan_node_id already have to include plannodes.h. Other than this, I think it's ready to go. I went over it multiple times, but I don't have any new comments. We need to decide whether to push this into PG19. This was primarily motivated by the index prefetching work, but we now know that won't happen in PG19 :-( But the instrumentation is useful even for the three scans using read streams, so I think it's a meaningful improvement. If you think you can get this pushed, I'll do my best to finalize the instrumentation and SeqScan/TidRangeScan parts. regards -- Tomas Vondra
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Melanie Plageman <melanieplageman@gmail.com> — 2026-04-07T00:19:06Z
On Mon, Apr 6, 2026 at 4:39 PM Tomas Vondra <tomas@vondra.me> wrote: > > On 4/6/26 18:50, Melanie Plageman wrote: > > > I cleaned up the first patch in the set that refactors index-only and > > index scan to use this pattern and realized that I wasn't sure what to > > do about the duplication between index-only and index scans for these > > functions. > <--snip--> > > I think this is ready to go, with a tiny amount of polishing. > > 1) "amount of DSM" sounds a bit strange to me. The wording "amount of > space in ..." from the other nodes seems better to me. Or maybe I'm just > used to it, not sure. Fixed that before pushing. > 2) I wonder if maybe PARALLEL_KEY_SCAN_INSTRUMENT_OFFSET should be > placed in plannodes.h, because that's where Plan->plan_node_id is > defined. instrument_node.h works too, but the places accessing the > plan_node_id already have to include plannodes.h. I don't think it really belongs there. It is very specific to execution. We use the plan node id as the key for convenience, but it isn't the purpose of plan_node_id. PARALLEL_KEY_SCAN_INSTRUMENT_OFFSET's only purpose is to be used during execution for instrumentation, so it feels like it belongs in node_instrument.h. And we don't need a separate include for it either. It goes with the other stuff being defined in instrument_node.h (like SharedIndexScanInstrumentation) and being used by those callers. I admit the comment above it is a bit odd, but I think it is ultimately okay. > We need to decide whether to push this into PG19. This was primarily > motivated by the index prefetching work, but we now know that won't > happen in PG19 :-( But the instrumentation is useful even for the three > scans using read streams, so I think it's a meaningful improvement. I think it is a meaningful improvement too. I think we should do it. > If you think you can get this pushed, I'll do my best to finalize the > instrumentation and SeqScan/TidRangeScan parts. I've pushed the first patch for index/index-only scans. Attached is the BHS fix that uses the new pattern. It needs at least one review before pushing. While I was polishing it, I realized I neglected to use add_size()/mul_size() in the index-only/index scan patches. So, 0002 is just a fix commit to do that. Feel free to push these if you think they're ready. Otherwise, I'll do so pending your review in my morning. - Melanie
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-04-07T01:14:49Z
On 4/7/26 02:19, Melanie Plageman wrote: > On Mon, Apr 6, 2026 at 4:39 PM Tomas Vondra <tomas@vondra.me> wrote: >> >> On 4/6/26 18:50, Melanie Plageman wrote: >> >>> I cleaned up the first patch in the set that refactors index-only and >>> index scan to use this pattern and realized that I wasn't sure what to >>> do about the duplication between index-only and index scans for these >>> functions. >> > <--snip--> >> >> I think this is ready to go, with a tiny amount of polishing. >> >> 1) "amount of DSM" sounds a bit strange to me. The wording "amount of >> space in ..." from the other nodes seems better to me. Or maybe I'm just >> used to it, not sure. > > Fixed that before pushing. > >> 2) I wonder if maybe PARALLEL_KEY_SCAN_INSTRUMENT_OFFSET should be >> placed in plannodes.h, because that's where Plan->plan_node_id is >> defined. instrument_node.h works too, but the places accessing the >> plan_node_id already have to include plannodes.h. > > I don't think it really belongs there. It is very specific to > execution. We use the plan node id as the key for convenience, but it > isn't the purpose of plan_node_id. > PARALLEL_KEY_SCAN_INSTRUMENT_OFFSET's only purpose is to be used > during execution for instrumentation, so it feels like it belongs in > node_instrument.h. And we don't need a separate include for it either. > It goes with the other stuff being defined in instrument_node.h (like > SharedIndexScanInstrumentation) and being used by those callers. I > admit the comment above it is a bit odd, but I think it is ultimately > okay. > WFM >> We need to decide whether to push this into PG19. This was primarily >> motivated by the index prefetching work, but we now know that won't >> happen in PG19 :-( But the instrumentation is useful even for the three >> scans using read streams, so I think it's a meaningful improvement. > > I think it is a meaningful improvement too. I think we should do it. OK >> If you think you can get this pushed, I'll do my best to finalize the >> instrumentation and SeqScan/TidRangeScan parts. > > I've pushed the first patch for index/index-only scans. Attached is > the BHS fix that uses the new pattern. It needs at least one review > before pushing. While I was polishing it, I realized I neglected to > use add_size()/mul_size() in the index-only/index scan patches. So, > 0002 is just a fix commit to do that. Feel free to push these if you > think they're ready. Otherwise, I'll do so pending your review in my > morning. > Thanks! I'll take a look in my morning, and will consider pushing the changes so that I can start pushing the parts adding the new instrumentation. I've spent a fair amount of time looking at those parts today. Attached is v12 (including the two parts you posted as v11), with two or three bigger changes compared to earlier versions: 1) default OFF After thinking about it a bit more, I decided to change the default to OFF. On the one hand I agree it's somewhat similar to BUFFERS, and that option is ON by default now. But on the other hand, we must not clutter EXPLAIN output with too much information, and I'm not convinced IO is worth it. So I changed to OFF by default. That also makes the patches smaller, due to not having to adjust that many tests. 2) auto_explain It also occurred to me we should add a matching option to auto_explain, similarly to log_buffers. 0005 does that. 3) INSTRUMENT_IO The auto_explain bit also implies we need a new INSTRUMENT_IO constant, to handle this just like BUFFERS. Which also means we can actually collect the stats only when the IO option is specified (instead of for all ANALYZE runs). Which is nice. I plan to do some more testing in my morning, add missing comments, polish the commit messages etc. And then eventually push it. regards -- Tomas Vondra
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Lukas Fittl <lukas@fittl.com> — 2026-04-07T08:00:28Z
On Mon, Apr 6, 2026 at 6:14 PM Tomas Vondra <tomas@vondra.me> wrote: > > On 4/7/26 02:19, Melanie Plageman wrote: > > On Mon, Apr 6, 2026 at 4:39 PM Tomas Vondra <tomas@vondra.me> wrote: > >> > >> On 4/6/26 18:50, Melanie Plageman wrote: > >> If you think you can get this pushed, I'll do my best to finalize the > >> instrumentation and SeqScan/TidRangeScan parts. > > > > I've pushed the first patch for index/index-only scans. Attached is > > the BHS fix that uses the new pattern. It needs at least one review > > before pushing. While I was polishing it, I realized I neglected to > > use add_size()/mul_size() in the index-only/index scan patches. So, > > 0002 is just a fix commit to do that. Feel free to push these if you > > think they're ready. Otherwise, I'll do so pending your review in my > > morning. > > > > Thanks! > > I'll take a look in my morning, and will consider pushing the changes so > that I can start pushing the parts adding the new instrumentation. FWIW, 0001 and 0002 in the v12 patch set also make sense to me from a code review just now. > > I've spent a fair amount of time looking at those parts today. Attached > is v12 (including the two parts you posted as v11) 0003 looks good, but commit message needs improvement / justification of the change (I assume you had already planned that). 0004: > diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml > index 5b8b521802e..a854c41e963 100644 > --- a/doc/src/sgml/ref/explain.sgml > +++ b/doc/src/sgml/ref/explain.sgml > @@ -46,6 +46,7 @@ EXPLAIN [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] <rep > TIMING [ <replaceable class="parameter">boolean</replaceable> ] > SUMMARY [ <replaceable class="parameter">boolean</replaceable> ] > MEMORY [ <replaceable class="parameter">boolean</replaceable> ] > + IO [ <replaceable class="parameter">boolean</replaceable> ] > FORMAT { TEXT | XML | JSON | YAML } > </synopsis> > </refsynopsisdiv> > @@ -298,6 +299,17 @@ ROLLBACK; > </listitem> > </varlistentry> > + <varlistentry> > + <term><literal>IO</literal></term> > + <listitem> > + <para> > + Include information on I/O performed by each node. > + This parameter may only be used when <literal>ANALYZE</literal> is also > + enabled. It defaults to <literal>FALSE</literal>. > + </para> > + </listitem> > + </varlistentry> + Assuming my understanding of where this works is correct, this needs to be qualified I think in terms of which nodes get I/O activity shown. Otherwise I as the user will expect e.g. an Index Scan to show I/O statistics. Additionally I think this should clearly say "read I/O", since writes I/O information wouldn't show for any nodes. > diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c > index f151f21f9b3..863a9dd0f0d 100644 > --- a/src/backend/commands/explain.c > +++ b/src/backend/commands/explain.c > @@ -3984,6 +3990,112 @@ show_tidbitmap_info(BitmapHeapScanState *planstate, ExplainState *es) > } > } > ... > + /* prefetch I/O info (only if there were actual I/Os) */ > + if (stats->io_count > 0) > + { > + ExplainIndentText(es); > + appendStringInfo(es->str, "I/O: count=%" PRIu64 " waits=%" PRIu64 > + " size=%.2f inprogress=%.2f\n", > + stats->io_count, stats->wait_count, > + (stats->io_nblocks * 1.0 / stats->io_count), > + (stats->io_in_progress * 1.0 / stats->io_count)); > + } I've briefly pondered again whether "I/O:" is a bit too broad here in EXPLAIN text output, i.e. whether it should qualified it somehow, e.g. "I/O Stream", since its just the read stream I/O activity. Right now it looks like this: Seq Scan on xxx (cost=0.00..7.14 rows=114 width=483) (actual time=0.140..0.222 rows=114.00 loops=1) Prefetch: avg=1.67 max=3 capacity=94 I/O: count=3 waits=3 size=2.00 inprogress=1.00 Buffers: shared read=6 I/O Timings: shared read=0.158 What if we did this instead: Seq Scan on xxx (cost=0.00..7.14 rows=114 width=483) (actual time=0.140..0.222 rows=114.00 loops=1) Prefetch: avg=1.67 max=3 capacity=94 I/O Stream: count=3 waits=3 size=2.00 inprogress=1.00 Buffers: shared read=6 I/O Timings: shared read=0.158 But I'm 50/50 whether that's a big enough problem to debate right now. Especially with index prefetching out of the picture this is actually less of a problem, since this should only show on scans where the I/O activity is mainly (fully?) relying on read streams. If we had index prefetching, my understanding is we would have had "I/O" show the read stream activity (i.e. table access), but not the index access, correct? Btw, in that same test, when I re-run I get Prefetch but not I/O -- why is that? Seq Scan on organizations (cost=0.00..7.42 rows=1 width=483) (actual time=0.034..0.042 rows=1.00 loops=1) Filter: (slug = 'pganalyze'::text) Rows Removed by Filter: 113 Prefetch: avg=1.00 max=1 capacity=94 Buffers: shared hit=6 (if its all buffers hit, why are there any prefetches at all?) > with two or three > bigger changes compared to earlier versions: > > 1) default OFF > > After thinking about it a bit more, I decided to change the default to > OFF. On the one hand I agree it's somewhat similar to BUFFERS, and that > option is ON by default now. But on the other hand, we must not clutter > EXPLAIN output with too much information, and I'm not convinced IO is > worth it. So I changed to OFF by default. That also makes the patches > smaller, due to not having to adjust that many tests. Seems fair in terms of what to do now. > > 2) auto_explain > > It also occurred to me we should add a matching option to auto_explain, > similarly to log_buffers. 0005 does that. Agreed. > > 3) INSTRUMENT_IO > > The auto_explain bit also implies we need a new INSTRUMENT_IO constant, > to handle this just like BUFFERS. Which also means we can actually > collect the stats only when the IO option is specified (instead of for > all ANALYZE runs). Which is nice. FWIW, it'd be the only kind that's not populated by instrument.c (and isn't a part of Instrumentation), but I don't see any immediate practical issues with having the extra flag. It does mean that INSTRUMENT_ALL will turn on INSTRUMENT_IO though for the Instrumentation, which will always be a no-op. In practice the only users of that are the InstrAlloc calls in pg_stat_statements/auto_explain, which I have a patch for to refactor anyway (over on the stack-based instrumentation thread, which to be clear is PG20 material at this point). We could probably just drop INSTRUMENT_ALL in the next cycle to avoid confusion. For context, that use of INSTRUMENT_ALL won't affect es_instrument, just how queryDesc->totaltime is initialized, and what it captures at a summary level - and it won't know how to capture I/O stats since they are not part of Instrumentation. I would suggest noting the fact that its a no-op for Instrumentation itself somewhere in code at least, e.g. where INSTRUMENT_IO is defined on InstrumentOption. 0005 looks good. I've skimmed 0006 and 0007 and it makes sense to me why they exist (and we should add them), but I haven't reviewed the code for correctness. Thanks, Lukas -- Lukas Fittl -
Re: EXPLAIN: showing ReadStream / prefetch stats
Lukas Fittl <lukas@fittl.com> — 2026-04-07T08:05:29Z
On Tue, Apr 7, 2026 at 1:00 AM Lukas Fittl <lukas@fittl.com> wrote: > > Btw, in that same test, when I re-run I get Prefetch but not I/O -- why is that? > > Seq Scan on organizations (cost=0.00..7.42 rows=1 width=483) (actual > time=0.034..0.042 rows=1.00 loops=1) > Filter: (slug = 'pganalyze'::text) > Rows Removed by Filter: 113 > Prefetch: avg=1.00 max=1 capacity=94 > Buffers: shared hit=6 > > (if its all buffers hit, why are there any prefetches at all?) Here is a more self-contained reproducer: CREATE TABLE test (id int); INSERT INTO test SELECT * FROM generate_series(0, 100000); -- restart server EXPLAIN (ANALYZE, IO) SELECT * FROM test; QUERY PLAN -------------------------------------------------------------------------------------------------------------- Seq Scan on test (cost=0.00..1572.65 rows=112965 width=4) (actual time=0.075..4.046 rows=100001.00 loops=1) Prefetch: avg=22.62 max=32 capacity=94 I/O: count=31 waits=5 size=14.29 inprogress=1.77 Buffers: shared read=443 I/O Timings: shared read=0.156 Planning: Buffers: shared hit=15 read=7 I/O Timings: shared read=0.114 Planning Time: 0.356 ms Execution Time: 7.020 ms (10 rows) EXPLAIN (ANALYZE, IO) SELECT * FROM test; QUERY PLAN -------------------------------------------------------------------------------------------------------------- Seq Scan on test (cost=0.00..1572.65 rows=112965 width=4) (actual time=0.027..5.520 rows=100001.00 loops=1) Prefetch: avg=1.00 max=1 capacity=94 Buffers: shared hit=443 Planning Time: 0.094 ms Execution Time: 9.311 ms (5 rows) Thanks, Lukas -- Lukas Fittl -
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-04-07T09:32:28Z
On 4/7/26 10:00, Lukas Fittl wrote: > On Mon, Apr 6, 2026 at 6:14 PM Tomas Vondra <tomas@vondra.me> wrote: >> >> On 4/7/26 02:19, Melanie Plageman wrote: >>> On Mon, Apr 6, 2026 at 4:39 PM Tomas Vondra <tomas@vondra.me> wrote: >>>> >>>> On 4/6/26 18:50, Melanie Plageman wrote: >>>> If you think you can get this pushed, I'll do my best to finalize the >>>> instrumentation and SeqScan/TidRangeScan parts. >>> >>> I've pushed the first patch for index/index-only scans. Attached is >>> the BHS fix that uses the new pattern. It needs at least one review >>> before pushing. While I was polishing it, I realized I neglected to >>> use add_size()/mul_size() in the index-only/index scan patches. So, >>> 0002 is just a fix commit to do that. Feel free to push these if you >>> think they're ready. Otherwise, I'll do so pending your review in my >>> morning. >>> >> >> Thanks! >> >> I'll take a look in my morning, and will consider pushing the changes so >> that I can start pushing the parts adding the new instrumentation. > > FWIW, 0001 and 0002 in the v12 patch set also make sense to me from a > code review just now. > >> >> I've spent a fair amount of time looking at those parts today. Attached >> is v12 (including the two parts you posted as v11) > > 0003 looks good, but commit message needs improvement / justification > of the change (I assume you had already planned that). > Yes, the commit messages need improvements. > 0004: > >> diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml >> index 5b8b521802e..a854c41e963 100644 >> --- a/doc/src/sgml/ref/explain.sgml >> +++ b/doc/src/sgml/ref/explain.sgml >> @@ -46,6 +46,7 @@ EXPLAIN [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] <rep >> TIMING [ <replaceable class="parameter">boolean</replaceable> ] >> SUMMARY [ <replaceable class="parameter">boolean</replaceable> ] >> MEMORY [ <replaceable class="parameter">boolean</replaceable> ] >> + IO [ <replaceable class="parameter">boolean</replaceable> ] >> FORMAT { TEXT | XML | JSON | YAML } >> </synopsis> >> </refsynopsisdiv> >> @@ -298,6 +299,17 @@ ROLLBACK; >> </listitem> >> </varlistentry> >> + <varlistentry> >> + <term><literal>IO</literal></term> >> + <listitem> >> + <para> >> + Include information on I/O performed by each node. >> + This parameter may only be used when <literal>ANALYZE</literal> is also >> + enabled. It defaults to <literal>FALSE</literal>. >> + </para> >> + </listitem> >> + </varlistentry> > + > > Assuming my understanding of where this works is correct, this needs > to be qualified I think in terms of which nodes get I/O activity > shown. Otherwise I as the user will expect e.g. an Index Scan to show > I/O statistics. Additionally I think this should clearly say "read > I/O", since writes I/O information wouldn't show for any nodes. > Yeah, I was wondering about this too. The "each node" seems to suggest all nodes get these stats, but then only some scan nodes actually do. I'm a bit concerned about listing which exact nodes get the stats, though. AFAICS the explain docs don't do that for any other option. >> diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c >> index f151f21f9b3..863a9dd0f0d 100644 >> --- a/src/backend/commands/explain.c >> +++ b/src/backend/commands/explain.c >> @@ -3984,6 +3990,112 @@ show_tidbitmap_info(BitmapHeapScanState *planstate, ExplainState *es) >> } >> } >> ... >> + /* prefetch I/O info (only if there were actual I/Os) */ >> + if (stats->io_count > 0) >> + { >> + ExplainIndentText(es); >> + appendStringInfo(es->str, "I/O: count=%" PRIu64 " waits=%" PRIu64 >> + " size=%.2f inprogress=%.2f\n", >> + stats->io_count, stats->wait_count, >> + (stats->io_nblocks * 1.0 / stats->io_count), >> + (stats->io_in_progress * 1.0 / stats->io_count)); >> + } > > I've briefly pondered again whether "I/O:" is a bit too broad here in > EXPLAIN text output, i.e. whether it should qualified it somehow, e.g. > "I/O Stream", since its just the read stream I/O activity. > Hmmm, I don't think we want to mention "Stream" here, that seems more like an implementation detail to me. Maybe "I/O Reads" or "Read I/O" would work, though. But OTOH there were discussions about maybe using the "I/O" both for reads and writes ... > Right now it looks like this: > > Seq Scan on xxx (cost=0.00..7.14 rows=114 width=483) (actual > time=0.140..0.222 rows=114.00 loops=1) > Prefetch: avg=1.67 max=3 capacity=94 > I/O: count=3 waits=3 size=2.00 inprogress=1.00 > Buffers: shared read=6 > I/O Timings: shared read=0.158 > > What if we did this instead: > > Seq Scan on xxx (cost=0.00..7.14 rows=114 width=483) (actual > time=0.140..0.222 rows=114.00 loops=1) > Prefetch: avg=1.67 max=3 capacity=94 > I/O Stream: count=3 waits=3 size=2.00 inprogress=1.00 > Buffers: shared read=6 > I/O Timings: shared read=0.158 > > But I'm 50/50 whether that's a big enough problem to debate right now. > Especially with index prefetching out of the picture this is actually > less of a problem, since this should only show on scans where the I/O > activity is mainly (fully?) relying on read streams. If we had index > prefetching, my understanding is we would have had "I/O" show the read > stream activity (i.e. table access), but not the index access, > correct? > I think it's OK the way it is. It's possible someone will come up with a better scheme later, in which case we can adjust this. > Btw, in that same test, when I re-run I get Prefetch but not I/O -- why is that? > > Seq Scan on organizations (cost=0.00..7.42 rows=1 width=483) (actual > time=0.034..0.042 rows=1.00 loops=1) > Filter: (slug = 'pganalyze'::text) > Rows Removed by Filter: 113 > Prefetch: avg=1.00 max=1 capacity=94 > Buffers: shared hit=6 > > (if its all buffers hit, why are there any prefetches at all?) > >> with two or three >> bigger changes compared to earlier versions: >> In the text output this is expected / intentional. The prefetch is based on number of buffers pulled from the stream - and we use the stream even with everything in shared buffers. We want to show the prefetch distance etc. even in this case. But we'll do no I/O in that case, so we don't show that line. In the non-text formats we include the fields always. >> 1) default OFF >> >> After thinking about it a bit more, I decided to change the default to >> OFF. On the one hand I agree it's somewhat similar to BUFFERS, and that >> option is ON by default now. But on the other hand, we must not clutter >> EXPLAIN output with too much information, and I'm not convinced IO is >> worth it. So I changed to OFF by default. That also makes the patches >> smaller, due to not having to adjust that many tests. > > Seems fair in terms of what to do now. > OK >> >> 2) auto_explain >> >> It also occurred to me we should add a matching option to auto_explain, >> similarly to log_buffers. 0005 does that. > > Agreed. > OK >> >> 3) INSTRUMENT_IO >> >> The auto_explain bit also implies we need a new INSTRUMENT_IO constant, >> to handle this just like BUFFERS. Which also means we can actually >> collect the stats only when the IO option is specified (instead of for >> all ANALYZE runs). Which is nice. > > FWIW, it'd be the only kind that's not populated by instrument.c (and > isn't a part of Instrumentation), but I don't see any immediate > practical issues with having the extra flag. > I believe INSTRUMENT_ROWS is not populated there either, so it's not the only flag not populated there. I've considered adding a need_io field, but that would be useless / unused. > It does mean that INSTRUMENT_ALL will turn on INSTRUMENT_IO though for > the Instrumentation, which will always be a no-op. In practice the > only users of that are the InstrAlloc calls in > pg_stat_statements/auto_explain, which I have a patch for to refactor > anyway (over on the stack-based instrumentation thread, which to be > clear is PG20 material at this point). > > We could probably just drop INSTRUMENT_ALL in the next cycle to avoid > confusion. For context, that use of INSTRUMENT_ALL won't affect > es_instrument, just how queryDesc->totaltime is initialized, and what > it captures at a summary level - and it won't know how to capture I/O > stats since they are not part of Instrumentation. > It is a bit confusing, yes. > I would suggest noting the fact that its a no-op for Instrumentation > itself somewhere in code at least, e.g. where INSTRUMENT_IO is defined > on InstrumentOption. I'll consider doing that, but I'm not sure which place would be best. > > 0005 looks good. > > I've skimmed 0006 and 0007 and it makes sense to me why they exist > (and we should add them), but I haven't reviewed the code for > correctness. > Thanks! regards -- Tomas Vondra -
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-04-07T12:36:01Z
Hi, I've pushed the first two parts earlier today. Here's a v13 with the remaining pieces rebased on top of that, with updated commit messages and some additional fixes. 1) There was a bogus else-if in explain.c that would make it impossible to enable "wal" and "io" at the same time. 2) Replaced one of the "typedef IOStats" with just forward declaration, to prevent possible conflicting typedefs in the future (even if that wasn't an issue now). 3) Used (es_instrument & INSTRUMENT_IO) more consistently. A couple places in the executor still checked just es_instrument, and so would collect stats even if not needed. Be consistent. 4) Various comment additions / improvements. I intend to push this later today, after giving it one more round of review etc. regards -- Tomas Vondra
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Andres Freund <andres@anarazel.de> — 2026-04-07T15:09:40Z
Hi, On 2026-04-07 01:00:28 -0700, Lukas Fittl wrote: > Btw, in that same test, when I re-run I get Prefetch but not I/O -- why is that? > > Seq Scan on organizations (cost=0.00..7.42 rows=1 width=483) (actual > time=0.034..0.042 rows=1.00 loops=1) > Filter: (slug = 'pganalyze'::text) > Rows Removed by Filter: 113 > Prefetch: avg=1.00 max=1 capacity=94 > Buffers: shared hit=6 > > (if its all buffers hit, why are there any prefetches at all?) It's not really prefetching, which is visible as the distance being 1. The read stream is created at the start of the scan, not at the first miss. Greetings, Andres Freund
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Melanie Plageman <melanieplageman@gmail.com> — 2026-04-07T15:27:00Z
On Tue, Apr 7, 2026 at 8:36 AM Tomas Vondra <tomas@vondra.me> wrote: > > 3) Used (es_instrument & INSTRUMENT_IO) more consistently. A couple > places in the executor still checked just es_instrument, and so would > collect stats even if not needed. Be consistent. I would argue for only allocating the shared instrumentation if they will be displayed, but I don't feel that strongly about it. I took another look at just 0002 just to double-check the read stream counters. It seems fine. Perhaps we should free the TableScanInstrumentation in heap_endscan(). It isn't that important of a leak, but the other things palloc'd in heap_beginscan() are freed. I also wondered if it is clear without a comment why we don't count a wait when READ_BUFFERS_SYNCHRONOUSLY sets needed_wait (which we added to make sure distance ramps up). I think it's fine; I'm just musing. - Melanie
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Melanie Plageman <melanieplageman@gmail.com> — 2026-04-07T16:07:18Z
On Tue, Apr 7, 2026 at 8:36 AM Tomas Vondra <tomas@vondra.me> wrote: > > I've pushed the first two parts earlier today. Here's a v13 with the > remaining pieces rebased on top of that, with updated commit messages > and some additional fixes. I noticed a couple of things while reviewing 0004. I see I had bitmapheapscan use if (!node->ss.ps.instrument || pcxt->nworkers == 0) return; while seq scan uses if (!estate->es_instrument || pcxt->nworkers == 0) return; That does seem worth being consistent about. Though the estate one is probably better to use and changing bitmapheapscan in this commit might be noisy... I don't feel strongly either way. In ExecSeqScanInstrumentInitWorker() -> shm_toc_lookup() noerror is true: node->sinstrument = shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id + PARALLEL_KEY_SCAN_INSTRUMENT_OFFSET, true); I went back and forth on this for bitmapheapscan and ended up going with the guard if (!node->ss.ps.instrument) return; and passing noerror as false. I thought it would be better to get an error if something went wrong. If you keep noerror true, there's no reason to check for es_instrument. I also noticed that seq scan doesn't use add_size/mul_size for the size calculations when estimating and allocating DSM. I switched to using them in bitmapheapscan because I realized other code in that file used them. Seems seq scan was doing plain arithmetic without those helpers already. And overflow is unlikely here. But I wonder if it is worth using them? It doesn't matter for the Retrieve functions because overflow would have caused us to error out at estimation or allocation time. Most of the seq scan specific stuff above applies to tid range scan as well. One other point about 0002: I wonder if the text explain output should say "in-progress" instead of "inprogress" - Melanie -
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-04-07T16:13:18Z
On 4/7/26 17:27, Melanie Plageman wrote: > On Tue, Apr 7, 2026 at 8:36 AM Tomas Vondra <tomas@vondra.me> wrote: >> >> 3) Used (es_instrument & INSTRUMENT_IO) more consistently. A couple >> places in the executor still checked just es_instrument, and so would >> collect stats even if not needed. Be consistent. > > I would argue for only allocating the shared instrumentation if they > will be displayed, but I don't feel that strongly about it. > OK > I took another look at just 0002 just to double-check the read stream > counters. It seems fine. > Perhaps we should free the TableScanInstrumentation in heap_endscan(). > It isn't that important of a leak, but the other things palloc'd in > heap_beginscan() are freed. > Good point, will fix. > I also wondered if it is clear without a comment why we don't count a > wait when READ_BUFFERS_SYNCHRONOUSLY sets needed_wait (which we added > to make sure distance ramps up). I think it's fine; I'm just musing. > I believe this is actually a bug, the read_stream_count_wait() should be after the block checking for READ_BUFFERS_SYNCHRONOUSLY. I recall Andres mentioned this in one of this reviews, but I either forgot to address that, or it got lost then transferring this between this thread and the index prefetching one. Thanks! -- Tomas Vondra
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-04-07T17:34:16Z
On 4/7/26 18:07, Melanie Plageman wrote: > On Tue, Apr 7, 2026 at 8:36 AM Tomas Vondra <tomas@vondra.me> wrote: >> >> I've pushed the first two parts earlier today. Here's a v13 with the >> remaining pieces rebased on top of that, with updated commit messages >> and some additional fixes. > > I noticed a couple of things while reviewing 0004. > > I see I had bitmapheapscan use > if (!node->ss.ps.instrument || pcxt->nworkers == 0) > return; > > while seq scan uses > if (!estate->es_instrument || pcxt->nworkers == 0) > return; > > That does seem worth being consistent about. Though the estate one is > probably better to use and changing bitmapheapscan in this commit > might be noisy... I don't feel strongly either way. > I'm not sure this is just a question of consistency, because BHS may need the shared instrumentation even if (es_instrument = 0), no? While the seqscan/tidrange scan only need it with EXPLAIN(IO). So I think the two nodes should check ((estate->es_instrument & INSTRUMENT_IO) == 0) The attached v14 does it this way. I left BHS to check ps.instrument, I think that's fine. > In ExecSeqScanInstrumentInitWorker() -> shm_toc_lookup() noerror is true: > > node->sinstrument = shm_toc_lookup(pwcxt->toc, > node->ss.ps.plan->plan_node_id > + PARALLEL_KEY_SCAN_INSTRUMENT_OFFSET, > true); > > I went back and forth on this for bitmapheapscan and ended up going > with the guard > if (!node->ss.ps.instrument) > return; > and passing noerror as false. I thought it would be better to get an > error if something went wrong. > > If you keep noerror true, there's no reason to check for es_instrument. I agree this should use noerror=false. It's an error to not find the expected DSM chunk, and we don't want to hide that. It'd probably fail with a segfault later, but that's hardly a better outcome. > > I also noticed that seq scan doesn't use add_size/mul_size for the > size calculations when estimating and allocating DSM. I switched to > using them in bitmapheapscan because I realized other code in that > file used them. Seems seq scan was doing plain arithmetic without > those helpers already. And overflow is unlikely here. But I wonder if > it is worth using them? > Seems better to use that, so done in v14. > It doesn't matter for the Retrieve functions because overflow would > have caused us to error out at estimation or allocation time. > > Most of the seq scan specific stuff above applies to tid range scan as well. > OK, fixed that too. > One other point about 0002: I wonder if the text explain output should > say "in-progress" instead of "inprogress" > True. Changed. Thanks! -- Tomas Vondra
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Andres Freund <andres@anarazel.de> — 2026-04-07T18:12:37Z
Hi, On 2026-04-07 18:13:18 +0200, Tomas Vondra wrote: > On 4/7/26 17:27, Melanie Plageman wrote: > > I took another look at just 0002 just to double-check the read stream > > counters. It seems fine. > > Perhaps we should free the TableScanInstrumentation in heap_endscan(). > > It isn't that important of a leak, but the other things palloc'd in > > heap_beginscan() are freed. > > > > Good point, will fix. > > > I also wondered if it is clear without a comment why we don't count a > > wait when READ_BUFFERS_SYNCHRONOUSLY sets needed_wait (which we added > > to make sure distance ramps up). I think it's fine; I'm just musing. > > > > I believe this is actually a bug, the read_stream_count_wait() should be > after the block checking for READ_BUFFERS_SYNCHRONOUSLY. I recall Andres > mentioned this in one of this reviews, but I either forgot to address > that, or it got lost then transferring this between this thread and the > index prefetching one. Yea, I think it's actually somewhat important that those will be counted as waits, since they *are* blocking reads. It's just that the blocking read happends during the start of the IO, so we don't have to wait in WaitReadBuffers(). You'd otherwise severely undercounting reads in cases where the distance ramps up and down a lot Greetings, Andres Freund
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Melanie Plageman <melanieplageman@gmail.com> — 2026-04-07T19:42:39Z
On Tue, Apr 7, 2026 at 1:34 PM Tomas Vondra <tomas@vondra.me> wrote: > > On 4/7/26 18:07, Melanie Plageman wrote: > > > I see I had bitmapheapscan use > > if (!node->ss.ps.instrument || pcxt->nworkers == 0) > > return; > > > > while seq scan uses > > if (!estate->es_instrument || pcxt->nworkers == 0) > > return; > > > > That does seem worth being consistent about. Though the estate one is > > probably better to use and changing bitmapheapscan in this commit > > might be noisy... I don't feel strongly either way. > > I'm not sure this is just a question of consistency, because BHS may > need the shared instrumentation even if (es_instrument = 0), no? While > the seqscan/tidrange scan only need it with EXPLAIN(IO). > > So I think the two nodes should check > > ((estate->es_instrument & INSTRUMENT_IO) == 0) Makes sense to me. I skimmed v14 quite quickly and LGTM. - Melanie
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-04-07T21:28:32Z
On 4/7/26 21:42, Melanie Plageman wrote: > On Tue, Apr 7, 2026 at 1:34 PM Tomas Vondra <tomas@vondra.me> wrote: >> >> On 4/7/26 18:07, Melanie Plageman wrote: >> >>> I see I had bitmapheapscan use >>> if (!node->ss.ps.instrument || pcxt->nworkers == 0) >>> return; >>> >>> while seq scan uses >>> if (!estate->es_instrument || pcxt->nworkers == 0) >>> return; >>> >>> That does seem worth being consistent about. Though the estate one is >>> probably better to use and changing bitmapheapscan in this commit >>> might be noisy... I don't feel strongly either way. >> >> I'm not sure this is just a question of consistency, because BHS may >> need the shared instrumentation even if (es_instrument = 0), no? While >> the seqscan/tidrange scan only need it with EXPLAIN(IO). >> >> So I think the two nodes should check >> >> ((estate->es_instrument & INSTRUMENT_IO) == 0) > > Makes sense to me. > > I skimmed v14 quite quickly and LGTM. > Thanks. I've now pushed all parts of this patch series. -- Tomas Vondra
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Andres Freund <andres@anarazel.de> — 2026-04-07T21:52:31Z
Hi, On 2026-04-07 23:28:32 +0200, Tomas Vondra wrote: > Thanks. I've now pushed all parts of this patch series. Yay. It'd have been much harder to do the read stream improvements I worked on in the last weeks without this patchset, and it also was much harder to understand the performance of the index prefetching patchset ( :( ) before / without it. The only thing I don't like about it is not having had it in the 18 cycle, when working on AIO. Greetings, Andres Freund
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-05-04T13:51:20Z
Hi, While looking for something in the EXPLAIN docs, I realized the docs added by 681daed93169 are considerably less detailed that for the other EXPLAIN options. It only said "information on I/O" provided by the scan node, while the other options go into much more details. The attached patch improves that a little bit - it lists the counters added to explain. I'll consider going into a little bit more detail, e.g. regarding what "capacity" or "waits" means. I'll add this as an open item for PG19, so that I don't forget about it. regards -- Tomas Vondra
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Lukas Fittl <lukas@fittl.com> — 2026-05-04T15:22:26Z
On Mon, May 4, 2026 at 6:51 AM Tomas Vondra <tomas@vondra.me> wrote: > > While looking for something in the EXPLAIN docs, I realized the docs > added by 681daed93169 are considerably less detailed that for the other > EXPLAIN options. It only said "information on I/O" provided by the scan > node, while the other options go into much more details. > > The attached patch improves that a little bit - it lists the counters > added to explain. I'll consider going into a little bit more detail, > e.g. regarding what "capacity" or "waits" means. I think that makes sense to add. Two thoughts: (1) I think going into a bit more detail makes sense to actually explain how to interpret the values (or maybe even what action to take?) - I think the BUFFERS documentation is probably a good style to follow in terms of how it lists out the individual values and their meaning. (2) What do you think of also clarifying when this information is tracked? I gave that feedback in an earlier patch in this thread, but I still feel like the "scan nodes providing such information" is very vague and not helpful for users trying to understand when this is shown (e.g. the fact that this currently has no visibility on index I/O, and won't tell you anything about writes being performed to clear out dirty buffers/etc). Maybe: "Include information on I/O performed by scan nodes providing such information, currently visible for read I/O issued by Bitmap Heap Scans, Sequential Scans and TID Scans." Of course that's a bit prone to get stale, but I think the clarity on what it does/doesn't do would be worth the effort to maintain it. Thanks, Lukas -- Lukas Fittl
-
Re: EXPLAIN: showing ReadStream / prefetch stats
Tomas Vondra <tomas@vondra.me> — 2026-05-04T17:16:37Z
On 5/4/26 17:22, Lukas Fittl wrote: > On Mon, May 4, 2026 at 6:51 AM Tomas Vondra <tomas@vondra.me> wrote: >> >> While looking for something in the EXPLAIN docs, I realized the docs >> added by 681daed93169 are considerably less detailed that for the other >> EXPLAIN options. It only said "information on I/O" provided by the scan >> node, while the other options go into much more details. >> >> The attached patch improves that a little bit - it lists the counters >> added to explain. I'll consider going into a little bit more detail, >> e.g. regarding what "capacity" or "waits" means. > > I think that makes sense to add. > > Two thoughts: > > (1) I think going into a bit more detail makes sense to actually > explain how to interpret the values (or maybe even what action to > take?) - I think the BUFFERS documentation is probably a good style to > follow in terms of how it lists out the individual values and their > meaning. > Agreed. > (2) What do you think of also clarifying when this information is > tracked? I gave that feedback in an earlier patch in this thread, but > I still feel like the "scan nodes providing such information" is very > vague and not helpful for users trying to understand when this is > shown (e.g. the fact that this currently has no visibility on index > I/O, and won't tell you anything about writes being performed to clear > out dirty buffers/etc). > > Maybe: > > "Include information on I/O performed by scan nodes providing such > information, currently visible for read I/O issued by Bitmap Heap > Scans, Sequential Scans and TID Scans." > > Of course that's a bit prone to get stale, but I think the clarity on > what it does/doesn't do would be worth the effort to maintain it. > Yeah, I'll think about it. At some point I didn't like mentioning exact scan types, because who knows what custom scans could do etc. But maybe that's OK. regards -- Tomas Vondra