性能为王:SQL标量子查询的优化案例分析


黄廷忠(网名:认真就输)

云和恩墨技术专家

个人博客:http://www.htz.pw/

本篇整理内容是黄廷忠在“云和恩墨大讲堂”微信分享中的讲解案例,SQL优化及SQL审核,是从源头解决性能问题的根本手段,无论是开发人员还是DBA,都应当持续深入的学习SQL开发技能,从而为解决性能问题打下根基。

本篇为系列案例之一:标量子查询优化

 

以下案例来自于某省电信系统EDW性能优化实践,数据库版本为11.2.0.3,运行在ORACLE Exadata一体机上,是个典型的OLAP环境,表上无索引,表无统计信息

SQL性能问题诊断

SQL的基本逻辑如下: 

SELECT OFFER_SERV_SUM AS N37364,

       LOCAL_CODE LOCAL_CODE,

       AREA_ID AREA_ID,

       DVLP_AREA_ID MG_AREA_ID,

       DVLP_ORG_ID ORG_ID,

       CASE

         WHEN OFFER_SPEC_ID IN (SELECT LOCAL_ITEM_CODE

                                  FROM PU_META_DIM.CODE_ITEM

                                 WHERE PROV_TYPE_ID = 49

                                   AND PROV_ITEM_ID = 64) THEN

          1 ELSE 0

       END || CASE

         WHEN TO_CHAR(OFFER_SPEC_ID) IN

              (SELECT LOCAL_ITEM_CODE

                 FROM PU_META_DIM.CODE_ITEM A

                WHERE PROV_TYPE_ID = 49

                  AND PROV_ITEM_ID IN (85, 86, 68, 69, 70, 71)) THEN

          1 ELSE 0

       END || CASE

         WHEN TO_CHAR(OFFER_SPEC_ID) IN

              (SELECT OFFER_SPEC_ID

                 FROM PU_META_DIM.D_DRAGON_PLAN

                WHERE BT_CODE = 1

                  AND (UPPER(NAME) LIKE'%4S%'ORNAMELIKE'%??%')) THEN

          1 ELSE 0

       END || CASE

         WHEN TO_CHAR(OFFER_SPEC_ID) IN

              (SELECT LOCAL_ITEM_CODE

                 FROM PU_META_DIM.CODE_ITEM

                WHERE PROV_TYPE_ID = 49

                  AND PROV_ITEM_ID = 64) THEN

         1 ELSE 0

       END || CASE

         WHEN TO_CHAR(OFFER_SPEC_ID) IN

              (SELECT OFFER_SPEC_ID 

                 FROM PU_META_DIM.TY_SUIT_CFG) THEN

         1 ELSE 0

       END || CASE

         WHEN TO_CHAR(OFFER_SPEC_ID) IN

              (SELECT OFFER_SPEC_ID

                 FROM PU_META_DIM.D_DRAGON_PLAN

                WHERE BT_CODE = 2

                  AND (UPPER(NAME) LIKE'%4S%'ORNAMELIKE'%??%')) THEN

          1 ELSE 0

       END | C_ALL

  FROM PU_BASE_IND.DM_SP_SUB_OFFER_SERV_D_201407 

  PARTITION(P20140727) A

 WHERE LOCAL_CODE = '028'

 

这个SQL一共格式化出来有600多行,由于篇幅的限制,这里把其它一些无用的列的信息去掉。下面来查询一下SQL的执行计划:

从上面的关键字部分STORAGE FULL FIRST ROWS这部分,我们也可以看到是一个Exadata一体机的环境。 

SQL的执行计划很简单,一共只有10行。FROM后对一个分区表的一个子分区执行全分区扫描。

 

下面来看看这个SQL每次执行消耗的物理读与逻辑读。

这里需要关注几点:

1,  每次执行消耗的物理读(diskpre exec)

2,  每次执行平均消耗的逻辑读(getpre exec)

3,  每次执行平均返回的行数(rowspre exec)

这个脚本的输出我们还需要计算一下

1,  每次执行SQL,返回的每行平均消耗的逻辑读338280770/4302704=78.62,大概逻辑读78才能换回一行。

2,  每次执行SQL,返回的每行平均消耗的物理读22610/4302704=0.005 

从上面几点,大概知道这个SQL存在性能问题。

基础信息分析
PU_BASE_IND.DM_SP_SUB_OFFER_SERV_D_201407是个分区表,下面查询一下表分区的信息:

 

这里可以看到表是一个RANGE-LIST的分区表。下面查询SQL关联的表的大小: 

由于是分区表,所有这里需要去查询访问的分区的大小:

这里看到访问的分区只有84M,加上code_item,ty_suit_cfg,d_dragon_plan表一共才4288+84=4372M,等于 =559616 个BLOCK。

 

