SQL高级功能:窗口函数、存储过程及经典排名问题、topN问题、WITH 子句

转载于:SQL高级功能:窗口函数、存储过程及经典排名问题、topN问题等 2323- 知乎

本篇文章主要是以下内容:

1.窗口函数:

partition by窗口函数 和 group by分组的区别:

partition by关键字是分析性函数的一部分,它和聚合函数(如group by)不同的地方在于它能返回一个分组中的多条记录,而聚合函数一般只有一条反映统计值的记录。
partition by用于给结果集分组,如果没有指定那么它把整个结果集作为一个分组。
partition by与group by不同之处在于前者返回的是分组里的每一条数据,并且可以对分组数据进行排序操作。后者只能返回聚合之后的组的数据统计值的记录。

partition by相比较于group by,能够在保留全部数据的基础上,只对其中某些字段做分组排序(类似excel中的操作),而group by则只保留参与分组的字段和聚合函数的结果

分区函数Partition By的用法_partitionby_惊寂123的博客-CSDN博客

row_number() over(partition by … order by …)
rank() over(partition by … order by …)
dense_rank() over(partition by … order by …)
count() over(partition by … order by …) 求分组后的总数
max() over(partition by … order by …) 求分组后的最大值
min() over(partition by … order by …) 求分组后的最小值
sum() over(partition by … order by …) 求分组后的总和
avg() over(partition by … order by …) 求分组后的平均值
first_value() over(partition by … order by …) 求分组后的第一个值
last_value() over(partition by … order by …) 求分组后的最后一个值
lag() over(partition by … order by …) 取出分组后前n行数据
lead() over(partition by … order by …) 取出分组后后n行数据

1)窗口函数的基本语法如下:

<窗口函数> over ( partition by<用于分组的列名>
order by <用于排序的列名>)

2)以上语法中<窗口函数>的位置,可以放置以下函数:

窗口函数是对where或者group by子句处理后的结果进行处理,所以窗口函数原则上只能写上select子句中。

2.如何使用窗口函数?

1)专用窗口函数rank。

若要在每个班级内按成绩排名,则sql语句则为:

select *,
rank() over (partition by 班级
order by 成绩 desc) as ranking from 班级表;

以上sql语句中的select子句,rank是排序的函数,要求是“每个班级内按成绩排名”。这句话分为两部分理解

a)每个班级内:按班级分组

partition by用来对表分组,在这个例子中,需要按“班级”进行分组(partition by 班级)

b)按成绩排名:

order by子句的功能是对分组后的结果进行排序,默认按升序排列,但是在本例中用了desc,表示按降序排序。

2)窗口函数已经具备了前几节中group by和order by子句的分组和排序的功能,但仍要用窗口函数是因为,group by分组汇总后改变了表的行数,一行只有一个类别,而partition by和rank函数不会减少原表中的行数。

-- group by分组汇总改变行数
select 班级,count(学号)
from 班级表 
group by 班级
order by 班级;

-- partition by分组汇总行数不变
select 学号,
count(学号) over (partition by 班级
order by 班级) as current_count from 班级表;

“窗口函数”之所以叫“窗口”函数,是因为partition by分组后的结果就称为“窗口”,这里的窗口是表示“范围”的意思。

3)窗口函数主要有以下功能:

a.同时具备分组和排序的功能

b.不减少原表的行数

c.语法如下:

<窗口函数> over ( partition by<用于分组的列名>
order by <用于排序的列名>)

3.其他专用窗口函数

1)专用窗口函数rank,dense_rank,row_number有什么区别?

-- 专用窗口函数rank,dense_rank(),row_number的区别
select *,
rank() over (order by 成绩 desc) as ranking,
dense_rank() over (order by 成绩 desc) as dese_rank,
row_number() over (order by 成绩 desc) as row_num from 班级表;

从以上结果来看:

rank()函数:这个例子中是5位,5位,5位,8位,也就是如果有并列名次的行,会占用下一个名次的位置。比如正常排名是1,2,3,4,但是现在前3名是并列的名次,所以结果是:1,1,1,4.

dense_rank()函数:这个例子中是5位,5位,5位,6位,也就是如果有并列的名次,它不会占用下一个名次的位置,比如比如正常排名是1,2,3,4,但是现在前3名是并列的名次,所以结果是:1,1,1,2.

row_number()函数:这个例子中是5位,6位,7位,8位,也就是不考虑并列的情况,比如前3名是并列的名次,排名是正常的1,2,3,4.

最后需要注意的是,以上三个专用窗口函数,函数后面的括号不需要任何参数,保持括号()为空即可。

案例1:面试经典排名问题

当涉及到排名问题时,可以使用窗口函数,但使用窗口函数前,要注意区份rank()函数、dense_rank()函数以及row_number()函数的区别。

例子:编写一个sql查询来实现分数排名,若两个分数相同,则分数排名相同。请注意:平分后下一个名字应该是下一个连续的整数值,换句话说就是,名次之间不该有“间隔”。因此考虑用dense_rank()函数。

sql查询语句应为:

select *,
dense_rank() over (order by 成绩 desc) as dens_rank
from 班级表;

案例2:面试经典topN问题

工作中常会遇到这样的业务问题:找出每个国家中进口最多的产品是哪个?找出每个国家进口贸易前5的商品是什么?诸如此类问题,都是常见的:分组取每组最大值,最小值,每组最大的N条(top N)记录。

面对这类问题,我们将通过以下例子给出答案?

成绩表里包含了学生的学号,课程号(学生选修课程的课程号),成绩(学生选修该课程取得的成绩).

1)分组取每组最大值:按课程号分组取成绩最大值所在行的数据。

(由于分组group by和汇总函数得到的是每组的一个值(最大值,最小值或平均值),而无法得到对应那一行的所有数据,所以group by不可用)。因此我们可以使用关联子查询来实现:

select * from score as a 
where 成绩=(select max(成绩) from score as b
where a.课程号=b.课程号);

以上查询结果中课程号0001有2行数据是因为最大成绩80有2个。

2)分组取每组最小值:按课程号分组取成绩最小值所在行的数据。

select * from score as a 
where 成绩=(select min(成绩) from score as b
where a.课程号=b.课程号);

3)每组最大的N条记录:

案例:现有“进口贸易表”,记录了每个国家各商品的进口额,表内容如下。问题:查找每个国家进口额最大的2个商品。

解题思路:

1.看到问题中要查找“每个”国家进口额最高的商品。当题目中出现“每个”时,首先要想到分组。这里指每个国家,所以要按国家来分组。

2.当表按国家分组后,按进口额降序排列,排在最前面2个就是我们想要查找的进口额最大的2个商品。

3.分组排序后,不能减少原表的行数,所以要用窗口函数。

4.对比各窗口函数,为不受并列进口额的影响,于是决定用row_number。

解题步骤:

步骤一:按国家分组(partition by 国家)、并按进口额降序排列(order by 进口额 desc),套入窗口函数后,sql语句为:

select *,
row_number() over (partition by 国家
order by 进口额 desc) as ranking from 进口贸易表;

步骤二:上表中红色框框内的数据,就是每个国家进口额最大的两个商品,也就是题目的解。要想得到只有这些解的答案,只需要提取出“ranking“值小于等于2的数据即可。这时候只需要在之前的sql语句中加入条件子句where就可以了。但是这样就会报错,原因是sql的书写顺序和运行顺序不一致,在运行过程中,select语句是最后运行的。

因此不能将sql语句写成如下:

select *,
row_number() over (partition by 国家
order by 进口额 desc) as ranking from 进口贸易表
where rangking<=2;

以上出错原因就是因为我们以为运行顺序是按书写顺序来运行的,这样是不对的,所以运行sql子句的时候,就会出错。运行where ranking的时候,select子句还没有运行,ranking列还未出现。

步骤三:这时候只能采用子查询,将第一步得到的查询结果作为一个新表,最后再运行where子句。

select * from
(select *, row_number() over (partition by 国家
order by 进口额 desc) as ranking from 进口贸易表) as a
where rangking<=2;

举一反三:

经典topN问题:每组最大的N条记录。这类问题既涉及分组,又涉及排序,这时候要用窗口函数来实现,这时候只需要将where子句中的2改成N即可。

