upgrades
authorRobert Haas <rhaas@postgresql.org>
Wed, 16 Jul 2025 12:43:40 +0000 (08:43 -0400)
committerRobert Haas <rhaas@postgresql.org>
Wed, 16 Jul 2025 12:43:40 +0000 (08:43 -0400)
contrib/pg_plan_advice/pgpa_output.c
contrib/pg_plan_advice/pgpa_walker.h

index 69ca0c36f44a0183df806ee357dad04842af480c..4fe389038163d0b99f873de2ef594ae0b60c7b07 100644 (file)
@@ -20,6 +20,7 @@ typedef struct pgpa_output_context
        StringInfo      buf;
        List       *unrolled_joins[NUM_PGPA_JOIN_STRATEGY];
        List       *clumped_joins[NUM_PGPA_CLUMP_JOIN_STRATEGY];
+       List       *query_features[NUM_PGPA_QF_TYPES];
        int                     wrap_column;
 } pgpa_output_context;
 
@@ -35,6 +36,17 @@ static char *pgpa_cstring_join_strategy(pgpa_join_strategy strategy);
 
 static void pgpa_maybe_linebreak(StringInfo buf, int wrap_column);
 
+/*
+ * Append query advice to the provided buffer.
+ *
+ * Before calling this function, 'walker' must be used to iterate over the
+ * main plan tree and all subplans from the PlannedStmt.
+ *
+ * 'rt_identifiers' is a table of unique identifiers, one for each RTI.
+ * See pgpa_create_identifiers_for_planned_stmt().
+ *
+ * Results will be appended to 'buf'.
+ */
 void
 pgpa_output_advice(StringInfo buf, pgpa_plan_walker_context *walker,
                                   const char **rt_identifiers)
