识别低效的执行计划
错误的估算
这个检查背后的思路很简单。查询优化器计算成本来决定哪些访问路径、联接顺序以及联接方法应该用于获取一个高效的执行计划。如果成本的计算有误,则很可能查询优化器会选择一个非最优的执行计划。换言之,错误的估算很容易导致选择错误的执行计划。
直接评价一个SQL语句本身的成本在实践中是不可行的。检查查询优化器执行的其他估算则相对容易得多,这种方式的成本估算基于由一个操作返回的行数(基数)。检查估算的基数十分容易,因为你可以使用dbms_xplan包的display_cursor函数,
比如说,可以直接使用真实的基数和估算的作对比。就像你刚刚看到的,只有当两个基数值接近时才表明查询优化器工作良好。这种方法的一个核心特性就是不需要SQL语句或数据库结构的相关信息来评价执行计划的优劣。你只需集中精力用实际的数据对比估算的信息。
让我通过一个例子来演示一下这个概念。下面这段来自wrong_estimations.sql脚本输出的摘录展示了一个带有估算(E-Rows)和实际基数(A-Rows)的执行计划。正如你所看到的,操作4的估算是完全错误的(因此还有操作2和操作3)。
查询优化器为操作4估算的是,只返回32行数据而不是80016行。更糟糕的是,操作2和操作3是关联组合操作。这意味着操作6和操作7,实际上被分别执行了80016和75808次,而不是估算的只执行32次。这是通过Starts列的值确定的。一定要注意操作6和操作7的估算是正确的。实际上,在作比较之前,实际的基数(A-Rows)必须除以执行的数量(Starts):
select count(t2.col2) FROM t1 JOIN t2
USING(id) WHERE t1.col1=666;
要理解这个问题,必须仔细分析为何查询优化器无法计算合理的估算。基数通过将选择率和表中的行数相乘计算得来。因此,如果基数是错误的,引发问题的原因只能有三个:
Ø 错误的选择率
Ø 错误的行数
Ø 查询优化器的bug。
在本例中,我们的分析应该始于查看为操作5执行的估算,也就是与"T1"."COL1"=666谓词相关的估算。因为查询优化器基于对象统计信息做估算,那我们来看一下它们是否代表了当前的数据。通过下面的查询,能够获取用于操作5的t1_col1索引的对象统计信息。同时,也可以计算每个键的平均数据行数。这基本上就是在没有直方图可用时查询优化器会使用的值:
select
num_rows,distinct_keys,num_rows/distinct_keys AS avg_rows_per_key
FROM user_indexes
WHERE index_name ='T1_COL1';
在本例中需要注意的是,平均行数32与上面的执行计划中估算的值一样。要检查这些对象统计信息是否正确,必须将它们与实际数据进行对比。那么,我们在t1表上执行下面的查询。正如你所看到的,该查询不仅计算上一个查询的对象统计信息而且还记录col1列上不等于666的行数:
select count(*) AS num_rows,count(DISTINCT
col1) AS distinct_keys,count(nullif(col1,666)) AS rows_per_key_666 from t1;
从输出中可以确认,对象统计信息是正确的,而且数据倾斜也很严重。因此,直方图对于正确的估算绝对是有必要的。通过下面的查询,可以确认在本例中没有直方图存在:
select histogram, num_buckets FROM
user_tab_col_statistics WHERE table_name='T1' AND column_name='COL1';
收集完缺失的直方图后,查询优化器设法正确地估算基数,进而认为另一个执行计划是最高效的:
注意,在12.1版本不禁用自适应执行计划时执行wrong_estimations.sql脚本时,查询优化器生成了一个自适应执行计划,结果,在运行时自动发现,对于这个查询,散列联接要比嵌套循环更合适。
未识别限制条件
我必须警告你上一节中呈现的检查要优越于本节的。我通常只在执行过第一个检查之后才使用本节的第二个检查。这个检查的思路是验证查询优化器是否正确地识别出SQL语句的限制条件,从而尽可能早地应用它。换言之,检查执行计划是否会导致不必要的处理。
让我通过一个例子来演示一下这个概念,这个例子基于下面的restriction_not_recognized.sql脚本产生输出的摘录。从中,可以看到查询优化器决定以联接t1和t2表开始。
第一个联接返回40000行的结果集。稍后,该结果集与t3表进行联接。只产生了100行的结果集,尽管该操作读取了t3表返回的80000行数据。这就意味着查询优化器没有识别限制条件,而当大量的处理已经被执行后再应用它就太晚了。顺便说一下,估算联接基数,是查询优化器必须执行的最难的任务之一:
select count(t1.pad),
count(t2.pad),count(t3.pad) FROM t1,t2,t3 WHERE t1.id = t2.t1_id AND t2.id =
t3.t2_id
遇到这样的问题时,你可能会束手无策。事实上,没有用来描述两张表之间的关系的对象统计信息。修正这种问题的一个可行的办法是使用SQL概要。在本例中应用一个SQL概要会给出如下的执行计划。(我在第11章中介绍了什么是SQL概要以及它是如何工作的。)目前,重要的是要意识到有解决方案存在。要注意,不仅是联接顺序改变了(t2>t3>t1),连访问t1表的方式也不同了: