【SQL开发实战技巧】系列(二十二):数仓报表场景☞ 从分析函数效率一定快吗聊一聊结果集分页和隔行抽样实现方式

简介: 怎样对SQL查询结果集分页比较好、平时你用分析函数优化传统查询,所以你会不会认为分析函数一定比传统查询效率高?一个实验告诉你答案、我想对数据进行隔行抽样应该怎么实现?【SQL开发实战技巧】这一系列博主当作复习旧知识来进行写作,毕竟SQL开发在数据分析场景非常重要且基础,面试也会经常问SQL开发和调优经验,相信当我写完这一系列文章,也能再有所收获,未来面对SQL面试也能游刃有余~。分析查询的一个小建议,可能大家平时为了方便,用row_number做分页的比较多,但是在有些场景,这个效率真的挺低。

前言

本篇文章讲解的主要内容是:怎样对SQL查询结果集分页比较好、平时你用分析函数优化传统查询,所以你会不会认为分析函数一定比传统查询效率高?一个实验告诉你答案、我想对数据进行隔行抽样应该怎么实现?
【SQL开发实战技巧】这一系列博主当作复习旧知识来进行写作,毕竟SQL开发在数据分析场景非常重要且基础,面试也会经常问SQL开发和调优经验,相信当我写完这一系列文章,也能再有所收获,未来面对SQL面试也能游刃有余~。


一、怎样对SQL查询结果集分页比较好

工作中我们为了便于查询网页中的数据,经常会对从数据库查询到的结果集进行分页显示。
比如我现在有一个需求:
要求员工表(EMP的数据)按工资排序后一次只显示5行数据,下次再显示接下来的5行。
下面以第二页数据(6到10行)为例进行分页。
前面已讲过,要先排序,然后在外层才能生成正确的序号:

SQL> with t as/*先排序*/
  2   (select sal, ename from emp where sal is not null order by sal),
  3  t1 as/*取得排序后的序号,并过滤掉10行以后的数据*/
  4   (select rownum as rn, sal, ename from t where rownum <= 10)
        /*根据前面生成的序号过滤掉6行以前的数据*/
  5  select * from t1 where rn >= 6
  6  ;

        RN       SAL ENAME
---------- --------- ----------
         6   1300.00 MILLER
         7   1500.00 TURNER
         8   1600.00 ALLEN
         9   2450.00 CLARK
        10   2850.00 BLAKE

SQL> 

可能有朋友会感觉我这么嵌套的有点麻烦,我列举一下你们想的:

  1. 为什么不直接在内层应用条件WHERE rownum<=10呢?

下面对比一下rownum的结果。

SQL> select rownum rnn,aa.*
  2  from (
  3  select rownum as rn ,sal, ename from emp where deptno=20 and sal is not null
  4  order by sal
  5  ) aa;

       RNN         RN       SAL ENAME
---------- ---------- --------- ----------
         1          1    800.00 SMITH
         2          4   1100.00 ADAMS
         3          2   2975.00 JONES
         4          5   3000.00 FORD
         5          3   3000.00 SCOTT

SQL> 

可以看到,内层直接生成的rownum(rn)与sal的顺序不一样,要想得到正确的顺序就要先排序后取序号。

  1. 为什么不直接用rownum<=10 and rownum>=6,而要分开写呢?

下面来看一下。

 SQL> select * from emp where rownum>=6 and rownum <=10;

EMPNO ENAME      JOB         MGR HIREDATE          SAL      COMM DEPTNO
----- ---------- --------- ----- ----------- --------- --------- ------

SQL> 

如前面博客所述,因为rownum是一个伪列,需要取出数据后,rownum才会有值,在执行where rownum>=6时,因为始终没取前10条数据出来,所以这个条件就查询不到数据,需要先在子查询中取出数据,然后外层用WHERE rn >=6来过滤。

你也可以先用row_number()生成序号,再过滤,这样就只需要嵌套一次。

SQL> 
SQL> select aa.*
  2  from (
  3  select row_number()over(order by sal) as rn ,sal, ename from emp where sal is not null
  4  order by sal
  5  ) aa
  6  where rn >=6 and rn<=10;

        RN       SAL ENAME
---------- --------- ----------
         6   1300.00 MILLER
         7   1500.00 TURNER
         8   1600.00 ALLEN
         9   2450.00 CLARK
        10   2850.00 BLAKE

SQL> 

这个语句比较简单,但因为分页语句的特殊性,在调用PLAN时可能会受到分析函数的影响,有些索引或PLAN(如:first_rows)不能用。所以,在此建议大家使用第一种分页方式,把第一种分页方式当作模板,然后套用。

二、分析函数的使用限制及Merge用对场景对性能提升非常大

