postgres窗口函数
postgresql之窗口函数
工作中可能会遇到按照部门业绩排名?找出前N的员工进行业绩奖励?两次消费时间隔了多久?等等这样的问题。
对于这样的问题,使用窗口函数能很好的简化sql。
窗口函数是sql中一类特别的函数,通过查询筛选出的行的某些部分,窗口调用函数实现了类似于聚合函数的功能。 但是两者又不同,通俗的讲,聚合函数是将结果合并成一行(每组一条数据),但是窗口函数是扫描所有的行,然后你的表有几行,结果就是有几行。
有的函数随着记录不同,窗口大小都是固定的,这种属于静态窗口;有的函数则相反,不同的记录对应着不同的窗口,这种动态变化的窗口叫滑动窗口。
窗口函数的语法
窗口函数调用语法如下:
function_name ([expression [, expression ... ]]) OVER ( window_definition )
function_name ([expression [, expression ... ]]) OVER window_name
function_name ( * ) OVER ( window_definition )
function_name ( * ) OVER window_name
这里的window_definiton语法如下:
[ existing_window_name ]
[ PARTITION BY expression [, ...] ]
[ ORDER BY expression [ ASC | DESC | USING operator ] [ NULLS { FIRST | LAST } ] [, ...] ]
[ frame_clause ]
window_name引用的是查询语句中WINDOW 子句定义的命名窗口规范。命名窗口规范通常只是用OVER window_name 来引用
PARTITION BY选项将查询的行分为一组进入partitions, 这些行在窗口函数中单独处理。PARTITION BY和查询级别GROUP BY 子句做相似的工作,除了它的表达式只能作为表达式不能作为输出列的名字或数。 没有PARTITION BY,所有由查询产生的行被视为一个单独的分区
ORDER BY 选项决定分区中的行被窗口函数处理的顺序。它和查询级别ORDER BY子句做相似的工作, 但是同样的它不能作为输出列的名字或数。没有ORDER BY,行以一个不被预知的顺序处理。
这里frame_clause用在动态窗口函数中,静态窗口函数哪怕用了也不生效, frame_clause可以是:
[ RANGE | ROWS ] frame_start
[ RANGE | ROWS ] BETWEEN frame_start AND frame_end
可以用RANGE 或 ROWS模式声明;不管哪种情况, 它的变化范围是从frame_start到frame_end。如果省略了frame_end 默认为CURRENT ROW。
frame_start和frame_end可以是:
UNBOUNDED PRECEDING # 表示框架从分区的第一行开始
UNBOUNDED FOLLOWING # 表示框架以分区的最后一行结束
CURRENT ROW # 1.rowsm模式:意味着框架以当前行开始或结束
# 2.在RANGE模式,意味着框架以当前行在 ORDER BY序列中的第一个或最后一个元素开始或结束
value PRECEDING # 边界是当前行减去value的值
value FOLLOWING # 边界是当前行加上value的值
注意:value PRECEDING和value FOLLOWING 当前只允许ROWS模式。这也就意味着,窗口范围从当前行之前或之后指定的行数启动或结束。 value必须是整型表达式,而不能包含变量,聚合函数,或者窗口函数。 该值不能为空或负,但可以是零,表示只选择当前行本身。
例如,以下写法都是可以的:
rows Between UNBOUNDED PRECEDING and UNBOUNDED FOLLOWING 窗口中当前分区的所有行,相当于不写
rows UNBOUNDED PRECEDING 表示窗口范围从分区的第一行到当前当前行
rows BETWEEN 1 PRECEDING AND 1 FOLLOWING 表示窗口范围包括前一行,当前行,下一行共3行的数据范围
窗口函数适用的函数
目前,窗口函数可以调用的不仅有内建的窗口函数,还有任何用户定义的聚合函数和内建聚合函数,常见的min(),max(),count(),sum(),avg()就是可用在窗口函数中的内建聚合函数,其余可用的内建函数见官网文档。
内建窗口函数:
函数 | 返回类型 | 描述 |
---|---|---|
row_number() | bigint | 在其分区中的当前行号,从1计 |
rank() | bigint | 有间隔的当前行排名;与它的第一个相同行的row_number相同 |
dense_rank() | bigint | 没有间隔的当前行排名;这个函数计数对等组 |
percent_rank() | double precision | 当前行的相对排名: (rank - 1) / (总行数 - 1) |
cume_dist() | double precision | 当前行的相对排名:(前面的行数或与当前行相同的行数)/(总行数) |
ntile(num_buckets integer) | integer | 从1到参数值的整数范围,尽可能相等的划分分区 |
lag(value any [, offset integer [, default any ]]) | 类型同 value | 计算分区当前行的前offset 行,返回value 。如果没有这样的行, 返回default替代。 offset和default 都是当前行计算的结果。如果忽略了,则offset 默认是1,default默认是 null。 |
lead(value any [, offset integer [, default any ]]) | 类型同value | 计算分区当前行的后offset行, 返回value。如果没有这样的行, 返回default替代。 offset和default 都是当前行计算的结果。如果忽略了,则offset 默认是1,default默认是 null |
first_value(value any) | 类型同value | 返回窗口第一行的计算value值 |
last_value(value any) | 类型同value | 返回窗口最后一行的计算value值 |
nth_value(value any, nth integer) | 类型同value | 返回窗口第nth行的计算 value值(行从1计数);没有这样的行则返回 null |
将内建窗口函数分类,可分为
序号函数:row_number() | rank() | dense_rank()
分布函数:percent_rank() | cume_dist()
前后函数:lag() | lead()
其他函数:ntile() | nth_value()
头尾函数:first_value() | last_value()
其中:nth_value(),first_value(),last_value()是可实现动态窗口的。
sql示例
接下来,给出sql实例,一起来看看这些窗口函数的用法。
插入测试数据:
create table window_test(
name varchar(255),
subjects varchar(255),
scores int4
);
insert into window_test values ('张三','语文',random()*100);
insert into window_test values ('张三','数学',random()*100);
insert into window_test values ('张三','英语',random()*100);
insert into window_test values ('张三','化学',random()*100);
insert into window_test values ('张三','物理',random()*100);
insert into window_test values ('张三','生物',random()*100);
insert into window_test values ('张三','历史',random()*100);
insert into window_test values ('张三','地理',random()*100);
insert into window_test values ('张三','政治',random()*100);
insert into window_test values ('李四','语文',random()*100);
insert into window_test values ('李四','数学',random()*100);
insert into window_test values ('李四','英语',random()*100);
insert into window_test values ('李四','化学',random()*100);
insert into window_test values ('李四','物理',random()*100);
insert into window_test values ('李四','生物',random()*100);
insert into window_test values ('李四','历史',random()*100);
insert into window_test values ('李四','地理',random()*100);
insert into window_test values ('李四','政治',random()*100);
insert into window_test values ('王五','语文',random()*100);
insert into window_test values ('王五','数学',random()*100);
insert into window_test values ('王五','英语',random()*100);
insert into window_test values ('王五','化学',random()*100);
insert into window_test values ('王五','物理',random()*100);
insert into window_test values ('王五','生物',random()*100);
insert into window_test values ('王五','历史',random()*100);
insert into window_test values ('王五','地理',random()*100);
insert into window_test values ('王五','政治',random()*100);
insert into window_test values ('赵六','语文',random()*100);
insert into window_test values ('赵六','数学',random()*100);
insert into window_test values ('赵六','英语',random()*100);
insert into window_test values ('赵六','化学',random()*100);
insert into window_test values ('赵六','物理',random()*100);
insert into window_test values ('赵六','生物',random()*100);
insert into window_test values ('赵六','历史',random()*100);
insert into window_test values ('赵六','地理',random()*100);
insert into window_test values ('赵六','政治',random()*100);
1. 序号函数:row_number() | rank() |dense_rank()
select name,subjects,scores,
row_number() over (partition by name order by scores desc) as row_number,
rank() over (partition by name order by scores desc) as rank,
dense_rank() over (partition by name order by scores desc) as drank
from window_test;
显示结果如下图。表示每个同学按照学习成绩降序显示结果。这三个函数都能得到序号结果。
wind=# select name,subjects,scores,
wind-# row_number() over (partition by name order by scores desc) as row_number,
wind-# rank() over (partition by name order by scores desc) as rank,
wind-# dense_rank() over (partition by name order by scores desc) as drank
wind-# from window_test;
name | subjects | scores | row_number | rank | drank
------+----------+--------+------------+------+-------
张三 | 生物 | 92 | 1 | 1 | 1
张三 | 数学 | 92 | 2 | 1 | 1
张三 | 语文 | 75 | 3 | 3 | 2
张三 | 历史 | 74 | 4 | 4 | 3
张三 | 地理 | 71 | 5 | 5 | 4
张三 | 政治 | 54 | 6 | 6 | 5
张三 | 英语 | 46 | 7 | 7 | 6
张三 | 物理 | 45 | 8 | 8 | 7
张三 | 化学 | 6 | 9 | 9 | 8
李四 | 英语 | 95 | 1 | 1 | 1
李四 | 数学 | 85 | 2 | 2 | 2
李四 | 地理 | 66 | 3 | 3 | 3
李四 | 语文 | 59 | 4 | 4 | 4
李四 | 物理 | 55 | 5 | 5 | 5
李四 | 历史 | 47 | 6 | 6 | 6
李四 | 化学 | 36 | 7 | 7 | 7
李四 | 生物 | 31 | 8 | 8 | 8
李四 | 政治 | 17 | 9 | 9 | 9
王五 | 政治 | 87 | 1 | 1 | 1
王五 | 英语 | 77 | 2 | 2 | 2
王五 | 地理 | 76 | 3 | 3 | 3
王五 | 化学 | 68 | 4 | 4 | 4
王五 | 历史 | 64 | 5 | 5 | 5
王五 | 数学 | 35 | 6 | 6 | 6
王五 | 物理 | 21 | 7 | 7 | 7
王五 | 生物 | 15 | 8 | 8 | 8
王五 | 语文 | 11 | 9 | 9 | 9
赵六 | 地理 | 96 | 1 | 1 | 1
赵六 | 英语 | 91 | 2 | 2 | 2
赵六 | 历史 | 88 | 3 | 3 | 3
赵六 | 物理 | 75 | 4 | 4 | 4
赵六 | 政治 | 70 | 5 | 5 | 5
赵六 | 数学 | 61 | 6 | 6 | 6
赵六 | 生物 | 21 | 7 | 7 | 7
赵六 | 化学 | 6 | 8 | 8 | 8
赵六 | 语文 | 5 | 9 | 9 | 9
(36 rows)
wind=#
这些函数可以运用于查询每个同学最好的几门课的成绩等这类问题。但是添加过滤条件的时候不能直接加在where条件中,而是需要将查询结果作为子表,在外面套查询语句再用where过滤。原因是窗口函数是在group by,having之后执行的,在where这一步还没执行到窗口函数
-- 若是直接 where条件中过滤,会报错
select name,subjects,scores,
row_number() over (partition by name order by scores desc) as row_number,
rank() over (partition by name order by scores desc) as rank,
dense_rank() over (partition by name order by scores desc) as drank
from window_test
where row_number<=4;
select * from (
select name,subjects,scores,
row_number() over (partition by name order by scores desc) as row_number,
rank() over (partition by name order by scores desc) as rank,
dense_rank() over (partition by name order by scores desc) as drank
from window_test
)a
where row_number<=4;
得到结果看下图。查询得到了每个同学最高的四门课的成绩。注意王五同学的成绩,因为王五同学存在三门课同分的情况,这时用row_number、rank、dense_rank得到的结果不同,这也就是这三个函数不同的地方。
wind=# select * from (
wind(# select name,subjects,scores,
wind(# row_number() over (partition by name order by scores desc) as row_number,
wind(# rank() over (partition by name order by scores desc) as rank,
wind(# dense_rank() over (partition by name order by scores desc) as drank
wind(# from window_test
wind(# )a
wind-# where row_number<=4;
name | subjects | scores | row_number | rank | drank
------+----------+--------+------------+------+-------
张三 | 生物 | 92 | 1 | 1 | 1
张三 | 数学 | 92 | 2 | 1 | 1
张三 | 语文 | 75 | 3 | 3 | 2
张三 | 历史 | 74 | 4 | 4 | 3
李四 | 英语 | 95 | 1 | 1 | 1
李四 | 数学 | 85 | 2 | 2 | 2
李四 | 地理 | 66 | 3 | 3 | 3
李四 | 语文 | 59 | 4 | 4 | 4
王五 | 政治 | 87 | 1 | 1 | 1
王五 | 英语 | 77 | 2 | 2 | 2
王五 | 地理 | 76 | 3 | 3 | 3
王五 | 化学 | 68 | 4 | 4 | 4
赵六 | 地理 | 96 | 1 | 1 | 1
赵六 | 英语 | 91 | 2 | 2 | 2
赵六 | 历史 | 88 | 3 | 3 | 3
赵六 | 物理 | 75 | 4 | 4 | 4
(16 rows)
wind=#
row_number()就是得到行号,不管你的成绩是否一样,都按序依次标号。rank()和dense_rank()都是对同样的成绩都是统一排序,同一名次,但是不同的是,rank()函数对于后面的数据是直接跳过并列数的结果往下编号,而dense_rank()是不跳过序号直接往下编号。
另外,这三个函数对于滑动窗口的使用都是无效的,不会报错,但是没什么改变,以row_number()举个例子看看。
select name,subjects,scores,row_number() over (partition by name order by scores desc),
row_number() over (partition by name order by scores desc rows between 1 preceding and 1 following)
from window_test;
wind=# select name,subjects,scores,row_number() over (partition by name order by scores desc),
wind-# row_number() over (partition by name order by scores desc rows between 1 preceding and 1 following)
wind-# from window_test;
name | subjects | scores | row_number | row_number
------+----------+--------+------------+------------
张三 | 生物 | 92 | 1 | 1
张三 | 数学 | 92 | 2 | 2
张三 | 语文 | 75 | 3 | 3
张三 | 历史 | 74 | 4 | 4
张三 | 地理 | 71 | 5 | 5
张三 | 政治 | 54 | 6 | 6
张三 | 英语 | 46 | 7 | 7
张三 | 物理 | 45 | 8 | 8
张三 | 化学 | 6 | 9 | 9
李四 | 英语 | 95 | 1 | 1
李四 | 数学 | 85 | 2 | 2
李四 | 地理 | 66 | 3 | 3
李四 | 语文 | 59 | 4 | 4
李四 | 物理 | 55 | 5 | 5
李四 | 历史 | 47 | 6 | 6
李四 | 化学 | 36 | 7 | 7
李四 | 生物 | 31 | 8 | 8
李四 | 政治 | 17 | 9 | 9
王五 | 政治 | 87 | 1 | 1
王五 | 英语 | 77 | 2 | 2
王五 | 地理 | 76 | 3 | 3
王五 | 化学 | 68 | 4 | 4
王五 | 历史 | 64 | 5 | 5
王五 | 数学 | 35 | 6 | 6
王五 | 物理 | 21 | 7 | 7
王五 | 生物 | 15 | 8 | 8
王五 | 语文 | 11 | 9 | 9
赵六 | 地理 | 96 | 1 | 1
赵六 | 英语 | 91 | 2 | 2
赵六 | 历史 | 88 | 3 | 3
赵六 | 物理 | 75 | 4 | 4
赵六 | 政治 | 70 | 5 | 5
赵六 | 数学 | 61 | 6 | 6
赵六 | 生物 | 21 | 7 | 7
赵六 | 化学 | 6 | 8 | 8
赵六 | 语文 | 5 | 9 | 9
(36 rows)
wind=#
2. 分布函数:percent_rank() | cume_dist()
percent_rank():(rank - 1) / (总行数 - 1)
cume_dist():(前面的行数或与当前行相同的行数)/(总行数)
select name,subjects,scores,
percent_rank() over (partition by name order by scores desc),
cume_dist() over (partition by name order by scores desc)
from window_test;
结果显示如下图。percent_rank()函数是根据rank-1去计算的,所以从0开始计算,两个占比分布函数的第一个数据一个是0/8,一个是1/9计算,然后遇到相同排名的时候,因为rank是并列排名,并列排名后是间隔排名,所以排第四的科目排名直接是(4-1)/8。而cume_dist则是并列排名的比例都一样,然后计算前面的不同成绩的行数(这里是0)+当前相同成绩的行数(这里是3)然后计算排名,就是3/9。
wind=# select name,subjects,scores,
wind-# percent_rank() over (partition by name order by scores desc),
wind-# cume_dist() over (partition by name order by scores desc)
wind-# from window_test;
name | subjects | scores | percent_rank | cume_dist
------+----------+--------+--------------+--------------------
张三 | 生物 | 92 | 0 | 0.2222222222222222
张三 | 数学 | 92 | 0 | 0.2222222222222222
张三 | 语文 | 75 | 0.25 | 0.3333333333333333
张三 | 历史 | 74 | 0.375 | 0.4444444444444444
张三 | 地理 | 71 | 0.5 | 0.5555555555555556
张三 | 政治 | 54 | 0.625 | 0.6666666666666666
张三 | 英语 | 46 | 0.75 | 0.7777777777777778
张三 | 物理 | 45 | 0.875 | 0.8888888888888888
张三 | 化学 | 6 | 1 | 1
李四 | 英语 | 95 | 0 | 0.1111111111111111
李四 | 数学 | 85 | 0.125 | 0.2222222222222222
李四 | 地理 | 66 | 0.25 | 0.3333333333333333
李四 | 语文 | 59 | 0.375 | 0.4444444444444444
李四 | 物理 | 55 | 0.5 | 0.5555555555555556
李四 | 历史 | 47 | 0.625 | 0.6666666666666666
李四 | 化学 | 36 | 0.75 | 0.7777777777777778
李四 | 生物 | 31 | 0.875 | 0.8888888888888888
李四 | 政治 | 17 | 1 | 1
王五 | 政治 | 87 | 0 | 0.1111111111111111
王五 | 英语 | 77 | 0.125 | 0.2222222222222222
王五 | 地理 | 76 | 0.25 | 0.3333333333333333
王五 | 化学 | 68 | 0.375 | 0.4444444444444444
王五 | 历史 | 64 | 0.5 | 0.5555555555555556
王五 | 数学 | 35 | 0.625 | 0.6666666666666666
王五 | 物理 | 21 | 0.75 | 0.7777777777777778
王五 | 生物 | 15 | 0.875 | 0.8888888888888888
王五 | 语文 | 11 | 1 | 1
赵六 | 地理 | 96 | 0 | 0.1111111111111111
赵六 | 英语 | 91 | 0.125 | 0.2222222222222222
赵六 | 历史 | 88 | 0.25 | 0.3333333333333333
赵六 | 物理 | 75 | 0.375 | 0.4444444444444444
赵六 | 政治 | 70 | 0.5 | 0.5555555555555556
赵六 | 数学 | 61 | 0.625 | 0.6666666666666666
赵六 | 生物 | 21 | 0.75 | 0.7777777777777778
赵六 | 化学 | 6 | 0.875 | 0.8888888888888888
赵六 | 语文 | 5 | 1 | 1
(36 rows)
如果还是不太理解cume_dist(),我们再举个例子。
这个函数可以应用于求每个科目及格的同学情况及及格率占比。
select * from (
select subjects,name,scores,
cume_dist() over (partition by subjects order by scores desc)
from window_test
)a where scores>=60;
wind=# select * from (
wind(# select subjects,name,scores,
wind(# cume_dist() over (partition by subjects order by scores desc)
wind(# from window_test
wind(# )a where scores>=60;
subjects | name | scores | cume_dist
----------+------+--------+-----------
化学 | 王五 | 68 | 0.25
历史 | 赵六 | 88 | 0.25
历史 | 张三 | 74 | 0.5
历史 | 王五 | 64 | 0.75
地理 | 赵六 | 96 | 0.25
地理 | 王五 | 76 | 0.5
地理 | 张三 | 71 | 0.75
地理 | 李四 | 66 | 1
政治 | 王五 | 87 | 0.25
政治 | 赵六 | 70 | 0.5
数学 | 张三 | 92 | 0.25
数学 | 李四 | 85 | 0.5
数学 | 赵六 | 61 | 0.75
物理 | 赵六 | 75 | 0.25
生物 | 张三 | 92 | 0.25
英语 | 李四 | 95 | 0.25
英语 | 赵六 | 91 | 0.5
英语 | 王五 | 77 | 0.75
语文 | 张三 | 75 | 0.25
(19 rows)
wind=#
同理,这两个函数只存在静态窗口,滑动窗口使用无效,以percent_rank()为例子。
select name,subjects,scores,percent_rank() over (partition by name order by scores desc),
percent_rank() over (partition by name order by scores desc rows between 1 preceding and 1 following)
from window_test;
--执行结果:
wind=# select name,subjects,scores,percent_rank() over (partition by name order by scores desc),
wind-# percent_rank() over (partition by name order by scores desc rows between 1 preceding and 1 following)
wind-# from window_test;
name | subjects | scores | percent_rank | percent_rank
------+----------+--------+--------------+--------------
张三 | 生物 | 92 | 0 | 0
张三 | 数学 | 92 | 0 | 0
张三 | 语文 | 75 | 0.25 | 0.25
张三 | 历史 | 74 | 0.375 | 0.375
张三 | 地理 | 71 | 0.5 | 0.5
张三 | 政治 | 54 | 0.625 | 0.625
张三 | 英语 | 46 | 0.75 | 0.75
张三 | 物理 | 45 | 0.875 | 0.875
张三 | 化学 | 6 | 1 | 1
李四 | 英语 | 95 | 0 | 0
李四 | 数学 | 85 | 0.125 | 0.125
李四 | 地理 | 66 | 0.25 | 0.25
李四 | 语文 | 59 | 0.375 | 0.375
李四 | 物理 | 55 | 0.5 | 0.5
李四 | 历史 | 47 | 0.625 | 0.625
李四 | 化学 | 36 | 0.75 | 0.75
李四 | 生物 | 31 | 0.875 | 0.875
李四 | 政治 | 17 | 1 | 1
王五 | 政治 | 87 | 0 | 0
王五 | 英语 | 77 | 0.125 | 0.125
王五 | 地理 | 76 | 0.25 | 0.25
王五 | 化学 | 68 | 0.375 | 0.375
王五 | 历史 | 64 | 0.5 | 0.5
王五 | 数学 | 35 | 0.625 | 0.625
王五 | 物理 | 21 | 0.75 | 0.75
王五 | 生物 | 15 | 0.875 | 0.875
王五 | 语文 | 11 | 1 | 1
赵六 | 地理 | 96 | 0 | 0
赵六 | 英语 | 91 | 0.125 | 0.125
赵六 | 历史 | 88 | 0.25 | 0.25
赵六 | 物理 | 75 | 0.375 | 0.375
赵六 | 政治 | 70 | 0.5 | 0.5
赵六 | 数学 | 61 | 0.625 | 0.625
赵六 | 生物 | 21 | 0.75 | 0.75
赵六 | 化学 | 6 | 0.875 | 0.875
赵六 | 语文 | 5 | 1 | 1
(36 rows)
wind=#
-
前后函数:lag() | lead()
lag()和lead()表示计算当前行的前n行或后n行,n不写默认是1,如果设置default,则表示没有这样的行默认用default代替。
这两个函数利用计算当前行的前n行或后n行,可以用来处理类似连续登陆n天,或者登陆时间最大间隔天数等的问题。
以lag()为例,首先插入测试数据:create table login_log( id varchar(255), log_time date ); INSERT INTO login_log(id, log_time) VALUES ('101', '2022-02-14'); INSERT INTO login_log(id, log_time) VALUES ('101', '2022-02-15'); INSERT INTO login_log(id, log_time) VALUES ('102', '2022-02-15'); INSERT INTO login_log(id, log_time) VALUES ('101', '2022-02-16'); INSERT INTO login_log(id, log_time) VALUES ('101', '2022-02-17'); INSERT INTO login_log(id, log_time) VALUES ('101', '2022-02-18'); INSERT INTO login_log(id, log_time) VALUES ('101', '2022-02-19'); INSERT INTO login_log(id, log_time) VALUES ('101', '2022-02-20'); INSERT INTO login_log(id, log_time) VALUES ('101', '2022-02-21'); INSERT INTO login_log(id, log_time) VALUES ('101', '2022-02-22'); INSERT INTO login_log(id, log_time) VALUES ('102', '2022-02-23'); INSERT INTO login_log(id, log_time) VALUES ('102', '2022-02-24'); INSERT INTO login_log(id, log_time) VALUES ('103', '2022-02-14'); INSERT INTO login_log(id, log_time) VALUES ('103', '2022-02-15'); INSERT INTO login_log(id, log_time) VALUES ('103', '2022-02-16'); INSERT INTO login_log(id, log_time) VALUES ('103', '2022-02-18'); INSERT INTO login_log(id, log_time) VALUES ('103', '2022-02-19'); INSERT INTO login_log(id, log_time) VALUES ('103', '2022-02-20'); INSERT INTO login_log(id, log_time) VALUES ('103', '2022-02-21');
若是想要看看哪些用户连续登陆7天,sql如下:
select *,lag(log_time,6) over (partition by id order by log_time) from login_log --执行结果: wind=# select *,lag(log_time,6) over (partition by id order by log_time) wind-# from login_log wind-# ; id | log_time | lag -----+------------+------------ 101 | 2022-02-14 | 101 | 2022-02-15 | 101 | 2022-02-16 | 101 | 2022-02-17 | 101 | 2022-02-18 | 101 | 2022-02-19 | 101 | 2022-02-20 | 2022-02-14 101 | 2022-02-21 | 2022-02-15 101 | 2022-02-22 | 2022-02-16 102 | 2022-02-15 | 102 | 2022-02-23 | 102 | 2022-02-24 | 103 | 2022-02-14 | 103 | 2022-02-15 | 103 | 2022-02-16 | 103 | 2022-02-18 | 103 | 2022-02-19 | 103 | 2022-02-20 | 103 | 2022-02-21 | 2022-02-14 (19 rows) wind=#
此时lag()函数将log_time下滑6行进行匹配,如果连续登陆的话,两个时间差之间的间隔应该是<=6的,以此来过滤出用户。
select id,log_time,lag from ( select *,lag(log_time,6) over (partition by id order by log_time) from login_log )a where log_time-lag<=6; --执行结果: wind=# select id,log_time,lag from ( wind(# select *,lag(log_time,6) over (partition by id order by log_time) wind(# from login_log wind(# )a where log_time-lag<=6; id | log_time | lag -----+------------+------------ 101 | 2022-02-20 | 2022-02-14 101 | 2022-02-21 | 2022-02-15 101 | 2022-02-22 | 2022-02-16 (3 rows) wind=#
如上图,最后得到只有101用户是连续7天登陆的。
例如想知道用户登录最大不登录时间间隔,sql如下:select id,max(log_time-lag) from ( select *,lag(log_time) over (partition by id order by log_time) from login_log )tmp group by id; --执行结果: wind=# select id,max(log_time-lag) from ( wind(# select *,lag(log_time) over (partition by id order by log_time) wind(# from login_log wind(# )tmp group by id; id | max -----+----- 101 | 1 102 | 8 103 | 2 (3 rows) wind=#
同样,这两个函数也只有静态窗口。
- 其他函数:ntile() | nth_value()
ntile()
ntile()函数表示将数据分成尽可能相等的分区,n传入多少就分成几组,若是n是1,则所有数据都在一组相当于没分区,若是n>=总的条数,那也不会报错,只是每条记录都是一组数据。
这个函数可以用来解决随机分组的问题,例如将班级中的学生随机分成2组。select name,ntile(2) over (order by random()) from window_test group by name --执行结果: wind=# select name,ntile(2) over (order by random()) wind-# from window_test wind-# group by name wind-# ; name | ntile ------+------- 王五 | 1 赵六 | 1 张三 | 2 李四 | 2 (4 rows) wind=#
order by random():表示随机排序,也就是一个打乱的一个过程,这里不是必须的,可以删掉的
window_function() over ():over后面不加partition by的时候,表示不再分区,而是对整表进行操作。- nth_value()
返回窗口中排名第n行的计算值。这个函数可以使用于在显示每个科目考得前n名的成绩的同学。以每科考前2名成绩的同学为例:
select subjects,name,scores,nth_value(scores,2) over (partition by subjects order by scores desc) from window_test wind=# select subjects,name,scores,nth_value(scores,2) over (partition by subjects order by scores desc) wind-# from window_test wind-# ; subjects | name | scores | nth_value ----------+------+--------+----------- 化学 | 王五 | 68 | 化学 | 李四 | 36 | 36 化学 | 张三 | 6 | 36 化学 | 赵六 | 6 | 36 历史 | 赵六 | 88 | 历史 | 张三 | 74 | 74 历史 | 王五 | 64 | 74 历史 | 李四 | 47 | 74 地理 | 赵六 | 96 | 地理 | 王五 | 76 | 76 地理 | 张三 | 71 | 76 地理 | 李四 | 66 | 76 政治 | 王五 | 87 | 政治 | 赵六 | 70 | 70 政治 | 张三 | 54 | 70 政治 | 李四 | 17 | 70 数学 | 张三 | 92 | 数学 | 李四 | 85 | 85 数学 | 赵六 | 61 | 85 数学 | 王五 | 35 | 85 物理 | 赵六 | 75 | 物理 | 李四 | 55 | 55 物理 | 张三 | 45 | 55 物理 | 王五 | 21 | 55 生物 | 张三 | 92 | 生物 | 李四 | 31 | 31 生物 | 赵六 | 21 | 31 生物 | 王五 | 15 | 31 英语 | 李四 | 95 | 英语 | 赵六 | 91 | 91 英语 | 王五 | 77 | 91 英语 | 张三 | 46 | 91 语文 | 张三 | 75 | 语文 | 李四 | 59 | 59 语文 | 王五 | 11 | 59 语文 | 赵六 | 5 | 59 (36 rows) wind=#
此时得到的结果如上图,这里因为是排名第2的值,所以在最高分的nth_value是null。基于这样的数据条件,想得到每个科目成绩前两名成绩的情况是,条件时nth_value是null,或者scores>=nth_value,综上,班级前2名成绩的sql如下:
select subjects,name,scores from ( select subjects,name,scores,nth_value(scores,2) over (partition by subjects order by scores desc) from window_test )a where scores>=nth_value or nth_value is null ; --执行结果: wind=# select subjects,name,scores from ( wind(# select subjects,name,scores,nth_value(scores,2) over (partition by subjects order by scores desc) wind(# from window_test wind(# )a where scores>=nth_value or nth_value is null wind-# ; subjects | name | scores ----------+------+-------- 化学 | 王五 | 68 化学 | 李四 | 36 历史 | 赵六 | 88 历史 | 张三 | 74 地理 | 赵六 | 96 地理 | 王五 | 76 政治 | 王五 | 87 政治 | 赵六 | 70 数学 | 张三 | 92 数学 | 李四 | 85 物理 | 赵六 | 75 物理 | 李四 | 55 生物 | 张三 | 92 生物 | 李四 | 31 英语 | 李四 | 95 英语 | 赵六 | 91 语文 | 张三 | 75 语文 | 李四 | 59 (18 rows) wind=#
这个函数是可以使用滑动窗口的,当使用了滑动窗口,那和静态窗口得到的数据就会有所不同
select subjects,name,scores, nth_value(scores,1) over (partition by subjects order by scores desc), nth_value(scores,1) over (partition by subjects order by scores desc rows between 1 preceding and 1 following) from window_test --执行结果: wind=# select subjects,name,scores, wind-# nth_value(scores,1) over (partition by subjects order by scores desc), wind-# nth_value(scores,1) over (partition by subjects order by scores desc rows between 1 preceding and 1 following) wind-# from window_test wind-# ; subjects | name | scores | nth_value | nth_value ----------+------+--------+-----------+----------- 化学 | 王五 | 68 | 68 | 68 化学 | 李四 | 36 | 68 | 68 化学 | 张三 | 6 | 68 | 36 化学 | 赵六 | 6 | 68 | 6 历史 | 赵六 | 88 | 88 | 88 历史 | 张三 | 74 | 88 | 88 历史 | 王五 | 64 | 88 | 74 历史 | 李四 | 47 | 88 | 64 地理 | 赵六 | 96 | 96 | 96 地理 | 王五 | 76 | 96 | 96 地理 | 张三 | 71 | 96 | 76 地理 | 李四 | 66 | 96 | 71 政治 | 王五 | 87 | 87 | 87 政治 | 赵六 | 70 | 87 | 87 政治 | 张三 | 54 | 87 | 70 政治 | 李四 | 17 | 87 | 54 数学 | 张三 | 92 | 92 | 92 数学 | 李四 | 85 | 92 | 92 数学 | 赵六 | 61 | 92 | 85 数学 | 王五 | 35 | 92 | 61 物理 | 赵六 | 75 | 75 | 75 物理 | 李四 | 55 | 75 | 75 物理 | 张三 | 45 | 75 | 55 物理 | 王五 | 21 | 75 | 45 生物 | 张三 | 92 | 92 | 92 生物 | 李四 | 31 | 92 | 92 生物 | 赵六 | 21 | 92 | 31 生物 | 王五 | 15 | 92 | 21 英语 | 李四 | 95 | 95 | 95 英语 | 赵六 | 91 | 95 | 95 英语 | 王五 | 77 | 95 | 91 英语 | 张三 | 46 | 95 | 77 语文 | 张三 | 75 | 75 | 75 语文 | 李四 | 59 | 75 | 75 语文 | 王五 | 11 | 75 | 59 语文 | 赵六 | 5 | 75 | 11 (36 rows)
此时,所用的滑动窗口是将前一行,当前行,后一行的数据作为一个整体比较,取这三个数据中的第一个数据。
- 头尾函数:first_value() | last_value()
其实first_value()和nth_value()当n=1的时候是一样的,但是对于不知道整体几行的时候,无法知道最后一行的行数的时候,可以使用last_value()。而且这两个函数也是可以使用滑动窗口。
select subjects,name,scores,
first_value(scores) over (partition by subjects ),
first_value(scores) over (partition by subjects rows between 1 preceding and 1 following),
last_value(scores) over (partition by subjects ),
last_value(scores) over (partition by subjects rows between 1 preceding and 1 following)
from window_test
--执行结果:
wind=# select subjects,name,scores,
wind-# first_value(scores) over (partition by subjects ),
wind-# first_value(scores) over (partition by subjects rows between 1 preceding and 1 following),
wind-# last_value(scores) over (partition by subjects ),
wind-# last_value(scores) over (partition by subjects rows between 1 preceding and 1 following)
wind-# from window_test
wind-# ;
subjects | name | scores | first_value | first_value | last_value | last_value
----------+------+--------+-------------+-------------+------------+------------
化学 | 李四 | 36 | 36 | 36 | 6 | 68
化学 | 王五 | 68 | 36 | 36 | 6 | 6
化学 | 赵六 | 6 | 36 | 68 | 6 | 6
化学 | 张三 | 6 | 36 | 6 | 6 | 6
历史 | 赵六 | 88 | 88 | 88 | 47 | 74
历史 | 张三 | 74 | 88 | 88 | 47 | 64
历史 | 王五 | 64 | 88 | 74 | 47 | 47
历史 | 李四 | 47 | 88 | 64 | 47 | 47
地理 | 李四 | 66 | 66 | 66 | 96 | 76
地理 | 王五 | 76 | 66 | 66 | 96 | 71
地理 | 张三 | 71 | 66 | 76 | 96 | 96
地理 | 赵六 | 96 | 66 | 71 | 96 | 96
政治 | 张三 | 54 | 54 | 54 | 70 | 17
政治 | 李四 | 17 | 54 | 54 | 70 | 87
政治 | 王五 | 87 | 54 | 17 | 70 | 70
政治 | 赵六 | 70 | 54 | 87 | 70 | 70
数学 | 王五 | 35 | 35 | 35 | 61 | 92
数学 | 张三 | 92 | 35 | 35 | 61 | 85
数学 | 李四 | 85 | 35 | 92 | 61 | 61
数学 | 赵六 | 61 | 35 | 85 | 61 | 61
物理 | 赵六 | 75 | 75 | 75 | 21 | 45
物理 | 张三 | 45 | 75 | 75 | 21 | 55
物理 | 李四 | 55 | 75 | 45 | 21 | 21
物理 | 王五 | 21 | 75 | 55 | 21 | 21
生物 | 王五 | 15 | 15 | 15 | 21 | 31
生物 | 李四 | 31 | 15 | 15 | 21 | 92
生物 | 张三 | 92 | 15 | 31 | 21 | 21
生物 | 赵六 | 21 | 15 | 92 | 21 | 21
英语 | 李四 | 95 | 95 | 95 | 91 | 77
英语 | 王五 | 77 | 95 | 95 | 91 | 46
英语 | 张三 | 46 | 95 | 77 | 91 | 91
英语 | 赵六 | 91 | 95 | 46 | 91 | 91
语文 | 王五 | 11 | 11 | 11 | 5 | 59
语文 | 李四 | 59 | 11 | 11 | 5 | 75
语文 | 张三 | 75 | 11 | 59 | 5 | 5
语文 | 赵六 | 5 | 11 | 75 | 5 | 5
(36 rows)
wind=#
6. 聚合函数
聚合函数也可以配合使用窗口函数,以sum()函数为例,利用窗口函数求每个同学的成绩。
select name,subjects,scores,
sum(scores) over (partition by name),
sum(scores) over (partition by name order by subjects),
sum(scores) over (partition by name order by subjects rows between 1 preceding and 2 following)
from window_test;
--执行借结果:
wind=# select name,subjects,scores,
wind-# sum(scores) over (partition by name),
wind-# sum(scores) over (partition by name order by subjects),
wind-# sum(scores) over (partition by name order by subjects rows between 1 preceding and 2 following)
wind-# from window_test;
name | subjects | scores | sum | sum | sum
------+----------+--------+-----+-----+-----
张三 | 化学 | 6 | 555 | 6 | 151
张三 | 历史 | 74 | 555 | 80 | 205
张三 | 地理 | 71 | 555 | 151 | 291
张三 | 政治 | 54 | 555 | 205 | 262
张三 | 数学 | 92 | 555 | 297 | 283
张三 | 物理 | 45 | 555 | 342 | 275
张三 | 生物 | 92 | 555 | 434 | 258
张三 | 英语 | 46 | 555 | 480 | 213
张三 | 语文 | 75 | 555 | 555 | 121
李四 | 化学 | 36 | 491 | 36 | 149
李四 | 历史 | 47 | 491 | 83 | 166
李四 | 地理 | 66 | 491 | 149 | 215
李四 | 政治 | 17 | 491 | 166 | 223
李四 | 数学 | 85 | 491 | 251 | 188
李四 | 物理 | 55 | 491 | 306 | 266
李四 | 生物 | 31 | 491 | 337 | 240
李四 | 英语 | 95 | 491 | 432 | 185
李四 | 语文 | 59 | 491 | 491 | 154
王五 | 化学 | 68 | 454 | 68 | 208
王五 | 历史 | 64 | 454 | 132 | 295
王五 | 地理 | 76 | 454 | 208 | 262
王五 | 政治 | 87 | 454 | 295 | 219
王五 | 数学 | 35 | 454 | 330 | 158
王五 | 物理 | 21 | 454 | 351 | 148
王五 | 生物 | 15 | 454 | 366 | 124
王五 | 英语 | 77 | 454 | 443 | 103
王五 | 语文 | 11 | 454 | 454 | 88
赵六 | 化学 | 6 | 513 | 6 | 190
赵六 | 历史 | 88 | 513 | 94 | 260
赵六 | 地理 | 96 | 513 | 190 | 315
赵六 | 政治 | 70 | 513 | 260 | 302
赵六 | 数学 | 61 | 513 | 321 | 227
赵六 | 物理 | 75 | 513 | 396 | 248
赵六 | 生物 | 21 | 513 | 417 | 192
赵六 | 英语 | 91 | 513 | 508 | 117
赵六 | 语文 | 5 | 513 | 513 | 96
(36 rows)
wind=#
由上图可以看出,使用sum()对每个同学求总分,就能得到一列sum列来显示每个同学的总分,若是添加order by,则是根据科目实现成绩累加的sum1。函数若是配合使用滑动窗口,则是得到窗口内的总分和,此时的order by的累加功能已经失效了,其实和不写效果一样。
注意事项
窗口函数一定配有 over ()语句,若是不需要排序分组,对整表进行操作,可以不写里面的内容,只跟一个()即可。
如果查询包含窗口函数,并且查询使用任何的聚合、group by或者having,那么使用窗口函数的行是聚合得到的组的行,而不是从from/where 得到的原始表行。
窗口调用函数只能在select列,或者查询的order by子句中使用
当查询涉及多个窗口函数时,可以写成每一个都带有单独的OVER子句, 但是,如果期待为多个窗口函数采用相同的窗口行为,这样做就会产生重复,并且容易出错。 作为代替,每个窗口行为可以在WINDOW子句中进行命名,然后再被OVER引用,例如:
SELECT sum(salary) OVER w, avg(salary) OVER w
FROM empsalary
WINDOW w AS (PARTITION BY depname ORDER BY salary DESC);
当语法over子句和window子句部分内置部分在Windows子句时,语法注意内容可看:[窗口定义:内置使用 OVER 子句和 WINDOW 子句 ]
window子句必须位于 select 语句的 order by 子句之前
其他参考文档内容:
http://postgres.cn/docs/9.3/tutorial-window.html
http://postgres.cn/docs/9.3/sql-expressions.html#SYNTAX-WINDOW-FUNCTIONS
好啦,本文到这里就结束了。
感谢您的阅读~
————————————————
版权声明:本文为CSDN博主「youzi85」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/youzi85/article/details/128004059