下面我们考虑一种极端的条件下,SQL访问的几张表都走全表扫描,并且走HASH连接。那么此时物理读加逻辑读应该接近 559,616 (这里不考虑TEMP等消耗,不考虑事务一致性等原因,只考虑表的大小),但是整个SQL消耗的物理都为22610,逻辑读是:338,280,770。远远大于SQL访问的表占用的物理大小。所以初步判断在执行计划中存在某个对象被轮询。

 

下面查询访问的分区的行数,这里由于是OLAP系统,无统计信息,只能手动运行SQL来查询,如果有统计信息,可以大概根据统计信息来计算,虽然不是很准确,但是我觉得足够我们判断SQL性能了,何况CBO都是基于统计信息的。

 

这里看到028返回了400W行的数据。

问题定位与SQL改写 
通过上面的信息,可以知道SQL变慢的原因 :

由于PU_BASE_IND.DM_SP_SUB_OFFER_SERV_D_201407 PARTITION(P20140727) 返回一行,就需要去执行所有的标量子查询,虽然在一体机环境这里有first rows,但是所有的标量子查询被轮询的次数达到了4,302,704次,并且子查询的表走的全表扫描,所以出现了逻辑读很高。

 

逻辑读比物理读性能好,并且逻辑读消耗的时间很短,但是过高的逻辑读会带来CPU使用率的增加,RAC环境会导致过多的GC等待,还有可能会影响后来的一些TX,INDEX ITL等等待事件的出现,前不久就曾经遇到一个逻辑读导致GC等待,又引起了TX,INDEX ITL,BBW等待事件,没有多久,业务连接池就满了,最后整个业务受影响。

 

为了减少标量子查询被轮询的次数,这里可以把标量子查询改为外连接。

为了减少SQL长度,下面是去掉SELECT中一些不需要的部分,我们手动测试一下SQL改为外连接与原SQL执行SQL的差异:

SELECT OFFER_SERV_SUM AS N37364,

      LOCAL_CODE LOCAL_CODE,

      AREA_ID AREA_ID,

      DVLP_AREA_ID MG_AREA_ID,

      DVLP_ORG_ID ORG_ID,

         CASE WHEN a.OFFER_SPEC_ID = b.LOCAL_ITEM_CODE THEN 1 ELSE 0 END

      || CASE

            WHEN TO_CHAR (a.OFFER_SPEC_ID) = e.LOCAL_ITEM_CODE THEN 1

            ELSE 0

         END

      || CASE

            WHEN     TO_CHAR (a.OFFER_SPEC_ID)= c.offer_spec_id

                  AND c.bt_code = 1

            THEN

                1

            ELSE

                0

         END

      || CASE WHEN a.OFFER_SPEC_ID = b.LOCAL_ITEM_CODE THEN 1 ELSE 0 END

      || CASE

            WHEN TO_CHAR (a.OFFER_SPEC_ID) = d.OFFER_SPEC_ID THEN 1

            ELSE 0

         END

      || CASE

            WHEN     TO_CHAR (a.OFFER_SPEC_ID)= c.offer_spec_id

                  AND c.bt_code = 2

            THEN

                1

            ELSE

                0

         END

         C_ALL

 FROM PU_BASE_IND.DM_SP_SUB_OFFER_SERV_D_201407 PARTITION (P20140727) A,

      (SELECT DISTINCT LOCAL_ITEM_CODE

         FROM PU_META_DIM.CODE_ITEM

        WHERE PROV_TYPE_ID = 49 AND PROV_ITEM_ID = 64) b,

      (SELECT DISTINCT OFFER_SPEC_ID, bt_code

         FROM PU_META_DIM.D_DRAGON_PLAN

        WHERE     BT_CODE IN (1, 2)

               AND (UPPER (NAME) LIKE '%4S%' ORNAME LIKE '%??%')) c,

      (SELECT DISTINCT OFFER_SPEC_ID FROM PU_META_DIM.TY_SUIT_CFG) d,

      (SELECT DISTINCT LOCAL_ITEM_CODE

         FROM PU_META_DIM.CODE_ITEM A

        WHERE PROV_TYPE_ID = 49 AND PROV_ITEM_ID IN (85, 86, 68, 69, 70, 71)) e

 WHERE    LOCAL_CODE = '028'

      AND a.OFFER_SPEC_ID = b.LOCAL_ITEM_CODE(+)

      AND TO_CHAR (a.OFFER_SPEC_ID) = c.OFFER_SPEC_ID(+)

      AND TO_CHAR (a.OFFER_SPEC_ID) = d.OFFER_SPEC_ID(+)

      AND TO_CHAR (a.OFFER_SPEC_ID) = e.LOCAL_ITEM_CODE(+)

 

执行计划及统计信息:

这里可以看到所有的表都是通过HASH 来做UNIQUE

 这里看到逻辑读是303475,行返回是4,302,704,不知道大家是否还记得在之前我们获取SQL的统计信息的时候,看到SQL每次执行平均换回的行数是4,302,704。 

两个值是一样的,说明我们在此条SQL改写后是等价的。 

这里用到了”此条”,因为如果在连接列有一些空值的情况下得到的结果可以不一样,大家可以测试一下。

性能优化效果对比

下面看看原SQL去掉SELECT不需要的部分执行的信息

SELECT OFFER_SERV_SUM AS N37364,

      LOCAL_CODE LOCAL_CODE,

      AREA_ID AREA_ID,

      DVLP_AREA_ID MG_AREA_ID,

       DVLP_ORG_ID ORG_ID,

        CASE

            WHEN OFFER_SPEC_ID IN

                     (SELECT LOCAL_ITEM_CODE

                        FROMPU_META_DIM.CODE_ITEM

                       WHERE PROV_TYPE_ID = 49AND PROV_ITEM_ID = 64)

            THEN

                1

            ELSE

                0

         END

      || CASE

            WHEN TO_CHAR (OFFER_SPEC_ID) IN

                     (SELECT LOCAL_ITEM_CODE

                        FROMPU_META_DIM.CODE_ITEM A

                       WHERE     PROV_TYPE_ID = 49

                             AND PROV_ITEM_IDIN (85, 86, 68, 69, 70, 71))

            THEN

                1

            ELSE

                0

         END

      || CASE

            WHEN TO_CHAR (OFFER_SPEC_ID) IN

                    (SELECT OFFER_SPEC_ID

                        FROMPU_META_DIM.D_DRAGON_PLAN

                       WHERE     BT_CODE = 1

                             AND (   UPPER (NAME) LIKE '%4S%'

                                  OR NAME LIKE'%??%'))

            THEN

                1

            ELSE

                0

         END

      || CASE

            WHEN TO_CHAR (OFFER_SPEC_ID) IN

                     (SELECT LOCAL_ITEM_CODE

                        FROMPU_META_DIM.CODE_ITEM

                       WHERE PROV_TYPE_ID = 49AND PROV_ITEM_ID = 64)

            THEN

                1

            ELSE

                0

         END

      || CASE

            WHEN TO_CHAR (OFFER_SPEC_ID) IN

                     (SELECT OFFER_SPEC_ID FROMPU_META_DIM.TY_SUIT_CFG)

            THEN

                1

            ELSE

                0

         END

      || CASE

            WHEN TO_CHAR (OFFER_SPEC_ID) IN

                     (SELECT OFFER_SPEC_ID

                        FROM PU_META_DIM.D_DRAGON_PLAN

                       WHERE     BT_CODE = 2

                             AND (   UPPER (NAME) LIKE '%4S%'

                                  OR NAME LIKE'%??%'))

            THEN

                1

            ELSE

                0

         END

         C_ALL

 FROM PU_BASE_IND.DM_SP_SUB_OFFER_SERV_D_201407 PARTITION (P20140727) A

 WHERE LOCAL_CODE = '028'

 

这条语句,运行了30分钟,都没有任何行返回:

可以看到执行了27分钟,才处理1908315,接近1/2的结果集行数。如果要处理完,估计需要60分钟以上。

总结 
下面来总结一下此案例:

1. 在标量子查询中,当主查询返回一行数据时,所有的标量子查询就要执行一次,如果在连接列有索引时,标量子查询在主表返回的行很少的情况下,对性能影响不大,常常出现在OLTP环境,并且连接列一般都有索引;如果在OLAP环境中,看到标量子查询千万要小心,通常,主表返回的行很多,并且子查询中的表通常在连接列上面无索引,导致性能很低下,本案例就是这种情况; 

2. 平均每次执行时消耗的逻辑读、物理读,返回的行平均消耗的逻辑读、物理读,平均返回的行数等信息可以用于初步判断SQL是否存在性能问题

 

遇到这种标量子查询,就得修改SQL,也就意味着业务需要修改代码,像电信运营商这种环境可能要好一点,有专门的开发团队,但是估计提交修改申请、开发修改、业务测试上线,差不多也要1到2个月的时间,如果在一些小的环境,估计开发商都找不到了,就跟谈不上改业务了。

12c新特性改进
对于类似以上的情况,在Oracle Database 12C中,优化器已经可以自动实现等价改写,但是需要注意的在12.1.0.2版本中有BUG,可能导致结果集不准确。

 

在12C中,标量子查询自动改写的功能由隐含参数 _optimizer_unnest_scalar_sq 控制,默认是TRUE,意味着开启,如果遇到BUG或者性能问题,可以更改为FALSE。

文章转自数据和云公众号,原文链接

时间: 2024-09-10 14:12:54

性能为王:SQL标量子查询的优化案例分析的相关文章

