Subject: RFC: Allow EXPLAIN to Output Page Fault Information

From: torikoshia <[email-masked]>
Date: Tue, 24 Dec 2024 17:53:06 +0900

Hi,

When reading the output of EXPLAIN (ANALYZE) to diagnose slow queries 
for our customers, I often want to know how many page faults occurred, 
especially major page faults, which involve disk access.

Currently, the BUFFERS option in EXPLAIN provides information on whether 
a page was found in the shared buffers, but it doesn't provide insight 
into whether the page was found in the OS cache or not and disk I/O 
occurred.

Since page faults especially major one impact performance compared to 
shared buffer and OS cache hits, it would be helpful to track these 
events.

I have attached a PoC patch that modifies EXPLAIN to include page fault 
information during both the planning and execution phases of a query. 
The output would look like this:

   =# EXPLAIN (ANALYZE, PAGEFAULTS)
           SELECT * FROM pgbench_branches b
             JOIN pgbench_accounts a ON b.bid = a.bid ORDER BY a.aid;

   QUERY PLAN
   
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
     Nested Loop  (cost=0.58..335386.98 rows=4999917 width=197) (actual 
time=3.785..5590.294 rows=5000000 loops=1)

       ... (omitted)

     Planning:
       Buffers: shared hit=50 read=48 dirtied=4 written=2
       Page Faults: minor=30 major=19 <-ADDED
     Planning Time: 22.080 ms
     Execution:
       Page Faults: minor=49 major=5 <-ADDED
     Execution Time: 5794.356 ms

The patch has not yet added this functionality to auto_explain, but I 
believe this feature would be more useful in auto_explain than in the 
EXPLAIN command itself. I plan to add it to auto_explain if there's 
interest in including page fault information in EXPLAIN.

While GUCs like log_statement_stats allow logging of getrusage(2) 
information, including page faults, always enabling this can lead to 
excessive log output. It would be better to log this data only when 
queries are slow. Therefore, adding this feature to auto_explain seems 
like a good solution.

The patch introduces a new option, PAGEFAULTS, but it may be more 
appropriate to include the page fault information in another option, 
such as SUMMARY, especially if there are other useful resources that can 
be obtained from getrusage(2).

The patch cannot be applied to Windows because getrusage() in PostgreSQL 
ported for Windows currently only tracks CPU times. I'm not sure if 
information on major and minor page faults is accessible on Windows, but 
it might be acceptable to treat it similarly to log_statement_stats and 
exclude it from Windows support.

I also tried to add page faults information for each plan node, similar 
to the BUFFERS option, but I decided against this approach due to the 
performance impact. The frequent calls to getrusage(2), i.e. each time 
getting one row, created significant overhead.

Any feedback would be greatly appreciated.

-- 
Regards,

--
Atsushi Torikoshi
Seconded from NTT DATA GROUP CORPORATION to SRA OSS K.K.

From 8c28ec288a99233580b15ae689e81610b2a8348c Mon Sep 17 00:00:00 2001
From: Atsushi Torikoshi <[email-masked]>
Date: Tue, 24 Dec 2024 17:26:18 +0900
Subject: [PATCH v1] PoC: Allow EXPLAIN to output page fault information

This patch adds PAGEFAULTS option to EXPLAIN and it tracks both
minor and major faults during planning and execution phases.

This is a PoC patch and only shows page faults when the FORMAT is TEXT.

---
 src/backend/commands/explain.c | 103 +++++++++++++++++++++++++++++++--
 src/backend/commands/prepare.c |  23 ++++++++
 src/include/commands/explain.h |  11 ++++
 3 files changed, 133 insertions(+), 4 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index a201ed3082..b8943c7ef9 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,8 @@
  */
 #include "postgres.h"
 
+#include <sys/resource.h>
+
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
@@ -145,6 +147,8 @@ static void show_foreignscan_info(ForeignScanState *fsstate, ExplainState *es);
 static const char *explain_get_index_name(Oid indexId);
 static bool peek_buffer_usage(ExplainState *es, const BufferUsage *usage);
 static void show_buffer_usage(ExplainState *es, const BufferUsage *usage);
+static bool peek_pagefault(ExplainState *es, const PageFaults * usage);
+static void show_pagefault(ExplainState *es, const PageFaults * usage);
 static void show_wal_usage(ExplainState *es, const WalUsage *usage);
 static void show_memory_counters(ExplainState *es,
 								 const MemoryContextCounters *mem_counters);
@@ -217,6 +221,8 @@ ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
 			buffers_set = true;
 			es->buffers = defGetBoolean(opt);
 		}
+		else if (strcmp(opt->defname, "pagefaults") == 0)
+			es->pagefaults = defGetBoolean(opt);
 		else if (strcmp(opt->defname, "wal") == 0)
 			es->wal = defGetBoolean(opt);
 		else if (strcmp(opt->defname, "settings") == 0)
@@ -475,6 +481,8 @@ standard_ExplainOneQuery(Query *query, int cursorOptions,
 				planduration;
 	BufferUsage bufusage_start,
 				bufusage;
+	PageFaults	pagefaults = {0};
+	PageFaults	pagefaults_start = {0};
 	MemoryContextCounters mem_counters;
 	MemoryContext planner_ctx = NULL;
 	MemoryContext saved_ctx = NULL;
@@ -499,6 +507,15 @@ standard_ExplainOneQuery(Query *query, int cursorOptions,
 		bufusage_start = pgBufferUsage;
 	INSTR_TIME_SET_CURRENT(planstart);
 
+	if (es->pagefaults)
+	{
+		struct rusage rusage;
+
+		getrusage(RUSAGE_SELF, &rusage);
+		pagefaults_start.minflt = rusage.ru_minflt;
+		pagefaults_start.majflt = rusage.ru_majflt;
+	}
+
 	/* plan the query */
 	plan = pg_plan_query(query, queryString, cursorOptions, params);
 
@@ -518,9 +535,19 @@ standard_ExplainOneQuery(Query *query, int cursorOptions,
 		BufferUsageAccumDiff(&bufusage, &pgBufferUsage, &bufusage_start);
 	}
 
+	if (es->pagefaults)
+	{
+		struct rusage rusage;
+
+		getrusage(RUSAGE_SELF, &rusage);
+		pagefaults.minflt = rusage.ru_minflt - pagefaults_start.minflt;
+		pagefaults.majflt = rusage.ru_majflt - pagefaults_start.majflt;
+	}
+
 	/* run it (if needed) and produce output */
 	ExplainOnePlan(plan, into, es, queryString, params, queryEnv,
 				   &planduration, (es->buffers ? &bufusage : NULL),
+				   (es->pagefaults ? &pagefaults : NULL),
 				   es->memory ? &mem_counters : NULL);
 }
 
@@ -644,7 +671,7 @@ void
 ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 			   const char *queryString, ParamListInfo params,
 			   QueryEnvironment *queryEnv, const instr_time *planduration,
-			   const BufferUsage *bufusage,
+			   const BufferUsage *bufusage, const PageFaults * planpagefaults,
 			   const MemoryContextCounters *mem_counters)
 {
 	DestReceiver *dest;
@@ -654,6 +681,9 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 	int			eflags;
 	int			instrument_option = 0;
 	SerializeMetrics serializeMetrics = {0};
+	PageFaults	pagefaults = {0};
+	PageFaults	pagefaults_start = {0};
+	struct rusage rusage;
 
 	Assert(plannedstmt->commandType != CMD_UTILITY);
 
@@ -674,6 +704,14 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 	 */
 	INSTR_TIME_SET_CURRENT(starttime);
 
+	if (es->pagefaults)
+	{
+		getrusage(RUSAGE_SELF, &rusage);
+
+		pagefaults_start.minflt = rusage.ru_minflt;
+		pagefaults_start.majflt = rusage.ru_majflt;
+	}
+
 	/*
 	 * Use a snapshot with an updated command ID to ensure this query sees
 	 * results of any previously executed queries.
@@ -748,7 +786,8 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 	ExplainPrintPlan(es, queryDesc);
 
 	/* Show buffer and/or memory usage in planning */
-	if (peek_buffer_usage(es, bufusage) || mem_counters)
+	if (peek_buffer_usage(es, bufusage) || peek_pagefault(es, planpagefaults) ||
+		mem_counters)
 	{
 		ExplainOpenGroup("Planning", "Planning", true, es);
 
@@ -758,10 +797,12 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 			appendStringInfoString(es->str, "Planning:\n");
 			es->indent++;
 		}
-
 		if (bufusage)
 			show_buffer_usage(es, bufusage);
 
+		if (es->pagefaults)
+			show_pagefault(es, planpagefaults);
+
 		if (mem_counters)
 			show_memory_counters(es, mem_counters);
 
@@ -813,6 +854,26 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 
 	totaltime += elapsed_time(&starttime);
 
+	if (es->pagefaults)
+	{
+		getrusage(RUSAGE_SELF, &rusage);
+
+		pagefaults.minflt = rusage.ru_minflt - pagefaults_start.minflt;
+		pagefaults.majflt = rusage.ru_majflt - pagefaults_start.majflt;
+
+		if (es->format == EXPLAIN_FORMAT_TEXT)
+		{
+			ExplainIndentText(es);
+			appendStringInfoString(es->str, "Execution:\n");
+			es->indent++;
+		}
+		show_pagefault(es, &pagefaults);
+
+		if (es->format == EXPLAIN_FORMAT_TEXT)
+			es->indent--;
+		ExplainCloseGroup("Execution", "Execution", true, es);
+	}
+
 	/*
 	 * We only report execution time if we actually ran the query (that is,
 	 * the user specified ANALYZE), and if summary reporting is enabled (the
@@ -822,7 +883,6 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 	if (es->summary && es->analyze)
 		ExplainPropertyFloat("Execution Time", "ms", 1000.0 * totaltime, 3,
 							 es);
-
 	ExplainCloseGroup("Query", NULL, true, es);
 }
 
@@ -4232,6 +4292,41 @@ show_buffer_usage(ExplainState *es, const BufferUsage *usage)
 	}
 }
 
+static bool
+peek_pagefault(ExplainState *es, const PageFaults * usage)
+{
+	if (usage == NULL)
+		return false;
+
+	if (usage->minflt <= 0 && usage->majflt <= 0)
+		return false;
+
+	else
+		return true;
+}
+
+/*
+ * Show majar/minor page faults.
+ */
+static void
+show_pagefault(ExplainState *es, const PageFaults * usage)
+{
+	/* Show only positive counter values. */
+	if (usage->minflt <= 0 && usage->majflt <= 0)
+		return;
+
+	if (es->format == EXPLAIN_FORMAT_TEXT)
+	{
+
+		ExplainIndentText(es);
+		appendStringInfoString(es->str, "Page Faults:");
+		appendStringInfo(es->str, " minor=%ld", (long) usage->minflt);
+		appendStringInfo(es->str, " major=%ld", (long) usage->majflt);
+
+		appendStringInfoChar(es->str, '\n');
+	}
+}
+
 /*
  * Show WAL usage details.
  */
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 39a71c1de2..4d944aaeb4 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -17,6 +17,7 @@
 #include "postgres.h"
 
 #include <limits.h>
+#include <sys/resource.h>
 
 #include "access/xact.h"
 #include "catalog/pg_type.h"
@@ -579,6 +580,8 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, IntoClause *into, ExplainState *es,
 	instr_time	planduration;
 	BufferUsage bufusage_start,
 				bufusage;
+	PageFaults	pagefaults_start,
+				pagefaults;
 	MemoryContextCounters mem_counters;
 	MemoryContext planner_ctx = NULL;
 	MemoryContext saved_ctx = NULL;
@@ -595,6 +598,16 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, IntoClause *into, ExplainState *es,
 
 	if (es->buffers)
 		bufusage_start = pgBufferUsage;
+
+	if (es->pagefaults)
+	{
+		struct rusage rusage;
+
+		getrusage(RUSAGE_SELF, &rusage);
+		pagefaults_start.minflt = rusage.ru_minflt;
+		pagefaults_start.majflt = rusage.ru_majflt;
+	}
+
 	INSTR_TIME_SET_CURRENT(planstart);
 
 	/* Look it up in the hash table */
@@ -646,6 +659,15 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, IntoClause *into, ExplainState *es,
 		BufferUsageAccumDiff(&bufusage, &pgBufferUsage, &bufusage_start);
 	}
 
+	if (es->pagefaults)
+	{
+		struct rusage rusage;
+
+		getrusage(RUSAGE_SELF, &rusage);
+		pagefaults.minflt = rusage.ru_minflt - pagefaults_start.minflt;
+		pagefaults.majflt = rusage.ru_majflt - pagefaults_start.majflt;
+	}
+
 	plan_list = cplan->stmt_list;
 
 	/* Explain each query */
@@ -656,6 +678,7 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, IntoClause *into, ExplainState *es,
 		if (pstmt->commandType != CMD_UTILITY)
 			ExplainOnePlan(pstmt, into, es, query_string, paramLI, pstate->p_queryEnv,
 						   &planduration, (es->buffers ? &bufusage : NULL),
+						   (es->pagefaults ? &pagefaults : NULL),
 						   es->memory ? &mem_counters : NULL);
 		else
 			ExplainOneUtility(pstmt->utilityStmt, into, es, pstate, paramLI);
diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h
index aa5872bc15..dd95e0447c 100644
--- a/src/include/commands/explain.h
+++ b/src/include/commands/explain.h
@@ -49,6 +49,7 @@ typedef struct ExplainState
 	bool		analyze;		/* print actual times */
 	bool		costs;			/* print estimated costs */
 	bool		buffers;		/* print buffer usage */
