可以在 Oracle 中禁用 SQL 执行计划吗?

Possible to disable an SQL execution plan in Oracle?

我遇到过 Oracle 为给定查询创建多个执行计划的情况。大多数时候,它会选择某个表现相当不错的。然而,有时它会选择一个包含笛卡尔连接的连接,这是非常错误的。如果我们删除笛卡尔连接计划和 运行 与其他计划之一的查询,它表现良好,这对我来说表明底层数据确实不需要笛卡尔。

我们已经尝试收集统计数据并摆弄直方图,但似乎笛卡尔连接执行计划最终会恢复并间歇性地使用(有时这需要数周或数月)。

Oracle 中是否可以禁用特定的执行计划?我们不能因为它似乎又回来了就删除它,但是将它留在那里并禁用它应该可以作为一种修复方法,但我真的不知道该怎么做或者是否可行。

如评论中所述,使用 SQL 计划基线是禁用执行计划的官方方法。它有效,但如以下代码所示,它非常痛苦。


在加载数据之前创建对象并收集统计数据,从而制定一个糟糕的计划。

drop table bad_index;
create table bad_index(a number, b number);
create index bad_index on bad_index(a);
begin
    dbms_stats.gather_table_stats(user, 'bad_index');
end;
/
insert into bad_index select level, level from dual connect by level <= 100000;
commit;

查询计划错误。当查询真正 return 100,000 行时,它认为只有一行。当它应该使用 HASH JOIN 而不是时,它使用了 NESTED LOOPS。

explain plan for
select count(*)
from bad_index bi1, bad_index bi2
where bi1.a = bi2.a
    and bi1.a > 0;

select * from table(dbms_xplan.display);

Plan hash value: 4168051245

--------------------------------------------------------------------------------
| Id  | Operation          | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |           |     1 |    26 |     0   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |           |     1 |    26 |            |          |
|   2 |   NESTED LOOPS     |           |     1 |    26 |     0   (0)| 00:00:01 |
|*  3 |    INDEX RANGE SCAN| BAD_INDEX |     1 |    13 |     0   (0)| 00:00:01 |
|*  4 |    INDEX RANGE SCAN| BAD_INDEX |     1 |    13 |     0   (0)| 00:00:01 |
--------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - access("BI1"."A">0)
   4 - access("BI1"."A"="BI2"."A")
       filter("BI2"."A">0)

运行上面的查询没有explain plan for生成一个真正的SQL_ID。然后找到SQL_ID(5ukbyc726cdu3).

select *
from gv$sql
where sql_text like '%bad_index bi1%'
    and sql_text not like '%quine%'
    and sql_text not like '%explain%';

使用那个 SQL_ID 创建一个 SQL 计划基线来捕获有关查询的信息。

declare
    v_result pls_integer;
begin
    v_result := dbms_spm.load_plans_from_cursor_cache(sql_id => '5ukbyc726cdu3');
end;
/

您可以在此处查看 SQL 计划基准。现在它只有一个计划:

select * from dba_sql_plan_baselines;

让我们通过收集统计数据并重新制定更好的计划运行。

begin
    dbms_stats.gather_table_stats(user, 'bad_index');
end;
/

select count(*)
from bad_index bi1, bad_index bi2
where bi1.a = bi2.a
    and bi1.a > 0;

但是等等,那个新计划还没有奏效。请注意,错误的计划仍在使用中。注意 Note 部分 - 它使用 SQL 计划基准。

explain plan for
select count(*)
from bad_index bi1, bad_index bi2
where bi1.a = bi2.a
    and bi1.a > 0;

select * from table(dbms_xplan.display);

Plan hash value: 4168051245

--------------------------------------------------------------------------------
| Id  | Operation          | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |           |     1 |    10 |   100K  (1)| 00:00:04 |
|   1 |  SORT AGGREGATE    |           |     1 |    10 |            |          |
|   2 |   NESTED LOOPS     |           |   100K|   976K|   100K  (1)| 00:00:04 |
|*  3 |    INDEX RANGE SCAN| BAD_INDEX |   100K|   488K|   201   (1)| 00:00:01 |
|*  4 |    INDEX RANGE SCAN| BAD_INDEX |     1 |     5 |     1   (0)| 00:00:01 |
--------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - access("BI1"."A">0)
   4 - access("BI1"."A"="BI2"."A")
       filter("BI2"."A">0)

Note
-----
   - SQL plan baseline "SQL_PLAN_fdcuj7gbjtgq561d678a6" used for this statement

