From 0b92a248d722ea1aba37853131123b19bd4d1412 Mon Sep 17 00:00:00 2001 From: TatsuyaKawata Date: Sat, 24 Oct 2026 23:20:58 +0900 Subject: [PATCH v6 2/2] Collect Function Scan storage statistics outside FunctionNext() Based on a patch by David Rowley. To be squashed into the previous commit, replacing the third paragraph of its commit message with: A function's tuplestore already holds its whole result set when it is created, and is only destroyed by a rescan with changed parameters or at the end of execution. So we record the total space used by the existing tuplestores right before a rescan destroys any of them, and EXPLAIN looks at the ones still remaining at the end directly. To allow that, FunctionScanPerFuncState is moved into execnodes.h. --- src/backend/commands/explain.c | 26 +++++++++ src/backend/executor/nodeFunctionscan.c | 64 ++++++++--------------- src/include/nodes/execnodes.h | 21 ++++++-- src/test/regress/expected/planner_est.out | 3 +- src/test/regress/sql/planner_est.sql | 3 +- 5 files changed, 67 insertions(+), 50 deletions(-) diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 1b7a3956e71..abcc47014c4 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -3580,9 +3580,35 @@ show_ctescan_info(CteScanState *ctescanstate, ExplainState *es) static void show_functionscan_info(FunctionScanState *fsstate, ExplainState *es) { + bool totalDiskUsed = false; + int64 totalSpaceUsed = 0; + if (!es->analyze) return; + /* + * Similar to the logic in ExecReScanFunctionScan(), we need to fetch the + * stats from the final rescan (or the only one) and use these values if + * they're higher than what's already stored in FunctionScanState.stats. + */ + for (int i = 0; i < fsstate->nfuncs; i++) + { + FunctionScanPerFuncState *fs = &fsstate->funcstates[i]; + + if (fs->tstore != NULL) + { + bool diskUsed; + int64 spaceUsed; + + tuplestore_get_stats(fs->tstore, &diskUsed, &spaceUsed); + totalSpaceUsed += spaceUsed; + totalDiskUsed |= diskUsed; + } + } + + fsstate->stats.maxSpaceUsed = Max(fsstate->stats.maxSpaceUsed, totalSpaceUsed); + fsstate->stats.usedDisk |= totalDiskUsed; + /* * Nothing to show for the leader if execution didn't get as far as * creating any tuplestore. diff --git a/src/backend/executor/nodeFunctionscan.c b/src/backend/executor/nodeFunctionscan.c index 8b37a812fd0..34293612289 100644 --- a/src/backend/executor/nodeFunctionscan.c +++ b/src/backend/executor/nodeFunctionscan.c @@ -29,30 +29,8 @@ #include "utils/memutils.h" #include "utils/tuplestore.h" - -/* - * Runtime data for each function being scanned. - */ -typedef struct FunctionScanPerFuncState -{ - SetExprState *setexpr; /* state of the expression being evaluated */ - TupleDesc tupdesc; /* desc of the function result type */ - int colcount; /* expected number of result columns */ - Tuplestorestate *tstore; /* holds the function result set */ - int64 rowcount; /* # of rows in result set, -1 if not known */ - TupleTableSlot *func_slot; /* function result slot (or NULL) */ - - /* - * Storage statistics of the current tuplestore, to report in EXPLAIN - * ANALYZE. Only meaningful while tstore is not NULL. - */ - bool usedDisk; - int64 spaceUsed; -} FunctionScanPerFuncState; - static TupleTableSlot *FunctionNext(FunctionScanState *node); -static void update_storage_stats(FunctionScanState *node, - FunctionScanPerFuncState *fs); +static void update_storage_stats(FunctionScanState *node); /* ---------------------------------------------------------------- @@ -113,10 +91,6 @@ FunctionNext(FunctionScanState *node) * call is fast, so the overhead shouldn't be an issue. */ tuplestore_rescan(tstore); - - /* Collect its storage statistics for EXPLAIN ANALYZE */ - if (node->ss.ps.instrument) - update_storage_stats(node, &node->funcstates[0]); } /* @@ -176,10 +150,6 @@ FunctionNext(FunctionScanState *node) * call is fast, so the overhead shouldn't be an issue. */ tuplestore_rescan(fs->tstore); - - /* Collect its storage statistics for EXPLAIN ANALYZE */ - if (node->ss.ps.instrument) - update_storage_stats(node, fs); } /* @@ -375,8 +345,6 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags) */ fs->tstore = NULL; fs->rowcount = -1; - fs->usedDisk = false; - fs->spaceUsed = 0; /* * Now build a tupdesc showing the result type we expect from the @@ -566,6 +534,9 @@ ExecEndFunctionScan(FunctionScanState *node) { FunctionScanInstrumentation *si; + /* Take the tuplestores still existing into account */ + update_storage_stats(node); + Assert(ParallelWorkerNumber < node->shared_info->num_workers); si = &node->shared_info->sinstrument[ParallelWorkerNumber]; si->maxSpaceUsed = Max(si->maxSpaceUsed, node->stats.maxSpaceUsed); @@ -590,7 +561,7 @@ ExecEndFunctionScan(FunctionScanState *node) /* * update_storage_stats * Update the storage statistics to report in EXPLAIN ANALYZE with the - * function's tuplestore, which already holds the whole result set. + * tuplestores that exist at this moment. * * A FunctionScan node has one tuplestore per function. We report the peak * of the total space used by the tuplestores existing at the same time. The @@ -598,22 +569,25 @@ ExecEndFunctionScan(FunctionScanState *node) * any rescan, spilled to disk. */ static void -update_storage_stats(FunctionScanState *node, FunctionScanPerFuncState *fs) +update_storage_stats(FunctionScanState *node) { int64 totalSpaceUsed = 0; - tuplestore_get_stats(fs->tstore, &fs->usedDisk, &fs->spaceUsed); - for (int i = 0; i < node->nfuncs; i++) { - FunctionScanPerFuncState *f = &node->funcstates[i]; + FunctionScanPerFuncState *fs = &node->funcstates[i]; + bool usedDisk; + int64 spaceUsed; - if (f->tstore != NULL) - totalSpaceUsed += f->spaceUsed; + if (fs->tstore == NULL) + continue; + + tuplestore_get_stats(fs->tstore, &usedDisk, &spaceUsed); + totalSpaceUsed += spaceUsed; + node->stats.usedDisk |= usedDisk; } node->stats.maxSpaceUsed = Max(node->stats.maxSpaceUsed, totalSpaceUsed); - node->stats.usedDisk |= fs->usedDisk; } /* ---------------------------------------------------------------- @@ -653,6 +627,14 @@ ExecReScanFunctionScan(FunctionScanState *node) { ListCell *lc; + /* + * Collect the storage statistics for EXPLAIN ANALYZE before any + * tuplestore is destroyed. The tuplestores of the functions that are + * not recomputed exist at the same time, so they are counted too. + */ + if (node->ss.ps.instrument) + update_storage_stats(node); + i = 0; foreach(lc, scan->functions) { diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 6138ab29be2..9ee3b23cf06 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -1910,6 +1910,20 @@ typedef struct SubqueryScanState PlanState *subplan; } SubqueryScanState; +/* ---------------- + * Runtime data for each function being scanned by a FunctionScan node. + * ---------------- + */ +typedef struct FunctionScanPerFuncState +{ + SetExprState *setexpr; /* state of the expression being evaluated */ + TupleDesc tupdesc; /* desc of the function result type */ + int colcount; /* expected number of result columns */ + Tuplestorestate *tstore; /* holds the function result set */ + int64 rowcount; /* # of rows in result set, -1 if not known */ + TupleTableSlot *func_slot; /* function result slot (or NULL) */ +} FunctionScanPerFuncState; + /* ---------------- * FunctionScanState information * @@ -1921,15 +1935,12 @@ typedef struct SubqueryScanState * simple true if we have 1 function and no ordinality * ordinal current ordinal column value * nfuncs number of functions being executed - * funcstates per-function execution states (private in - * nodeFunctionscan.c) + * funcstates per-function execution states * argcontext memory context to evaluate function arguments in * stats storage statistics to report in EXPLAIN ANALYZE * shared_info statistics for parallel workers * ---------------- */ -struct FunctionScanPerFuncState; - typedef struct FunctionScanState { ScanState ss; /* its first field is NodeTag */ @@ -1938,7 +1949,7 @@ typedef struct FunctionScanState bool simple; int64 ordinal; int nfuncs; - struct FunctionScanPerFuncState *funcstates; /* array of length nfuncs */ + FunctionScanPerFuncState *funcstates; /* array of length nfuncs */ MemoryContext argcontext; FunctionScanInstrumentation stats; SharedFunctionScanInfo *shared_info; diff --git a/src/test/regress/expected/planner_est.out b/src/test/regress/expected/planner_est.out index fbe2d5ff1a1..6a8d223831d 100644 --- a/src/test/regress/expected/planner_est.out +++ b/src/test/regress/expected/planner_est.out @@ -29,8 +29,7 @@ BEGIN EXECUTE format('explain (analyze %s, costs on, summary off, timing off, buffers off) %s', analyze_str, query) LOOP - -- Ignore Storage output because the size varies depending on - -- the platform + -- ignore Storage output because the size varies depending on platform CONTINUE WHEN ln ~ '^\s*Storage: '; IF hide_costs = true THEN diff --git a/src/test/regress/sql/planner_est.sql b/src/test/regress/sql/planner_est.sql index 72aae8807ec..1da208eccce 100644 --- a/src/test/regress/sql/planner_est.sql +++ b/src/test/regress/sql/planner_est.sql @@ -30,8 +30,7 @@ BEGIN EXECUTE format('explain (analyze %s, costs on, summary off, timing off, buffers off) %s', analyze_str, query) LOOP - -- Ignore Storage output because the size varies depending on - -- the platform + -- ignore Storage output because the size varies depending on platform CONTINUE WHEN ln ~ '^\s*Storage: '; IF hide_costs = true THEN -- 2.34.1