SQL优化:紧急情况下提高SQL性能竟是这样实现的!

作者 | 黄堋 ,多年一线 Oracle DBA 经验,长期服务电信、电网、医院、政府等行业客户。擅长数据库优化、数据库迁移升级、数据库故障处理。

在某运营商的优化经历中曾经遇到了一条比较有意思的 SQL,具体如下:

1 该最开始的 sql 执行情况如下

SQL> SELECT 2 NVL(T.RELA_OFFER_SPEC_ID, SUBOS.SUB_OFFER_SPEC_ID) "offerSpecId" 3 FROM OFFER_SPEC_RELA T 4 LEFT JOIN OFFER_SPEC_GRP_RELA SUBOS 5 ON T.RELA_GRP_ID = SUBOS.OFFER_SPEC_GRP_ID 6 AND subos.start_dt <= SYSDATE 7 AND subos.end_dt >= SYSDATE 8 WHERE T.RELA_TYPE_CD = 2 9 AND t.start_dt <= SYSDATE 10 AND t.end_dt >= SYSDATE 11 AND (T.OFFER_SPEC_ID = 109910000618 12 OR EXISTS 13 (SELECT A.OFFER_SPEC_GRP_ID 14 FROM OFFER_SPEC_GRP_RELA A 15 WHERE A.SUB_OFFER_SPEC_ID = 109910000618 16 AND T.OFFER_SPEC_GRP_ID = A.OFFER_SPEC_GRP_ID 17 )) 18 AND rownum<500; no rows selected Execution Plan ---------------------------------------------------------- Plan hash value: 1350156609

Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter(ROWNUM<500) 2 - filter("T"."OFFER_SPEC_ID"=109910000618 OR EXISTS (SELECT 0 FROM "SPEC"."OFFER_SPEC_GRP_RELA" "A" WHERE "A"."OFFER_SPEC_GRP_ID"=:B1 AND "A"."SUB_OFFER_SPEC_ID"=109910000618)) 3 - access("T"."RELA_GRP_ID"="SUBOS"."OFFER_SPEC_GRP_ID"(+)) 4 - filter("T"."RELA_TYPE_CD"=2 AND "T"."END_DT">=SYSDATE@! AND "T"."START_DT"<=SYSDATE@!) 5 - filter("SUBOS"."END_DT"(+)>=SYSDATE@! AND "SUBOS"."START_DT"(+)<=SYSDATE@!) 6 - access("A"."SUB_OFFER_SPEC_ID"=109910000618 AND "A"."OFFER_SPEC_GRP_ID"=:B1) Statistics ---------------------------------------------------------- 0 recursive calls 0 db block gets 12444 consistent gets 0 physical reads 0 redo size 339 bytes sent via SQL*Net to client 509 bytes received via SQL*Net from client 1 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 0 rows processed PLAN GET DISK WRITE ROWS ROWS USER_IO(MS) ELA(MS) CPU(MS) CLUSTER(MS) PLSQL END_TI I HASH VALUE EXEC PRE EXEC PRE EXEC PER EXEC ROW_P PRE EXEC PRE FETCH PER EXEC PRE EXEC PRE EXEC PER EXEC PER EXEC

2 第一次分析

此时应该有以下个地方值得注意

1) 该 sql 每天执行上千次,平均每次执行返回不到 10 行数据,但是平均逻辑读达到1.2W,可能存在性能问题。

2)ID 为 4,5 的执行计划路径中出现了两个全表扫描,看到这儿我们可以想到可能是没有合适的索引导致走了全表扫描从而执行效率低下。

3)ID 为 2 的执行计划路径出现了 FILTER,且 3,和 6 为其子路径,如果FILTER有两个及两个以上的子路径,那么他的执行原理将类似于嵌套循环,id 号最小的子路径如果返回行数较多,可能会导致多次执行id号更小的子路径,导致性能低下。一般存在 “OR EXISTS” 的时候会出现此情况,可以根据情况避免。

4)存在条件“ rownum<500 ”,但是从历史的执行情况来看,返回行数都远小于 500 行,此处我们先予以忽略。

3 第一次分析的处理

1)进过探查,发现存在两个表都有可用的索引,且两个表都只有几十 M 的大小

2)去掉“OR EXISTS”子句查看执行效率

此处可用看到,去掉 “OR EXISTS” 之后两个表走了合适的索引,并且执行效率极高。