select * from
(select *, row_number() over (partition by 要分组的列名
order by 要排序的列名 desc) as ranking from 表名) as a
where rangking<=N;

4.聚合函数作为窗口函数

聚合函数作为窗口函数和专用窗口函数用法相同,只需要把聚合函数写在窗口函数的位置即可,但聚合函数括号里不能为空,必须写好聚合的列名。

select *,
sum(成绩) over ( partition by 课程号 order by 学号) as current_sum,
avg(成绩) over (partition by 课程号 order by 学号) as current_avg,
max(成绩) over (partition by 课程号 order by 学号) as current_max,
min(成绩) over (partition by 课程号 order by 学号) as current_min,
count(成绩) over (partition by 课程号 order by 学号) as current_count 
from score;

如上图,聚合函数sum在窗口函数中,对自身记录以及位于自身以上的数据进行求和,如课程号0002对应的学号0002后面的sum结果就是课程号0002中学号为0001和0002对应的成绩之和,课程号0002对应的学号0003后面的sum结果就是课程号0002中学号0001、0002和0003对应的成绩之和。除此之外,avg()、max()、min()等聚合函数作为窗口函数时,结果都与sum()函数类似。

这样使用窗口函数的用处是:聚合函数作为窗口函数,可以在每一行的数据里直观看到,截止到本行数据,统计数据有多少,最大值、最小值是多少等,从而可以看出每一行数据,对整体数据的影响。

举例:累计求和问题

下表为确诊人数表,包含日期和该日期对应的新增确诊人数,

按照日期进行升序排列,查找日期,确诊人数以及对应的累计确诊人数。

select 日期,确诊人数,
sum(确诊人数) over(order by 日期) as 累计确诊人数 
from 确诊人数表;

案例:如何在每个组里比较

题目:现有进口贸易表,记录了每个国家各商品的进口额,表内容如下:

问题:查找单个商品进口额高于该商品平均进口额的国家名单。

解题思路:

1.查找单个商品高于该商品平均进口额,也就是要在每个商品里比较,这就涉及到分组,而sql中有分组功能的就:group by和窗口函数partition by。

2.使用聚合函数avg()求出每个商品的平均进口额后,找出进口额大于平均进口额的数据。并且要求分组后不减少原表的行数。

3.由于group by分组汇总后会改变表的行数,一行只有一个类别,而partition by不会减少原表行数,因此用partition by。

解题步骤:

1.将avg()作为窗口函数,将每个商品的平均进口额求出。

select *,
avg(进口额) over (partition by 商品编码 ) as 平均进口额 
from 进口贸易表;

2.在第1步的基础上,筛选出大于平均进口额的数据即可。这时,就需要在上一步的sql语句中加入条件子句where即可。在写sql子句前,要注意sql的书写顺序与运行顺序。

select *
from (select *,
avg(进口额) over (partition by 商品编码 ) as 平均进口额 
from 进口贸易表) as b 
where 进口额>平均进口额;

举一反三:

查找每个组里大于平均值的数据,可以有两种方法:

1)使用以上的窗口函数

2)使用关联子查询。

5、窗口函数的移动平均

select *,
avg(成绩) over (order by 学号 rows 2 preceding) as current_avg 
from score;

以上窗口函数,用了rows和preceding这两个关键字,是“之前~行“的意思,在上面也就是之前2行的意思,也就是得到的结果是自身记录及前2行的平均。

例如学号0002、课程号0002成绩60的结果为:学号0001课程号0002和学号0001课程号0003以及学号0002、课程号0002对应的三个成绩的平均值。也就是学号0002课程号0002这位以及其前两行同学的平均成绩。

想要计算当前行与前n行(共n+1行)的平均时,只有调整rows 与preceding中间的数字即可。这样使用窗口函数注意是可以通过preceding关键字调整作用范围,在以下的场景中非常适用:

在公司业绩名单排名中,可以通过移动平均,直观地查看与相邻名次业绩的平均、求和等统计数据。

6.窗口函数总结

1)注意事项:

partition 子句可以省略,省略时就是不指定分组,且窗口函数原则上只能写在select子句中

2)窗口函数语法:

select *
<窗口函数> over (partition by <分组的列名> order by <排序的列名>)
as <自己定义的列名> from 从哪张表中查找;

