
# 查看计划跳变历史
当发生计划跳变时，从当前DFX相关系统表和系统视图中无法快速感知查询语句的历史计划，导致因计划跳变引起的慢查询分析变得复杂，且无法快速回退至之前更优的计划（快速回退）。目前，SPM已支持计划捕获，本特性在此基础进行增强，以完成历史计划的记录，并提供接口实现计划的快速回退，提升SPM的易用性。
#### 功能描述
GaussDB当前已有慢查询相关视图，并具备了SPM的基础能力。然而，当发生计划跳变时，无法对非慢查询的执行计划进行历史记录。因此，本特性在基于SPM的基础上，对历史执行计划进行记录，以提升SPM的易用性。由于SPM当前仅支持Generic-Plan，因此本功能所管理的计划类型限定在Generic-Plan。
当前计划跳变存在两种主要情况：
- 优化器对相同查询产生了不同的执行计划，并最终选用了新的计划。
- 每次执行时，优化器在历史已有的计划中切换。
这两种事件，均被称为跳变事件。
由于本特性涉及对计划跳变的快速感知和快速恢复两部分，以下将分别进行简明介绍。
1. 快速感知 如果要实现全量的计划跳变历史记录，最直接的方法是记录每次执行计划与上一次的不同。这种方法会带来极大的性能开销。因此，出于性能和资源的考虑，本特性在以下两种场景进行跳变事件记录：
   - 当优化器产生了SPM的baseline中不存在的新的计划。
   
   - 当优化器产生了SPM的baseline中存在的，但超过一段时间未执行的老计划。
   
   
   具体的执行流程，如[图1]所示。
   图1计划跳变支持快速感知流程图   
   ![](https://support.huaweicloud.com/centralized-devg-v8-gaussdb/figure/zh-cn_image_0000002558058963.png "点击放大")
   此特性需要保证GUC参数spm_enable_plan_capture设置为STORE模式，且打开跳变历史记录开关spm_enable_plan_history_logging。
   从[图1]可知，除了以下情况，均会对因产生新计划导致的跳变事件进行记录。
   - 场景一：如果当前所捕获的基线个数已经超出上限 （spm_plan_capture_max_plannum），则查看当前计划是否是SPM所捕获过的查询的新计划。当前spm_enable_plan_history_logging打开并且历史记录数未超过上限，则进行额外的基线捕获，并进行历史记录。
   
   
   
   - 场景二：如果当前所记录的跳变事件个数已经超出上限 （spm_plan_capture_max_plannum），则不进行历史记录。
   
   
   为了确保在长期运行中能够记录更多的跳变历史，SPM会设置定时任务，在数据库启动起的每24小时触发一次历史记录清理，其校验准则是查看当前的历史记录数占最大上限的百分比，若超出此百分比，则会删除一些最旧的历史记录以保证记录数在此百分比要求之下，此百分比由spm_plan_history_reserved_percentage设置。
   
2. 快速恢复 当发生计划跳变时，可以通过设置历史时间点的某条计划相应的baseline状态为ACC/FIXED，再结合计划选择功能spm_enable_plan_selection，即可实现回退至历史计划完成计划固化，也即完成快速回退。
   
 
#### 示例
下面分别对快速感知和快速回退两种场景进行示例演示。
1. 快速感知跳变历史
   ```
   --0.修改数据库参数。
   gaussdb=# SET plan_cache_mode = 'force_generic_plan';
   gaussdb=# SET explain_perf_mode = pretty;
   gaussdb=# DEALLOCATE ALL;
   --1.创建表。
   gaussdb=# CREATE TABLE test_spm_perception_t1(a int,b int, c int);
   gaussdb=# INSERT INTO test_spm_perception_t1 SELECT a, (random()*200)::int, (random()*10000)::int FROM (SELECT generate_series(1,10000) a) test_spm_perception_t1;
   gaussdb=# ANALYZE test_spm_perception_t1;
   --2.捕获SeqScan和计划跳变。
   gaussdb=# SET spm_enable_plan_capture = STORE;
   gaussdb=# SET spm_enable_plan_history_logging = ON;
   gaussdb=# PREPARE p_seq AS SELECT * FROM test_spm_perceptiON_t1 WHERE a = $1;
   gaussdb=# EXPLAIN(costs OFF) EXECUTE p_seq(1);
   --3.查看基线和跳变历史。
   gaussdb=# \x
   gaussdb=# SELECT sql_namespace, sql_hash, plan_hash, outline, source, status, user, creation_time, last_used_time, modification_time FROM  pg_catalog.gs_spm_baseline WHERE sql_namespace = get_schema_oid(CURRENT_SCHEMA::cstring);
   -[ RECORD 1 ]-----+-------------------------------------------------------------------------------------
   sql_namespace     | 16782
   sql_hash          | 812490094
   plan_hash         | 1950233227
   outline           |  begin_outline_data
                     |  TableScan(@"sel$1" test_spm_gplan_change_perception.test_spm_perception_t1@"sel$1")
                     |  version("1.0.0")
                     |  end_outline_data
   source            | STORE
   status            | 0
   current_user      | Mike
   creation_time     | Mon Jan 13 06:04:22.614726 2025 PST
   last_used_time    | Mon Jan 13 06:04:22.614726 2025 PST
   modification_time | Mon Jan 13 06:04:22.614726 2025 PST
   gaussdb=# SELECT * FROM pg_catalog.gs_spm_plan_history WHERE sql_hash IN (SELECT sql_hash FROM pg_catalog.gs_spm_baseline WHERE sql_namespace = get_schema_oid(CURRENT_SCHEMA::cstring));
   -[ RECORD 1 ]------+------------------------------------
   sql_hash           | 812490094
   plan_hash          | 1950233227
   plan_hash_previous | 0
   userid             | 10
   creation_time      | Mon Jan 13 06:04:22.614726 2025 PST
   gaussdb=# SELECT * FROM gs_spm_plan_history_detail WHERE sql_hash IN (SELECT sql_hash FROM pg_catalog.gs_spm_baseline WHERE sql_namespace = get_schema_oid(CURRENT_SCHEMA::cstring));
   -[ RECORD 1 ]----------+-------------------------------------------------------------------------------------
   sql_hash               | 812490094
   plan_hash              | 1950233227
   plan_hash_previous     | 0
   unique_sql_id          | 2670486394
   outline                |  begin_outline_data
                          |  TableScan(@"sel$1" test_spm_gplan_change_perception.test_spm_perception_t1@"sel$1")
                          |  version("1.0.0")
                          |  end_outline_data
   status                 | UNACC
   gplan                  | t
   cost                   | 180
   sql_text               | select * from test_spm_perception_t1 where a = $1
   param_num              | 1
   source                 | STORE
   userid                 | 10
   baseline_creation_time | Mon Jan 13 06:04:22.614726 2025 PST
   history_creation_time  | Mon Jan 13 06:04:22.614726 2025 PST
   modification_time      | Mon Jan 13 06:04:22.614726 2025 PST
   gaussdb=# SELECT dbe_sql_util.gs_spm_get_plan_history(sql_hash, creation_time + 1) FROM pg_catalog.gs_spm_plan_history WHERE sql_hash IN (SELECT sql_hash FROM pg_catalog.gs_spm_baseline WHERE sql_namespace = get_schema_oid(CURRENT_SCHEMA::cstring));
   -[ RECORD 1 ]-----------+---------------------------------------------------------------------------------------------------------------------------------------
   gs_spm_get_plan_history | (812490094,1950233227," begin_outline_data
                           |  TableScan(@""sel$1"" test_spm_gplan_change_perception.test_spm_perception_t1@""sel$1"")
                           |  version(""1.0.0"")
                           |  end_outline_data",UNACC,t,180,"select * from test_spm_perception_t1 where a = $1",1,STORE,10,"Mon Jan 13 06:04:22.614726 2025 PST",f)
   --4.环境清理。
   gaussdb=# DROP TABLE test_spm_perception_t1;
   gaussdb=# RESET plan_cache_mode;
   gaussdb=# RESET explain_perf_mode;
   gaussdb=# RESET spm_enable_plan_capture;
   gaussdb=# RESET spm_enable_plan_history_logging;
   ```
   
2. 快速回滚历史计划
   ```
   --0.修改数据库参数。
   gaussdb=# SET plan_cache_mode = 'force_generic_plan';
   gaussdb=# SET explain_perf_mode = pretty;
   gaussdb=# DEALLOCATE ALL;
   --1.创建表。
   gaussdb=# CREATE TABLE test_spm_rollback_t1(a int,b int, c int);
   gaussdb=# INSERT INTO test_spm_rollback_t1 SELECT a, (random()*200)::int, (random()*10000)::int FROM (SELECT generate_series(1,10000) a) test_spm_rollback_t1;
   gaussdb=# CREATE INDEX on test_spm_rollback_t1(a);
   gaussdb=# ANALYZE test_spm_rollback_t1;
   --2.捕获SeqScan和计划跳变。
   gaussdb=# SET enable_indexscan = OFF;
   gaussdb=# SET enable_bitmapscan = OFF;
   gaussdb=# SET spm_enable_plan_capture = STORE;
   gaussdb=# SET spm_enable_plan_history_logging = ON;
   gaussdb=# PREPARE p_seq AS SELECT * FROM test_spm_rollback_t1 WHERE a = $1;
   gaussdb=# EXPLAIN(costs OFF) EXECUTE p_seq(1);
    id |              operation               
   ----+--------------------------------------
     1 | ->  Seq Scan on test_spm_rollback_t1
   (1 row)
    Predicate Information (identified by plan id) 
   -----------------------------------------------
      1 --Seq Scan on test_spm_rollback_t1
            Filter: (a = $1)
   (2 rows)
   --3.查看基线和跳变历史。
   gaussdb=# \x
   gaussdb=# SELECT sql_namespace, sql_hash, plan_hash, outline, source, status, user, creation_time, last_used_time, modification_time
   FROM pg_catalog.gs_spm_baseline WHERE sql_namespace = get_schema_oid(CURRENT_SCHEMA::cstring);
   -[ RECORD 1 ]-----+---------------------------------------------------------------------------------
   sql_namespace     | 16782
   sql_hash          | 2903888774
   plan_hash         | 602237302
   outline           |  begin_outline_data
                     |  TableScan(@"sel$1" test_spm_gplan_change_rollback.test_spm_rollback_t1@"sel$1")
                     |  version("1.0.0")
                     |  end_outline_data
   source            | STORE
   status            | 0
   current_user      | Mike
   creation_time     | Tue Jan 14 06:38:14.314703 2025 PST
   last_used_time    | Tue Jan 14 06:38:14.314703 2025 PST
   modification_time | Tue Jan 14 06:38:14.314703 2025 PST
   gaussdb=# SELECT * FROM pg_catalog.gs_spm_plan_history WHERE sql_hash in (SELECT sql_hash FROM pg_catalog.gs_spm_baseline WHERE sql_namespace = get_schema_oid(CURRENT_SCHEMA::cstring));
   -[ RECORD 1 ]------+------------------------------------
   sql_hash           | 2903888774
   plan_hash          | 602237302
   plan_hash_previous | 0
   userid             | 10
   creation_time      | Tue Jan 14 06:38:14.314703 2025 PST
   --4.设定跳变历史计划对应基线为ACC。
   gaussdb=# SELECT dbe_sql_util.gs_spm_accept_historical_plan(sql_hash, creation_time + 1, 'ACC') FROM gs_spm_plan_history WHERE sql_hash in (SELECT sql_hash FROM pg_catalog.gs_spm_baseline WHERE sql_namespace = get_schema_oid(CURRENT_SCHEMA::cstring));
    gs_spm_accept_historical_plan 
   -------------------------------
                        602237302
   (1 row)
   gaussdb=# SELECT sql_namespace, sql_hash, plan_hash, outline, source, status, user, creation_time, last_used_time, modification_time
   FROM pg_catalog.gs_spm_baseline WHERE sql_namespace = get_schema_oid(CURRENT_SCHEMA::cstring);
   -[ RECORD 1 ]-----+---------------------------------------------------------------------------------
   sql_namespace     | 16782
   sql_hash          | 2903888774
   plan_hash         | 602237302
   outline           |  begin_outline_data
                     |  TableScan(@"sel$1" test_spm_gplan_change_rollback.test_spm_rollback_t1@"sel$1")
                     |  version("1.0.0")
                     |  end_outline_data
   source            | STORE
   status            | 1
   current_user      | Mike
   creation_time     | Tue Jan 14 06:38:14.314703 2025 PST
   last_used_time    | Tue Jan 14 06:38:14.314703 2025 PST
   modification_time | Tue Jan 14 06:38:14.314703 2025 PST
   --5.打开计划选择，完成快速回退，回退至SeqScan。
   gaussdb=# SET spm_enable_plan_history_logging = OFF;
   gaussdb=# RESET enable_indexscan;
   gaussdb=# RESET enable_bitmapscan;
   gaussdb=# DEALLOCATE ALL;
   gaussdb=# PREPARE p_seq AS SELECT * FROM test_spm_rollback_t1 WHERE a = $1;
   gaussdb=# EXPLAIN (costs OFF) EXECUTE p_seq(1);
    id |                                operation                                
   ----+-------------------------------------------------------------------------
     1 | ->  Index Scan using test_spm_rollback_t1_a_idx on test_spm_rollback_t1
   (1 row)
                  Predicate Information (identified by plan id)               
   ---------------------------------------------------------------------------
      1 --Index Scan using test_spm_rollback_t1_a_idx on test_spm_rollback_t1
            Index Cond: (a = $1)
   (2 rows)
   gaussdb=# SET spm_enable_plan_selection = ON;
   gaussdb=# EXPLAIN (costs OFF) EXECUTE p_seq(1);
    id |              operation               
   ----+--------------------------------------
     1 | ->  Seq Scan on test_spm_rollback_t1
   (1 row)
                     ====== Query Others =====
   --------------------------------------------------------------
    use_baseline: Yes, sql_hash: 2903888774, plan_hash: 602237302
   (1 row)
   --6.环境清理。
   gaussdb=# DROP TABLE test_spm_rollback_t1;
   gaussdb=# RESET plan_cache_mode;
   gaussdb=# RESET explain_perf_mode;
   gaussdb=# RESET enable_indexscan;
   gaussdb=# RESET enable_bitmapscan;
   gaussdb=# RESET spm_enable_plan_capture;
   gaussdb=# RESET spm_enable_plan_history_logging;
   ```
   
 
#### 最佳实践
通常建议在业务低谷期打开spm_enable_plan_capture为STORE进行计划捕获，在批量执行业务完成后，打开历史记录开关spm_enable_plan_history_logging进行跳变历史记录。
在发生计划跳变时，可参考如下步骤操作。
1. 获取unique_sql_id，比如通过statement_history查询。 
   ```
   gaussdb=# SELECT unique_query_id FROM DBE_PERF.statement_history;
   ```
   
   
2. 根据unique_sql_id从PG_CATALOG.GS_SPM_ID_HASH_JOIN中查出sql_hash。 
   ```
   gaussdb=# SELECT sql_hash FROM PG_CATALOG.gs_spm_id_hash_join WHERE unique_sql_id = $unique_query_id;
   ```
   
   
3. 根据sql_hash和发生跳变的历史时间，利用系统函数（DBE_SQL_UTIL.GS_SPM_GET_PLAN_HISTORY）/视图(PG_CATALOG.GS_SPM_PLAN_HISTORY_DETAIL) /系统表（PG_CATALOG.GS_SPM_PLAN_HISTORY）中查出跳变历史。 
   ```
   gaussdb=# SELECT * FROM pg_catalog.gs_spm_plan_history WHERE sql_hash = $sql_hash;
   ```
   
   
4. 锁定发生跳变的计划后，找出其plan_hash，并利用系统函数（DBE_SQL_UTIL.GS_SPM_ACCEPT_HISTORICAL_PLAN）接受指定时间的历史计划，或者直接使用系统函数（DBE_SQL_UTIL.GS_SPM_SET_PLAN_STATUS）来完成对应基线状态的设置，可设置为ACC/FIXED。 
   ```
   gaussdb=# SELECT dbe_sql_util.gs_spm_accept_historical_plan($sql_hash, $given_time, 'ACC');
   ```
   
   
5. 打开spm_enable_plan_selection完成计划快速回退。 
   ```
   gaussdb=# SET spm_enable_plan_selection = ON;
   ```
   
   
![](https://support.huaweicloud.com/centralized-devg-v8-gaussdb/public_sys-resources/notice_3.0-zh-cn.png)
- 本特性仅支持Generic Plan。
- 与本特性相关的GUC参数有spm_enable_plan_history_logging，spm_enable_plan_history_logging_expired_time和spm_plan_history_reserved_percentage。
- 由于gs_spm_plan_history可写入记录数存在上限，因此新增GUC参数spm_plan_history_reserved_percentage用于指定gs_spm_plan_history可被占用的百分比。
- 快速感知和快速回退要求必须设置spm_enable_plan_capture = STORE 且 spm_enable_plan_history_logging = on。
- 该特性旨在解决当生成新计划时导致的计划跳变，对于智能优化器从现存的多个计划中进行最佳计划选择导致的计划变化，若未超出过期时间，则不属于新计划产生导致的计划跳变，不属于特性支持范围。
 