现有数据如下:

CREATE  TABLE hotel(floor_nbr,room_nbr)AS
SELECT  1,100 FROM dual UNION ALL
SELECT  1,100 FROM dual UNION ALL
SELECT  2,100 FROM dual UNION ALL
SELECT  2,100 FROM dual UNION ALL
SELECT  3,100 FROM dual;

现在有个需求:将floor_nbr编号更新为类似:101、102、201、202的数据。
我们可以用row_number重新生成编号,或许马上会有读者想到UPDATE语句。让我们来执行一下。

SQL> update hotel set room_nbr =(floor_nbr*100+row_number()over(partition by floor_nbr order by floor_nbr));
update hotel set room_nbr =(floor_nbr*100+row_number()over(partition by floor_nbr order by floor_nbr))

ORA-30483: window  函数在此禁用

SQL> 

有人想用“可更新VIEW",语句如下:

SQL> update (
  2  select rowid as rid,room_nbr,(floor_nbr*100+row_number()over(partition by floor_nbr order by floor_nbr)) as new_room_nbr
  3  from hotel
  4  )
  5  set room_nbr=new_room_nbr;
update (
select rowid as rid,room_nbr,(floor_nbr*100+row_number()over(partition by floor_nbr order by floor_nbr)) as new_room_nbr
from hotel
)
set room_nbr=new_room_nbr

ORA-01732: 此视图的数据操纵操作非法

这些方法都不可靠,当然,把UPDATE再嵌套一层也可以:

SQL> update hotel a
  2     set room_nbr =
  3         (select new_room_nbr
  4            from (select rowid as rid,
  5                         room_nbr,
  6                         (floor_nbr * 100 + row_number()
  7                          over(partition by floor_nbr order by floor_nbr)) as new_room_nbr
  8                    from hotel) bb
  9           where a.rowid = bb.rid);

5 rows updated


SQL> select * from hotel;

 FLOOR_NBR   ROOM_NBR
---------- ----------
         1        101
         1        102
         2        201
         2        202
         3        301

SQL> rollback;

Rollback complete


SQL> 

执行计划如下:

 Plan Hash Value  : 4147990906 

-------------------------------------------------------------------------
| Id  | Operation              | Name  | Rows | Bytes | Cost | Time     |
-------------------------------------------------------------------------
|   0 | UPDATE STATEMENT       |       |    5 |    30 |   28 | 00:00:01 |
|   1 |   UPDATE               | HOTEL |      |       |      |          |
|   2 |    TABLE ACCESS FULL   | HOTEL |    5 |    30 |    3 | 00:00:01 |
| * 3 |    VIEW                |       |    5 |   125 |    4 | 00:00:01 |
|   4 |     WINDOW SORT        |       |    5 |    30 |    4 | 00:00:01 |
|   5 |      TABLE ACCESS FULL | HOTEL |    5 |    30 |    3 | 00:00:01 |
-------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 3 - filter("BB"."RID"=:B1)

这种方法虽然能成功,但由执行计划可以看到,子查询执行了5次,对HOTEL有5次全表扫描。那么如果表更大,是不是更慢?博主之前就经常遇到有同事写出类似update SQL,上线的时候可能跑一晚上也跑不出来。
另一个方法是用MERGE语句:

SQL> merge into hotel a
  2  using (select rowid as rid,
  3                room_nbr,
  4                (floor_nbr * 100 + row_number()
  5                 over(partition by floor_nbr order by floor_nbr)) as new_room_nbr
  6           from hotel) bb
  7  on (a.rowid = bb.rid)
  8  when matched then update set a.room_nbr = bb.new_room_nbr;

5 rows merged

SQL> select * from hotel;

 FLOOR_NBR   ROOM_NBR
---------- ----------
         1        101
         1        102
         2        201
         2        202
         3        301

SQL> 

执行计划如下:

 Plan Hash Value  : 1282884214 

---------------------------------------------------------------------------
| Id  | Operation                | Name  | Rows | Bytes | Cost | Time     |
---------------------------------------------------------------------------
|   0 | MERGE STATEMENT          |       |    5 |    80 |    7 | 00:00:01 |
|   1 |   MERGE                  | HOTEL |      |       |      |          |
|   2 |    VIEW                  |       |      |       |      |          |
| * 3 |     HASH JOIN            |       |    5 |   220 |    7 | 00:00:01 |
|   4 |      VIEW                |       |    5 |   190 |    4 | 00:00:01 |
|   5 |       WINDOW SORT        |       |    5 |    30 |    4 | 00:00:01 |
|   6 |        TABLE ACCESS FULL | HOTEL |    5 |    30 |    3 | 00:00:01 |
|   7 |      TABLE ACCESS FULL   | HOTEL |    5 |    30 |    3 | 00:00:01 |
---------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 3 - access("A".ROWID="BB"."RID")

