2012 年,一位DBA 请求协助优化如下 SQL。
SELECT "A1"."CODE", "A1"."DEVICE_ID", "A1"."SIDEB_PORT_ID", "A1"."VERSION"
FROM (SELECT
"A2"."CODE" "CODE",
"A2"."DEVICE_ID" "DEVICE_ID",
"A2"."SIDEB_PORT_ID" "SIDEB_PORT_ID",
"A3"."VERSION" "VERSION",
ROW_NUMBER() OVER(PARTITION BY "A4"."PROD_ID" ORDER BY "A4"."HIST_TIME" DESC) "RN"
FROM "RM"."H_PROD_2_RF_SERV" "A4",
"RM"."H_RSC_FACING_SERV_LINE_ITEM" "A3",
"RM"."CONNECTOR" "A2"
WHERE "A4"."SERV_ID" = "A3"."SERV_ID"
AND "A3"."LINE_ID" = "A2"."CONNECTOR_ID"
AND EXISTS (SELECT 0
FROM "RM"."DEVICE_ITEM" "A5"
WHERE "A5"."DEVICE_ID" = "A2"."DEVICE_ID"
AND "A5"."ITEM_SPEC_ID" = 200006
AND "A5"."VALUE" ='7')
AND "A4"."PROD_ID" = 313) "A1"
WHERE "A1"."RN" = 1;
执行计划如下。
-------------------------------------------------------------------------------------
| Id |Operation |Name |Rows|Bytes| Cost (%CPU)|
-------------------------------------------------------------------------------------
| 0 |SELECT STATEMENT | | 1| 175| 20 (10)|
|* 1 | VIEW | | 1| 175| 20 (10)|
|* 2 | WINDOW SORT PUSHED RANK | | 1| 109| 20 (10)|
| 3 | NESTED LOOPS | | 1| 109| 19 (6)|
| 4 | NESTED LOOPS | | 1| 80| 17 (6)|
| 5 | MERGE JOIN CARTESIAN | | 1| 60| 13 (8)|
| 6 | SORT UNIQUE | | 1| 36| 6 (0)|
|* 7 | TABLE ACCESS BY INDEX ROWID|DEVICE_ITEM | 1| 36| 6 (0)|
|* 8 | INDEX RANGE SCAN |IDX_DEVICE_ITEM_VALE | 9| | 4 (0)|
| 9 | BUFFER SORT | | 4| 96| 7 (15)|
| 10 | TABLE ACCESS BY INDEX ROWID|H_PROD_2_RF_SERV | 4| 96| 6 (0)|
|*11 | INDEX RANGE SCAN |IDX_HP2RS_PRODID_SERVID | 4| | 2 (0)|
| 12 | TABLE ACCESS BY INDEX ROWID |H_RSC_FACING_SERV_LINE_ITEM| 2| 40| 4 (0)|
|*13 | INDEX RANGE SCAN |IDX_HRFSLI_SERV | 2| | 2 (0)|
|*14 | TABLE ACCESS BY INDEX ROWID |CONNECTOR | 1| 29| 2 (0)|
|*15 | INDEX UNIQUE SCAN |PK_CONNECTOR | 1| | 1 (0)|
-------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("A1"."RN"=1)
2 - filter(ROW_NUMBER() OVER ( PARTITION BY "A4"."PROD_ID" ORDER BY
INTERNAL_FUNCTION("A4"."HIST_TIME") DESC )<=1)
7 - filter("A5"."ITEM_SPEC_ID"=200006)
8 - access("A5"."VALUE"='7')
11 - access("A4"."PROD_ID"=313)
13 - access("A4"."SERV_ID"="A3"."SERV_ID")
14 - filter("A5"."DEVICE_ID"="A2"."DEVICE_ID")
15 - access("A3"."LINE_ID"="A2"."CONNECTOR_ID")
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
2539920 consistent gets
0 physical reads
0 redo size
735 bytes sent via SQL*Net to client
492 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
3 sorts (memory)
0 sorts (disk)
1 rows processed
该 SQL 要执行 9.437 秒,只返回一行数据,其中 A5 有 48 194 511 行数据,A2 有 35 467 304 行数据,其余表都是小表。
首先,笔者运用 SQL 三段分拆方法,检查 SQL 写法,经过检查,SQL 写法没有问题。
其次笔者检查执行计划。执行计划中 Id=5 出现了 MERGE JOIN CARTESIAN,这一般都是统计信息收集不准确,将离 MERGE JOIN CARTESIAN 关键字最近的表(Id=7)Rows 估算为 1 导致。
正常情况下,应该先检查 SQL 中所有表的统计信息是否过期,如果统计信息过期了应该立即收集。因为做了太多的 SQL 优化,遇到 SQL 出现了性能问题,已经形成条件反射想要立刻优化它,所以,当时没有立即对表收集统计信息。
如果想要从执行计划入手优化 SQL,我们一般要从执行计划的入口开始检查,检查 Rows 估算是否准确。当然了,如果执行计划中有明显值得怀疑的地方,我们也可以直接检查值得怀疑之处。
执行计划的入口是 Id=8,Id=8 是索引范围扫描,通过 Id=7 回表。于是让朋友运行如下 SQL。
SELECT COUNT(*)
FROM "RM"."DEVICE_ITEM" "A5"
WHERE "A5"."ITEM_SPEC_ID" = 200006
AND "A5"."VALUE" = '7';
得到反馈,上面查询返回 68 384 行数据。其次,查询执行计划中 Id=11 和 Id=10 应该返回多少数据(A4),运行如下 SQL。
select count(*) from H_PROD_2_RF_SERV where prod_id = 313;
得到反馈,上面查询返回 6 行数据。根据以上信息我们知道应该怎么优化上述 SQL 了。我们再来查看原始 SQL 的部分代码。
FROM "RM"."H_PROD_2_RF_SERV" "A4",
"RM"."H_RSC_FACING_SERV_LINE_ITEM" "A3",
"RM"."CONNECTOR" "A2"
WHERE "A4"."SERV_ID" = "A3"."SERV_ID"
AND "A3"."LINE_ID" = "A2"."CONNECTOR_ID"
AND EXISTS (SELECT 0
FROM "RM"."DEVICE_ITEM" "A5"
WHERE "A5"."DEVICE_ID" = "A2"."DEVICE_ID"
AND "A5"."ITEM_SPEC_ID" = 200006
AND "A5"."VALUE" ='7')
AND "A4"."PROD_ID" = 313)
A4 过滤后只返回 6 行数据,A3 是小表,A2 有 35 467 304 行数据,A5 过滤后返回 6 万行数据,其中 A3,A2 都没有过滤条件。
SQL 语句中 A4 与 A3 进行关联,因为 A4 过滤后返回 6 行数据,A3 是小表,所以让 A4 作为驱动表 leading(a4),与 A3 使用嵌套循环 use_nl(a4,a3)方式进行关联,关联之后得到一个结果集,因为 A4 与 A3 返回数据量都很小,所以关联之后的结果集也必然很小。
因为 A2 表很大,而且 A2 没有过滤条件,所以我们不能让 A2 走 HASH 连接,因为没有过滤条件,使用 HASH 进行关联只能走全表扫描。如果让 A2 走嵌套循环,作为嵌套循环被驱动表,那么我们可以让 A2 走连接列的索引,这样就避免了大表 A2 因为没有过滤条件而走全表扫描。因此,我们将 A4 与 A3 关联之后的结果集作为嵌套循环驱动表,然后再与 A2 使用嵌套循环进行关联:use_nl(a3,a2)。
因为 A5 过滤后有 6 万行数据,所以我们让 A5 与 A2 进行 HASH 连接,最终添加如下 HINT。
SELECT "A1"."CODE", "A1"."DEVICE_ID", "A1"."SIDEB_PORT_ID", "A1"."VERSION"
FROM (SELECT /*+ leading(a4) use_nl(a4,a3) use_nl(a3,a2) */
"A2"."CODE" "CODE",
"A2"."DEVICE_ID" "DEVICE_ID",
"A2"."SIDEB_PORT_ID" "SIDEB_PORT_ID",
"A3"."VERSION" "VERSION",
ROW_NUMBER() OVER(PARTITION BY "A4"."PROD_ID" ORDER BY "A4"."HIST_TIME" DESC) "RN"
FROM "RM"."H_PROD_2_RF_SERV" "A4",
"RM"."H_RSC_FACING_SERV_LINE_ITEM" "A3",
"RM"."CONNECTOR" "A2"
WHERE "A4"."SERV_ID" = "A3"."SERV_ID"
AND "A3"."LINE_ID" = "A2"."CONNECTOR_ID"
AND EXISTS (SELECT /*+ hash_sj */ 0
FROM "RM"."DEVICE_ITEM" "A5"
WHERE "A5"."DEVICE_ID" = "A2"."DEVICE_ID"
AND "A5"."ITEM_SPEC_ID" = 200006
AND "A5"."VALUE" ='7')
AND "A4"."PROD_ID" = 313) "A1"
WHERE "A1"."RN" = 1;
执行计划如下。
-------------------------------------------------------------------------------------
| Id|Operation |Name |Rows|Bytes|Cost(%CPU)|
----------------------------------------------------------------------------------------
| 0|SELECT STATEMENT | | 1| 175| 40 (3)|
|* 1| VIEW | | 1| 175| 40 (3)|
|* 2| WINDOW SORT PUSHED RANK | | 1| 109| 40 (3)|
|* 3| HASH JOIN SEMI | | 1| 109| 39 (0)|
| 4| NESTED LOOPS | | 7| 511| 33 (0)|
| 5| NESTED LOOPS | | 7| 308| 19 (0)|
| 6| TABLE ACCESS BY INDEX ROWID|H_PROD_2_RF_SERV | 4| 96| 7 (0)|
|* 7| INDEX RANGE SCAN |IDX_HP2RS_PRODID_SERVID | 4| | 3 (0)|
| 8| TABLE ACCESS BY INDEX ROWID|H_RSC_FACING_SERV_LINE_ITEM| 2| 40| 4 (0)|
|* 9| INDEX RANGE SCAN |IDX_HRFSLI_SERV | 2| | 2 (0)|
| 10| TABLE ACCESS BY INDEX ROWID|CONNECTOR | 1| 29| 2 (0)|
|*11| INDEX UNIQUE SCAN |PK_CONNECTOR | 1| | 1 (0)|
|*12| TABLE ACCESS BY INDEX ROWID |DEVICE_ITEM | 1| 36| 6 (0)|
|*13| INDEX RANGE SCAN |IDX_DEVICE_ITEM_VALE | 9| | 4 (0)|
-------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("A1"."RN"=1)
2 - filter(ROW_NUMBER() OVER ( PARTITION BY "A4"."PROD_ID" ORDER BY
INTERNAL_FUNCTION("A4"."HIST_TIME") DESC )<=1)
3 - access("A5"."DEVICE_ID"="A2"."DEVICE_ID")
7 - access("A4"."PROD_ID"=313)
9 - access("A4"."SERV_ID"="A3"."SERV_ID")
11 - access("A3"."LINE_ID"="A2"."CONNECTOR_ID")
12 - filter("A5"."ITEM_SPEC_ID"=200006)
13 - access("A5"."VALUE"='7')
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
14770 consistent gets
0 physical reads
0 redo size
735 bytes sent via SQL*Net to client
492 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
1 sorts (memory)
0 sorts (disk)
1 rows processed
最终该 SQL 只需 0.188 秒就能出结果,逻辑读从最开始的 2 539 920 下降到 14 770。
当具备一定优化理论知识之后,我们可以不看执行计划,直接根据 SQL 写法找到 SQL 语句中返回数据量最小的表作为驱动表,然后看它与谁进行关联,根据关联返回的数据量判断走 NL 还是 HASH,然后一直这样进行下去,直到 SQL 语句中所有表都关联完毕。如果大家长期采用此方法进行锻炼,久而久之,你自己的脑袋就是 CBO。