Add memory/disk usage for Material nodes in EXPLAIN
authorDavid Rowley <[email protected]>
Fri, 5 Jul 2024 02:05:08 +0000 (14:05 +1200)
committerDavid Rowley <[email protected]>
Fri, 5 Jul 2024 02:05:08 +0000 (14:05 +1200)
Up until now, there was no ability to easily determine if a Material
node caused the underlying tuplestore to spill to disk or even see how
much memory the tuplestore used if it didn't.

Here we add some new functions to tuplestore.c to query this information
and add some additional output in EXPLAIN ANALYZE to display this
information for the Material node.

There are a few other executor node types that use tuplestores, so we
could also consider adding these details to the EXPLAIN ANALYZE for
those nodes too.  Let's consider those independently from this.  Having
the tuplestore.c infrastructure in to allow that is step 1.

Author: David Rowley
Reviewed-by: Matthias van de Meent, Dmitry Dolgov
Discussion: https://postgr.es/m/CAApHDvp5Py9g4Rjq7_inL3-MCK1Co2CRt_YWFwTU2zfQix0p4A@mail.gmail.com

src/backend/commands/explain.c
src/backend/utils/sort/tuplestore.c
src/include/utils/tuplestore.h
src/test/regress/expected/partition_prune.out
src/test/regress/sql/partition_prune.sql

index 30de9de9d4f4037699937364abd5cb83126fb0ca..1b5ab5038981e5481369f0b6f7e7c9b43227eaaa 100644 (file)
@@ -125,6 +125,7 @@ static void show_sort_info(SortState *sortstate, ExplainState *es);
 static void show_incremental_sort_info(IncrementalSortState *incrsortstate,
                                       ExplainState *es);
 static void show_hash_info(HashState *hashstate, ExplainState *es);
+static void show_material_info(MaterialState *mstate, ExplainState *es);
 static void show_memoize_info(MemoizeState *mstate, List *ancestors,
                              ExplainState *es);
 static void show_hashagg_info(AggState *aggstate, ExplainState *es);
@@ -2251,6 +2252,9 @@ ExplainNode(PlanState *planstate, List *ancestors,
        case T_Hash:
            show_hash_info(castNode(HashState, planstate), es);
            break;
+       case T_Material:
+           show_material_info(castNode(MaterialState, planstate), es);
+           break;
        case T_Memoize:
            show_memoize_info(castNode(MemoizeState, planstate), ancestors,
                              es);
@@ -3322,6 +3326,39 @@ show_hash_info(HashState *hashstate, ExplainState *es)
    }
 }
 
+/*
+ * Show information on material node, storage method and maximum memory/disk
+ * space used.
+ */
+static void
+show_material_info(MaterialState *mstate, ExplainState *es)
+{
+   Tuplestorestate *tupstore;
+   const char *storageType;
+   int64       spaceUsedKB;
+
+   if (!es->analyze)
+       return;
+
+   tupstore = mstate->tuplestorestate;
+   storageType = tuplestore_storage_type_name(tupstore);
+   spaceUsedKB = BYTES_TO_KILOBYTES(tuplestore_space_used(tupstore));
+
+   if (es->format != EXPLAIN_FORMAT_TEXT)
+   {
+       ExplainPropertyText("Storage", storageType, es);
+       ExplainPropertyInteger("Maximum Storage", "kB", spaceUsedKB, es);
+   }
+   else
+   {
+       ExplainIndentText(es);
+       appendStringInfo(es->str,
+                        "Storage: %s  Maximum Storage: " INT64_FORMAT "kB\n",
+                        storageType,
+                        spaceUsedKB);
+   }
+}
+
 /*
  * Show information on memoize hits/misses/evictions and memory usage.
  */
index 947a868e569cee4cf08ef1d7807cd004fad32626..24bb49ca874680006f291539c39acb096e40825a 100644 (file)
@@ -109,6 +109,7 @@ struct Tuplestorestate
    bool        truncated;      /* tuplestore_trim has removed tuples? */
    int64       availMem;       /* remaining memory available, in bytes */
    int64       allowedMem;     /* total memory allowed, in bytes */
+   int64       maxSpace;       /* maximum space used in memory */
    int64       tuples;         /* number of tuples added */
    BufFile    *myfile;         /* underlying file, or NULL if none */
    MemoryContext context;      /* memory context for holding tuples */
@@ -238,6 +239,7 @@ static Tuplestorestate *tuplestore_begin_common(int eflags,
                                                int maxKBytes);
 static void tuplestore_puttuple_common(Tuplestorestate *state, void *tuple);
 static void dumptuples(Tuplestorestate *state);
+static void tuplestore_updatemax(Tuplestorestate *state);
 static unsigned int getlen(Tuplestorestate *state, bool eofOK);
 static void *copytup_heap(Tuplestorestate *state, void *tup);
 static void writetup_heap(Tuplestorestate *state, void *tup);