+	bool		pagefaults;		/* print pagefaults */
 	bool		wal;			/* print WAL usage */
 	bool		timing;			/* print detailed node timing */
 	bool		summary;		/* print total planning and execution timing */
@@ -73,6 +74,15 @@ typedef struct ExplainState
 	ExplainWorkersState *workers_state; /* needed if parallel plan */
 } ExplainState;
 
+typedef struct PageFaults
+{
+	long		minflt;			/* # of page faults which were serviced
+								 * without requiring any I/O */
+	long		majflt;			/* # of page faults which were serviced by
+								 * doing I/O */
+}			PageFaults;
+
+
 /* Hook for plugins to get control in ExplainOneQuery() */
 typedef void (*ExplainOneQuery_hook_type) (Query *query,
 										   int cursorOptions,
@@ -108,6 +118,7 @@ extern void ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into,
 						   ParamListInfo params, QueryEnvironment *queryEnv,
 						   const instr_time *planduration,
 						   const BufferUsage *bufusage,
+						   const PageFaults * planpagefaults,
 						   const MemoryContextCounters *mem_counters);
 
 extern void ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc);

base-commit: 6f3820f37aba94232468365bae7ba5de697fe993
-- 
2.39.2

---

From: vellaipandiyan sm <[email-masked]>
Date: Thu, 21 May 2026 11:08:33 +0530

Hello hackers,

I reviewed the EXPLAIN storage I/O patch and the overall direction seems
useful, especially for distinguishing shared-buffer hits from actual
storage reads during query analysis.

One concern that stood out to me from the later discussion is the
interaction with asynchronous I/O and worker-based I/O accounting.

Since the patch currently relies on per-process getrusage() statistics, it
seems possible that the reported values could become partial or misleading
once I/O is performed outside the backend process context. In particular,
worker-based AIO could undercount storage reads/writes while still
returning non-zero values, which may make the output appear more accurate
than it actually is.

I also wonder whether this should affect the semantics/documentation of the
feature itself — i.e. whether the reported numbers should be described more
explicitly as “backend-observed synchronous storage I/O” rather than total
query I/O.

That said, the examples showing differences between OS cache hits and
actual storage reads were quite compelling, especially for query
performance debugging.

Thanks for working on the patch.

Regards,
Vellaipandiyan

---

From: Atsushi Torikoshi <[email-masked]>
Date: Sat, 23 May 2026 01:07:12 +0900

Thanks for the review!

