Allow extensions to control scan strategy.
authorRobert Haas <rhaas@postgresql.org>
Mon, 7 Oct 2024 19:41:51 +0000 (15:41 -0400)
committerRobert Haas <rhaas@postgresql.org>
Wed, 20 Aug 2025 19:20:15 +0000 (15:20 -0400)
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
src/backend/optimizer/path/indxpath.c
src/backend/optimizer/path/tidpath.c
src/backend/optimizer/plan/planner.c
src/backend/optimizer/util/plancat.c
src/backend/optimizer/util/relnode.c
src/include/nodes/pathnodes.h
src/include/optimizer/paths.h

index 3b1c6e38971ff08bf3d73236516bf24e011f3437..9547420383745cc2708887fa292e92ab876dfdc0 100644 (file)
@@ -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;
index 601354ea3e056efe2a743a9f511295f8faecf28e..9e629c6c6ac866a17d5769dc2f404239a3a409db 100644 (file)
@@ -2232,8 +2232,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;
 
        /*
index 2bfb338b81ced8da158d6d5143ca6d50a8207bd4..3fa9dd3fa7625fb4176e04d26bbb3f7dddf46138 100644 (file)
@@ -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;
 
        /*
index 621ea162e0a49564fbae71c9c68d64ef1c9ed2ae..defd0950ce18649c3a5d116f9200560924bd0165 100644 (file)
@@ -431,6 +431,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)
index c6a58afc5e5063802aa7a2223c4269fe7017ecdb..85155ae72ab05a0cfb62f26302fabc6a263c877b 100644 (file)
@@ -555,6 +555,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);
index 6e35834ca2717e10b0b11747790039d643e5dd34..95b8180896116790055956f93aa2430f60283b4a 100644 (file)
@@ -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 */
index 80f8f36dd63ba16882fab82f5f8d86f64caa8f12..d5380592ec38ee8711e080ec8eb6c8467e17cd7a 100644 (file)
@@ -186,6 +186,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;
 
@@ -975,6 +978,8 @@ typedef struct RelOptInfo
        int32      *attr_widths pg_node_attr(read_write_ignore);
        /* zero-based set containing attnums of NOT NULL columns */
        Bitmapset  *notnullattnums;
+       /* scan strategy advice */
+       uint32          ssa_mask;
        /* relids of outer joins that can null this baserel */
        Relids          nulling_relids;
        /* LATERAL Vars and PHVs referenced by rel */
index c45a63edc33670519e5ed82f2da3117ecb3e5468..2bffe5e384d351c4f34c464580142e53f4238891 100644 (file)
 
 #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.
  *