开发版本: 19 / devel
此文档适用于不受支持的 PostgreSQL 版本。
您可能需要查看当前版本的相同页面,或上面列出的其他受支持版本。

F.30. pg_plan_advice —— 帮助规划器获得正确的计划 #

pg_plan_advice模块允许使用一种专门用途的“计划建议”迷你语言来描述、重现并更改关键的规划器决策。它的目标是帮助稳定用户认为合适的计划选择,同时也便于尝试规划器认为并非最优的计划。

请注意,由于规划器通常会做出不错的决定,强行覆盖它的判断很容易适得其反。例如,如果底层数据的分布发生变化,规划器通常可以调整计划,以尽量保持良好的性能。如果计划建议阻止了这种调整,就可能选出一个非常糟糕的计划。只有在约束规划器选择所带来的风险小于收益时,才应使用计划建议。

F.30.1. 入门 #

首先,必须设法加载 pg_plan_advice 模块。可以通过把 pg_plan_advice 加入 shared_preload_libraries 并重启服务器来在整个系统范围内加载它; 也可以把它加入 session_preload_libraries 并启动一个新会话; 或者使用 LOAD 命令把它加载到单个会话中。

一旦加载了 pg_plan_advice 模块, EXPLAIN 就会支持 PLAN_ADVICE 选项。可以用这个选项查看所选计划对应的计划建议字符串。例如:

EXPLAIN (COSTS OFF, PLAN_ADVICE)
        SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id;
             QUERY PLAN
------------------------------------
 Hash Join
   Hash Cond: (f.dim_id = d.id)
   ->  Seq Scan on join_fact f
   ->  Hash
         ->  Seq Scan on join_dim d
 Generated Plan Advice:
   JOIN_ORDER(f d)
   HASH_JOIN(d)
   SEQ_SCAN(f d)
   NO_GATHER(f d)

在这个例子中,用户并没有指定任何建议;相反,允许规划器自行做出它认为最优的决定,然后把这些决定整理成一条建议字符串。JOIN_ORDER(f d) 表示 f 应该是驱动表,并且首先与 d 连接。HASH_JOIN(d) 表示 d 应该出现在哈希连接的内侧。SEQ_SCAN(f d) 表示 fd 都应该通过顺序扫描访问。NO_GATHER(f d) 表示 fd 都不应出现在 GatherGather Merge 节点下方。有关计划建议迷你语言的更多细节,请参阅下面的 建议目标建议标签

一旦你为某个查询得到了一条建议字符串,就可以用它来控制该查询的规划方式。做法是将 pg_plan_advice.advice 设置为你选定的建议字符串。这个字符串既可以是系统生成的,也可以是你自己编写的。编写自己的建议字符串的一种好办法,是取系统生成的字符串,然后只保留你希望强制执行的那些元素。在上面的例子中,pg_plan_advice 会为连接顺序、连接方法、扫描方法以及并行度的使用输出建议,但你可能只想控制连接顺序:

SET pg_plan_advice.advice = 'JOIN_ORDER(f d)';
EXPLAIN (COSTS OFF)
        SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id;
             QUERY PLAN
------------------------------------
 Hash Join
   Hash Cond: (f.dim_id = d.id)
   ->  Seq Scan on join_fact f
   ->  Hash
         ->  Seq Scan on join_dim d
 Supplied Plan Advice:
   JOIN_ORDER(f d) /* matched */

由于 EXPLAIN 并未指定 PLAN_ADVICE 选项,所以不会为该计划生成建议字符串。不过,提供的计划建议仍会显示出来,这样查看 EXPLAIN 输出的人就能知道该计划受到了计划建议的影响。如果不希望显示提供的计划建议,可以把 pg_plan_advice.always_explain_supplied_advice = false。对于每一条提供的建议,输出都会显示 建议反馈,表明该建议是否成功应用到查询中。在这个例子中,反馈显示为 /* matched */,表示 fd 在查询中都找到了,而且生成的查询计划符合所指定的建议。

F.30.2. 工作原理 #

计划建议是以命令式方式编写的,也就是说,它指定应该做什么。不过在实现层面上,pg_plan_advice 的工作方式是告诉核心规划器什么应该做。换句话说,它是通过约束规划器的选择来工作的,而不是替换规划器。因此,无论你提供什么建议,你得到的都只会是核心规划器本来就会为相关查询考虑的计划。如果你试图通过提供一条建议字符串来强行得到你认为正确的计划,而规划器仍然无法生成所需计划,这意味着要么你的建议字符串有 bug,要么核心规划器根本没有把该计划视为可行。这通常有两个原因。第一,规划器可能认为你试图强制的计划在语义上不正确,也就是会产生错误结果,因此没有考虑它。第二,规划器可能因为除代价之外的原因拒绝了你希望生成的计划。例如,给定一个非常简单的查询,比如 SELECT * FROM some_table,查询规划器会在进行任何代价计算之前就判断这里使用索引毫无价值。即使你设置了 enable_seqscan = false,也无法强迫它在这个查询中使用索引;同样,也不能通过计划建议强迫它这样做。

