签到成功

知道了

CNDBA社区CNDBA社区

SQL优化系列--一种逼格相当高的SQL优化方案

2016-12-19 16:58 5946 3 原创
作者: arealman

先来说说需求:生产环境中有一张大表大概有400W+的数据记录。按照业务的需求,需要先对表中的某个字段进行group by分组后,再对记录集进行分页处理。因为可能涉及到别人公司业务机密,不好拿原案例出来分析,所以这里做了一个模拟案例,并且尽量将模型最简单化。

测试案例如下:

create table node(a number,b varchar2(100),c varchar2(10),d date);
insert into node
select rownum as a,
       dbms_random.string('a', 50),
       dbms_random.string('u', 3) as c,
       to_date('2016-01-01', 'yyyy-mm-dd') + dbms_random.value * 360 as d
  from dual
connect by level <= 4000000;  
commit;
create index idx_node_c_a on node(c,a);
create index idx_node_c on node(c);
create index uk_node on node(a);

对T表生成400W条记录,字段A可以理解为表的业务主键,建立了唯一索引,字段C建立普通索引。从上面的生成算法来看,按字段C分组后的每个组平均记录数会有 400W/26/26/26 < 1000 条记录。 业务的需求是按照C字段分组后,取分组中a列最大的那一行数据。或者大家可以理解为获取每个分组里最新的记录,记录的新旧通过a列值大小来衡量。

根据上面的业务描述,我们很容易想到一个方案,将表node进行group分组max(a)获取该分组最大a值,通过a关联node表获取到的记录是符合条件的。然后将得到的记录集分页即可。

方案一 通过group by方式实现

SQL> select *
  2    from (select t2.*,rownum rn
  3            from (select max(a) a, c
  4                    from node
  5                   group by c) t1
  6            left join node t2
  7              on t1.a = t2.a
  8           where rownum <= 10)
  9   where rn >= 0;
已选择10行。
已用时间:  00: 00: 06.44
执行计划
----------------------------------------------------------
Plan hash value: 3600688307
------------------------------------------------------------------------------------------
| Id  | Operation                      | Name    | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |         |    10 |   940 |    37   (0)| 00:00:01 |
|*  1 |  VIEW                          |         |    10 |   940 |    37   (0)| 00:00:01 |
|*  2 |   COUNT STOPKEY                |         |       |       |            |          |
|   3 |    NESTED LOOPS OUTER          |         |    11 |   891 |    37   (0)| 00:00:01 |
|   4 |     VIEW                       |         |    11 |   143 |     4   (0)| 00:00:01 |
|   5 |      HASH GROUP BY             |         | 17548 |   171K|  6003   (6)| 00:01:13 |
|   6 |       TABLE ACCESS FULL        | NODE    |  3001K|    28M|  5737   (2)| 00:01:09 |
|   7 |     TABLE ACCESS BY INDEX ROWID| NODE    |     1 |    68 |     3   (0)| 00:00:01 |
|*  8 |      INDEX RANGE SCAN          | UK_NODE |     1 |       |     2   (0)| 00:00:01 |
------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter("RN">=0)
   2 - filter(ROWNUM<=10)
   8 - access("T1"."A"="T2"."A"(+))
统计信息
----------------------------------------------------------
          1  recursive calls
          0  db block gets
      41764  consistent gets
      24927  physical reads
          0  redo size
       1562  bytes sent via SQL*Net to client
        491  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
         10  rows processed

当然,也可以通过exists方式实现

方案二,exists的方式实现

http://www.cndba.cn/cndba/arealman/article/1643

SQL> select *
  2    from (select rownum rn, t.*
  3            from node t
  4           where not exists (select null
  5                    from node t2
  6                   where t2.c = t.c
  7                     and t.a < t2.a)
  8             and rownum <= 10)
  9   where rn > 0;
已选择10行。
已用时间:  00: 00: 28.57
执行计划
----------------------------------------------------------
Plan hash value: 1216091477
-------------------------------------------------------------------------------------
| Id  | Operation            | Name         | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |              |    10 |   940 |    26   (0)| 00:00:01 |
|*  1 |  VIEW                |              |    10 |   940 |    26   (0)| 00:00:01 |
|*  2 |   COUNT STOPKEY      |              |       |       |            |          |
|   3 |    NESTED LOOPS ANTI |              |    11 |   858 |    26   (0)| 00:00:01 |
|   4 |     TABLE ACCESS FULL| NODE         |    12 |   816 |     2   (0)| 00:00:01 |
|*  5 |     INDEX RANGE SCAN | IDX_NODE_C_A |   144K|  1414K|     2   (0)| 00:00:01 |
-------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter("RN">0)
   2 - filter(ROWNUM<=10)
   5 - access("T2"."C"="T"."C" AND "T"."A"<"T2"."A")
统计信息
----------------------------------------------------------
          1  recursive calls
          0  db block gets
    6088721  consistent gets
      24084  physical reads
          0  redo size
       1562  bytes sent via SQL*Net to client
        491  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
         10  rows processed

还有,也可以使用分析函数实现

方案三,分析函数方式

select *
  from (select rownum rn, t.*
          from (select distinct first_value(a) over(partition by c order by a) as a,
                                first_value(b) over(partition by c order by a) as b,
                                c,
                                first_value(d) over(partition by c order by a) as d
                  from node) t
         where rownum <= 10)
 where rn > 0;


总结一下上面的几种实现方案,除了方案2,其他都用了分组算法,用原生的SQL处理,不可避免都会读取到表的所有数据,或者通过hash group by, 或者使用 sort group by,将数据按分组存放后,再获取分组里面满足条件的记录。 然后再是分页,这两种方案的执行时间都是5s+。唯一一个方案2,没有使用到分组算法,从执行结果来看,却是比其他两种方案更是惨不忍睹,方案2的执行时间用了28s。方案2是从段node的起始块开始,逐条检查每条记录是否满足,每次检查都会产生额外的开销,而满足条件的记录,通常又是在段node的最后,几乎每次都要检查到最后才会得到满足条件的记录,因此实际效果会比方案1,3差很多。


http://www.cndba.cn/cndba/arealman/article/1643

但是,拜托,我只是想要前面满足条件的几十条记录而已,有必火急火燎去检查完所有记录后,然后才给我吐出这区区几十条记录吗。但是看起来又好像没有更好的SQL方案能更加优雅地完成这个工作了,至少我是没有想到更好的处理这个工作的SQL了。好吧,既然原生的SQL方案如此丑陋,那我们完全可以自己编写程序来完成这个工作。那就是下面要说的数据流函数方案与管道函数方案。

我设想PLSQL引擎来处理这个事,应该这样处理才是更优雅的:假如分页条件为10,从段node的起始块开始检索,找到满足分页条件的10个分组记录(运气好的话,可能前面10条记录就可找到10个不同分组),然后通过字段c(索引)去查找这10个分组中每个分组的最大的a列值,然后通过a列值(唯一索引)去获取node表的那条记录,而这条记录,就是我想要的记录。


好吧,来看看我的方案四是怎样完成的:

http://www.cndba.cn/cndba/arealman/article/1643
http://www.cndba.cn/cndba/arealman/article/1643

方案四,数据流函数/管道函数实现


先定义需要用到的集合数据类型:

SQL> create type ty_node is object(a number,b varchar2(200),c varchar2(200),d date);
  2  /
Type created
SQL> create type ty_node_set is table of ty_node;
  2  /
Type created

编写数据流函数

/*------------http://www.cndba.cn/cndba/arealman/article/1643

stopkey_num: 分页记录数
http://www.cndba.cn/cndba/arealman/article/1643

*/

http://www.cndba.cn/cndba/arealman/article/1643

create or replace function get_nodes_by_stream(stopkey_num number) return ty_node_set
is
  cursor c is select * from node;
  c_entity c%rowtype;
  v_ty_node ty_node := ty_node(null,null,null,null);
  v_ty_node_set ty_node_set := ty_node_set();
  v_stopkey_num number := stopkey_num;
  v_len number;
  --v_tmp_node ty_node(null,null,null,null);
  b boolean := false;