On Thu, May 21, 2026 at 2:38 PM vellaipandiyan sm
<[email-masked]> wrote:
>
> Hello hackers,
>
> I reviewed the EXPLAIN storage I/O patch and the overall direction seems useful, especially for distinguishing shared-buffer hits from actual storage reads during query analysis.
>
> One concern that stood out to me from the later discussion is the interaction with asynchronous I/O and worker-based I/O accounting.
>
> Since the patch currently relies on per-process getrusage() statistics, it seems possible that the reported values could become partial or misleading once I/O is performed outside the backend process context. In particular, worker-based AIO could undercount storage reads/writes while still returning non-zero values, which may make the output appear more accurate than it actually is.

Yeah, to avoid reporting the misleadingly underestimated values, no
output is shown when worker-based AIO is used, as described in the
docs:

+ ..Also, when
+ <xref linkend="guc-io-method"/> is set to <literal>worker</literal>, no output
+ will be shown, as I/O handled by asynchronous workers cannot be measured
+ accurately.

> I also wonder whether this should affect the semantics/documentation of the feature itself — i.e. whether the reported numbers should be described more explicitly as “backend-observed synchronous storage I/O” rather than total query I/O.

Updated the patch to clarify that the reported values include not only
the backend process executing the query, but also any parallel query
worker processes involved in the query.

--
Regards,

Atsushi Torikoshi

---

From: Jelte Fennema-Nio <[email-masked]>
Date: Sun, 24 May 2026 11:50:00 +0200

> worker processes involved in the query.

For PG19 Tomas added an IO option to EXPLAIN. I think it would be
better to have the Storage IO tracking be enabled by the IO option,
rather than the BUFFERS option.

---

From: Atsushi Torikoshi <[email-masked]>
Date: Mon, 25 May 2026 21:17:56 +0900

> rather than the BUFFERS option.

Thank you for pointing that out.
I agree that having this under the IO option seems more intuitive.

One thing I am slightly concerned about, though, is that according to
the documentation, the IO option reports only I/O performed by scan
nodes:

> Include information on I/O performed by scan nodes providing such information.

In contrast, the information produced by this patch is based on
getrusage(), so it includes all Storage I/O performed by the backend
and parallel workers, not just scan nodes.
For example, it would also include temporary file I/O generated by
operations such as sorts that spill to disk when they exceed work_mem.

I suppose this could be clarified in the documentation, but I would be
interested to hear if anyone has concerns or objections about that
approach.

--
Regards,
Atsushi Torikoshi

---

From: Lukas Fittl <[email-masked]>
Date: Mon, 25 May 2026 11:43:43 -0700

> output is shown when worker-based AIO is used, as described in the

I think having something like this patch proposes would be extremely
valuable, but:

Do we even have a path forward here if this simply won't work with I/O workers?

This was discussed before on this thread, but if anything it seems to
me the situation has become more clear that I/O workers are going to
be used for the majority of Postgres 19+ installations.

At least for my part, I've seen both managed providers only offering
I/O workers (e.g. AWS RDS/Aurora), as well as challenges in container
environments where io_uring is not enabled.

Maybe we should try to figure out what would be needed to do better
I/O tracking on the Linux side in a way that is compatible with I/O
workers?

e.g. I assume rusage is too expensive to run on individual I/Os that
the workers process (so its not just a communication problem) -- but
would be good to benchmark.

Thanks,
Lukas

-- 
Lukas Fittl

---

From: Ilmar Y <[email-masked]>
Date: Sat, 30 May 2026 09:44:08 +0000

The following review has been posted through the commitfest application:
make installcheck-world:  not tested
Implements feature:       tested, failed
Spec compliant:           not tested
Documentation:            not tested

Hi,

I looked at v11, focusing on structured EXPLAIN output and a small local
build/test pass.

The patch applies cleanly on current master at
db5ed03217b9c238703df8b4b286115d6e940488, but git am warned about trailing
whitespace.  git diff --check origin/master...HEAD reports:

src/test/modules/test_misc/t/011_explain_storage_io.pl:47: trailing whitespace
src/test/modules/test_misc/t/011_explain_storage_io.pl:55: trailing whitespace
src/test/modules/test_misc/t/011_explain_storage_io.pl:61: trailing whitespace