指定计划建议不应导致规划器失败。不过,如果你指定了要求某些不可能之事的计划建议,EXPLAIN 输出中可能会有一些计划节点被标记为 Disabled: true。在某些情况下,这样的计划与完全没有提供建议时得到的计划基本相同,但在另一些情况下,它们可能糟得多。例如:

SET pg_plan_advice.advice = 'JOIN_ORDER(x f d)';
EXPLAIN (COSTS OFF)
        SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id;
                     QUERY PLAN
----------------------------------------------------
 Nested Loop
   Disabled: true
   ->  Seq Scan on join_fact f
   ->  Index Scan using join_dim_pkey on join_dim d
         Index Cond: (id = f.dim_id)
 Supplied Plan Advice:
   JOIN_ORDER(x f d) /* partially matched */

由于 fd 都不是 JOIN_ORDER() 规格中的第一个表,规划器会禁用它们之间的所有直接连接,认为应该先与 x 连接。由于规划不允许失败,最终还是会选出一个被禁用的两关系计划,但这里得到的是一个 Nested Loop,而不是前面那个未指定任何建议时选出的 Hash Join。这种情况可以通过多种不同方式发生;一旦发生,得到的计划通常会比完全没有指定建议时更差。因此,最好验证你指定的建议确实适用于它所对应的查询,而且结果符合预期。

F.30.3. 建议目标 #

建议目标用于唯一标识某个特定查询中涉及的某个关系的某个实例。在前面展示的简单例子中,建议目标就是关系别名。不过,当使用子查询、表分区,或者同一个关系别名在同一子查询中出现不止一次时,就需要更复杂的语法(例如 (foo JOIN bar ON foo.a = bar.a) x JOIN foo ON x.b = foo.b)。这三种情况还可以同时出现:一个关系可以被引用多次,可以是分区表,也可以位于子查询内部。

因此,关系标识符的一般语法是:

alias_name#occurrence_number/partition_schema.partition_name@plan_name

除了 alias_name 之外,其余所有组成部分都是可选的,只在必要时才出现。某个组成部分省略时,前面的标点也必须同时省略。对于给定子查询中的某个关系首次出现,系统生成的建议会省略出现次数编号,但如果愿意,也可以写成 #1。分区模式和分区名只用于分区表的子项。在系统生成的建议中,pg_plan_advice 总是同时包含两者,但模式可以省略。计划名对顶层计划会省略,而对任何子计划都必须写出。

仅凭查看查询,并不总是容易确定正确的建议目标。比如,如果规划器把一个子查询拉升到父查询级别,那么其中的所有内容都会变成父查询级别的一部分,并使用父查询的子计划名(如果已拉升到顶层,则不使用子计划名)。此外,正确的子查询名有时也并不明显。例如,当两个查询通过 UNIONINTERSECT 之类的操作组合时,SQL 语法里不会给子查询指定名字;相反,系统会为每个分支分配一个生成的名字。发现正确建议目标的最简单方法是使用 EXPLAIN (PLAN_ADVICE) 并检查生成的建议。

F.30.4. 建议标签 #

建议标签用于指定查询某一部分应强制遵循的特定行为,例如某个特定的连接顺序或连接方法。所有建议标签都以 建议目标 作为参数,而且其中很多都允许使用建议目标列表,而这些列表在某些情况下可以嵌套多层。建议标签有好几类,每一类控制查询规划的一个不同方面。

F.30.4.1. 扫描方法建议 #

SEQ_SCAN(target [ ... ])
TID_SCAN(target [ ... ])
INDEX_SCAN(target index_name [ ... ])
INDEX_ONLY_SCAN(target index_name [ ... ])
FOREIGN_JOIN((target [ ... ]) [ ... ])
BITMAP_HEAP_SCAN(target [ ... ])
DO_NOT_SCAN(target [ ... ])

SEQ_SCAN 指定每个目标都应使用 Seq Scan 扫描。TID_SCAN 指定每个目标都应使用 TID ScanTID Range Scan 扫描。BITMAP_HEAP_SCAN 指定每个目标都应使用 Bitmap Heap Scan 扫描。