其中窗口函数的位置可以放:

a.专用窗口函数:rank()、dense_rank()、row_number等。

b.聚合函数:sum()、avg()、max()、min()等。

3)窗口函数的功能:

a.同时具备分组partition by和排序order by的功能。

b.不减少原表的行数,所以经常用来在每组内排名。

4)窗口函数使用场景:

7、触发器

触发器概念:触发器是一种特殊的存储过程,它在试图更改触发器所保护的数据时自动执行。

触发器与存储过程的异同
相同点:1. 触发器是一种特殊的存储过程,触发器和存储过程一样是一个能够完成特定功能、存储在数据库服务器上的SQL片段。
不同点:2. 存储器调用时需要调用SQL片段,而触发器不需要调用,当对数据库表中的数据执行DML操作时自动触发这个SQL片段的执行,无需手动调用。

MySQL的触发器_mysql触发器_莱维贝贝、的博客-CSDN博客

8.存储过程。

MySQL中的存储过程(详细篇)_mysql存储过程学习_普通网友的博客-CSDN博客(学习该博客)

1)在工作中经常遇到重复性的工作,这时候就可以把常用的sql写好存储起来,这个过程就是存储过程。这样下次遇到同样的问题,就可以直接使用存储过程了,这样就可以极大地提高工作效率。

2)如何使用存储过程?

使用存储过程需要先定义存储过程,然后是使用已经定义好的存储过程。

a.无参数的存储过程。

定义存储过程的语法形式:

create procedure 存储过程名称() begin <sql语句> ;end;

语法中的begin……end用于表示sql语句的开始和结束。语法中的sql语句就是重复的sql语句。

举个例子:查找进口贸易表中的国家名称。

sql语句就是:

select 国家from 进口贸易表;

把这个sql语句放入存储过程的语法里,并给这个存储过程起名叫a_trade1:

create procedure a_trade1 () 
begin select 国家from 进口贸易表;end;

在navicat-查询中运行后,建立的存储过程就会出现在以上位置,这样下次就可以用以下的sql语句直接使用了,就不用另外再写一次sql语句了。

call 存储过程名();

如:call a_trade1 ();

b.有参数的存储过程:

a.中的存储过程名称后是(),括号里没有参数,当括号有参数时,就是以下的语法:

create procedure 存储过程名称(参数1,参数2,…) begin <sql语句>;end;

例如:要在进口贸易表中查找指定商品编码的国家有哪些?如果指定商品编码为88,那么sql语句是:

select 国家 from 进口贸易表 where 商品编码=88;

在实际工作中,有时候并不能一次就能指定国家是哪个,有时候业务需要指定国家为中国,有时候需要指定成美国,这个时候就需要参数,来灵活应对这种情况。这时候把sql放入存储过程就是:

create procedure getNum2(num varchar(100))
begin select 国家 from 进口贸易表 where 商品编码=num;end;

其中getNum2是存储过程的名称,后面括号里面的num varchar(100)是参数,参数由两部分组成,参数名称是num;参数类型是varchar(100);这里表示字符串类型。存储过程里面的sql语句(where 商品编码=num)使用了这个参数num,这样在使用存储过程时,给定参数值就可以灵活地运用了。

比如现在要查商品编码是89的国家名称,那么就可以在使用存储过程的参数来实现了,也就是下面括号里的89.

call getNum2(89);

c.默认参数的存储过程*

前面的存储过程名称后是(参数1,参数2,…),括号里面只包含了参数的类型和名称,方便调用。其实存储过程还包含了一种情况,就是存在默认参数的情况。

in输入参数:

参数初始值在存储过程前被指定为默认值,在存储过程中修改该参数的值不能被返回。

set @num=0;-- 初始化参数
-- 初始化存储过程
create procedure in1(in num int)
begin
select num;
set num=1;
select num;
end;
-- in参数调用
call in1(@num);
select num;

out输出参数:

参数初始值为空,该值可在存储过程内部被改变,并可返回。

set @num=0;-- 初始化参数
-- 初始化存储过程
create procedure out1(out num int)
begin
select num;
set num=1;
select num;
end;
-- out参数调用
call out1(@num);
select num;