begin
  if v_stopkey_num is null or v_stopkey_num<= 0 then
    raise_application_error(-20000,'传入下标参数为空或者下标小于1');
  end if;
  v_ty_node_set.extend();
  open c;
  loop
    fetch c into c_entity;
    exit when c%notfound or v_ty_node_set.last > v_stopkey_num ;
    v_ty_node.a := c_entity.a;
    v_ty_node.b := c_entity.b;
    v_ty_node.c := c_entity.c;
    v_ty_node.d := c_entity.d;
    b := false;
    for i in 1 .. v_ty_node_set.count loop
      if v_ty_node_set(i).c =v_ty_node.c then
        b := true;
        exit;
      end if;
    end loop;
    if b = false then
      select max(a) into v_ty_node.a from node where c = v_ty_node.c; --获取c类型中最大的a值
      v_ty_node_set(v_ty_node_set.last) := v_ty_node;
      --if v_ty_node_set.last < v_stopkey_num then
         v_ty_node_set.extend();
      --end if;
    end if;
  end loop;
  v_ty_node_set.delete(v_stopkey_num +1);
  return v_ty_node_set;
end;

OK,用数据流函数来检索出符合我们要求的数据:

没错,consistent gets只有207, 执行所用的时间小到不在oracle的计量单位以内!


http://www.cndba.cn/cndba/arealman/article/1643

管道函数方式实现http://www.cndba.cn/cndba/arealman/article/1643

编写代码:

create or replace function get_nodes_by_pipe(stopkey_num number) return ty_node_set pipelined
is
  cursor c is select * from node;
  c_entity c%rowtype;
  v_ty_node ty_node := ty_node(null,null,null,null);
  v_ty_node_set ty_node_set := ty_node_set();
  v_stopkey_num number := stopkey_num;
  v_len number;
  --v_tmp_node ty_node(null,null,null,null);
  b boolean := false;
begin
  if v_stopkey_num is null or v_stopkey_num<= 0 then
    raise_application_error(-20000,'传入下标参数为空或者下标小于1');
  end if;
  v_ty_node_set.extend();
  open c;
  loop
    fetch c into c_entity;
    exit when c%notfound or v_ty_node_set.last > v_stopkey_num;
    v_ty_node.a := c_entity.a;
    v_ty_node.b := c_entity.b;
    v_ty_node.c := c_entity.c;
    v_ty_node.d := c_entity.d;
    b := false;
    for i in 1 .. v_ty_node_set.count loop
      if v_ty_node_set(i).c =v_ty_node.c then
        b := true;
        exit;
      end if;
    end loop;
    if b = false then
      select max(a) into v_ty_node.a from node where c = v_ty_node.c;
      v_ty_node_set(v_ty_node_set.last) := v_ty_node;
         v_ty_node_set.extend();
         pipe row(v_ty_node);
    end if;
  end loop;
  return;
end;

用管道函数来检索出符合我们要求的数据:

在这个案例里面,管道函数与数据流函数的效果差别不大。实际上对于要求返回大数据量的需求来说,一般推荐使用管道函数,因为它的工作方式是边执行边返回。跟SQL引擎的工作方式是类似的。当然,这个案例仅仅是个引子,案例的代码也不完善,例如分页功能的实现上只有stopkey,没有start_key。

看到改进后的威力了吧,大家对比前后几种方案的实际效果就知道了。实际应用上,如果我们发现某些SQL无论怎么改写都没有性能的改善,你甚至都怀疑ORACLE能不能支持你心目中理想的算法的时候,不妨使用管道函数、数据流函数的方式,自己写代码解决吧。

http://www.cndba.cn/cndba/arealman/article/1643


版权声明:本文为博主原创文章,未经博主允许不得转载。

管道函数 数据流函数 SQL优化

用户评论
* 以下用户言论只代表其个人观点,不代表CNDBA社区的观点或立场
arealman

arealman

关注
  • 10
    原创
  • 0
    翻译
  • 0
    转载
  • 5
    评论
  • 访问:53900次
  • 积分:51
  • 等级:注册会员
  • 排名:第42名
精华文章
    最新问题
    查看更多+
    热门文章
      热门用户
      推荐用户
        Copyright © 2016 All Rights Reserved. Powered by CNDBA · 皖ICP备2022006297号-1·

        QQ交流群

        注册联系QQ