INDEX_SCAN 指定每个目标都应使用给定索引名上的 Index Scan 扫描。INDEX_ONLY_SCAN 类似,但指定使用 Index Only Scan。在这两种情况下,索引名都可以带模式限定,也可以不带。

FOREIGN_JOIN 指定两个或多个外部表之间的连接应被下推到远端服务器,从而可以作为一个单独的 Foreign Scan 实现。对单个外部表指定 FOREIGN_JOIN 既没有必要,也不允许:无论如何都必须使用 Foreign Scan。如果想阻止某个连接被下推,可以考虑改用 JOIN_ORDER 标签。

DO_NOT_SCAN 指定某个特定目标在最终计划中根本不应出现。在大多数情况下,这不可能实现,只会让该目标关系的扫描被标记为禁用。不过,在某些情况下,规划器会考虑把计划树的一部分复制并修改,然后把它作为原始计划的替代方案。在这种情况下,DO_NOT_SCAN 可用于排除不优选的替代方案。

规划器还支持这里未列出的许多其他扫描类型;不过,在大多数这些情况下,并不存在有意义的决策可供选择,因此也就没有必要使用建议。例如,出现在 FROM 子句中的返回集合函数输出,只能通过 Function Scan 扫描,因此没有机会通过建议改变任何东西。

F.30.4.2. 连接顺序建议 #

JOIN_ORDER(join_order_item [ ... ])

其中 join_order_item 为:

advice_target |
( join_order_item [ ... ] ) |
{ join_order_item [ ... ] }

JOIN_ORDER 不带任何子列表使用时,它指定一个外深连接,第一条建议目标作为驱动表,然后按指定顺序依次与后续每个建议目标连接。例如,JOIN_ORDER(a b c) 表示 a 应该是驱动表,并且先与 b 连接,再与 c 连接。如果查询中的关系比 abc 更多,其余部分之后可以以任意方式连接。

如果 JOIN_ORDER 列表包含一个括号包围的子列表,它就表示一种非外深连接。子列表中的关系必须先彼此连接,方式类似于该子列表本身是一个顶层 JOIN_ORDER 列表,然后得到的连接结果必须在连接顺序中的适当位置出现在某个连接的内侧。例如,JOIN_ORDER(a (b c) d) 要求得到如下形式的计划:

Join
  ->  Join
        -> Scan on a
        -> Join
             -> Scan on b
             -> Scan on c
  ->  Scan on d

如果 JOIN_ORDER 列表包含一个由大括号包围的子列表,它同样表示一种非外深连接。不过,子列表内部的连接顺序不受约束。例如,指定 JOIN_ORDER(a {b c} d) 就会允许上一个例子中 bc 的扫描顺序互换,而使用圆括号时则不允许这样做。

括号子列表可以任意嵌套,但大括号子列表内部不能再包含子列表。

有时需要多个 JOIN_ORDER() 实例,才能完全约束连接顺序。这会发生在规划器分别优化多个连接问题时,例如因为存在子查询,或者因为存在分区式连接。在后一种情况下,分区式连接的每个分支都可以拥有自己的连接顺序,且与其他分支彼此独立。

F.30.4.3. 连接方法建议 #

join_method_name(join_method_item [ ... ])

其中 join_method_name 为:

{ MERGE_JOIN_MATERIALIZE | MERGE_JOIN_PLAIN | NESTED_LOOP_MATERIALIZE | NESTED_LOOP_MEMOIZE | NESTED_LOOP_PLAIN | HASH_JOIN }

join_method_item 为:

{ advice_target |
( advice_target [ ... ] ) }

连接方法建议指定某个关系或某组关系在使用指定连接方法时应出现在连接的内侧。例如,HASH_JOIN(a b) 表示 ab 都应该出现在哈希连接的内侧;符合要求的计划必须至少包含两个哈希连接,其中一个在内侧只有 a 而没有别的东西,另一个在内侧只有 b 而没有别的东西。另一方面,HASH_JOIN((a b)) 表示 ab 的连接结果应当一起出现在单个哈希连接的内侧。

注意,连接方法建议会隐含一个负向的连接顺序约束。由于被命名的关系必须出现在使用指定方法的连接内侧,它们都不能成为整个连接问题的驱动表。此外,在这些关系彼此先连接完之前,集合中的任何关系都不能先与集合外的任何关系连接。例如,如果建议指定了 HASH_JOIN((a b)),而系统却先把其中任一关系与第三个关系 c 连接,那么得到的计划就永远不可能符合“把这两个关系恰好放在哈希连接内侧”的要求。当同一查询同时使用连接顺序建议和连接方法建议时,最好确保它们不会强制出彼此不兼容的连接顺序。

