2011 年,一位 ITPUB 的网友请求优化如下 SQL,该 SQL 执行不出结果。
explain plan for select ((v.yvalue * 300) / (u.xvalue * 50)), u.xtime
from (select x.index_value xvalue, substr(x.update_time, 1, 14) xtime
from tb_indexs x
where x.id in (select min(a.id)
from tb_indexs a
where a.code = 'HSI'
and a.update_time > 20110701000000
and a.update_time < 20110722000000
group by a.update_time)) u,
(select y.index_value yvalue, substr(y.update_time, 1, 14) ytime
from tb_indexs y
where y.id in (select min(b.id)
from tb_indexs b
where b.code = '000300'
and b.update_time > 20110701000000
and b.update_time < 20110722000000
group by b.update_time)) v
where u.xtime = v.ytime
order by u.xtime;
Explained.
select * from table(dbms_xplan.display);
PLAN_TABLE_OUTPUT
-------------------------------------------------------------------------------------
Plan hash value: 573554298
-------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |Cost(%CPU)|
-------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 54 | 13 (8)|
| 1 | SORT ORDER BY | | 1 | 54 | 13 (8)|
| 2 | NESTED LOOPS | | 1 | 54 | 12 (0)|
| 3 | MERGE JOIN CARTESIAN | | 1 | 33 | 10 (0)|
| 4 | NESTED LOOPS | | 1 | 27 | 6 (0)|
| 5 | VIEW | VW_NSO_2 | 1 | 6 | 4 (0)|
| 6 | HASH GROUP BY | | 1 | 41 | 4 (0)|
| 7 | TABLE ACCESS BY INDEX ROWID| TB_INDEXS | 1 | 41 | 4 (0)|
|* 8 | INDEX RANGE SCAN | IDX_UPDATE_TIME | 1 | | 3 (0)|
| 9 | TABLE ACCESS BY INDEX ROWID | TB_INDEXS | 1 | 21 | 2 (0)|
|*10 | INDEX UNIQUE SCAN | PK_INDEXS | 1 | | 1 (0)|
| 11 | BUFFER SORT | | 1 | 6 | 8 (0)|
| 12 | VIEW | VW_NSO_1 | 1 | 6 | 4 (0)|
| 13 | HASH GROUP BY | | 1 | 41 | 4 (0)|
| 14 | TABLE ACCESS BY INDEX ROWID| TB_INDEXS | 1 | 41 | 4 (0)|
|*15 | INDEX RANGE SCAN | IDX_UPDATE_TIME | 1 | | 3 (0)|
|*16 | TABLE ACCESS BY INDEX ROWID | TB_INDEXS | 1 | 21 | 2 (0)|
|*17 | INDEX UNIQUE SCAN | PK_INDEXS | 1 | | 1 (0)|
-------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
8 - access("A"."UPDATE_TIME">20110701000000 AND "A"."CODE"='HSI' AND
"A"."UPDATE_TIME"<20110722000000)
filter("A"."CODE"='HSI')
10 - access("X"."ID"="$nso_col_1")
15 - access("B"."UPDATE_TIME">20110701000000 AND "B"."CODE"='000300' AND
"B"."UPDATE_TIME"<20110722000000)
filter("B"."CODE"='000300')
16 - filter(SUBSTR(TO_CHAR("X"."UPDATE_TIME"),1,14)=SUBSTR(TO_CHAR("Y"."UPDATE_TIME"),1,14)
)
17 - access("Y"."ID"="$nso_col_1")
38 rows selected.
大家请仔细观察 SQL 语句,该 SQL 访问的都是同一个表 TB_INDEXS,表在 SQL 语句中被访问了 4 次,我们可以对 SQL 进行等价改写,让 SQL 只访问一次,从而就达到了优化目的。
但是,网友希望在不改写 SQL 的前提下优化该 SQL 语句,因此只能从执行计划入手优化 SQL。执行计划中,Id=3 是笛卡儿积,这就是为什么该 SQL 执行不出结果。为什么会产生笛卡儿积呢?因为执行计划中所有的步骤 Rows 都估算返回为 1 行数据,所以优化器选择了笛卡儿积连接(在 5.4 节中我们讲过,离笛卡儿积关键字最近的「表」被错误地估算为 1 行的时候,优化器很容易选择走笛卡儿积连接)。
执行计划的入口是 Id=8,也就是 SQL 语句中的 in 子查询,优化器评估 Id=8 返回 1 行数据,但是实际上 Id=8 要返回 2 万行数据。笔者曾经尝试对表 TB_INDEXS 重新收集统计信息,但是收集完统计信息之后,优化器还是评估 Id=8 返回 1 行数据。
为什么优化器会评估 Id=8 返回 1 行数据呢?这是因为字段 UPDATE_TIME 被设计为了 NUMBER 类型,而实际上 UPDATE_TIME 应该是 DATE 类型,同时 where 条件中还有一个选择性较低的过滤条件,优化器估算返回的行数等于表的总行数与 UPDATE_TIME 的选择性、CODE 的选择性的乘积。UPDATE_TIME 因为字段类型设计错误,本来应该估算返回 21 天的数据,但是因为 UPDATE_TIME 设计为了 NUMBER 类型,导致优化器在估算返回行数的时候不是利用 DATE 类型估算返回行数,而是利用 NUMBER 类型估算返回行数。大家请注意观察 UPDATE_TIME 的过滤条件,将年月日存储为 NUMBER 类型是一个天文数字,然后 where 条件只是取出一个天文数字中极小一部分数据,因此估算返回的行数始终会被估算为 1 行,
因为执行计划入口的 Rows 估算错误,所以后面的执行计划不用看,全是错误的。因为 UPDATE_TIME 已经被设计为 NUMBER 类型了,想要通过修改 UPDATE_TIME 为 DATE 类型来纠正优化器估算返回的 Rows 是不可行的,因为需要申请停机时间。
怎么才可以让优化器知道真实 Rows 呢?我们可以使用 HINT:CARDINALITY。
/*+ cardinality(a 10000)*/
表示指定 a 表有 1 万行数据。
/*+ cardinality(@a 10000)*/
表示指定 query block a 有 1 万行数据。
添加完 HINT 后的执行计划如下。
set autot trace
select /*+ cardinality(@a 20000) cardinality(@b 20000) */((v.yvalue * 300)/(u.xvalue * 50)), u.xtime
from (select x.index_value xvalue, substr(x.update_time, 1, 14) xtime
from tb_indexs x
where x.id in (select /*+ QB_NAME(a) */ min(a.id)
from tb_indexs a
where a.code = 'HSI'
and a.update_time > 20110701000000
and a.update_time < 20110722000000
group by a.update_time)) u,
(select y.index_value yvalue, substr(y.update_time, 1, 14) ytime
from tb_indexs y
where y.id in (select /*+ QB_NAME(b) */ min(b.id)
from tb_indexs b
where b.code = '000300'
and b.update_time > 20110701000000
and b.update_time < 20110722000000
group by b.update_time)) v
where u.xtime = v.ytime
order by u.xtime;
3032 rows selected.
Elapsed: 00:00:15.07
Execution Plan
----------------------------------------------------------
Plan hash value: 2679503093
-------------------------------------------------------------------------------------
| Id | Operation | Name | Rows| Bytes |Cost(%CPU)|
-------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 935| 50490 | 1393 (7)|
| 1 | SORT ORDER BY | | 935| 50490 | 1393 (7)|
|* 2 | HASH JOIN | | 935| 50490 | 1392 (7)|
| 3 | VIEW | VW_NSO_1 |20000| 117K| 4 (0)|
| 4 | HASH GROUP BY | |20000| 800K| 4 (0)|
| 5 | TABLE ACCESS BY INDEX ROWID | TB_INDEXS | 1| 41 | 4 (0)|
|* 6 | INDEX RANGE SCAN | IDX_UPDATE_TIME | 1| | 3 (0)|
|* 7 | HASH JOIN | |31729| 1487K| 1386 (7)|
|* 8 | HASH JOIN | |20000| 527K| 695 (7)|
| 9 | VIEW | VW_NSO_2 |20000| 117K| 4 (0)|
| 10 | HASH GROUP BY | |20000| 800K| 4 (0)|
| 11 | TABLE ACCESS BY INDEX ROWID| TB_INDEXS | 1| 41 | 4 (0)|
|*12 | INDEX RANGE SCAN | IDX_UPDATE_TIME | 1| | 3 (0)|
| 13 | TABLE ACCESS FULL | TB_INDEXS | 678K| 13M| 678 (5)|
| 14 | TABLE ACCESS FULL | TB_INDEXS | 678K| 13M| 678 (5)|
-------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("Y"."ID"="$nso_col_1")
6 - access("B"."UPDATE_TIME">20110701000000 AND "B"."CODE"='000300' AND
"B"."UPDATE_TIME"<20110722000000)
filter("B"."CODE"='000300')
7 - access(SUBSTR(TO_CHAR("X"."UPDATE_TIME"),1,14)=SUBSTR(TO_CHAR("Y"."UPDATE_TIME"),1,14)
)
8 - access("X"."ID"="$nso_col_1")
12 - access("A"."UPDATE_TIME">20110701000000 AND "A"."CODE"='HSI' AND
"A"."UPDATE_TIME"<20110722000000)
filter("A"."CODE"='HSI')
Statistics
----------------------------------------------------------
29 recursive calls
0 db block gets
8351 consistent gets
4977 physical reads
72 redo size
141975 bytes sent via SQL*Net to client
2622 bytes received via SQL*Net from client
204 SQL*Net roundtrips to/from client
1 sorts (memory)
0 sorts (disk)
3032 rows processed
通过指定执行计划入口(子查询)返回 2 万行数据,纠正了之前错误的执行计划,SQL 最终执行了 15 秒就返回了所有的结果。
如果不知道有 CARDINALITY 这个 HINT,怎么优化 SQL 呢?我们可以启用动态采样 Level 4 及以上(最好别超过 6),让优化器能较为准确地评估出子查询返回的 Rows,这样也能达到优化目的。如果不知道动态采样怎么优化 SQL 呢?我们可以直接使用 HINT,比如 USE_HASH 等,让 SQL 走我们认为正确的执行计划也能达到优化目的。当然了,最佳的优化方法应该是直接从业务上入手,从表设计上入手,从 SQL 写法上入手,而不是退而求其次从执行计划入手,但是很多时候我们往往只能从执行计划上入手优化 SQL,这或许是绝大多数 DBA 的无奈。