SQL为王:oracle标量子查询和表连接改写

小鱼(邓秋爽) 云和恩墨专家,有超过5年超大型数据库专业服务经验,擅长oracle 数据库优化.SQL优化和troubleshooting 编辑手记:如何提高数据的查询效率是每个人都关注的问题,今天让我们来学习如何合理使用标量子查询和表连接方式来提高查询速度吧~ 之前小鱼就听过了标量子查询,不过对于其中的细节理解还是远远不够,借助一部分资料和自己测试对标量子查询做一点简单的分析和介绍. Oracle允许在select子句中包含单行子查询,这个也就是oracle的标量子查询,标量子查询有点类似于外

都是标量子查询惹的祸

都是标量子查询惹的祸 系统又报了一个跑的慢的sql语句,看图就知道这个很恐怖的,已经跑了1天了,还需要跑6个月的时间,   把sql语句拿出来瞅瞅: --201406 XX select SUM(UNPOSTING_AMT_CYC) 分期未结清余额, SUM(greatest(nvl(BAL_TOTAL,0),0)) 余额,COUNT(1),DELQ_LEVEL_CI,city,FQ_TOTAL, CASE WHEN greatest(nvl(BAL_TOTAL,0),0) 500 THEN '

在MySQL中使用子查询和标量子查询的基本操作教程_Mysql

MySQL 子查询子查询是将一个 SELECT 语句的查询结果作为中间结果,供另一个 SQL 语句调用.MySQL 支持 SQL 标准要求的所有子查询格式和操作,也扩展了特有的几种特性. 子查询没有固定的语法,一个子查询的例子如下: SELECT * FROM article WHERE uid IN(SELECT uid FROM user WHERE status=1) 对应的两个数据表如下: article 文章表: user 用户表: 查询返回结果如下所示: 在该例子中,首先通过子查询语

[20140125]关于标量子查询.txt

  最近一致在优化一个垃圾项目,我发现一个奇怪的想象,就是开发很喜欢使用标量子查询,我发现这个东西像传染病一样,一个人使用其 他人也跟着仿效,而不考虑具体的使用场合.还有一些出现在视图里面. 我想通过一些例子来说明情况: @ver BANNER                                                                               CON_ID ------------------------------------------

实例简析SQL嵌套子查询

  实例简析SQL嵌套子查询: 一些初级程序员常常对SQL语法中的子查询,由其对嵌套子查询(子查询中包含一个子查询)的使用比较生疏,本文就此做一个基本讲解,相信新手会有一定收获. 使用子查询的原则 1.一个子查询必须放在圆括号中. 2.将子查询放在比较条件的右边以增加可读性. 子查询不包含 ORDER BY 子句.对一个 SELECT 语句只能用一个 ORDER BY 子句, 并且如果指定了它就必须放在主 SELECT 语句的最后. ORDER BY 子句可以使用,并且在进行 Top-N 分析时

sql server子查询 问题

问题描述 sql server子查询 问题 一张表,现在多加一个字段 f__name ,sql server怎么通过子查询 插入进去f__name 解决方案 子查询的定义是:嵌入到其它SQL语句的select语句,又称为嵌套查询. 每个子查询语句都是一个select语句,所以它都有一个返回值. 这个返回值有以下3种情况: ①单个值 ②一个字段 ③一张表 使用子查询的目的,在于利用它的返回值. 图1 employee表 以下,我就根据它的3个返回值各举一个实例说明: 例①:显示employee表中

[20150727]使用标量子查询小问题.txt

[20150727]使用标量子查询小问题.txt --最近一段时间一直在做优化,仔细看我前面的blog,不主张使用标量子查询,实际上还是有一些小细节要注意. 1.测试环境: SCOTT@test> @ver1 PORT_STRING                    VERSION        BANNER ------------------------------ -------------- ----------------------------------------------

mysql优化系列 DELETE子查询改写优化_Mysql

1.问题描述 朋友遇到一个怪事,一个用子查询的DELETE,执行效率非常低.把DELETE改成SELECT后执行起来却很快,百思不得其解. 下面就是这个用了子查询的DELETE了: [yejr@imysql.com]mydb > EXPLAIN delete from trade_info where id in ( select id from ( select a.id from trade_info a, order_info b, user c where b.buyer = c.id

SQL Server子查询运算学习总结教程

技术准备 数据库版本为SQL Server2008R2,利用微软的一个更简洁的案例库(Northwind)进行分析. 一.独立的子查询方式 所谓的独立的子查询方式,就是说子查询和主查询没有相关性,这样带来的好处就是子查询不依赖于外部查询,所以可以独立外部查询而被评估,形成自己的执行计划执行. 举个例子   SELECT O1.OrderID,O1.Freight FROM Orders O1 WHERE O1.Freight> ( SELECT AVG(O2.Freight) FROM Orde