SQL> SELECT 2 NVL(T.RELA_OFFER_SPEC_ID, SUBOS.SUB_OFFER_SPEC_ID) "offerSpecId" 3 FROM OFFER_SPEC_RELA T 4 LEFT JOIN OFFER_SPEC_GRP_RELA SUBOS 5 ON T.RELA_GRP_ID = SUBOS.OFFER_SPEC_GRP_ID 6 AND subos.start_dt <= SYSDATE 7 AND subos.end_dt >= SYSDATE 8 WHERE T.RELA_TYPE_CD = 2 9 AND t.start_dt <= SYSDATE 10 AND t.end_dt >= SYSDATE 11 AND T.OFFER_SPEC_ID = 109910000618; Elapsed: 00:00:00.00 Execution Plan ---------------------------------------------------------- Plan hash value: 510876366

Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter("T"."RELA_TYPE_CD"=2 AND "T"."END_DT">=SYSDATE@! AND "T"."START_DT"<=SYSDATE@!) 3 - access("T"."OFFER_SPEC_ID"=109910000618) 4 - filter("SUBOS"."END_DT"(+)>=SYSDATE@! AND "SUBOS"."START_DT"(+)<=SYSDATE@!) 5 - access("T"."RELA_GRP_ID"="SUBOS"."OFFER_SPEC_GRP_ID"(+)) Statistics ---------------------------------------------------------- 0 recursive calls 0 db block gets 4 consistent gets 0 physical reads 0 redo size 339 bytes sent via SQL*Net to client 510 bytes received via SQL*Net from client 1 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 0 rows processed

3)去掉 “OR EXISTS” 中的子句查看执行效率。

此处可用看到 “ OR EXISTS ” 中的子句单独执行返回行数并不多,且效率依旧很快。

SQL> SELECT A.OFFER_SPEC_GRP_ID 2 FROM OFFER_SPEC_GRP_RELA A 3 WHERE A.SUB_OFFER_SPEC_ID = 109910000618; OFFER_SPEC_GRP_ID ----------------- 100000048 109090086 Elapsed: 00:00:00.01 Execution Plan ---------------------------------------------------------- Plan hash value: 4223340843

Predicate Information (identified by operation id): --------------------------------------------------- 1 - access("A"."SUB_OFFER_SPEC_ID"=109910000618) Statistics ---------------------------------------------------------- 1 recursive calls 0 db block gets 4 consistent gets 0 physical reads 0 redo size 597 bytes sent via SQL*Net to client 521 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 2 rows processed

4)我们把该条 sql 语句分为 “OR EXISTS” 的子句和其他部分两块,到此我们可以看到,两块的执行效率都很高,但是合在一起就低了很多。在这种情况下,几乎可以确认,将该存在 “OR EXISTS” 的子句改写为 union 必将提升效率。

SQL> SELECT * 2 FROM 3 (SELECT NVL(T.RELA_OFFER_SPEC_ID, SUBOS.SUB_OFFER_SPEC_ID) "offerSpecId" 4 FROM OFFER_SPEC_RELA T 5 LEFT JOIN OFFER_SPEC_GRP_RELA SUBOS 6 ON T.RELA_GRP_ID = SUBOS.OFFER_SPEC_GRP_ID 7 AND subos.start_dt <= SYSDATE 8 AND subos.end_dt >= SYSDATE 9 WHERE T.RELA_TYPE_CD = 2 10 AND t.start_dt <= SYSDATE 11 AND t.end_dt >= SYSDATE 12 AND T.OFFER_SPEC_ID = 109910000618 13 UNION 14 SELECT NVL(T.RELA_OFFER_SPEC_ID, SUBOS.SUB_OFFER_SPEC_ID) "offerSpecId" 15 FROM OFFER_SPEC_RELA T 16 LEFT JOIN OFFER_SPEC_GRP_RELA SUBOS 17 ON T.RELA_GRP_ID = SUBOS.OFFER_SPEC_GRP_ID 18 AND subos.start_dt <= SYSDATE 19 AND subos.end_dt >= SYSDATE 20 WHERE T.RELA_TYPE_CD = 2 21 AND t.start_dt <= SYSDATE 22 AND t.end_dt >= SYSDATE 23 AND EXISTS 24 (SELECT A.OFFER_SPEC_GRP_ID 25 FROM OFFER_SPEC_GRP_RELA A 26 WHERE A.SUB_OFFER_SPEC_ID = 109910000618 27 AND T.OFFER_SPEC_GRP_ID = A.OFFER_SPEC_GRP_ID 28 ) 29 ) 30 WHERE rownum<500; Elapsed: 00:00:00.01 Execution Plan ---------------------------------------------------------- Plan hash value: 3072450155

