将指定SQL的执行计划从共享池删除

如果Oracle的优化器产生了某种错误的执行计划,或者我们希望Oracle对于某个SQL重新进行分析,那么就需要这个SQL的执行计划在共享池中过期,而简单的方法在10.2.0.4以后才出现。
对于以前的版本而言,最显而易见的方法莫过于直接刷新共享池,但是如果是数据库中绝大部分的SQL都存在问题,那么这种方法无可厚非,也可能是见效最快的方法,而如果数据库中仅仅是个别的SQL存在问题,那么这种方法就过于暴力了。

SQL> SELECT COUNT(*) FROM dual;
  COUNT(*)
----------
         1
SQL> SELECT sql_id, address, hash_value, executions, loads, parse_calls, invalidations
  2  FROM v$sqlarea
  3  WHERE sql_text = 'select count(*) from dual';
SQL_ID        ADDRESS          HASH_VALUE EXECUTIONS      LOADS PARSE_CALLS INVALIDATIONS
------------- ---------------- ---------- ---------- ---------- ----------- -------------
4m94ckmu16f9k 00000000B6C61FC0 4094900530          1          1           1             0
SQL> SELECT COUNT(*) FROM v$sqlarea;
  COUNT(*)
----------
      3061
SQL> ALTER system FLUSH shared_pool;
System altered.
SQL> SELECT sql_id, address, hash_value, executions, loads, parse_calls, invalidations
  2  FROM v$sqlarea
  3  WHERE sql_text = 'select count(*) from dual';
no ROWS selected
SQL> SELECT COUNT(*) FROM v$sqlarea;
  COUNT(*)
----------
        37

为了一个SQL而清空整个共享池,这个代价确实太大了,何况对于一个繁忙的OLTP系统而言,这个刷新共享池的操作所带来的风险和后果与直接关闭数据库相比,也没有太大的差别。
那么有没有细粒度一点的办法呢,其实方法有很多,相关表上任何的DDL都会导致SQL执行计划的失效,但是DDL本身风险就毕竟高,如果想要对系统影响最小,那么这个DDL就非GRANT莫属。只需要当前用户将这个表的权限授权给自己,就可以达到想要的效果:

SQL> SELECT COUNT(*) FROM dual;
  COUNT(*)
----------
         1
SQL> SELECT sql_id, address, hash_value, executions, loads, parse_calls, invalidations
  2  FROM v$sqlarea
  3  WHERE sql_text = 'select count(*) from dual';
SQL_ID        ADDRESS          HASH_VALUE EXECUTIONS      LOADS PARSE_CALLS INVALIDATIONS
------------- ---------------- ---------- ---------- ---------- ----------- -------------
4m94ckmu16f9k 00000000B6C61FC0 4094900530          1          1           1             0
SQL> SELECT 1 FROM dual;
         1
----------
         1
SQL> SELECT * FROM dual;
D
-
X
SQL> SELECT 'a' FROM dual;
'
-
a
SQL> select count(1) from dual;
  COUNT(1)
----------
         1
SQL> select sql_id, address, hash_value, executions, loads, parse_calls, invalidations
  2  from v$sqlarea
  3  where lower(sql_text) like '%dual%';
SQL_ID        ADDRESS          HASH_VALUE EXECUTIONS      LOADS PARSE_CALLS INVALIDATIONS
------------- ---------------- ---------- ---------- ---------- ----------- -------------
gr7s3j0cg8pr6 00000000B6A5A470  418666214          1          1           1             0
40p7rprfbt1as 00000000B69BDC38 3703342424          1          1           1             0
520mkxqpf15q8 00000000B6DD9610 2866845384          1          1           1             0
ak90gdq0udv37 00000000B6E3C6B0 2175200359          2          2           2             1
4m94ckmu16f9k 00000000B6C61FC0 4094900530          1          1           1             0
a5ks9fhw2v9s1 00000000B698DA88  942515969          1          1           1             0
800hwktjz3zuc 00000000B6999268 1676803916          1          1           1             0
7 rows selected.
SQL> grant select on dual to sys;
grant select on dual to sys
                        *
ERROR at line 1:
ORA-01749: you may not GRANT/REVOKE privileges to/from yourself
 
SQL> grant select on dual to public;
Grant succeeded.
SQL> select sql_id, address, hash_value, executions, loads, parse_calls, invalidations
  2  from v$sqlarea
  3  where lower(sql_text) like '%dual%';
SQL_ID        ADDRESS          HASH_VALUE EXECUTIONS      LOADS PARSE_CALLS INVALIDATIONS
------------- ---------------- ---------- ---------- ---------- ----------- -------------
gr7s3j0cg8pr6 00000000B6A5A470  418666214          2          1           2             0
ak90gdq0udv37 00000000B6E3C6B0 2175200359          2          2           2             1