@@ -262,6 +264,7 @@ tuplestore_begin_common(int eflags, bool interXact, int maxKBytes)
    state->truncated = false;
    state->allowedMem = maxKBytes * 1024L;
    state->availMem = state->allowedMem;
+   state->maxSpace = 0;
    state->myfile = NULL;
    state->context = CurrentMemoryContext;
    state->resowner = CurrentResourceOwner;
@@ -420,6 +423,9 @@ tuplestore_clear(Tuplestorestate *state)
    int         i;
    TSReadPointer *readptr;
 
+   /* update the maxSpace before doing any USEMEM/FREEMEM adjustments */
+   tuplestore_updatemax(state);
+
    if (state->myfile)
        BufFileClose(state->myfile);
    state->myfile = NULL;
@@ -1402,6 +1408,9 @@ tuplestore_trim(Tuplestorestate *state)
    Assert(nremove >= state->memtupdeleted);
    Assert(nremove <= state->memtupcount);
 
+   /* before freeing any memory, update maxSpace */
+   tuplestore_updatemax(state);
+
    /* Release no-longer-needed tuples */
    for (i = state->memtupdeleted; i < nremove; i++)
    {
@@ -1444,6 +1453,49 @@ tuplestore_trim(Tuplestorestate *state)
    }
 }
 
+/*
+ * tuplestore_updatemax
+ *     Update maxSpace field
+ */
+static void
+tuplestore_updatemax(Tuplestorestate *state)
+{
+   if (state->status == TSS_INMEM)
+       state->maxSpace = Max(state->maxSpace,
+                             state->allowedMem - state->availMem);
+}
+
+/*
+ * tuplestore_storage_type_name
+ *     Return a string description of the storage method used to store the
+ *     tuples.
+ */
+const char *
+tuplestore_storage_type_name(Tuplestorestate *state)
+{
+   if (state->status == TSS_INMEM)
+       return "Memory";
+   else
+       return "Disk";
+}
+
+/*
+ * tuplestore_space_used
+ *     Return the maximum space used in memory unless the tuplestore has spilled
+ *     to disk, in which case, return the disk space used.
+ */
+int64
+tuplestore_space_used(Tuplestorestate *state)
+{
+   /* First, update the maxSpace field */
+   tuplestore_updatemax(state);
+
+   if (state->status == TSS_INMEM)
+       return state->maxSpace;
+   else
+       return BufFileSize(state->myfile);
+}
+
 /*
  * tuplestore_in_memory
  *
@@ -1513,6 +1565,7 @@ writetup_heap(Tuplestorestate *state, void *tup)
    if (state->backward)        /* need trailing length word? */
        BufFileWrite(state->myfile, &tuplen, sizeof(tuplen));
 
+   /* no need to call tuplestore_updatemax() when not in TSS_INMEM */
    FREEMEM(state, GetMemoryChunkSpace(tuple));
    heap_free_minimal_tuple(tuple);
 }