Predicate Information (identified by operation id):

--------------------------------------------------- 1 - filter(ROWNUM<500) 3 - filter(ROWNUM<500) 6 - filter("T"."RELA_TYPE_CD"=2 AND "T"."END_DT">=SYSDATE@! AND "T"."START_DT"<=SYSDATE@!) 7 - access("T"."OFFER_SPEC_ID"=109910000618) 8 - filter("SUBOS"."END_DT"(+)>=SYSDATE@! AND "SUBOS"."START_DT"(+)<=SYSDATE@!) 9 - access("T"."RELA_GRP_ID"="SUBOS"."OFFER_SPEC_GRP_ID"(+)) 13 - access("A"."SUB_OFFER_SPEC_ID"=109910000618) 14 - filter("T"."RELA_TYPE_CD"=2 AND "T"."END_DT">=SYSDATE@! AND "T"."START_DT"<=SYSDATE@!) 15 - access("T"."OFFER_SPEC_GRP_ID"="A"."OFFER_SPEC_GRP_ID") filter("T"."OFFER_SPEC_GRP_ID" IS NOT NULL) 16 - filter("SUBOS"."END_DT"(+)>=SYSDATE@! AND "SUBOS"."START_DT"(+)<=SYSDATE@!) 17 - access("T"."RELA_GRP_ID"="SUBOS"."OFFER_SPEC_GRP_ID"(+)) Statistics ---------------------------------------------------------- 0 recursive calls 0 db block gets 11 consistent gets 0 physical reads 0 redo size 339 bytes sent via SQL*Net to client 510 bytes received via SQL*Net from client 1 SQL*Net roundtrips to/from client 2 sorts (memory) 0 sorts (disk) 0 rows processed

此处我们可以看到,改写之后逻辑读仅仅 11,较优化前提升了上千倍。到了此处,我们已经将 sql 优化到几乎最快的效率了。

4 第二次分析,确实改写能够提升效率,但是如果改写sql会涉及到修改代码,当前能否在不修改代码的情况下对其进行优化。

1)我们再来回顾一下最开始的执行计划路径。

我们可以看到 “ OR EXISTS ” 中的子句是在 ID 为 6 的路径才开始执行的,这儿有一个知识点即为一个 sql 中的子句,一般情况下默认会将其放到最后执行。

2)ID 为 4 , 5 的执行计划路径中在有高效索引的情况下却出现了两个全表扫描,可以推断 CBO 可能没有正常评估执行的 cost。

3)“OR EXISTS” 中的子句执行效率很快,返回行数并不多,我们可以考虑提升 CBO 将其提前执行,看能否影响 CBO 选择出更高效的执行计划。

SQL> SELECT NVL(T.RELA_OFFER_SPEC_ID, SUBOS.SUB_OFFER_SPEC_ID) "offerSpecId" 2 FROM OFFER_SPEC_RELA T 3 LEFT JOIN OFFER_SPEC_GRP_RELA SUBOS 4 ON T.RELA_GRP_ID = SUBOS.OFFER_SPEC_GRP_ID 5 AND subos.start_dt <= SYSDATE 6 AND subos.end_dt >= SYSDATE 7 WHERE T.RELA_TYPE_CD = 2 8 AND t.start_dt <= SYSDATE 9 AND t.end_dt >= SYSDATE 10 AND (T.OFFER_SPEC_ID = 109910000618 11 OR EXISTS 12 (SELECT /*+ push_subq */ A.OFFER_SPEC_GRP_ID 13 FROM OFFER_SPEC_GRP_RELA A 14 WHERE A.SUB_OFFER_SPEC_ID = 109910000618 15 AND T.OFFER_SPEC_GRP_ID = A.OFFER_SPEC_GRP_ID 16 )) 17 AND rownum<500; Elapsed: 00:00:00.03 Execution Plan ---------------------------------------------------------- Plan hash value: 849330408