F.30.4.4. 分区式建议 #

PARTITIONWISE(partitionwise_item [ ... ])

其中 partitionwise_item 为:

{ advice_target |
( advice_target [ ... ] ) }

当应用于单个目标时,PARTITIONWISE 指定该表不应成为任何分区式连接的一部分。当应用于目标列表时,PARTITIONWISE 指定恰好这一组关系应以分区式方式连接。请注意,无论指定了什么建议,如果 enable_partitionwise_join = off,就不可能执行任何分区式连接。

F.30.4.5. 半连接唯一性建议 #

SEMIJOIN_UNIQUE(sj_unique_item [ ... ])
SEMIJOIN_NON_UNIQUE(sj_unique_item [ ... ])

其中 sj_unique_item 为:

{ advice_target |
( advice_target [ ... ] ) }

规划器有时可以在直接实现半连接,和先把可空侧做成唯一再执行内连接,这两种方式之间选择。SEMIJOIN_UNIQUE 指定后一种策略,而 SEMIJOIN_NON_UNIQUE 指定前一种策略。在这两种情况下,参数都是位于连接可空侧下方的单个关系或关系列表。

F.30.4.6. 并行查询建议 #

GATHER(gather_item [ ... ])
GATHER_MERGE(gather_item [ ... ])
NO_GATHER(advice_target [ ... ])

其中 gather_item 为:

{ advice_target |
( advice_target [ ... ] ) }

GATHERGATHER_MERGE 指定应分别在单个目标关系之上,或在目标所指定的一组关系的连接之上放置 GatherGather Merge。这意味着 GATHER(a b c) 是要求三个不同的 Gather 节点,而 GATHER((a b c)) 则是要求在一个三路连接之上放置一个 Gather 节点。

NO_GATHER 指定在任何一个目标上方都不应出现 GatherGather Merge 节点,但它只约束单个子查询的规划,而外层子查询级别仍然可以使用并行查询。例如,NO_GATHER(inner_example@any_1) 会禁止在 any_1 子查询中使用 Parallel Seq Scan 访问 inner_example 表,但不会阻止规划器把 SubPlan any_1 放在 GatherGather Merge 节点下方。下面的计划与 NO_GATHER(inner_example@any_1) 兼容,但与 NO_GATHER(outer_example) 不兼容:

 Finalize Aggregate
   ->  Gather
         ->  Partial Aggregate
               ->  Parallel Seq Scan on outer_example
                     Filter: (something = (hashed SubPlan any_1).col1)
                     SubPlan any_1
                       ->  Seq Scan on inner_example
                             Filter: (something_else > 100)

下面是相反的情况,也就是一个与 NO_GATHER(outer_example) 兼容、但与 NO_GATHER(inner_example@any_1) 不兼容的计划:

 Aggregate
   ->  Seq Scan on outer_example
         Filter: (something = (hashed SubPlan any_1).col1)
         SubPlan any_1
           ->  Gather
                 -> Parallel Seq Scan on inner_example
                      Filter: (something_else > 100)

F.30.5. 建议反馈 #

EXPLAIN 会通过在每一条提供的建议上附加注释的形式,反馈所提供的建议是否成功应用到了查询中。例如:

SET pg_plan_advice.advice = 'hash_join(f g) join_order(f g) index_scan(f no_such_index)';
EXPLAIN (COSTS OFF)
    SELECT * FROM jo_fact f
    LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id
    LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id
    WHERE val1 = 1 AND val2 = 1;
                            QUERY PLAN
-------------------------------------------------------------------
 Hash Join
   Hash Cond: ((d1.id = f.dim1_id) AND (d2.id = f.dim2_id))
   ->  Nested Loop
         ->  Seq Scan on jo_dim2 d2
               Filter: (val2 = 1)
         ->  Materialize
               ->  Seq Scan on jo_dim1 d1
                     Filter: (val1 = 1)
   ->  Hash
         ->  Seq Scan on jo_fact f
 Supplied Plan Advice:
   INDEX_SCAN(f no_such_index) /* matched, inapplicable, failed */
   HASH_JOIN(f) /* matched */
   HASH_JOIN(g) /* not matched */
   JOIN_ORDER(f g) /* partially matched */

