以下梳理SQL中常用的两种分析函数的使用,窗口函数和聚合表达式,这里把用于分组计算、排序、提取并需要在函数后直接使用over
开窗的分析函数归为窗口函数,而使用聚合函数的分组、排序等语句称为聚合表达式(有些聚合表达式后面也可以用over
来划定范围,另外,一些用于统计等功能的聚合函数如corr
、stddev
等由于使用方式与聚合函数基本相同也没有在这篇中进行整理),以上是根据学习习惯进行的分类,实际使用中这些函数常被称为“分析函数”。对不同的数据库这些函数支持的情况不同,这里主要参考Hive
和PostgreSQL
中的相关函数。
窗口函数(Window Function)是主要用于分组聚合计算或排序,“窗口”的含义是“范围”,即先按照某列进行圈定,在根据函数的功能进行计算排序等。
窗口函数的基本形式
# 函数 over 窗口
<窗口函数> over (partition by <用于分组的列名>
order by <用于排序的列名>)
<起止行描述的窗口子句>
over
后面的部分用于“开窗”——指定范围和顺序,有两种方式 over(distribute by…sort by…)
和 over(partition by…order by…)
,它们是固定搭配。
order by
或 sort by
后面是窗口子句,一般以 rows between
开始,用于描述选取行的范围,只能在order by
后面而不能单独出现,主要有:
- PRECEDING:往前
- FOLLOWING:往后
- CURRENT ROW:当前行
- UNBOUNDED:起点,UNBOUNDED PRECEDING 表示从前面的起点, UNBOUNDED FOLLOWING:表示到后面的终点
例如 rows between 3 PRECEDING and 1 FOLLOWING
就表示向前取3行及向后取1行,rows between CURRENT ROW and UNBOUNDED FOLLOWING
就表示从当前行开始取到之后所有行。
窗口函数与基本的聚合计算(聚合函数 + group by
)的最大不同是窗口函数不改变原表中记录的数量(行数),以count()
函数为例:
-- 如果用聚合函数和 group by,聚合字段每一类会形成一行记录,返回一条结果
select company, count(staff_id) from com_info where c_time = '2020-02-29' group by company;
-- 如果使用窗口函数,则返回记录的数量与原表的条数是一样的
select company, count(staff_id) over (partition by company order by district)
from com_info where c_time = '2020-02-29';
rank()
、dense_rank()
和 row_number()
是用于排序的窗口函数,这些函数的over
中不能写窗口子句。
- row_number()从1开始,按照顺序,生成分组内记录的序列号,row_number()值不会重复,当排序值相同时,按照表中记录的顺序进行排序;
- rank() 生成数据项在分组中的排名,排名相等会占用下一名次的位置(排名如:1,2,2,4);
- dence_rank() 生成数据项在分组中的排名,排名相等不占用下一名次的位置(排名如:1,2,2,3)
select staff_id, rank() over(partition by department order by staff_his_perf) as perf_rank
from stf_pr_info where c_time = '2020-02-01_2020-02-29'
一个示例,问题是:用户登录日志表为user_id, log_id, session_id, plat 用sql查询近30天每天平均登录用户数量,以及用sql查询出近30天连续访问7天以上的用户数量
select user_id,max(count_date_on)
from(
(select user_id, count(date_on) count_date_on
from
(select user_id,log_id,row_number() over(partition by user_id order by log_id desc) rnk,log_id-(max(log_id)-rnk) date_on
from TB
group by user_id ) A
group by user_id,date_on))B
group by user_id
having max(count_date_on)>=7
另一个例子,计算用户最长连续登陆的天数:
select UID,max(cnt) as cnt
from (
select UID,Grp_No,count(*) as cnt
from (
select UID,LoadTime,(Day(LoadTime)-ROW_NUMBER() OVER (Partition By UID Order By UID,LoadTime)) as Grp_No
from #Tmp_Data) a
group by UID,Grp_No) b
group by UID
Hive
中可以在over
中使用聚合函数:
select staff_id, rank() over (order by sum(stf_perf_cnt)) as staff_his_od
from TB
group by staff_id;
ntile
ntile
将有序分区的行分配到指定数量的大致相等的桶中。 它从一个开始为每个组分配一个桶号。 对于组中的每一行,ntile
函数分配一个桶号,表示该行所属的组。大致相等意味着分组结果之间的方差会尽可能小,所有桶中的记录要么都相同,要么从某一个记录较少的桶开始后面所有捅的记录数都与该桶的记录数相同。,例如53条数据被分成5组,会是11、11、11、10、10,而不会是11、11、11、11、9。
一个示例,1000家店铺的价格数据。计算价格排名前30%和后70%的的店铺的平均价格:
-- 1 把记录按价格顺序拆分成10片
drop table if exists test_dp_price_rk;
create table test_dp_price_rk
select id, price, NTILE(10) over (order by price desc) as rn
from test_dp_price;
-- 2 按片取30%和70%,分别计算平均值
select new_rn,
max(case when new_rn=1 then 'avg_price_first_30%' when new_rn=2 then 'avg_price_last_70%' end)
as avg_price_name,
avg(price) avg_price
from
(select id, price, rn,
case when rn in (1,2,3) then 1 else 2 end as new_rn
from test_dp_price_rk)a group by new_rn;
lag()
和lead()
函数可以在同一次查询中取出同一字段的前N行的数据(lag)或后N行的数据(lead)作为独立的列,基本语法是
lag(col,n,DEFAULT) --窗口内向上n行的值,如果向上第n行为空值(null)时取DEFAULT,n默认为1,DEFAULT默认为null
lead(col,n,DEFAULT) --窗口内向下n行的值,如果向下第n行为空值(null)时取DEFAULT,n默认为1,DEFAULT默认为null
一个示例,在消费记录中取出最近2次消费金额
select sal_time, cust_id, sal, lag(sal,1,0) over (partition by cust_id order by sal_time) as lest_sal
from cust_info_base where sal_time between '2020-02-01' and '2020-02-29'
first_value
和 last_value
first_value
取窗口内的第一条记录,last_value
取窗口内的最后一条记录,有两个参数(columnName, isSkipNull=false)
;例如取出部门中入职时间最早和最晚的员工:
select dep_id, first_value(join_time) over (partition by dep_id order by join_time) as ld_time,
last_value(join_time) over (partition by dep_id order by join_time) as yn_time
from staff_info_base
where c_time = '2020-02-29'
cume_dist
和 percent_rank
cume_dist
用于计算窗口内小于等于当前值的行数与分组内总行数的比值,即统计值在窗口范围内的分位置,例如计算某员工收入在全公司和部门内的分位置:
select part_id, staff_name, compen, cume_dist() over (order by compen) as pct_company,
cume_dist() over (partition by part_id order by compen) as pct_part
from staff_payment_his where c_time = '2020-02-29'
order by part_id, compen
percent_rank
用于计算百分比排名,这里的百分比排名从0开始,即(x - 1) / (the number of rows in the window or partition - 1)
,一个示例计算员工收入的百分比排名及与整体收入分位置的比较:
select part_id, staff_name, compen,
percent_rank() over (partition by part_id order by compen) as pct_rank_part,
precentile_cont(0) within group(order by compen desc) over(partition by part_id) max_com,
precentile_cont(0.25) within group(order by compen desc) over(partition by part_id) b75_com,
precentile_cont(0.5) within group(order by compen desc) over(partition by part_id) medium_com,
precentile_cont(0.75) within group(order by compen desc) over(partition by part_id) b25_com
from staff_payment_his where c_time = '2020-02-29'
order by part_id, compen
对有序排列的聚合主要用于统计分位值和众数,包括 percentile_cont
、percentile_disc
、mode
,(Hive
中是 percentile
和 percentile_approx
,percentile
的功能和用法与 pg
中的 percentile_cont
类似,percentile_approx
则可以设置近似程度,官方文档提示,Use PERCENTILE_APPROX if your input is non-integral.)
-- 计算分组中的中位值/分位值
select staff_rank, precentile_cont(0.5) within group(order by staff_perf_cnt) as perf_medium
from stf_pr_info where c_time = '2020-02-01_2020-02-29'
percentile_disc
与percentile_cont
略微的不同是,如果某个求取分位值上没有对应的记录,则percentile_cont
会计算最接近的两个记录的均值,而percentile_disc
则取排序上最接近的一个值(如升序排列则向上取)。
下面的示例计算各部门在职时长的众数和这个众数的人数,由于聚合函数之间是不允许嵌套的,因此需要用连表的方式进行处理:
select t1.part_id as de_part,
count(t1.staff_id) as stf_cnt,
count(case when t1.on_wtime = t2.mode_tm then t1.staff_id else null end) as mode_cnt
from
(select part_id, staff_id, on_wtime
from public.dt_link_agent
where c_time = '2020-02-29') t1
left join
(select part_id,
mode() within group(order by on_wtime) as mode_tm
from public.dt_link_agent
where c_time = '2020-02-29'
group by part_id) t2
on t1.part_id = t2.part_id
group by de_part
string_agg
可以进行字符串的拼接操作,例如排序并拼接一个中心大部门下的所有二级部门:
select org_id, string_agg(part_id,',' order by part_id) as dep
from public.dt_link_agent
where c_time = '2020-02-29'
group by org_id
array_agg
可以拼接成一个列表(在 pg
中用{}
表示),但即使用group by
聚合,这个拼接也需要distinct
进行去重:
select org_id, array_agg(distinct part_id) as dep
from public.dt_link_agent
where c_time = '2020-02-29'
group by org_id
如果需要将列表转换成字符串则需要函数 array_to_string
。
此外 pg
中还有 json_agg
、json_object_agg
、xmlagg
等,处理各种数据结构。
pg
中支持使用 filter
进行筛选过滤,例如仅统计中心组织下,员工人数多于200人的部门名单:
select t.org_id as org_id, string_agg(t.part_id, ',' order by t.part_id)
filter (where t.staff_cnt > 200)
from
(select org_id, part_id, count(staff_id) as staff_cnt
from public.dt_link_agent
where c_time = '2020-02-29'
group by org_id, part_id) t
group by org_id
Hive Operators and User-Defined Functions (UDFs)
9.20. Aggregate Functions
PostgreSQL 聚合表达式 FILTER , order , within group 用法
SQL Server Ntile()函数
SQL2005四个排名函数(row_number、rank、dense_rank和ntile)的比较
PERCENT_RANK 窗口函数
Oracle所有分析函数