inout输入输出参数:

参数初始值在存储过程前被指定为默认值,并且可在存储过程中被改变和在调用完毕后可被返回

set @num=0;-- 初始化参数
-- 初始化存储过程
create procedure inout1(inout num int)
begin
select num;
set num=1;
select num;
end;
-- inout参数调用
call inout1(@num);
select num;

3)注意事项

a.定义存储过程语法里的sql语句代码块必须是完整的sql语句,必须用分号;结尾。

create procedure 存储过程名称(参数1,参数2,…) begin <sql语句>;end;

b.定义不同的存储过程,要用不同的存储过程名称,相同的存储过程名字会引起系统报错。

9、WITH 语句

WITH 子句提供了一种编写辅助语句的方法,以便在更大的查询中使用。

WITH 子句有助于将复杂的大型查询分解为更简单的表单,便于阅读。这些语句通常称为通用表表达式(Common Table Express, CTE),也可以当做一个为查询而存在的临时表。

SQL常用语法( WITH 语句)_sql with-CSDN博客

  • 7
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 1. SQL中的窗口函数实现:SQL中的窗口函数是通过OVER子句实现的。OVER子句可以在聚合函数中使用,用于指定窗口的大小和位置。窗口函数可以在分组聚合的基础上进行计算,例如计算每个分组内的排名、累计和等。 2. Pandas中的窗口函数实现:Pandas中的窗口函数是通过rolling()方法实现的。rolling()方法可以对数据进行滑动窗口计算,例如计算移动平均值、移动标准差等。rolling()方法还可以指定窗口的大小和窗口的位置,例如计算每个分组内的排名、累计和等。 3. Spark中的窗口函数实现:Spark中的窗口函数是通过Window函数实现的。Window函数可以在聚合函数中使用,用于指定窗口的大小和位置。Window函数可以在分组聚合的基础上进行计算,例如计算每个分组内的排名、累计和等。Window函数还可以指定窗口的排序方式和排序字段,例如按照时间排序、按照数值排序等。 ### 回答2: SQL窗口函数是一种强大又灵活的数据分析工具,它可以让您快速计算复杂的聚合值和行排名。在SQL中,可以通过以下三种方式来实现窗口函数: 1. 使用OVER子句:OVER子句允许您定义窗口规范,描述窗口函数如何计算,并告诉SQL如何按照特定的顺序进行排序。例如,以下查询使用AVG函数计算每个部门的平均薪水,并根据平均薪水对结果进行排序: SELECT empno, deptno, sal, AVG(sal) OVER (PARTITION BY deptno) AS avg_sal FROM emp ORDER BY avg_sal DESC; 2. 使用子查询:子查询是一种在SELECT语句中嵌套另一个SELECT语句的方法。通过使用子查询,在查询中使用窗口函数来计算聚合值。例如,以下查询使用子查询计算每个部门的平均薪水,并将结果与主查询中的每个员工的薪水进行比较: SELECT empno, deptno, sal, (SELECT AVG(sal) FROM emp e2 WHERE e2.deptno = e1.deptno) AS avg_sal FROM emp e1; 3. 使用公用表表达式:公用表表达式(CTE)是一种定义在查询中使用的命名结果集的方法。可以在CTE中定义窗口规范,并在主查询中使用窗口函数来计算聚合值。例如,以下查询使用CTE计算每个部门的平均薪水,并将结果与主查询中的每个员工的薪水进行比较: WITH dept_avg_sal AS ( SELECT deptno, AVG(sal) AS avg_sal FROM emp GROUP BY deptno ) SELECT empno, deptno, sal, avg_sal FROM emp JOIN dept_avg_sal ON emp.deptno = dept_avg_sal.deptno; pandas是一种基于Python语言的数据分析库,它提供了灵活的数据处理和分析工具。在pandas中,可以使用以下三种方法来实现窗口函数: 1. 使用rolling方法:rolling方法允许您定义一个滑动窗口,并在滑动窗口内对数据进行聚合。例如,以下代码使用rolling方法计算每个员工的3个月移动平均薪水: df['rolling_avg_sal'] = df['sal'].rolling(window=3).mean() 2. 使用groupby和expanding方法:groupby方法允许您按照一个或多个列对数据进行分组,并在每个组中使用expanding方法计算聚合值。例如,以下代码使用groupby和expanding方法计算每个员工的累计平均薪水: df['cumulative_avg_sal'] = df.groupby('empno')['sal'].expanding().mean() 3. 使用apply方法:apply方法允许您使用自定义函数对数据进行操作,并返回一个新的数据集。您可以定义一个函数,该函数使用rolling、groupby和expanding等方法来计算窗口函数。例如,以下代码使用apply方法计算每个员工的移动平均薪水和累计平均薪水: def rolling_avg_sal(series): return series.rolling(window=3).mean() def cumulative_avg_sal(series): return series.expanding().mean() df['rolling_avg_sal'] = df.groupby('empno')['sal'].apply(rolling_avg_sal) df['cumulative_avg_sal'] = df.groupby('empno')['sal'].apply(cumulative_avg_sal) Spark是一种基于Scala语言的大数据处理框架,它提供了灵活的数据处理和分析工具。在Spark中,可以使用以下三种方法来实现窗口函数: 1. 使用窗口函数:Spark支持和SQL相同的窗口函数,您可以使用窗口函数来计算聚合值。例如,以下代码使用窗口函数计算每个部门的平均薪水: import org.apache.spark.sql.expressions.Window val windowSpec = Window.partitionBy("deptno") val df2 = df.withColumn("avg_sal", avg("sal").over(windowSpec)) 2. 使用groupby和agg方法:与pandas相似,Spark也支持groupby和agg方法,可以对数据进行分组和聚合。例如,以下代码使用groupby和agg方法计算每个部门的平均薪水: val df2 = df.groupBy("deptno").agg(avg("sal")) 3. 使用reduceByKey和window方法:reduceByKey方法是一种在Spark中对数据进行分组和聚合的方法。您可以使用reduceByKey方法将数据分组并计算聚合值,然后可以使用window方法来计算窗口函数。例如,以下代码使用reduceByKey和window方法计算每个部门的平均薪水: val rdd = df.rdd.map(row => (row.getInt(1), row.getDouble(2))) val windowSpec = org.apache.spark.streaming.WindowSpec .orderBy("timestamp") .partitionBy("deptno") .rowsBetween(-2, 0) val result = rdd.reduceByKeyAndWindow((x,y) => x+y, (x,y) => x-y, windowSpec) result.foreachRDD(rdd => rdd.foreach(println)) ### 回答3: 窗口函数是一种强大的数据处理工具,能够在关系型数据库和数据处理框架中实现复杂的计算和分析任务。在SQL、Pandas和Spark中,都有多种方法可以实现窗口函数,下面分别介绍它们的三种实现方式。 SQL窗口函数实现方式: SQL中常用的窗口函数有ROW_NUMBER、RANK、DENSE_RANK等,这些函数可以通过OVER子句实现。OVER子句可以将查询结果分为若干组,在每组中进行计算,并返回每个组的结果。OVER子句中的PARTITION BY子句用于指定分组的键,ORDER BY子句用于指定分组内排序的键,窗口函数可以应用在分组后的结果上。 Pandas的窗口函数实现方式: 在Pandas中,可以使用rolling函数实现窗口函数计算。rolling函数可以对数据进行滑动窗口的操作,并对窗口内的数据执行指定的计算。rolling函数包括多个参数,如窗口大小、窗口位置、计算方法等。使用窗口函数,可以进行时间序列分析、数据平滑等操作。 Spark的窗口函数实现方式: 在Spark中,窗口函数是通过Window函数实现的。Window函数可以按照指定的分区键和排序键对数据进行分区和排序,创建一个用于窗口函数计算的数据窗口,类似于SQL中的OVER子句。使用Window函数,可以进行分组统计、排序等操作。对于Spark SQL来说,Window函数支持分组窗口函数和排序窗口函数两种类型。 综上所述,SQL、Pandas和Spark中都有不同的窗口函数实现方式,可以根据具体的业务需求和数据处理场景选择合适的实现方式。在实际应用中,可根据数据量和处理能力选择处理引擎,从而获得合理的性能和灵活性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值