A second thing I noticed is that, with io_method=sync, structured EXPLAIN
output can show an Execution Storage I/O section even when ANALYZE is not
used.

For example:

CREATE TABLE t AS SELECT generate_series(1, 3) AS a;
EXPLAIN (BUFFERS, FORMAT JSON) SELECT * FROM t;

returns, in part:

    "Planning": {
      ...
      "Storage I/O Read": 0,
      "Storage I/O Write": 0
    },
    "Execution": {
      "Storage I/O Read": 0,
      "Storage I/O Write": 0
    }

The same happens with FORMAT XML and FORMAT YAML.  The query was not
executed, so I don't think an Execution section should be emitted here.

The existing BUFFERS documentation says that when BUFFERS is true without
ANALYZE, only buffer usage during query planning is reported.  Since this
output is under BUFFERS, an Execution Storage I/O section without ANALYZE
looks surprising to me.

For comparison, text output for the same query reports only the Planning
section and has no Execution section:

Planning:
  Buffers: shared hit=12 read=7

It looks like this comes from ExplainOnePlan() considering execution Storage
I/O whenever es->buffers is true, and peek_storageio_usage() returning true
for non-text formats even when both counters are zero.  Should the execution
Storage I/O section be gated on es->analyze, similar to Execution Time?

Local checks I ran:

git diff --check origin/master...HEAD: failed with the whitespace issue above
./configure --prefix="$PWD/pg-install" --without-readline --without-zlib --without-icu: passed
make -s -j8: passed
make -s install: passed
make -C src/test/regress check TESTS=explain: passed

I tried to run the new TAP test, but this local environment is missing the
IPC::Run Perl module required by --enable-tap-tests, so I could not verify it
locally.

I could not test Linux io_uring behavior in this local macOS environment, and
I have not benchmarked the getrusage() overhead.

Regards,
Ilmar Yunusov

The new status of this patch is: Waiting on Author

---

From: Atsushi Torikoshi <[email-masked]>
Date: Mon, 1 Jun 2026 22:10:41 +0900

On Tue, May 26, 2026 at 3:44 AM Lukas Fittl <[email-masked]> wrote:

Thanks for your comment!

> I/O tracking on the Linux side in a way that is compatible with I/O

That may well be true.
Unfortunately, I do not currently have a good idea of how to achieve
this in a way that works with I/O workers.
 I'll keep thinking about whether there might be another approach.

> would be good to benchmark.

Yes, I suspect that would be the case.
Although this is somewhat different from the current discussion, I
once experimented with an implementation that called getrusage() for
each plan node, and the overhead was high enough that it was not
practically usable. [1]

I agree that many users will choose, or be required to choose,
io_method = worker. However, there are also users who will continue
to use sync or choose io_uring, so I do not think providing I/O
statistics for those users would be without value.

In particular, from a performance perspective, io_uring seems
likely to be the preferred option in the long run.
In high-performance on-premises environments, for example, it would
not be surprising to see users selecting io_uring.
Those users would also be among those most interested in
understanding how much I/O is being generated against storage,
which is the motivation behind this proposal.

On Sat, May 30, 2026 at 6:45 PM Ilmar Y <[email-masked]> wrote:

Thanks for your review!

> src/test/modules/test_misc/t/011_explain_storage_io.pl:61: trailing whitespace

I'll fix it.

> output can show an Execution Storage I/O section even when ANALYZE is not

We are currently considering moving this functionality to a new IO option that,
unlike BUFFERS, would require ANALYZE to be specified.[2]

[1] https://www.postgresql.org/message-id/CAGXjcj%3DhnEZCCDDMRv06EQnDcv0mRe6%2BPh1gv8%3DHb7NEc51y5A%40mail.gmail.com
[2] https://www.postgresql.org/message-id/CAM6-o%3DBL59LgruKXm3DCjzFzvr7TTJPtryEPj51YeGMFrWO0EQ%40mail.gmail.com

---
Regards,

Atsushi Torikoshi