@@ -47,6 +59,31 @@ pgpa_output_advice(StringInfo buf, pgpa_plan_walker_context *walker,
        context.buf = buf;
        context.wrap_column = 79;       /* XXX */
 
+       /*
+        * Put all the top-level clumped joins for each strategy into a single
+        * list.
+        */
+       foreach(lc, walker->clumped_joins)
+       {
+               pgpa_clumped_join *cjoin = lfirst(lc);
+
+               context.clumped_joins[cjoin->strategy] =
+                       lappend(context.clumped_joins[cjoin->strategy], cjoin->relids);
+       }
+
+       /*
+        * Each piece of JOIN_ORDER() advice fully describes the join order for a
+        * a single unrolled join. Merging is not permitted, because that would
+        * change the meaning, e.g. SEQ_SCAN(a b c d) means simply that sequential
+        * scans should be used for all of those relations, and is thus equivalent
+        * to SEQ_SCAN(a b) SEQ_SCAN(c d), but JOIN_ORDER(a b c d) means that "a"
+        * is the driving table which is then joined to "b" then "c" then "d",
+        * which is totally different from JOIN_ORDER(a b) and JOIN_ORDER(c d).
+        *
+        * As a side effect, the calls to pgpa_output_unrolled_join() will
+        * add to the context.clumped_joins[] and context.unrolled_joins[] lists,
+        * which will be important for emitting join strategy advice further down.
+        */
        foreach(lc, walker->unrolled_joins)
        {
                pgpa_unrolled_join *ujoin = lfirst(lc);
@@ -58,14 +95,10 @@ pgpa_output_advice(StringInfo buf, pgpa_plan_walker_context *walker,
                appendStringInfoChar(context.buf, ')');
        }
 
-       foreach(lc, walker->clumped_joins)
-       {
-               pgpa_clumped_join *cjoin = lfirst(lc);
-
-               context.clumped_joins[cjoin->strategy] =
-                       lappend(context.clumped_joins[cjoin->strategy], cjoin->relids);
-       }
-
+       /*
+        * Emit just one piece of advice for each unrolled-join strategy that is
+        * used at least once in the query.
+        */
        for (int s = 0; s < NUM_PGPA_JOIN_STRATEGY; ++s)
        {
                char *strategy = pgpa_cstring_join_strategy(s);
@@ -102,6 +135,18 @@ pgpa_output_advice(StringInfo buf, pgpa_plan_walker_context *walker,
                appendStringInfoChar(buf, ')');
        }
 
+       /*
+        * Emit just one piece of advice for each clumped-join strategy that is
+        * used at least once in the query.
+        *
+        * XXX. It's not entirely clear that emitting DEGENERATE() advice is
+        * useful at all, since there is no real decision to be made: you can't
+        * decide whether or not something is provably empty. On the other hand,
+        * it might be useful to human beings even if the computer doesn't need it.
+        * FOREIGN() and PARTITIONWISE() are clearly useful, but they are scan
+        * types as well as join types, and the question of how to handle that
+        * deserves more thought.
+        */
        for (int c = 0; c < NUM_PGPA_CLUMP_JOIN_STRATEGY; ++c)
        {
                char *cstrategy = pgpa_cstring_join_clump_strategy(c);
@@ -133,13 +178,30 @@ pgpa_output_advice(StringInfo buf, pgpa_plan_walker_context *walker,
                appendStringInfoChar(buf, ')');
        }
 
+       /* Sort query features into one list per query feature type. */
        foreach(lc, walker->query_features)
        {
                pgpa_query_feature *qf = lfirst(lc);
 
+               context.query_features[qf->type] =
+                       lappend(context.query_features[qf->type], qf);
+       }
+
+       /*
+        * Emit just one piece of advice for each type of query feature that is
+        * used at least once in the query.
+        */
+       for (int t = 0; t < NUM_PGPA_QF_TYPES; ++t)
+       {
+               bool    first = true;
+
+               if (context.query_features[t] == NIL)
+                       continue;
+
                if (buf->len > 0)
                        appendStringInfoChar(buf, '\n');
-               switch (qf->type)
+
+               switch (t)
                {
                        case PGPAQF_GATHER:
                                appendStringInfo(buf, "GATHER(");
@@ -154,11 +216,40 @@ pgpa_output_advice(StringInfo buf, pgpa_plan_walker_context *walker,
                                appendStringInfo(buf, "SEMIJOIN_UNIQUE(");
                                break;
                }
-               pgpa_output_relations(&context, buf, qf->relids);
+
+               foreach(lc, context.query_features[t])
+               {
+                       pgpa_query_feature *qf = lfirst(lc);
+
+                       if (first)
+                               first = false;
+                       else
+                       {
+                               pgpa_maybe_linebreak(context.buf, context.wrap_column);
+                               appendStringInfoChar(context.buf, ' ');
+                       }
+
+                       if (bms_membership(qf->relids) == BMS_SINGLETON)
+                               pgpa_output_relations(&context, buf, qf->relids);
+                       else
+                       {
+                               appendStringInfoChar(buf, '(');
+                               pgpa_output_relations(&context, buf, qf->relids);
+                               appendStringInfoChar(buf, ')');
+                       }
+               }
                appendStringInfoChar(buf, ')');
        }
 }
 
+/*
+ * Output the members of an unrolled join, first the outermost member, and
+ * then the inner members one by one, as part of JOIN_ORDER() advice.
+ *
+ * As we visit members, we add to the context->unrolled_joins[] and
+ * context->clumped_joins[] lists. The former updates are done by this
+ * function directly, and the latter via pgpa_output_join_member.
+ */
 static void
 pgpa_output_unrolled_join(pgpa_output_context *context,
                                                  pgpa_unrolled_join *join)
@@ -185,6 +276,12 @@ pgpa_output_unrolled_join(pgpa_output_context *context,
        }
 }
 
+/*
+ * Output a single member of an unrolled join as part of JOIN_ORDER() advice.
+ *
+ * If that member happens to be a clumped join, also add it to the appropriate
+ * context->clump_joins[] list.
+ */
 static void
 pgpa_output_join_member(pgpa_output_context *context,
                                                pgpa_join_member *member)
index 692018bfe9f193abe32f2ceefb6d8694d393e2ff..f724e53801b2c167c8d177956c8f4c5b5cb2c646 100644 (file)
@@ -43,9 +43,12 @@ typedef enum pgpa_qf_type
        PGPAQF_GATHER,
        PGPAQF_GATHER_MERGE,
        PGPAQF_SEMIJOIN_NON_UNIQUE,
+       /* update NUM_PGPA_QF_TYPES if you add anything here */
        PGPAQF_SEMIJOIN_UNIQUE
 } pgpa_qf_type;
 
+#define NUM_PGPA_QF_TYPES ((int) PGPAQF_SEMIJOIN_UNIQUE + 1)
+
 /*
  * For each query feature, we keep track of the feature type and the set of
  * relids that we found underneath the relevant plan node. See the comments