对于其他用户而言,都可以使用将表的查询权限授权给OWNER本身的方法,但是测试用户本身为SYS,因此需要其他用户授权,方便起见使用了授权给PUBLIC的方式。可以看到,这种方式同样可以生效,但是仍然存在打击面过大的问题。对于系统中一个频繁访问的表,很可能这个授权的操作,导致少则几十,多则几百个SQL都是失效,这个风险仍然不可小觑。
那么对于就没有一个可以将粒度控制在SQL本身上的方法吗?在11g中,Oracle的DBMS_SHARED_POOL包新增了PURGE功能,可以完美的解决这个问题,这个方法在10.2.0.4和10.2.0.5补丁集中也被添加进来,使得10.2的高版本同样可以实现这个功能,使用方法很简单:

SQL> SELECT COUNT(*) FROM dual;
  COUNT(*)
----------
         1
SQL> SELECT sql_id, address, hash_value, executions, loads, parse_calls, invalidations
  2  FROM v$sqlarea
  3  WHERE sql_text = 'select count(*) from dual';
SQL_ID        ADDRESS          HASH_VALUE EXECUTIONS      LOADS PARSE_CALLS INVALIDATIONS
------------- ---------------- ---------- ---------- ---------- ----------- -------------
4m94ckmu16f9k 00000000B6C61FC0 4094900530          1          2           1             1
SQL> SELECT 1 FROM dual;
         1
----------
         1
SQL> SELECT * FROM dual;
D
-
X
SQL> SELECT sql_id, address, hash_value, executions, loads, parse_calls, invalidations
  2  FROM v$sqlarea
  3  WHERE LOWER(sql_text) LIKE '%dual%';
SQL_ID        ADDRESS          HASH_VALUE EXECUTIONS      LOADS PARSE_CALLS INVALIDATIONS
------------- ---------------- ---------- ---------- ---------- ----------- -------------
gr7s3j0cg8pr6 00000000B6A5A470  418666214          3          1           3             0
520mkxqpf15q8 00000000B6DD9610 2866845384          1          2           1             1
ak90gdq0udv37 00000000B6E3C6B0 2175200359          3          2           3             1
4m94ckmu16f9k 00000000B6C61FC0 4094900530          1          2           1             1
a5ks9fhw2v9s1 00000000B698DA88  942515969          1          2           1             1
SQL> EXEC dbms_shared_pool.purge('00000000B6C61FC0,4094900530', 'c')
PL/SQL PROCEDURE successfully completed.
SQL> SELECT sql_id, address, hash_value, executions, loads, parse_calls, invalidations
  2  FROM v$sqlarea
  3  WHERE LOWER(sql_text) LIKE '%dual%';
SQL_ID        ADDRESS          HASH_VALUE EXECUTIONS      LOADS PARSE_CALLS INVALIDATIONS
------------- ---------------- ---------- ---------- ---------- ----------- -------------
gr7s3j0cg8pr6 00000000B6A5A470  418666214          4          1           4             0
520mkxqpf15q8 00000000B6DD9610 2866845384          1          2           1             1
ak90gdq0udv37 00000000B6E3C6B0 2175200359          3          2           3             1
a5ks9fhw2v9s1 00000000B698DA88  942515969          1          2           1             1

过程PURGE的第一个参数为V$SQLAREA中用逗号分隔的ADDRESS列和HASH_VALUE列的值,第二个参数’c’表示PURGE的对象是CURSOR,不过实际上这里可以使用除了P(PROCEDURE/FUNCTION/PACKAGE)、T(TYPE)、R(TRIGGER)和Q(SEQUENCE)的任何值
使用这种方法,就可以精确的将一个SQL从共享池中删除,从而使得Oracle为这个SQL重新生成执行计划。这种方法只针对单个SQL语句,使得解决问题的同时不会造成任何的误伤。
不过需要注意一点,在10.2.0.4中,虽然PURGE过程已经存在,但是要使这个过程可以真正的生效,还必须设置一个EVENT:

SQL> ALTER system SET event = '5614566 trace name context forever' scope = spfile;
System altered.

设置EVENT后需要重启,DBMS_SHARED_POOL的PURGE才可以生效。也就是说,除非提前进行过设置,否则这个PURGE的功能对于一个产品环境而言,必须在10.2.0.5以上版本才可以使用。

This entry was posted in ORACLE and tagged , , , , , . Bookmark the permalink.

2 Responses to 将指定SQL的执行计划从共享池删除

  1. lzp00 says:

    其实我一直用comment on table的方式来使表上的sql cursor失效。效果不错,建议你试试。
    另外,如果需要做表分析。gather_table_stat方法有一个参数no_invalidate参数,设置false的话,也可以使表上的cursor失效。

    • yangtingkun says:

      感谢lzp00,你给出的两个都是好的方法,尤其是comment on table确实是我没有想到的。

      NO_INVALIDATE参数的作用我了解,其实这里没有展开描述,最后用PURGE清除出共享池的SQL涉及的是一张大表,而这张表的统计信息在我来之前就已经收集了,但是由于Oracle收集统计信息的默认策略是不马上改变执行计划,因此问题仍然存在。由于表的数据量比较大,而且绝大部分执行计划已经正确,因此没有选择再次收集,而是使用了DBMS_SHARED_POOL过程。

Leave a Reply

Your email address will not be published. Required fields are marked *