通过PLAN可以看到,使用MERGE子查询只对hotel访问了一次,效率提高了很多。

三、隔行抽样

有时为了取样而不是查看所有的数据,要对数据进行抽样,前面介绍过选取随机行,这里将介绍隔行返回。
现在有下面需求:对员工表中的数据每隔一行返回一个员工。

为了实现这个目标,用求余函数mod即可,我们看一下mod的结果。

SQL> select *
  2    from (select mod(rn, 2) as md, aa.*
  3            from (select rownum as rn, a.*
  4                    from (select ename, empno, sal from emp order by ename) a) aa)
  5   where md = 0;

        MD         RN ENAME      EMPNO       SAL
---------- ---------- ---------- ----- ---------
         0          2 ALLEN       7499   1600.00
         0          4 CLARK       7782   2450.00
         0          6 JAMES       7900    950.00
         0          8 KING        7839   5000.00
         0         10 MILLER      7934   1300.00
         0         12 SMITH       7369    800.00
         0         14 WARD        7521   1250.00

7 rows selected

通过这个函数,想间隔几行返回都可以实现。


总结

本篇博客主要给大家介绍了:

  • 分析查询的一个小建议,可能大家平时为了方便,用row_number做分页的比较多,但是在有些场景,这个效率真的挺低。
  • 我身边很多人都会认为分析函数比传统写法简单并且效率高,无论什么场景都一股脑用分析函数,但是很多时候都会出现效率低下的问题,究其原因类似第二种案例,也建议大家在实际开发过程中,注意下。
  • 最后介绍的这个隔行抽样是某网友问我的一个某国企的面试题,也放上来啦。
相关实践学习
数据库实验室挑战任务-初级任务
本场景介绍如何开通属于你的免费云数据库,在RDS-MySQL中完成对学生成绩的详情查询,执行指定类型SQL。
阿里云云原生数据仓库AnalyticDB MySQL版 使用教程
云原生数据仓库AnalyticDB MySQL版是一种支持高并发低延时查询的新一代云原生数据仓库,高度兼容MySQL协议以及SQL:92、SQL:99、SQL:2003标准,可以对海量数据进行即时的多维分析透视和业务探索,快速构建企业云上数据仓库。 了解产品 https://www.aliyun.com/product/ApsaraDB/ads
相关文章
|
1月前
|
SQL 存储 大数据
数据仓库(10)数仓拉链表开发实例
拉链表是数据仓库中特别重要的一种方式,它可以保留数据历史变化的过程,这里分享一下拉链表具体的开发过程。 维护历史状态,以及最新状态数据的一种表,拉链表根据拉链粒度的不同,实际上相当于快照,只不过做了优化,去除了一部分不变的记录,通过拉链表可以很方便的还原出拉链时点的客户记录。
166 13
数据仓库(10)数仓拉链表开发实例
|
2月前
|
SQL 存储 数据管理
阿里云视觉智能开放平台的逻辑数仓基于统一的SQL语法
【2月更文挑战第9天】阿里云视觉智能开放平台的逻辑数仓基于统一的SQL语法
52 2
|
16天前
|
SQL 数据库 索引
SQL索引失效原因分析与解决方案
SQL索引失效原因分析与解决方案
22 0
|
23天前
|
SQL 关系型数据库 MySQL
【MySQL】慢SQL分析流程
【4月更文挑战第1天】【MySQL】慢SQL分析流程
|
1月前
|
SQL 关系型数据库 API
Star 4.7k!高效SQL Parser!纯Python开发!自称目前最快的纯Python SQL解析器!
Star 4.7k!高效SQL Parser!纯Python开发!自称目前最快的纯Python SQL解析器!
|
1月前
|
存储 JSON 数据处理
数仓学习---数仓开发之DWD层
数仓学习---数仓开发之DWD
137 6
数仓学习---数仓开发之DWD层
|
2月前
|
SQL 消息中间件 Kafka
flink问题之做实时数仓sql保证分topic区有序如何解决
Apache Flink是由Apache软件基金会开发的开源流处理框架,其核心是用Java和Scala编写的分布式流数据流引擎。本合集提供有关Apache Flink相关技术、使用技巧和最佳实践的资源。
706 3
|
2月前
|
SQL 关系型数据库 MySQL
10个SQL中常用的分析技巧
10个SQL中常用的分析技巧
|
2月前
|
SQL 存储 数据库
SQL数据开发
SQL数据开发
|
2月前
|
数据挖掘 数据库
数仓学习---数仓开发之DIM层
数仓学习---数仓开发之DIM层 维度建模、维度表介绍、
152 1