--- /dev/null
+/*-------------------------------------------------------------------------
+ *
+ * create_advice.c
+ * generate advice from a finished plan that can be fed back into
+ * future planning cycles if desired
+ *
+ * Copyright (c) 2016-2024, PostgreSQL Global Development Group
+ *
+ * contrib/pg_plan_advice/pg_plan_advice.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "commands/explain.h"
+#include "parser/parsetree.h"
+#include "pg_plan_advice.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+
+typedef enum
+{
+ JOIN_NONE,
+ JOIN_FOREIGN,
+ JOIN_MERGEJOIN_PLAIN,
+ JOIN_MERGEJOIN_MATERIALIZE,
+ JOIN_NESTLOOP_PLAIN,
+ JOIN_NESTLOOP_MATERIALIZE,
+ JOIN_NESTLOOP_MEMOIZE,
+ JOIN_HASHJOIN,
+ JOIN_PARTITIONWISE
+} join_strategy;
+
+typedef struct
+{
+ PlannedStmt *stmt;
+ int rtable_length;
+ SubPlanRTInfo **rtable_subplans;
+ char **rtable_names;
+ Plan **rtable_scans;
+ bool *rtable_drivingjoin;
+ join_strategy *rtable_joinstrat;
+ List *all_join_orders;
+} advice_context;
+
+typedef struct
+{
+ bool inner_side;
+ join_strategy jstrat;
+ Node *join_order;
+} join_traversal;
+
+static void scan_plan_tree(advice_context *context, Plan *plan,
+ join_traversal *jtraversal);
+static void scan_plan_tree_list(advice_context *context, List *list);
+
+static void
+init_advice_context(advice_context *context, PlannedStmt *stmt)
+{
+ int rtable_length = list_length(stmt->rtable);
+
+ context->stmt = stmt;
+ context->rtable_length = list_length(stmt->rtable);
+ context->rtable_subplans = palloc0_array(SubPlanRTInfo *, rtable_length);
+ context->rtable_names = palloc0_array(char *, rtable_length);
+ context->rtable_scans = palloc0_array(Plan *, rtable_length);
+ context->rtable_drivingjoin = palloc0_array(bool, rtable_length);
+ context->rtable_joinstrat = palloc0_array(join_strategy, rtable_length);
+ context->all_join_orders = NIL;
+}
+
+static void
+generate_relation_identifiers(advice_context *context)
+{
+ PlannedStmt *stmt = context->stmt;
+ Index rti;
+ Index *topparent;
+
+ topparent = CreateParentRelationMap(stmt->rtable, stmt->appendRelations);
+
+ /* Main loop over the entire flattened range table. */
+ for (rti = 1; rti <= context->rtable_length; ++rti)
+ {
+ const char *result;
+
+ result = MakeRelationIdentifier(stmt->rtable,
+ topparent,
+ stmt->subrtinfos,
+ rti);
+
+ /* Save the name we just generated. */
+ context->rtable_names[rti - 1] = unconstify(char *, result);
+ }
+}
+
+static void
+scan_plan_tree(advice_context *context, Plan *plan, join_traversal *jtraversal)
+{
+ Node *join_order = NULL;
+ Index rti;
+
+ if (IsA(plan, NestLoop) || IsA(plan, HashJoin) || IsA(plan, MergeJoin))
+ {
+ join_traversal outer_join_traversal;
+ join_traversal inner_join_traversal;
+
+ /*
+ * The outermost table in a set of joins needs no join strategy advice;
+ * in the case of a bushy join, the outermost table of each clump
+ * needs join strategy advice to indicate how the clump as a whole
+ * should be joined.
+ */
+ outer_join_traversal.inner_side = false;
+ outer_join_traversal.jstrat = JOIN_NONE;
+ outer_join_traversal.join_order = NULL;
+ if (jtraversal != NULL)
+ outer_join_traversal.jstrat = jtraversal->jstrat;
+ else
+ outer_join_traversal.jstrat = JOIN_NONE;
+ outer_join_traversal.join_order = NULL;
+ scan_plan_tree(context, plan->lefttree, &outer_join_traversal);
+
+ /*
+ * Any table on the inner side of a join needs join strategy advice.
+ */
+ inner_join_traversal.inner_side = true;
+ inner_join_traversal.join_order = NULL;
+ if (IsA(plan, MergeJoin))
+ {
+ if (IsA(plan->righttree, Material))
+ inner_join_traversal.jstrat = JOIN_MERGEJOIN_MATERIALIZE;
+ else
+ inner_join_traversal.jstrat = JOIN_MERGEJOIN_PLAIN;
+ }
+ else if (IsA(plan, NestLoop))
+ {
+ if (IsA(plan->righttree, Material))
+ inner_join_traversal.jstrat = JOIN_NESTLOOP_MATERIALIZE;
+ else if (IsA(plan->righttree, Memoize))
+ inner_join_traversal.jstrat = JOIN_NESTLOOP_MEMOIZE;
+ else
+ inner_join_traversal.jstrat = JOIN_NESTLOOP_PLAIN;
+ }
+ else if (IsA(plan, HashJoin))
+ inner_join_traversal.jstrat = JOIN_HASHJOIN;
+ else
+ elog(ERROR, "unknown node type: %d", nodeTag(plan));
+ scan_plan_tree(context, plan->righttree, &inner_join_traversal);
+
+ /*
+ * We assume that left-deep (or outer-deep) join trees are the norm;
+ * hence JOIN(JOIN(A,B),C) is represented as (A B C), whereas
+ * JOIN(A,JOIN(B,C)) is represnted as (A (B C)).
+ */
+ if (IsA(outer_join_traversal.join_order, List))
+ join_order = (Node *)
+ lappend((List *) outer_join_traversal.join_order,
+ inner_join_traversal.join_order);
+ else
+ join_order = (Node *)
+ list_make2(outer_join_traversal.join_order,
+ inner_join_traversal.join_order);
+ }
+ else if (jtraversal != NULL && !jtraversal->inner_side &&
+ IsA(plan, Sort))
+ {
+ Assert(plan->lefttree != NULL && plan->righttree == NULL);
+ scan_plan_tree(context, plan->lefttree, jtraversal);
+ }
+ else if (jtraversal != NULL && jtraversal->inner_side &&
+ (IsA(plan, Material) || IsA(plan, Memoize) || IsA(plan, Hash) ||
+ IsA(plan, Sort)))
+ {
+ Assert(plan->lefttree != NULL && plan->righttree == NULL);
+ scan_plan_tree(context, plan->lefttree, jtraversal);
+ }
+ else
+ {
+ if (plan->lefttree != NULL)
+ scan_plan_tree(context, plan->lefttree, NULL);
+ if (plan->righttree != NULL)
+ scan_plan_tree(context, plan->righttree, NULL);
+
+ if (jtraversal != NULL)
+ join_order = (Node *) plan;
+ }
+
+ if (join_order != NULL)
+ {
+ if (jtraversal == NULL)
+ context->all_join_orders = lappend(context->all_join_orders,
+ join_order);
+ else
+ {
+ Assert(jtraversal->join_order == NULL);
+ jtraversal->join_order = join_order;
+ }
+ }
+
+ /* recurse into any special children */
+ switch (nodeTag(plan))
+ {
+ case T_Append:
+ scan_plan_tree_list(context, ((Append *) plan)->appendplans);
+ break;
+ case T_MergeAppend:
+ scan_plan_tree_list(context, ((MergeAppend *) plan)->mergeplans);
+ break;
+ case T_BitmapAnd:
+ scan_plan_tree_list(context, ((BitmapAnd *) plan)->bitmapplans);
+ break;
+ case T_BitmapOr:
+ scan_plan_tree_list(context, ((BitmapOr *) plan)->bitmapplans);
+ break;
+ case T_SubqueryScan:
+ scan_plan_tree(context, ((SubqueryScan *) plan)->subplan, NULL);
+ break;
+ case T_CustomScan:
+ scan_plan_tree_list(context, ((CustomScan *) plan)->custom_plans);
+ break;
+ default:
+ break;
+ }
+
+ rti = GetScannedRTI(context->stmt, plan);
+ if (rti != 0)
+ {
+ if (context->rtable_scans[rti - 1] != NULL)
+ elog(ERROR, "rti %d is duplicated", rti);
+ context->rtable_scans[rti - 1] = plan;
+ if (jtraversal != NULL)
+ {
+ context->rtable_drivingjoin[rti - 1] = !jtraversal->inner_side;
+ context->rtable_joinstrat[rti - 1] = jtraversal->jstrat;
+ }
+ }
+}
+
+static void
+scan_plan_tree_list(advice_context *context, List *list)
+{
+ ListCell *lc;
+
+ foreach(lc, list)
+ {
+ scan_plan_tree(context, lfirst(lc), NULL);
+ }
+}
+
+static void
+join_method_dump(StringInfo result, advice_context *context)
+{
+ Index rti;
+
+ for (rti = 1; rti <= context->rtable_length; ++rti)
+ {
+ char *name = context->rtable_names[rti - 1];
+ bool drivingjoin = context->rtable_drivingjoin[rti - 1];
+ join_strategy jstrat = context->rtable_joinstrat[rti - 1];
+ char *jstrat_name = NULL;
+
+ switch (jstrat)
+ {
+ case JOIN_NONE:
+ break;
+ case JOIN_FOREIGN:
+ jstrat_name = drivingjoin ? "BUSHY_FOREIGN_JOIN" : "FOREIGN_JOIN";
+ break;
+ case JOIN_MERGEJOIN_PLAIN:
+ jstrat_name = drivingjoin ? "BUSHY_MERGE_JOIN" : "MERGE_JOIN";
+ break;
+ case JOIN_MERGEJOIN_MATERIALIZE:
+ jstrat_name = drivingjoin ? "BUSHY_MERGE_JOIN_MATERIALIZE" : "MERGE_JOIN_MATERIALIZE";
+ break;
+ case JOIN_NESTLOOP_PLAIN:
+ jstrat_name = drivingjoin ? "BUSHY_NESTED_LOOP" : "NESTED_LOOP";
+ break;
+ case JOIN_NESTLOOP_MATERIALIZE:
+ jstrat_name = drivingjoin ? "BUSHY_NESTED_LOOP_MATERIALIZE" : "NESTED_LOOP_MATERIALIZE";
+ break;
+ case JOIN_NESTLOOP_MEMOIZE:
+ jstrat_name = drivingjoin ? "BUSHY_NESTED_LOOP_MEMOIZE" : "NESTED_LOOP_MEMOIZE";
+ break;
+ case JOIN_HASHJOIN:
+ jstrat_name = drivingjoin ? "BUSHY_HASH_JOIN" : "HASH_JOIN";
+ break;
+ case JOIN_PARTITIONWISE:
+ jstrat_name = drivingjoin ? "BUSHY_PARTITIONWISE" : "PARTITIONWISE";
+ break;
+ }
+
+ if (name == NULL)
+ name = "???";
+ if (jstrat_name != NULL)
+ appendStringInfo(result, "%s(%s)\n", jstrat_name, name);
+ }
+}
+
+static void
+scan_method_dump(StringInfo result, advice_context *context)
+{
+ Index rti;
+
+ for (rti = 1; rti <= context->rtable_length; ++rti)
+ {
+ char *name = context->rtable_names[rti - 1];
+ Plan *plan = context->rtable_scans[rti - 1];
+ char *scan_name = NULL;
+ Oid index_oid = InvalidOid;
+
+ /*
+ * Convert the node tag of the scan to a string. We can ignore scan
+ * types for which there is no alternative, e.g. SubqueryScan,
+ * FunctionScan, ValuesScan.
+ */
+ if (plan != NULL)
+ {
+ switch (nodeTag(plan))
+ {
+ case T_SeqScan:
+ scan_name = "SEQ_SCAN";
+ break;
+ case T_IndexScan:
+ scan_name = "INDEX_SCAN";
+ index_oid = ((IndexScan *) plan)->indexid;
+ break;
+ case T_IndexOnlyScan:
+ scan_name = "INDEX_ONLY_SCAN";
+ index_oid = ((IndexOnlyScan *) plan)->indexid;
+ break;
+ case T_BitmapHeapScan:
+ scan_name = "BITMAP_HEAP_SCAN";
+ break;
+ case T_TidScan:
+ scan_name = "TID_SCAN";
+ break;
+ case T_TidRangeScan:
+ scan_name = "TID_RANGE_SCAN";
+ break;
+ case T_CustomScan:
+ scan_name = "CUSTOM_SCAN";
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (name == NULL)
+ name = "???";
+ if (OidIsValid(index_oid))
+ {
+ char *indnsp;
+ char *indrel;
+
+ indnsp = get_namespace_name_or_temp(get_rel_namespace(index_oid));
+ indrel = get_rel_name(index_oid);
+ appendStringInfo(result, "%s(%s, %s.%s)\n", scan_name, name,
+ quote_identifier(indnsp), quote_identifier(indrel));
+ }
+ else if (scan_name != NULL)
+ appendStringInfo(result, "%s(%s)\n", scan_name, name);
+ }
+}
+
+static void
+join_order_dump_recursive(StringInfo result, advice_context *context, Node *n)
+{
+ if (IsA(n, List))
+ {
+ ListCell *lc;
+ bool first = true;
+
+ appendStringInfoString(result, "(");
+ foreach(lc, (List *) n)
+ {
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(result, " ");
+ join_order_dump_recursive(result, context, (Node *) lfirst(lc));
+ }
+ appendStringInfoString(result, ")");
+ }
+ else
+ {
+ Index rti = GetScannedRTI(context->stmt, (Plan *) n);
+
+ if (rti != 0)
+ appendStringInfoString(result, context->rtable_names[rti - 1]);
+ else
+ appendStringInfo(result, "?[rti=%d,nodetag=%d]", (int) rti,
+ (int) nodeTag(n));
+ }
+}
+
+static void
+join_order_dump(StringInfo result, advice_context *context)
+{
+ ListCell *lc;
+
+ foreach(lc, context->all_join_orders)
+ {
+ List *l = lfirst_node(List, lc);
+ ListCell *lc2;
+ bool first = true;
+
+ appendStringInfoString(result, "JOIN_ORDER(");
+ foreach(lc2, l)
+ {
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(result, " ");
+ join_order_dump_recursive(result, context, (Node *) lfirst(lc2));
+ }
+ appendStringInfoString(result, ")\n");
+ }
+}
+
+/*
+ * Generate advice from a PlannedStmt, and append it to the buffer provided.
+ *
+ * Returns false if the PlannedStmt has no plan tree, otherwise true.
+ */
+bool
+append_advice_from_plan(StringInfo buf, PlannedStmt *stmt)
+{
+ advice_context context;
+ ListCell *lc;
+
+ /* If this is a utility statement, there's no useful work to be done. */
+ if (stmt->planTree == NULL)
+ return false;
+
+ /* Initialization steps. */
+ init_advice_context(&context, stmt);
+ generate_relation_identifiers(&context);
+
+ /* First, scan the main plan tree. */
+ scan_plan_tree(&context, stmt->planTree, NULL);
+
+ /*
+ * Now, scan the list of InitPlans and SubPlans, which, confusingly,
+ * are collectively known as subplans. Some subplans may have been elided
+ * during planning, so skip any NULL entries in the array.
+ */
+ foreach(lc, stmt->subplans)
+ {
+ Plan *plan = lfirst(lc);
+
+ if (plan != NULL)
+ scan_plan_tree(&context, plan, NULL);
+ }
+
+ /* Finally, append advice to the output buffer. */
+ join_order_dump(buf, &context);
+ join_method_dump(buf, &context);
+ scan_method_dump(buf, &context);
+
+ return true;
+}
--- /dev/null
+/*-------------------------------------------------------------------------
+ *
+ * pg_plan_advice.c
+ * Main entrypoints for generating and applying planner advice
+ *
+ * Copyright (c) 2016-2024, PostgreSQL Global Development Group
+ *
+ * contrib/pg_plan_advice/pg_plan_advice.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "funcapi.h"
+#include "pg_plan_advice.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+
+static const struct config_enum_entry loglevel_options[] = {
+ {"disabled", -1, false},
+ {"debug5", DEBUG5, false},
+ {"debug4", DEBUG4, false},
+ {"debug3", DEBUG3, false},
+ {"debug2", DEBUG2, false},
+ {"debug1", DEBUG1, false},
+ {"debug", DEBUG2, true},
+ {"log", LOG, false},
+ {"info", INFO, true},
+ {"notice", NOTICE, false},
+ {"warning", WARNING, false},
+ {"error", ERROR, false},
+ {NULL, 0, false}
+};
+
+PG_MODULE_MAGIC;
+
+/* GUC variables */
+static int pg_plan_advice_log_level = -1; /* disabled */
+
+PG_FUNCTION_INFO_V1(pg_get_advice);
+
+/* Saved hook values */
+static ExecutorStart_hook_type prev_ExecutorStart = NULL;
+
+static void pg_plan_advice_ExecutorStart(QueryDesc *queryDesc, int eflags);
+
+/*
+ * Initialize this module
+ */
+void
+_PG_init(void)
+{
+ /* Define custom GUC variables. */
+ DefineCustomEnumVariable("pg_plan_advice.log_level",
+ "Generate advice for each executed plan and log it at the given level.",
+ NULL,
+ &pg_plan_advice_log_level,
+ -1,
+ loglevel_options,
+ PGC_USERSET,
+ 0,
+ NULL,
+ NULL,
+ NULL);
+
+ MarkGUCPrefixReserved("pg_plan_advice");
+
+ /* Install hooks. */
+ prev_ExecutorStart = ExecutorStart_hook;
+ ExecutorStart_hook = pg_plan_advice_ExecutorStart;
+}
+
+static void
+pg_plan_advice_ExecutorStart(QueryDesc *queryDesc, int eflags)
+{
+ if (pg_plan_advice_log_level != -1)
+ {
+ StringInfoData buf;
+
+ initStringInfo(&buf);
+ append_advice_from_plan(&buf, queryDesc->plannedstmt);
+ if (buf.len > 0)
+ ereport(pg_plan_advice_log_level,
+ errmsg("plan advice: %s", buf.data));
+ }
+
+ if (prev_ExecutorStart)
+ prev_ExecutorStart(queryDesc, eflags);
+ else
+ standard_ExecutorStart(queryDesc, eflags);
+}
+
+/*
+ * Generate and advice for a single query and return it via a text column.
+ */
+Datum
+pg_get_advice(PG_FUNCTION_ARGS)
+{
+ char *query = text_to_cstring(PG_GETARG_TEXT_PP(0));
+ List *raw_parsetree_list;
+ StringInfoData result;
+
+ initStringInfo(&result);
+
+ raw_parsetree_list = pg_parse_query(query);
+ foreach_node(RawStmt, parsetree, raw_parsetree_list)
+ {
+ List *stmt_list;
+
+ stmt_list = pg_analyze_and_rewrite_fixedparams(parsetree, query,
+ NULL, 0, NULL);
+ stmt_list = pg_plan_queries(stmt_list, query, CURSOR_OPT_PARALLEL_OK,
+ NULL);
+
+ foreach_node(PlannedStmt, stmt, stmt_list)
+ {
+ append_advice_from_plan(&result, stmt);
+ }
+ }
+
+ PG_RETURN_TEXT_P(cstring_to_text(result.data));
+}