对于这个查询,f 是一个有效的建议目标,但 g 不是。因此,把 f 放到哈希连接内侧的请求会标记为 matched,而把 g 放到哈希连接内侧的请求会标记为 not matchedJOIN_ORDER 建议标签涉及一个有效目标和一个无效目标,因此会标记为 partially matched。请注意,HASH_JOIN(f g) 实际上是对两个逻辑上彼此独立的行为的请求,所以在反馈中它会被拆分成 HASH_JOIN(f)HASH_JOIN(g)。相比之下,JOIN_ORDER(f g) 是单个请求,因此会原样显示。

建议反馈可以包含以下任意一种状态:

  • matched 表示在查询规划期间,所有指定的建议目标都被一起观察到,而且当时建议可以被强制执行。

  • partially matched 表示在查询规划期间,只观察到部分而非全部指定的建议目标,或者虽然观察到了所有建议目标,但它们没有同时出现。例如,如果 JOIN_ORDER 建议的所有目标各自都与查询匹配,但所建议的连接顺序不合法,就可能发生这种情况。

  • not matched 表示在查询规划期间,没有观察到任何指定的建议目标。如果建议本身就与查询不匹配,就会发生这种情况;如果查询中相关部分根本没有被规划,也可能出现这种情况,比如它被一个简化为常量 false 的条件挡住了。

  • inapplicable 表示由于某种原因,该建议标签无法应用到这些建议目标上。例如,在请求使用一个不存在的索引时,或者试图对一个并非半连接的对象控制半连接唯一性时,就会发生这种情况。

  • conflicting 表示两条或更多建议请求了彼此不兼容的行为。例如,如果你对同一张表同时建议顺序扫描和索引扫描,这两个请求都会被标记为冲突。如果连接方法建议或半连接唯一性建议隐含的连接顺序与显式指定的连接顺序不兼容,这种情况也很常见;参见 Section F.30.4.3

  • failed 表示查询计划不符合该建议。这只会出现在同样被显示为 matched 的条目上。它经常出现在同时被标记为 conflictinginapplicable 的条目上。不过,它也可能出现在这样一种情况下:就 pg_plan_advice 能判断的范围而言,该建议本身是有效的,但规划器无法构造出一个能满足该建议的合法计划。需要注意的是,pg_plan_advice 执行的健全性检查相当表面化,主要只是检查建议字符串中的逻辑矛盾;真正能否工作,只有规划器才知道。

所有建议都应且只能被标记为 matchedpartially matchednot matched 中的一种。

F.30.6. 配置参数 #

pg_plan_advice.advice (string)

pg_plan_advice.advice 是查询规划期间要使用的建议字符串。

pg_plan_advice.always_explain_supplied_advice (boolean)

pg_plan_advice.always_explain_supplied_advice 会让 EXPLAIN 始终显示任何提供的建议及其相关的 建议反馈。默认值为 true。如果设置为 false,则只有在使用 EXPLAIN (PLAN_ADVICE) 时才会显示这些信息。

pg_plan_advice.always_store_advice_details (boolean)

pg_plan_advice.always_store_advice_details 允许 EXPLAIN 在使用预备查询时也显示与计划建议相关的细节。默认值为 false。在规划一个预备查询时,不可能知道之后是否会使用 EXPLAIN,因此默认情况下,为了降低开销,pg_plan_advice 不会为该查询生成计划建议或提供建议反馈。这意味着如果对预备查询使用 EXPLAIN EXECUTE,就无法显示这些信息。把这个设置改成 true 可以避免这个问题,但会增加额外开销。通常更好的做法是只在需要的会话中启用这个选项,而不是在整个系统范围内启用。

pg_plan_advice.feedback_warnings (boolean)

设为 true 时,pg_plan_advice.feedback_warnings 会在提供的计划建议未能成功强制执行时发出警告。默认值为 false

pg_plan_advice.trace_mask (boolean)

pg_plan_advice.trace_masktrue 时,pg_plan_advice 会在查询规划期间,每次根据提供的计划建议修改允许的查询计划类型掩码时输出消息。默认值为 false。此设置打印的消息除了用于调试该模块之外,通常并无实际价值。

F.30.7. 限制 #

目前还不能通过计划建议来控制规划器在聚合方面的任何行为。这既包括聚合是通过排序还是通过哈希来计算,也包括是否使用诸如 急切聚合分区式聚合 之类的策略。

目前也还不能通过计划建议来控制规划器在 UNIONINTERSECT 等集合操作方面的任何行为。

如上面 工作原理 所述,计划建议只能影响规划器从它认为可行的计划中选择哪一个。它永远不能强迫规划器选择一个它一开始就拒绝考虑的计划。

F.30.8. 作者 #

Robert Haas