一次关于SQL子查询的优化

近日,某金融公司的客户抱怨他们已经无法忍受他们的数据仓库系统加载数据的时间了。按理说该公司的大表的数据量也就百万级,根本不应该这么慢,系统肯定有问题。

    通过现场对数据加载日志进行分析,发现该数据仓库在分析层加载的地方耗费大量时间,达到了不可思议的7-8小时,造成加载缓慢的原因看来就是在这个地方了。通过对该部分加载的进一步分析,发现该部分作用是从历史ODS中抽取当日数据为后续的统计报表进行数据准备,由于涉及的表比较少且转化较少,于是系统设计的时候采用的是将insert语句封装到shell里的方式来加载。既然如此,马上迫不及待的找到处理的shell程序,使用编辑器打开shell文件的瞬间,发现了性能问题的根源所在-----shell中的SQL语句逻辑异常复杂,执行效率低下。

既然发现了问题的根源,那么就着手进行修改,修改大致有以下两类:

1.大量使用相同子查询结果集作为主查询的表。

对于这类SQL,由于使用太多的嵌套查询,造成过多全表扫描,于是采用的是引入中间过程表的方式,先将子查询的结果保存为临时表,然后主查询 select from临时表完成相关查询操作。

为了具体说明,使用oracle HR用户下employees表作为演示。

首先建立一张大表

begin

  for i in 1 .. 29876 loop

    insert into t

    value

      select last_name||i,

             email||i,

             phone_number,

             hire_date+trunc(i/10,0),

             job_id,

             salary+i,

             manager_id,

             department_id,

             to_char(i*1000+employee_id)            

        from employees;

  end loop;

end;

先来看看拆分前那种方式的SQL的执行计划

insert into tw value

select a.first_name,a.last_name,b.job_id,a.department_id from

(select last_name, job_id, manager_id,salary

  from t

 where hire_date between to_date('19900101', 'yyyymmdd') and

       to_date('19990101', 'yyyymmdd'))b left join employees a on a.manager_id=b.manager_id

       where a.department_id ='50'

union all

select a.first_name,a.last_name,b.job_id,a.department_id from

(select last_name, job_id, manager_id,salary

  from t

 where hire_date between to_date('19900101', 'yyyymmdd') and

       to_date('19990101', 'yyyymmdd'))b left join employees a on a.manager_id=b.manager_id

       where a.salary >1500

 

bb

接下来,我们根据子查询构建一张临时表

create table tmp  as select last_name, job_id, manager_id,salary

  from t

 where hire_date between to_date('19900101', 'yyyymmdd') and

       to_date('19990101', 'yyyymmdd')

改变SQL逻辑关系后再来看看执行计划

insert into tw value

select a.first_name,a.last_name,b.job_id,a.department_id from

tmp b left join employees a on a.manager_id=b.manager_id

       where a.department_id ='50'

union all

select a.first_name,a.last_name,b.job_id,a.department_id from

tmp b left join employees a on a.manager_id=b.manager_id

       where a.salary >1500

bb

虽然在创建临时表的时候对于大表同样进行了一次全表扫描,但对于多次用到该子查询作为主查询表的情况可以使执行效率大大提高。

2.使用子查询作为主查询查询字段

下面这段是执行特别缓慢的某段SQL的一部分,对敏感信息处理后如下:

insert into a_ls_fac

SELECT trim(a.acct_no),

         a.CUST_NO,

         b.APP_TYP,

         case

           when b.APP_SRC = 'S' then

            'SALSE'

           when b.APP_SRC = 'I' then

            '内部录入'

           when b.APP_SRC = 'E' then

            'excel 内部录入'

         end as APP_SRC,

        (select STORE_NAME

            from ff_merch_store a1

           where a1.store_no = a.store_no

             and to_date('20121102', 'yyyy-mm-dd') between a1.begin_dt and

                 a1.end_dt - 1) as STORE_NAME,

         b.SALES_NO,

        round(b.FIRST_PAY_AMT / decode(b.MERCH_TOTAL_PRICE,0,null,b.MERCH_TOTAL_PRICE), 2)/* round(b.FIRST_PAY_AMT / b.MERCH_TOTAL_PRICE, 2) */as FIRST_PAY_RATIO,

         a.CAMPAIGN_ID,

         (select CAMPAIGN_DESC

            from ff_campaign a1

           where a.CAMPAIGN_ID = a1.CAMPAIGN_ID

             and to_date('20121102', 'yyyy-mm-dd') between a1.begin_dt and

                 a1.end_dt - 1) as CAMPAIGN_NAME,

         a.MERCH_NO,

         (select MERCH_NAME

            from ff_cus_merch a1

           where a1.MERCH_NO = a.MERCH_NO

             and to_date('20121102', 'yyyy-mm-dd') between a1.begin_dt and

                 a1.end_dt - 1) as MERCH_NAME,

         a.INT_BASIS,

         (select int_rate

            from ff_rate_def a1

           where to_date('20121102', 'yyyy-mm-dd') between a1.begin_dt and

                 a1.end_dt - 1

             and c.int_basis = a1.int_basis

             and c.eff_dt = a1.eff_dt) int_rate,

         a.RATE_FLOAT_RATIO,

         b.appv_id,

         (select opr_name

            from ff_sys_opr a1

           where b.APPV_ID = a1.OPR_ID

             and to_date('20121102', 'yyyy-mm-dd') between a1.begin_dt and

                 a1.end_dt - 1) as APPV_NAME,

         (select max(REPAY_DT)

            from ff_repay a1

           where a1.acct_no = trim(a.acct_no)

             and to_date('20121102', 'yyyy-mm-dd') between a1.begin_dt and

                 a1.end_dt - 1) as MATURITY_DT,

         a.CREATE_DT,

         a.AUTH_DT,

         a.AUTH_NO,

         a.PERI_AMT,

         a.OVER_PRD,

         (select count(*)

            from ff_repay a1

           where trim(a1.ACCT_NO) = trim(a.acct_no)

             and a1.SETTLE_FLG = 'Y'

             and to_date('20121102', 'yyyy-mm-dd') between a1.begin_dt and

                 a1.end_dt - 1) as SETTLE_PRD_CNT,

         a.CUR_BUCKET,

         a.LAST_REPAY_AMT,

         (select max(REPAY_DT)

            from fud_LN_OD_DTL a1

           where trim(a1.ACCT_NO) = trim(a.acct_no)

             and to_date('20121102', 'yyyy-mm-dd') between a1.begin_dt and

                 a1.end_dt - 1) as last_OD_dt,

         a.MAX_BUCKET,

         a.MAX_OD_AMT,

         (select nvl(to_date('20121102', 'yyyy-mm-dd') -

                     to_date(min(REPAY_DT), 'yyyy-mm-dd')+1,

                     0)

            from ln_OD_dtl a1

           where trim(a1.ACCT_NO) = trim(a.acct_no)

             and to_date('20121102', 'yyyy-mm-dd') between a1.begin_dt and

                 a1.end_dt - 1) as OD_DAYS,

         (select min(REPAY_DT)

            from ln_OD_dtl a1

           where trim(a1.ACCT_NO) = trim(a.acct_no)

             and to_date('20121102', 'yyyy-mm-dd') between a1.begin_dt and

                 a1.end_dt - 1) as LONG_OD_DT,

         a.AUTOREPAY_TYP,

         a.ADV_REPAY_CNT,

         to_date('20121102', 'yyyy-mm-dd') data_dt

    FROM ff_contract a

    left join ff_app_list b on trim(a.acct_no) = b.app_no and

    to_date('20121102', 'yyyy-mm-dd') between b.begin_dt and

         b.end_dt - 1

    left join rate_t c on trim(a.acct_no) = trim(c.acct_no)

   where to_date('20121102', 'yyyy-mm-dd') between a.begin_dt and

         a.end_dt - 1

 

可以直观的看到,里面有很多子查询作为主查询查询字段的情况,由于子查询关联的表也为相关事实表并非维表,故在实际的执行计划中,每个子查询的COST大致在200000600000左右,由于这类方式在该SQL中很多,故该条SQLCOST达到了惊人的数量级。在对每个子查询的作用进一步分析后,决定修改此SQL逻辑方式,

以插入STORE_NAME字段为例:

(select STORE_NAME

            from ff_merch_store a1

           where a1.store_no = a.store_no

             and to_date('20121102', 'yyyy-mm-dd') between a1.begin_dt and

                 a1.end_dt - 1) as STORE_NAME

该子查询目的是在主查询执行时关联相关的店铺名称以方便构造便于报表展现的宽表,于是首先构建一张维表dim_store,包含所有店铺的store_nostore_name字段,子查询中的表记录条数一下从几十万条降低至几百条,同时修改INSERT语句,在插入数据的时候仍然插入store_no到目标表,insert 完成后使用update更新该字段

update a_ls_fac a set STORE_NAME=

(select store_name

         from  dim_store e

        where e.store_no=a.store_no)

 where a.data_dt=to_date('20121102','yyyymmdd')

采用此方式完成所有这类子查询的修改后,SQL的执行效率得到了大大的提高。

至此,整个数据仓库的调优已基本达到预期要求,分析层的加载时间由以前的9小时缩短到至1小时左右,系统性能得到极大提高。事后了解,该部分SQL由客户公司的数据分析部门编写,在进行SQL开发时由于测试环境数据量很小,故根本没考虑SQL的执行效率。

最后,对于子查询的使用,我觉得应该注意以下方面:

1.避免相关子查询
一个列的标签同时在主查询和where子句中的查询中出现,那么很可能当主查询中的列值改变之后,子查询必须重新查询一次。查询嵌套层次越多,效率越低,因此应当尽量避免子查询。如果子查询不可避免,那么要在子查询中过滤掉尽可能多的行。

2. 使用临时表加速查询
把表的一个子集进行排序并创建临时表,有时能加速查询。它有助于避免多重排序操作,而且在其他方面还能简化优化器的工作。临时表中的行要比主表中的行少,而且物理顺序就是所要求的顺序,减少了磁盘I/O,所以查询工作量可以得到大幅减少。
注意:临时表创建后不会反映主表的修改。在主表中数据频繁修改的情况下,注意不要丢失数据

fj.png1.jpg

fj.png2.jpg

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/21609733/viewspace-748319/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/21609733/viewspace-748319/

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值