执行计划中,最需要关心的有 Id,Operation,Name,Rows。
看 Id 是为了观察 Id 前面是否有「*」号。
Operation 表示表的访问路径或者连接方式。第 4 章我们会详细介绍常见访问路径,第 5 章会详细介绍表连接方式。
Name 是 SQL 语句中对象的名字,可以是表名、索引名、视图名、物化视图名或者 CBO 自动生成的名字。
Rows 是 CBO 根据统计信息以及数学公式计算出来的,也就是说 Rows 是假的,不是真实的。这里的 Rows 也被称作执行计划中返回的基数。再一次强调,Rows 是假的,别被它骗了。前面介绍过带有 A-Time 的执行计划,带有 A-Time 的执行计划中 E-Rows 就是普通执行计划中的 Rows,A-Rows 才是真实的。在进行 SQL 优化的时候,我们经常需要手工计算某个访问路径的真实 Rows,然后对比执行计划中的 Rows。如果手工计算的 Rows 与执行计划中的 Rows 相差很大,执行计划往往就出错了。
有些人可能还会特意查看执行计划中的 Cost,在进行 SQL 优化的时候,千万别看 Cost!如果一个 SQL 语句都需要优化了,那么它的 Cost 还是准确的吗?有很大概率算错了!既然算错了,你还去看错误的 Cost 干什么呢?关于 Cost,我们会在第 6 章详细介绍,同时由此引出 SQL 优化核心思想。
下面我们将为大家介绍如何利用光标移动大法阅读执行计划。
现有如下执行计划。
-------------------------------------------------------------------------------------
| Id | Operation | Name | Rows |
-------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 |
| 1 | TABLE ACCESS BY INDEX ROWID | INTRC_PROD_DIM | 1 |
| 2 | NESTED LOOPS | | 1 |
| 3 | NESTED LOOPS | | 1 |
| 4 | NESTED LOOPS | | 330 |
| 5 | NESTED LOOPS | | 1312K|
|* 6 | HASH JOIN | | 6558 |
| 7 | TABLE ACCESS FULL | INTRC_GEO_DIM | 2532 |
|* 8 | HASH JOIN | | 6558 |
|* 9 | TABLE ACCESS FULL | INTRC_INITV_DIM | 833 |
|* 10 | HASH JOIN | | 6558 |
| 11 | PARTITION RANGE SINGLE | | 171 |
|* 12 | TABLE ACCESS FULL | INTRC_TIME_DIM | 171 |
|* 13 | HASH JOIN | | 6558 |
| 14 | PARTITION RANGE SINGLE | | 171 |
|* 15 | TABLE ACCESS FULL | INTRC_TIME_DIM | 171 |
| 16 | PARTITION RANGE SINGLE | | 6558 |
|* 17 | TABLE ACCESS FULL | INTRC_INITV_TIME_BRDG_DIM | 6558 |
| 18 | PARTITION RANGE SINGLE | | 200 |
|* 19 | TABLE ACCESS BY LOCAL INDEX ROWID| INTRC_INBR_FCT | 200 |
| 20 | BITMAP CONVERSION TO ROWIDS | | |
| 21 | BITMAP INDEX FULL SCAN | INTRC_INBR_FCT_BX1 | |
| 22 | PARTITION RANGE SINGLE | | 1 |
| 23 | BITMAP CONVERSION TO ROWIDS | | 1 |
| 24 | BITMAP AND | | |
|* 25 | BITMAP INDEX SINGLE VALUE | INTRC_TIME_DIM_BX1 | |
| 26 | BITMAP CONVERSION FROM ROWIDS | | |
| 27 | SORT ORDER BY | | |
|* 28 | INDEX RANGE SCAN | INTRC_TIME_DIM_PK | 1 |
| 29 | BITMAP CONVERSION FROM ROWIDS | | |
|* 30 | INDEX RANGE SCAN | INTRC_TIME_DIM_NX1 | 1 |
| 31 | BITMAP CONVERSION TO ROWIDS | | 1 |
| 32 | BITMAP AND | | |
| 33 | BITMAP CONVERSION FROM ROWIDS | | |
|* 34 | INDEX RANGE SCAN | INTRC_INPR_BRDG_DIM_PK | 1 |
|* 35 | BITMAP INDEX SINGLE VALUE | INTRC_INPR_BRDG_DIM_BX1 | |
|* 36 | INDEX RANGE SCAN | INTRC_PROD_DIM_PK | 1 |
-------------------------------------------------------------------------------------
有些读者可能会认为 Id=15 最先执行,因为 Id=15 的缩进最大,其实这是错误的。
现在给大家介绍一种方法:光标移动大法。光标就是我们打字的时候,鼠标点到某个地方,闪烁的光标。阅读执行计划的时候,一般从上往下看,找到执行计划的入口之后,再往上看。
阅读执行计划的时候,我们将光标移动到 Id=0 SELECT 的 S 前面,然后按住键盘的向下移动的箭头,向下移动,然后向右移动,然后再向下,再向右……Id=0 和 Id=1 相差一个空格(缩进),上下相差一个空格(缩进)就是父子关系,上面的是父亲,下面的是儿子,儿子比父亲先执行。那么这里 Id=1 是 Id=0 的儿子,Id=1 先执行。Id=2 是 Id=1 的儿子,Id=2 先执行。Id=3 是 Id=2 的儿子,Id=3 先执行。这样我们一直将光标移动到 Id=7(向下,向右移动),Id=7 与 Id=8 对齐,表示 Id=7 与 Id=8 是兄弟关系,上面的是兄,下面的是弟,兄优先于弟先执行,也就是说 Id=7 先于 Id=8 执行。Id=7 也跟 Id=19、Id=24、Id=34 对齐,将光标移动到 Id=7 前面,向下移动光标,Id=19 在 Id=18 的下面,光标移动大法是不能「穿墙」的,从 Id=7 移动到 Id=19 会穿过 Id=18,同理 Id=24、Id=34 也「穿墙」了,因此 Id=7 只是和 Id=8 对齐。因为 Id=7 下面没有儿子,所以执行计划的入口是 Id=7,整个执行计划中 Id=7 最先执行。
提问:怎么快速找到执行计划的入口?
回答:我们可以利用光标移动大法,先将光标放在 Id=0 这一步,然后一直向下向右移动光标,直到找到没有儿子的 Id,这个 Id 就是执行计划的入口。
提问:怎么判断是哪个表与哪个表进行关联的?
回答:我们先找到表在执行计划中的 Id,然后看这个 Id(或者是这个 Id 的父亲)与谁对齐(利用光标上下移动),它与谁对齐,就与谁进行关联。比如 Id=17 这个表,它本身没有和任何 Id 对齐,但是 Id=17 的父亲是 Id=16,与 Id=14 对齐,Id=14 的儿子是 Id=15,所以 Id=17 这个表是与 Id=15 这个表进行关联的,并且两个表是进行 HASH 连接的。
提问:在 SQL 优化实战中,怎么应用光标移动大法优化 SQL?
回答:例如,有如下执行计划。
如果是 SQL 优化初学者(高手可以一眼看出执行计划哪里有性能问题),可以先利用光标移动大法找到执行计划入口,检查入口 Rows 返回的真实行数与 CBO 估算的行数是否存在较大差异。比如,这里执行计划入口为 Id=15,优化器估算返回 47 行(E-Rows=47),实际上返回了 25 行(A-Rows=25),E-Rows 与 A-Rows 差别不大。找到执行计划入口之后,我们应该从执行计划入口往上检查,Id=15 上面的是 Id=14,Id=14 上面的是 Id=13,这样一直检查到 Id=11。Id=11 估算返回 5 行(E-Rows=5),但是实际上返回了 11 248 行(A-Rows=11 248),所以执行计划 Id=11 这步有问题,由于 Id=11 Rows 估算错误,它会导致后面整个执行计划出错,应该想办法让 CBO 估算出较为准确的 Rows。
我们还可以利用光标移动大法找出哪个表与哪个表进行关联的,例如下面执行计划。
Id=29 的表它与 Id=8 对齐,这表示 Id=29 的表是与一个结果集进行关联的,关联方式为嵌套循环(Id=7,NESTED LOOPS)。从执行计划中我们可以看到 Id=29 是嵌套循环的被驱动表,但是没走索引,走的是全表扫描。如果 Id=29 的表是一个大表,会出现严重的性能问题,因为它会被扫描多次,而且每次扫描的时候都是全表扫描,所以,我们需要在 Id=29 的表中创建一个索引(连接列上创建索引)。