From 8d4640c7f9722ff4bf3d1af918f5afa61a7de3b7 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 7 Oct 2024 15:41:51 -0400
Subject: [PATCH v4 2/4] Allow extensions to control scan strategy.

At the start of planning, we build a bitmask of allowable scan
strategies beased on the value of the various enable_* planner GUCs.
Extensions can override the behavior on a per-rel basis using
get_relation_info_hook.

As with the join strategy advice, this isn't sufficient for all
needs. If you want to control which index is used, the same hook,
get_relation_info_hook, that you use to set scan strategy can also
editorialize on the index list. However, that doesn't appear to be
sufficient to fully control the shape of bitmap plans.

Another gap is that it's not clear what to do if you want to
encourage parallel plans or non-parallel plans. It is not entirely
clear to me whether that is a problem that is specific to the scan
level or whether it is something more general.
---
 src/backend/optimizer/path/costsize.c | 25 +++++++++++++---------
 src/backend/optimizer/path/indxpath.c |  4 ++--
 src/backend/optimizer/path/tidpath.c  |  7 ++++---
 src/backend/optimizer/plan/planner.c  | 22 ++++++++++++++++++++
 src/backend/optimizer/util/plancat.c  |  3 +++
 src/backend/optimizer/util/relnode.c  |  7 +++++++
 src/include/nodes/pathnodes.h         |  5 +++++
 src/include/optimizer/paths.h         | 30 +++++++++++++++++++++++++++
 8 files changed, 88 insertions(+), 15 deletions(-)

diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 5888ecac65d..3a0f30a1a6d 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -354,7 +354,7 @@ cost_seqscan(Path *path, PlannerInfo *root,
 		path->rows = clamp_row_est(path->rows / parallel_divisor);
 	}
 
-	path->disabled_nodes = enable_seqscan ? 0 : 1;
+	path->disabled_nodes = (baserel->ssa_mask & SSA_SEQSCAN) != 0 ? 0 : 1;
 	path->startup_cost = startup_cost;
 	path->total_cost = startup_cost + cpu_run_cost + disk_run_cost;
 }
@@ -583,6 +583,7 @@ cost_index(IndexPath *path, PlannerInfo *root, double loop_count,
 	double		pages_fetched;
 	double		rand_heap_pages;
 	double		index_pages;
+	bool		enabled;
 
 	/* Should only be applied to base relations */
 	Assert(IsA(baserel, RelOptInfo) &&
@@ -614,8 +615,12 @@ cost_index(IndexPath *path, PlannerInfo *root, double loop_count,
 											  path->indexclauses);
 	}
 
-	/* we don't need to check enable_indexonlyscan; indxpath.c does that */
-	path->path.disabled_nodes = enable_indexscan ? 0 : 1;
+	/* is this scan type disabled? */
+	if (indexonly)
+		enabled = (baserel->ssa_mask & SSA_INDEXONLYSCAN) ? 1 : 0;
+	else
+		enabled = (baserel->ssa_mask & SSA_INDEXSCAN) ? 1 : 0;
+	path->path.disabled_nodes = enabled ? 0 : 1;
 
 	/*
 	 * Call index-access-method-specific code to estimate the processing cost
@@ -1109,7 +1114,7 @@ cost_bitmap_heap_scan(Path *path, PlannerInfo *root, RelOptInfo *baserel,
 	startup_cost += path->pathtarget->cost.startup;
 	run_cost += path->pathtarget->cost.per_tuple * path->rows;
 
-	path->disabled_nodes = enable_bitmapscan ? 0 : 1;
+	path->disabled_nodes = (baserel->ssa_mask & SSA_BITMAPSCAN) != 0 ? 0 : 1;
 	path->startup_cost = startup_cost;
 	path->total_cost = startup_cost + run_cost;
 }
@@ -1287,10 +1292,10 @@ cost_tidscan(Path *path, PlannerInfo *root,
 
 		/*
 		 * We must use a TID scan for CurrentOfExpr; in any other case, we
-		 * should be generating a TID scan only if enable_tidscan=true. Also,
+		 * should be generating a TID scan only if TID scans are allowed. Also,
 		 * if CurrentOfExpr is the qual, there should be only one.
 		 */
-		Assert(enable_tidscan || IsA(qual, CurrentOfExpr));
+		Assert((baserel->ssa_mask & SSA_TIDSCAN) != 0 || IsA(qual, CurrentOfExpr));
 		Assert(list_length(tidquals) == 1 || !IsA(qual, CurrentOfExpr));
 
 		if (IsA(qual, ScalarArrayOpExpr))
@@ -1342,8 +1347,8 @@ cost_tidscan(Path *path, PlannerInfo *root,
 
 	/*
 	 * There are assertions above verifying that we only reach this function
-	 * either when enable_tidscan=true or when the TID scan is the only legal
-	 * path, so it's safe to set disabled_nodes to zero here.
+	 * either when baserel->ssa_mask includes SSA_TIDSCAN or when the TID scan
+	 * is the only legal path, so it's safe to set disabled_nodes to zero here.
 	 */
 	path->disabled_nodes = 0;
 	path->startup_cost = startup_cost;
@@ -1438,8 +1443,8 @@ cost_tidrangescan(Path *path, PlannerInfo *root,
 	startup_cost += path->pathtarget->cost.startup;
 	run_cost += path->pathtarget->cost.per_tuple * path->rows;
 
-	/* we should not generate this path type when enable_tidscan=false */
-	Assert(enable_tidscan);
+	/* we should not generate this path type when TID scans are disabled */
+	Assert((baserel->ssa_mask & SSA_TIDSCAN) != 0);
 	path->disabled_nodes = 0;
 	path->startup_cost = startup_cost;
 	path->total_cost = startup_cost + run_cost;
diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c
index c0fcc7d78df..a42dbc38251 100644
--- a/src/backend/optimizer/path/indxpath.c
+++ b/src/backend/optimizer/path/indxpath.c
@@ -1735,8 +1735,8 @@ check_index_only(RelOptInfo *rel, IndexOptInfo *index)
 	ListCell   *lc;
 	int			i;
 
-	/* Index-only scans must be enabled */
-	if (!enable_indexonlyscan)
+	/* If we're not allowed to consider index-only scans, give up now */
+	if ((rel->ssa_mask & SSA_CONSIDER_INDEXONLY) == 0)
 		return false;
 
 	/*
diff --git a/src/backend/optimizer/path/tidpath.c b/src/backend/optimizer/path/tidpath.c
index b0323b26eca..efe569457fa 100644
--- a/src/backend/optimizer/path/tidpath.c
+++ b/src/backend/optimizer/path/tidpath.c
@@ -500,18 +500,19 @@ create_tidscan_paths(PlannerInfo *root, RelOptInfo *rel)
 	List	   *tidquals;
 	List	   *tidrangequals;
 	bool		isCurrentOf;
+	bool		enabled = (rel->ssa_mask & SSA_TIDSCAN) != 0;
 
 	/*
 	 * If any suitable quals exist in the rel's baserestrict list, generate a
 	 * plain (unparameterized) TidPath with them.
 	 *
-	 * We skip this when enable_tidscan = false, except when the qual is
+	 * We skip this when TID scans are disabled, except when the qual is
 	 * CurrentOfExpr. In that case, a TID scan is the only correct path.
 	 */
 	tidquals = TidQualFromRestrictInfoList(root, rel->baserestrictinfo, rel,
 										   &isCurrentOf);
 
-	if (tidquals != NIL && (enable_tidscan || isCurrentOf))
+	if (tidquals != NIL && (enabled || isCurrentOf))
 	{
 		/*
 		 * This path uses no join clauses, but it could still have required
@@ -533,7 +534,7 @@ create_tidscan_paths(PlannerInfo *root, RelOptInfo *rel)
 	}
 
 	/* Skip the rest if TID scans are disabled. */
-	if (!enable_tidscan)
+	if (!enabled)
 		return false;
 
 	/*
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 7c1000879ec..8b0a507f2d9 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -419,6 +419,28 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 		tuple_fraction = 0.0;
 	}
 
+	/*
+	 * Compute the initial scan strategy advice mask.
+	 *
+	 * It may seem surprising that enable_indexscan sets both SSA_INDEXSCAN
+	 * and SSA_INDEXONLYSCAN. However, the historical behavior of this GUC
+	 * corresponds to this exactly: enable_indexscan=off disables both
+	 * index-scan and index-only scan paths, whereas enable_indexonlyscan=off
+	 * converts the index-only scan paths that we would have considered into
+	 * index scan paths.
+	 */
+	glob->default_ssa_mask = 0;
+	if (enable_tidscan)
+		glob->default_ssa_mask |= SSA_TIDSCAN;
+	if (enable_seqscan)
+		glob->default_ssa_mask |= SSA_SEQSCAN;
+	if (enable_indexscan)
+		glob->default_ssa_mask |= SSA_INDEXSCAN | SSA_INDEXONLYSCAN;
+	if (enable_indexonlyscan)
+		glob->default_ssa_mask |= SSA_CONSIDER_INDEXONLY;
+	if (enable_bitmapscan)
+		glob->default_ssa_mask |= SSA_BITMAPSCAN;
+
 	/* Compute the initial join strategy advice mask. */
 	glob->default_jsa_mask = JSA_FOREIGN;
 	if (enable_hashjoin)
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index b913f91ff03..97066ec1f86 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -570,6 +570,9 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,
 	 * Allow a plugin to editorialize on the info we obtained from the
 	 * catalogs.  Actions might include altering the assumed relation size,
 	 * removing an index, or adding a hypothetical index to the indexlist.
+	 *
+	 * An extension can also modify rel->ssa_mask here to control the scan
+	 * strategy.
 	 */
 	if (get_relation_info_hook)
 		(*get_relation_info_hook) (root, relationObjectId, inhparent, rel);
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 9e328c5ac7c..8d56f5717e4 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -321,6 +321,12 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptInfo *parent)
 		rel->direct_lateral_relids = parent->direct_lateral_relids;
 		rel->lateral_relids = parent->lateral_relids;
 		rel->lateral_referencers = parent->lateral_referencers;
+
+		/*
+		 * By default, a parent's scan strategy advice is preserved for each
+		 * inheritance child.
+		 */
+		rel->ssa_mask = parent->ssa_mask;
 	}
 	else
 	{
@@ -331,6 +337,7 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptInfo *parent)
 		rel->direct_lateral_relids = NULL;
 		rel->lateral_relids = NULL;
 		rel->lateral_referencers = NULL;
+		rel->ssa_mask = root->glob->default_ssa_mask;
 	}
 
 	/* Check type of rtable entry */
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index ca328d6edad..8f13f2c80e8 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -161,6 +161,9 @@ typedef struct PlannerGlobal
 	/* worst PROPARALLEL hazard level */
 	char		maxParallelHazard;
 
+	/* default scan strategy advice, except where overrriden by hooks */
+	uint32		default_ssa_mask;
+
 	/* default join strategy advice, except where overrriden by hooks */
 	uint32		default_jsa_mask;
 
@@ -931,6 +934,8 @@ typedef struct RelOptInfo
 	Relids	   *attr_needed pg_node_attr(read_write_ignore);
 	/* array indexed [min_attr .. max_attr] */
 	int32	   *attr_widths pg_node_attr(read_write_ignore);
+	/* scan strategy advice */
+	uint32		ssa_mask;
 
 	/*
 	 * Zero-based set containing attnums of NOT NULL columns.  Not populated
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h
index f9f346f86a1..4ee0344e5e1 100644
--- a/src/include/optimizer/paths.h
+++ b/src/include/optimizer/paths.h
@@ -16,6 +16,36 @@
 
 #include "nodes/pathnodes.h"
 
+/*
+ * Scan strategy advice.
+ *
+ * If SSA_CONSIDER_INDEXONLY is not set, index-only scan paths will not even
+ * be generated, and we'll generated index-scan paths for the same cases
+ * instead. If any other bit is not set, paths of that type will still be
+ * generated but will be marked as disabled.
+ *
+ * So, if you want to avoid an index-only scan, you can either unset
+ * SSA_CONSIDER_INDEXONLY (in which case you'll get an index-scan instead,
+ * which may end up disabled if you also unset SSA_INDEXSCAN) or you can
+ * unset SSA_INDEXONLYSCAN (in which the index-only scan will be disabled
+ * and the cheapest non-disabled alternative, if any, will be chosen, but
+ * no corresponding index scan will be considered). If, on the other hand,
+ * you want to encourage an index-only scan, you can set just SSA_INDEXONLYSCAN
+ * and SSA_CONSIDER_INDEXONLY and clear all of the other bits.
+ *
+ * A default scan strategy advice mask is stored in the PlannerGlobal object
+ * based on the values of the various enable_* GUCs. This value is propagted
+ * into each RelOptInfo for a baserel, and from baserels to their inheritance
+ * children when partitions are expanded. In either case, the value can be
+ * usefully changed in get_relation_info_hook.
+ */
+#define SSA_TIDSCAN						0x0001
+#define SSA_SEQSCAN						0x0002
+#define SSA_INDEXSCAN					0x0004
+#define SSA_INDEXONLYSCAN				0x0008
+#define SSA_BITMAPSCAN					0x0010
+#define SSA_CONSIDER_INDEXONLY			0x0020
+
 /*
  * Join strategy advice.
  *
-- 
2.39.3 (Apple Git-145)

