On Fri, 16 Oct 2026 at 04:01, Tatsuya Kawata
<[email protected]> wrote:
> Thanks for pointing that out! That makes sense. Attached is v5.
I looked at v5. Can we not switch back to how you had it in v2 by
moving the FunctionScanPerFuncState into execnodes.h and using that
directly in explain.c? It's the FunctionNext() changes that I'm not
keen on. I feel that's adding too much code to a function that
executes on every row. I quickly put that together in the attached,
which applies on top of v5. It seems to pass the tests. I didn't look
further than that, so please check.
David
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 1b7a3956e71..32b19d873c8 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..d6d1777d5ac 100644
--- a/src/backend/executor/nodeFunctionscan.c
+++ b/src/backend/executor/nodeFunctionscan.c
@@ -29,27 +29,6 @@
#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);
@@ -113,10 +92,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 +151,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 +346,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
@@ -565,11 +534,31 @@ ExecEndFunctionScan(FunctionScanState *node)
if (node->shared_info != NULL && IsParallelWorker())
{
FunctionScanInstrumentation *si;
+ bool totalDiskUsed = false;
+ int64 totalSpaceUsed = 0;
+
+ for (int i = 0; i < node->nfuncs; i++)
+ {
+ FunctionScanPerFuncState* fs = &node->funcstates[i];
+
+ if (fs->tstore != NULL)
+ {
+ bool diskUsed;
+ int64 spaceUsed;
+
+ tuplestore_get_stats(fs->tstore, &diskUsed,
&spaceUsed);
+ totalSpaceUsed += spaceUsed;
+ totalDiskUsed |= diskUsed;
+ }
+ }
+
+ totalSpaceUsed = Max(node->stats.maxSpaceUsed, totalSpaceUsed);
+ totalDiskUsed |= node->stats.usedDisk;
Assert(ParallelWorkerNumber < node->shared_info->num_workers);
si = &node->shared_info->sinstrument[ParallelWorkerNumber];
- si->maxSpaceUsed = Max(si->maxSpaceUsed,
node->stats.maxSpaceUsed);
- si->usedDisk |= node->stats.usedDisk;
+ si->maxSpaceUsed = Max(si->maxSpaceUsed, totalSpaceUsed);
+ si->usedDisk |= totalDiskUsed;
}
/*
@@ -587,35 +576,6 @@ 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.
- *
- * 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
- * storage type is reported as disk if any tuplestore, of any function and in
- * any rescan, spilled to disk.
- */
-static void
-update_storage_stats(FunctionScanState *node, FunctionScanPerFuncState *fs)
-{
- 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];
-
- if (f->tstore != NULL)
- totalSpaceUsed += f->spaceUsed;
- }
-
- node->stats.maxSpaceUsed = Max(node->stats.maxSpaceUsed,
totalSpaceUsed);
- node->stats.usedDisk |= fs->usedDisk;
-}
-
/* ----------------------------------------------------------------
* ExecReScanFunctionScan
*
@@ -652,18 +612,35 @@ ExecReScanFunctionScan(FunctionScanState *node)
if (chgparam)
{
ListCell *lc;
+ bool totalDiskUsed = false;
+ int64 totalSpaceUsed = 0;
i = 0;
foreach(lc, scan->functions)
{
RangeTblFunction *rtfunc = (RangeTblFunction *)
lfirst(lc);
+ FunctionScanPerFuncState *fs = &node->funcstates[i];
if (bms_overlap(chgparam, rtfunc->funcparams))
{
- if (node->funcstates[i].tstore != NULL)
+ if (fs->tstore != NULL)
{
+ /* Collect its storage statistics for
EXPLAIN ANALYZE */
+ if (node->ss.ps.instrument)
+ {
+ bool diskUsed;
+ int64 spaceUsed;
+
+
tuplestore_get_stats(fs->tstore, &diskUsed, &spaceUsed);
+ totalDiskUsed |= diskUsed;
+ totalSpaceUsed += spaceUsed;
+ }
+
tuplestore_end(node->funcstates[i].tstore);
node->funcstates[i].tstore = NULL;
+
+ node->stats.maxSpaceUsed =
Max(node->stats.maxSpaceUsed, totalSpaceUsed);
+ node->stats.usedDisk |= totalDiskUsed;
}
node->funcstates[i].rowcount = -1;
}
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 4017d8b9c0f..4823a36349c 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,8 +1935,7 @@ 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
diff --git a/src/test/regress/expected/explain.out
b/src/test/regress/expected/explain.out
index 1603c806876..3d6b8d12bde 100644
--- a/src/test/regress/expected/explain.out
+++ b/src/test/regress/expected/explain.out
@@ -858,7 +858,7 @@ select explain_filter('explain (analyze,buffers off,costs
off) select count(*) f
-- Test tuplestore storage usage in Function Scan with ROWS FROM, which uses
-- one tuplestore per function
-select explain_filter('explain (analyze,buffers off,costs off) select count(*)
from rows from (generate_series(1,2500), generate_series(1,2500)) a(n,m)');
+select explain_filter('explain (analyze,buffers off,costs off) select count(*)
from rows from (generate_series(1,1250), generate_series(1,1250)) a(n,m)');
explain_filter
------------------------------------------------------------------
Aggregate (actual time=N.N..N.N rows=N.N loops=N)
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/explain.sql b/src/test/regress/sql/explain.sql
index cb18f495343..aa58a548b6e 100644
--- a/src/test/regress/sql/explain.sql
+++ b/src/test/regress/sql/explain.sql
@@ -197,5 +197,5 @@ select explain_filter('explain (analyze,buffers off,costs
off) select count(*) f
select explain_filter('explain (analyze,buffers off,costs off) select count(*)
from generate_series(1,2500) a(n)');
-- Test tuplestore storage usage in Function Scan with ROWS FROM, which uses
-- one tuplestore per function
-select explain_filter('explain (analyze,buffers off,costs off) select count(*)
from rows from (generate_series(1,2500), generate_series(1,2500)) a(n,m)');
+select explain_filter('explain (analyze,buffers off,costs off) select count(*)
from rows from (generate_series(1,1250), generate_series(1,1250)) a(n,m)');
reset work_mem;
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