--------------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter(ROWNUM<500) 3 - filter("T"."RELA_TYPE_CD"=2 AND "T"."END_DT">=SYSDATE@! AND "T"."START_DT"<=SYSDATE@! AND ("T"."OFFER_SPEC_ID"=109910000618 OR EXISTS (SELECT /*+ PUSH_SUBQ */ 0 FROM "SPEC"."OFFER_SPEC_GRP_RELA" "A" WHERE "A"."OFFER_SPEC_GRP_ID"=:B1 AND "A"."SUB_OFFER_SPEC_ID"=109910000618))) 4 - access("A"."SUB_OFFER_SPEC_ID"=109910000618 AND "A"."OFFER_SPEC_GRP_ID"=:B1) 5 - filter("SUBOS"."END_DT"(+)>=SYSDATE@! AND "SUBOS"."START_DT"(+)<=SYSDATE@!) 6 - access("T"."RELA_GRP_ID"="SUBOS"."OFFER_SPEC_GRP_ID"(+)) Statistics ---------------------------------------------------------- 0 recursive calls 0 db block gets 2531 consistent gets 0 physical reads 0 redo size 339 bytes sent via SQL*Net to client 510 bytes received via SQL*Net from client 1 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 0 rows processedSYS@crmadb1> SELECT NVL(T.RELA_OFFER_SPEC_ID, SUBOS.SUB_OFFER_SPEC_ID) "offerSpecId" 2 FROM OFFER_SPEC_RELA T 3 LEFT JOIN OFFER_SPEC_GRP_RELA SUBOS 4 ON T.RELA_GRP_ID = SUBOS.OFFER_SPEC_GRP_ID 5 AND subos.start_dt <= SYSDATE 6 AND subos.end_dt >= SYSDATE 7 WHERE T.RELA_TYPE_CD = 2 8 AND t.start_dt <= SYSDATE 9 AND t.end_dt >= SYSDATE 10 AND (T.OFFER_SPEC_ID = 109910000618 11 OR EXISTS 12 (SELECT /*+ push_subq */ A.OFFER_SPEC_GRP_ID 13 FROM OFFER_SPEC_GRP_RELA A 14 WHERE A.SUB_OFFER_SPEC_ID = 109910000618 15 AND T.OFFER_SPEC_GRP_ID = A.OFFER_SPEC_GRP_ID 16 )) 17 AND rownum<500; Elapsed: 00:00:00.03 Execution Plan ---------------------------------------------------------- Plan hash value: 849330408 ---------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter(ROWNUM<500) 3 - filter("T"."RELA_TYPE_CD"=2 AND "T"."END_DT">=SYSDATE@! AND "T"."START_DT"<=SYSDATE@! AND ("T"."OFFER_SPEC_ID"=109910000618 OR EXISTS (SELECT /*+ PUSH_SUBQ */ 0 FROM "SPEC"."OFFER_SPEC_GRP_RELA" "A" WHERE "A"."OFFER_SPEC_GRP_ID"=:B1 AND "A"."SUB_OFFER_SPEC_ID"=109910000618))) 4 - access("A"."SUB_OFFER_SPEC_ID"=109910000618 AND "A"."OFFER_SPEC_GRP_ID"=:B1) 5 - filter("SUBOS"."END_DT"(+)>=SYSDATE@! AND "SUBOS"."START_DT"(+)<=SYSDATE@!) 6 - access("T"."RELA_GRP_ID"="SUBOS"."OFFER_SPEC_GRP_ID"(+)) Statistics ---------------------------------------------------------- 0 recursive calls 0 db block gets 2531 consistent gets 0 physical reads 0 redo size 339 bytes sent via SQL*Net to client 510 bytes received via SQL*Net from client 1 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 0 rows processed

此处我们在子句中加了一个 HINT /*+ push_subq */ ,该HINT的作用即使提醒 CBO 将子句提前执行。我们可以看到,执行效率较之前也得到了显著提升,逻辑读降低了 7 倍作用,虽然相对于改写效率还是高很多,但是在急需处理的情况下该方案还是更加可取的,此时对执行计划进行绑定即可,无需修改代码。

4)最后执行计划中还是存在全表扫描,我使用 hint 使其强制走索引查看情况:

SQL> SELECT /*+ index(@"SEL$9E43CB6E" "T"@"SEL$2") */ NVL(T.RELA_OFFER_SPEC_ID, SUBOS.SUB_OFFER_SPEC_ID) "offerSpecId" 2 FROM OFFER_SPEC_RELA T 3 LEFT JOIN OFFER_SPEC_GRP_RELA SUBOS 4 ON T.RELA_GRP_ID = SUBOS.OFFER_SPEC_GRP_ID 5 AND subos.start_dt <= SYSDATE 6 AND subos.end_dt >= SYSDATE 7 WHERE T.RELA_TYPE_CD = 2 8 AND t.start_dt <= SYSDATE 9 AND t.end_dt >= SYSDATE 10 AND (T.OFFER_SPEC_ID = 109910000618 11 OR EXISTS 12 (SELECT /*+ push_subq */ A.OFFER_SPEC_GRP_ID 13 FROM OFFER_SPEC_GRP_RELA A 14 WHERE A.SUB_OFFER_SPEC_ID = 109910000618 15 AND T.OFFER_SPEC_GRP_ID = A.OFFER_SPEC_GRP_ID 16 )) 17 AND rownum<500; Elapsed: 00:00:00.69 Execution Plan ---------------------------------------------------------- Plan hash value: 355757032 --------------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): ---------------------------------------------------

