什么是窗口函数?
窗口函数(Window Function)是SQL2003标准中定义的一项新特性,并在 SQL2011、SQL2016 中又加以完善,添加了若干处拓展。窗口函数不同于我们熟悉的普通函数和聚合函数,它为每行数据进行一次计算:输入多行(一个窗口)、返回一个值。在报表等分析型查询中,窗口函数能优雅地表达某些需求,发挥不可替代的作用。
window_function (expression) OVER (
[ PARTITION BY part_list ]
[ ORDER BY order_list ]
[ { ROWS | RANGE } BETWEEN frame_start AND frame_end ] )
窗口函数出现在 SELECT 子句的表达式列表中,它最显著的特点就是 OVER 关键字。
PARTITION BY 表示将数据先按 part_list 进行分区。
ORDER BY 表示将各个分区内的数据按 order_list 进行排序(在区域内排序)。
最后一项表示 Frame 的定义,即:当前窗口包含哪些数据?
ROWS 选择前后几行
例如 ROWS BETWEEN 3 PRECEDING AND 3 FOLLOWING 表示往前3行到往后3行,一共7行数据(或小于7行,如果碰到了边界)
RANGE 选择数据范围
例如 RANGE BETWEEN 3 PRECEDING AND 3 FOLLOWING 表示所有值在[c−3,c+3]这个范围内的行,c为当前行的值。
如果不指定 PARTITION BY,则不对数据进行分区;换句话说,所有数据看作同一个分区。
如果不指定 ORDER BY,则不对各分区做排序,通常用于那些顺序无关的窗口函数,例如 SUM()。
如果不指定 Frame 子句,则默认采用以下的 Frame 定义:
若不指定 ORDER BY,默认使用分区内所有行 RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING。
若指定了 ORDER BY,默认使用分区内第一行到当前值 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW。
案例
CREATE TABLE `ban_ji` (
`id` int(11) NOT NULL,
`achievement` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '成绩',
`ban_ji` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '班级',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
INSERT INTO `ban_ji`(`id`, `achievement`, `ban_ji`) VALUES (1, '90', '1');
INSERT INTO `ban_ji`(`id`, `achievement`, `ban_ji`) VALUES (2, '91', '1');
INSERT INTO `ban_ji`(`id`, `achievement`, `ban_ji`) VALUES (3, '89', '2');
INSERT INTO `ban_ji`(`id`, `achievement`, `ban_ji`) VALUES (4, '88', '2');
INSERT INTO `ban_ji`(`id`, `achievement`, `ban_ji`) VALUES (5, '99', '2');
INSERT INTO `ban_ji`(`id`, `achievement`, `ban_ji`) VALUES (6, '90', '1');
INSERT INTO `ban_ji`(`id`, `achievement`, `ban_ji`) VALUES (7, '59', '3');
INSERT INTO `ban_ji`(`id`, `achievement`, `ban_ji`) VALUES (8, '50', '3');
INSERT INTO `ban_ji`(`id`, `achievement`, `ban_ji`) VALUES (9, '10', '3');
INSERT INTO `ban_ji`(`id`, `achievement`, `ban_ji`) VALUES (10, '99', '1');
INSERT INTO `ban_ji`(`id`, `achievement`, `ban_ji`) VALUES (11, '98', '1');
INSERT INTO `ban_ji`(`id`, `achievement`, `ban_ji`) VALUES (12, '97', '1');
Example1
SELECT a.id,a.ban_ji,a.achievement,
rank() OVER(PARTITION BY a.ban_ji ORDER BY a.achievement asc) as ranking,
dense_rank() OVER(PARTITION BY a.ban_ji ORDER BY a.achievement asc) as denseRank,
row_number() OVER(PARTITION BY a.ban_ji ORDER BY a.achievement asc) as rowNum
FROM ban_ji a
rank函数:这个例子中是1位,1位,3位,也就是如果有并列名次的行,会占用下一名次的位置。比如正常排名是1,2,3,但是现在前2名是并列的名次,结果是:1,1,3。
dense_rank函数:这个例子中是1位,1位,3位,也就是如果有并列名次的行,不占用下一名次的位置。比如正常排名是1,2,3,但是现在前3名是并列的名次,结果是:1,1,2。
row_number函数;这个例子中是1位,2位,3位,也就是不考虑并列名次的情况。比如前3名是并列的名次,排名是正常的1,2,3。
Example2
rows between的作用:控制窗口函数的范围来满足业务需求。
current row就是指当前行,unbounded following就是指最后一行。unbounded preceding 就是第一行。
n preceding 上 n 行数据。n following 下 n行数据。
SELECT a.id,a.ban_ji,a.achievement,
SUM(a.achievement) OVER(PARTITION BY a.ban_ji ORDER BY a.achievement asc
rows between current row and unbounded following) as rowNum,
SUM(a.achievement) OVER(PARTITION BY a.ban_ji ORDER BY a.achievement asc
rows between unbounded preceding and current row) as rowNum2,
SUM(a.achievement) OVER(PARTITION BY a.ban_ji ORDER BY a.achievement asc
rows between 1 preceding and current row) as rowNum3,
SUM(a.achievement) OVER(PARTITION BY a.ban_ji ORDER BY a.achievement asc
rows between current row and 1 following) as rowNum4
FROM ban_ji a
Frame 定义并非所有窗口函数都适用,比如 ROW_NUMBER()、RANK()、LEAD() 等。这些函数总是应用于整个分区,而非当前 Frame。
窗口函数可以分为以下 3 类
聚合(Aggregate):AVG(), COUNT(), MIN(), MAX(), SUM()…
取值(Value):FIRST_VALUE(), LAST_VALUE(), LEAD(), LAG()…
排序(Ranking):RANK(), DENSE_RANK(), ROW_NUMBER(), NTILE()
FIRST_VALUE 当前窗口第一个值,LAST_VALUE 当前窗口最后一个值。
SELECT a.id,a.ban_ji,a.achievement,
FIRST_VALUE(a.id) OVER(PARTITION BY a.ban_ji ORDER BY a.achievement asc) as rowNum,
LAST_VALUE(a.id) OVER(PARTITION BY a.ban_ji ORDER BY a.achievement asc) as rowNum2
FROM ban_ji a
注意:在测试LAST_VALUE的时候,感觉id为1的数据返回不对。原因未知。
窗口函数 VS 聚合函数
似乎窗口函数和 Group By 聚合函数都能做到同样的事情。但是,它们之间的相似点也仅限于此了!这其中的关键区别在于:窗口函数仅仅只会将结果附加到当前的结果上,它不会对已有的行或列做任何修改。而 Group By 的做法完全不同:对于各个 Group 它仅仅会保留一行聚合结果。