index 419613c17bab13f11cb5a5249548759ef8f69930..3d8a90caaf93f9cd029de9568ebfb2c7074c147b 100644 (file)
@@ -65,6 +65,10 @@ extern void tuplestore_copy_read_pointer(Tuplestorestate *state,
 
 extern void tuplestore_trim(Tuplestorestate *state);
 
+extern const char *tuplestore_storage_type_name(Tuplestorestate *state);
+
+extern int64 tuplestore_space_used(Tuplestorestate *state);
+
 extern bool tuplestore_in_memory(Tuplestorestate *state);
 
 extern bool tuplestore_gettupleslot(Tuplestorestate *state, bool forward,
index 7ca98397aecca6595756c3c17b30fb4a6a05117e..7a03b4e3607ab213e45cb380383f483e81a60694 100644 (file)
@@ -1,6 +1,24 @@
 --
 -- Test partitioning planner code
 --
+-- Helper function which can be used for masking out portions of EXPLAIN
+-- ANALYZE which could contain information that's not consistent on all
+-- platforms.
+create function explain_analyze(query text) returns setof text
+language plpgsql as
+$$
+declare
+    ln text;
+begin
+    for ln in
+        execute format('explain (analyze, costs off, summary off, timing off) %s',
+            query)
+    loop
+        ln := regexp_replace(ln, 'Maximum Storage: \d+', 'Maximum Storage: N');
+        return next ln;
+    end loop;
+end;
+$$;
 -- Force generic plans to be used for all prepared statements in this file.
 set plan_cache_mode = force_generic_plan;
 create table lp (a char) partition by list (a);
@@ -2826,9 +2844,9 @@ deallocate ab_q5;
 deallocate ab_q6;
 -- UPDATE on a partition subtree has been seen to have problems.
 insert into ab values (1,2);
-explain (analyze, costs off, summary off, timing off)
-update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;
-                                        QUERY PLAN                                         
+select explain_analyze('
+update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
+                                      explain_analyze                                      
 -------------------------------------------------------------------------------------------
  Update on ab_a1 (actual rows=0 loops=1)
    Update on ab_a1_b1 ab_a1_1
@@ -2851,6 +2869,7 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
          ->  Materialize (actual rows=1 loops=1)
+               Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1 loops=1)
                      ->  Bitmap Heap Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
                            Recheck Cond: (a = 1)
@@ -2866,7 +2885,7 @@ update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                                  Index Cond: (a = 1)
-(36 rows)
+(37 rows)
 
 table ab;
  a | b 
@@ -2877,9 +2896,9 @@ table ab;
 -- Test UPDATE where source relation has run-time pruning enabled
 truncate ab;
 insert into ab values (1, 1), (1, 2), (1, 3), (2, 1);
-explain (analyze, costs off, summary off, timing off)
-update ab_a1 set b = 3 from ab_a2 where ab_a2.b = (select 1);
-                                  QUERY PLAN                                  
+select explain_analyze('
+update ab_a1 set b = 3 from ab_a2 where ab_a2.b = (select 1);');
+                               explain_analyze                                
 ------------------------------------------------------------------------------
  Update on ab_a1 (actual rows=0 loops=1)
    Update on ab_a1_b1 ab_a1_1
@@ -2893,6 +2912,7 @@ update ab_a1 set b = 3 from ab_a2 where ab_a2.b = (select 1);
                ->  Seq Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
                ->  Seq Scan on ab_a1_b3 ab_a1_3 (actual rows=1 loops=1)
          ->  Materialize (actual rows=1 loops=3)
+               Storage: Memory  Maximum Storage: NkB
                ->  Append (actual rows=1 loops=1)
                      ->  Seq Scan on ab_a2_b1 ab_a2_1 (actual rows=1 loops=1)
                            Filter: (b = (InitPlan 1).col1)
@@ -2900,7 +2920,7 @@ update ab_a1 set b = 3 from ab_a2 where ab_a2.b = (select 1);
                            Filter: (b = (InitPlan 1).col1)
                      ->  Seq Scan on ab_a2_b3 ab_a2_3 (never executed)
                            Filter: (b = (InitPlan 1).col1)
-(19 rows)
+(20 rows)
 
 select tableoid::regclass, * from ab;
  tableoid | a | b 
@@ -4419,3 +4439,4 @@ explain (costs off) select * from hp_contradict_test where a === 1 and b === 1 a
 drop table hp_contradict_test;
 drop operator class part_test_int4_ops2 using hash;
 drop operator ===(int4, int4);
+drop function explain_analyze(text);
index a09b27d820c98d555e0689a56512e8297b55f92d..442428d937cb6c8f72692a2ffa2a1fc2a2b2758c 100644 (file)
@@ -2,6 +2,25 @@
 -- Test partitioning planner code
 --
 
+-- Helper function which can be used for masking out portions of EXPLAIN
+-- ANALYZE which could contain information that's not consistent on all
+-- platforms.
+create function explain_analyze(query text) returns setof text
+language plpgsql as
+$$
+declare
+    ln text;
+begin
+    for ln in
+        execute format('explain (analyze, costs off, summary off, timing off) %s',
+            query)
+    loop
+        ln := regexp_replace(ln, 'Maximum Storage: \d+', 'Maximum Storage: N');
+        return next ln;
+    end loop;
+end;
+$$;
+
 -- Force generic plans to be used for all prepared statements in this file.
 set plan_cache_mode = force_generic_plan;
 
@@ -676,15 +695,15 @@ deallocate ab_q6;
 
 -- UPDATE on a partition subtree has been seen to have problems.
 insert into ab values (1,2);
-explain (analyze, costs off, summary off, timing off)
-update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;
+select explain_analyze('
+update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
 table ab;
 
 -- Test UPDATE where source relation has run-time pruning enabled
 truncate ab;
 insert into ab values (1, 1), (1, 2), (1, 3), (2, 1);
-explain (analyze, costs off, summary off, timing off)
-update ab_a1 set b = 3 from ab_a2 where ab_a2.b = (select 1);
+select explain_analyze('
+update ab_a1 set b = 3 from ab_a2 where ab_a2.b = (select 1);');
 select tableoid::regclass, * from ab;
 
 drop table ab, lprt_a;
@@ -1318,3 +1337,5 @@ explain (costs off) select * from hp_contradict_test where a === 1 and b === 1 a
 drop table hp_contradict_test;
 drop operator class part_test_int4_ops2 using hash;
 drop operator ===(int4, int4);
+
+drop function explain_analyze(text);