1 - filter(ROWNUM<500) 3 - filter("T"."RELA_TYPE_CD"=2 AND "T"."END_DT">=SYSDATE@! AND "T"."START_DT"<=SYSDATE@! AND ("T"."OFFER_SPEC_ID"=109910000618 OR EXISTS (SELECT /*+ PUSH_SUBQ */ 0 FROM "SPEC"."OFFER_SPEC_GRP_RELA" "A" WHERE "A"."OFFER_SPEC_GRP_ID"=:B1 AND "A"."SUB_OFFER_SPEC_ID"=109910000618))) 5 - access("A"."SUB_OFFER_SPEC_ID"=109910000618 AND "A"."OFFER_SPEC_GRP_ID"=:B1) 6 - filter("SUBOS"."END_DT"(+)>=SYSDATE@! AND "SUBOS"."START_DT"(+)<=SYSDATE@!) 7 - access("T"."RELA_GRP_ID"="SUBOS"."OFFER_SPEC_GRP_ID"(+)) Statistics ---------------------------------------------------------- 1 recursive calls 0 db block gets 10527 consistent gets 406 physical reads 0 redo size 339 bytes sent via SQL*Net to client 510 bytes received via SQL*Net from client 1 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 0 rows processed

此时虽然走了索引,但是却是 ”INDEX FULL SCAN“ ,逻辑读也增加了很多,所以此时可以保持之前全表扫描的执行计划。

作者:黄堋

转载:意向文章下方留言。

原文发布于微信公众号 - 数据和云(OraNews)

原文发表时间:2018-07-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏互联网开发者交流社区

SQL触发器实例(下)

16640
来自专栏Grace development

电商系统设计之用户系统

设计以以下为工具讲起 – PHP为开发语言 – 基于Laravel框架 – MySQL为数据存储

1.1K50
来自专栏小怪聊职场

MySQL(八)|MySQL中In与Exists的区别(2)

19530
来自专栏杨建荣的学习笔记

MySQL 5.6,5.7的优化器对于count(*)的处理方式

最近看了很多阿里同学的MySQL文章,阿里内核同学的文章一言不合就上代码,不光让我们看到了结果,还能有代码可读,如果碰到了类似的问题,这样的解读确实是很难...

38560
来自专栏Grace development

电商系统设计之商品 (中)

上一篇文章我们讲了关于电商SPU,SKU的概念,以及为何要设计自定义属性与自定义规格并解释了何时可以用到它们。我一直在说电商是一个既简单又复杂的东西,本章我们再...

31520
来自专栏程序猿DD

一个不可思议的MySQL慢查分析与解决

前言 开发需要定期的删除表里一定时间以前的数据,SQL如下 mysql > delete from testtable WHERE biz_date <= '2...

35650
来自专栏数据和云

元宵快乐:看SQL大师们用SQL绘制的团圆

题记:在多年以前,论坛活跃的时代,在ITPUB上你能看到各种新奇有趣的知识,及时新鲜的信息,出类拔萃的技巧,有很多让人多年以后还记忆犹新。 这个帖子让我忍不住在...

33370
来自专栏java学习

Oracle基础试题与答案!

表结构: create table tbEmp --职员表 ( eID number(7) primarykey, ...

383120
来自专栏数据之美

关于mysql 索引自动优化机制: 索引选择性(Cardinality:索引基数)

1、两个同样结构的语句一个没有用到索引的问题: 查1到20号的就不用索引,查1到5号的就用索引,为什么呢?不稳定? mysql> expla...

39580
来自专栏沃趣科技

SQL优化案例-自定义函数索引(五)

SQL文本如下,表本身很小,走全表扫描也很快,但因业务重要性,要求尽可能缩短查询时间(为保证客户隐私,已经将注释和文字部分去掉):

10930

扫码关注云+社区

领取腾讯云代金券