现在 SQL 计划基准有两个计划。第一个被接受,更新更好的方案没有被接受。

select sql_handle, plan_name, origin, enabled, accepted, fixed from dba_sql_plan_baselines order by origin desc;

SQL_HANDLE             PLAN_NAME                        ORIGIN                          ENABLED   ACCEPTED   FIXED
SQL_e6b3513bd71cbec5   SQL_PLAN_fdcuj7gbjtgq561d678a6   MANUAL-LOAD-FROM-CURSOR-CACHE   YES       YES        NO
SQL_e6b3513bd71cbec5   SQL_PLAN_fdcuj7gbjtgq52b66d432   AUTO-CAPTURE                    YES       NO         NO

制定可能接受新计划的计划。该函数的确切输出在这里无关紧要,但如果您感到好奇,可以查看它。

declare
    v_clob clob;
begin
    v_clob := dbms_spm.evolve_sql_plan_baseline(sql_handle => 'SQL_e6b3513bd71cbec5');
    dbms_output.put_line(v_clob);
end;
/

再看一下基线,它们都被接受了。

select sql_handle, plan_name, origin, enabled, accepted, fixed from dba_sql_plan_baselines order by origin desc;

SQL_HANDLE             PLAN_NAME                        ORIGIN                          ENABLED   ACCEPTED   FIXED
SQL_e6b3513bd71cbec5   SQL_PLAN_fdcuj7gbjtgq561d678a6   MANUAL-LOAD-FROM-CURSOR-CACHE   YES       YES        NO
SQL_e6b3513bd71cbec5   SQL_PLAN_fdcuj7gbjtgq52b66d432   AUTO-CAPTURE                    YES       YES        NO

将旧计划的 ENABLED 设置为 NO,为新计划将其设置为 YES。如果新计划更好,则不需要这样做,但这将确保永远不会使用旧计划。

declare
    v_result pls_integer;
begin
    v_result := dbms_spm.alter_sql_plan_baseline(sql_handle => 'SQL_e6b3513bd71cbec5', plan_name => 'SQL_PLAN_fdcuj7gbjtgq561d678a6', attribute_name => 'ENABLED', attribute_value => 'NO');
    v_result := dbms_spm.alter_sql_plan_baseline(sql_handle => 'SQL_e6b3513bd71cbec5', plan_name => 'SQL_PLAN_fdcuj7gbjtgq52b66d432', attribute_name => 'ENABLED', attribute_value => 'YES');
end;
/

确认旧计划不再启用。

select sql_handle, plan_name, origin, enabled, accepted, fixed from dba_sql_plan_baselines order by origin desc;

SQL_HANDLE             PLAN_NAME                        ORIGIN                          ENABLED   ACCEPTED   FIXED
SQL_e6b3513bd71cbec5   SQL_PLAN_fdcuj7gbjtgq561d678a6   MANUAL-LOAD-FROM-CURSOR-CACHE   NO        YES        NO
SQL_e6b3513bd71cbec5   SQL_PLAN_fdcuj7gbjtgq52b66d432   AUTO-CAPTURE                    YES       YES        NO

现在查询将只使用更新、更好的计划,Rows 设置为 100K 并使用 HASH JOIN 而不是 NESTED LOOPs。

explain plan for
select count(*)
from bad_index bi1, bad_index bi2
where bi1.a = bi2.a
    and bi1.a > 0;

select * from table(dbms_xplan.display);

Plan hash value: 544904072

--------------------------------------------------------------------------------------------
| Id  | Operation              | Name      | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |           |     1 |    10 |       |   278   (2)| 00:00:01 |
|   1 |  SORT AGGREGATE        |           |     1 |    10 |       |            |          |
|*  2 |   HASH JOIN            |           |   100K|   976K|  1664K|   278   (2)| 00:00:01 |
|*  3 |    INDEX FAST FULL SCAN| BAD_INDEX |   100K|   488K|       |    57   (2)| 00:00:01 |
|*  4 |    INDEX FAST FULL SCAN| BAD_INDEX |   100K|   488K|       |    57   (2)| 00:00:01 |
--------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("BI1"."A"="BI2"."A")
   3 - filter("BI1"."A">0)
   4 - filter("BI2"."A">0)

Note
-----
   - SQL plan baseline "SQL_PLAN_fdcuj7gbjtgq52b66d432" used for this statement

恭喜你做到了这一步

上面的代码太可怕了。 Oracle 在这个系统上确实失误了,但这是 "official" 的方法。

通常最好避免 SQL 计划基线并找到其他解决方案。找出导致错误执行计划的原因并停止它。