我先给一个统一的示例表,后面所有 SQL 都基于它讲:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| create table abc_test(
a string,
b int,
c int
)
row format delimited fields terminated by '\t'
stored as textfile;
insert into abc_test values
('A',10,100),
('A',20,200),
('A',30,300),
('A',40,400),
('B',15,150),
('A',25,250),
('A',35,350)
select * from abc_test;
|
| a | b | c |
|---|
| A | 10 | 100 |
| A | 20 | 200 |
| A | 30 | 300 |
| A | 40 | 400 |
| B | 15 | 150 |
| B | 25 | 250 |
| B | 35 | 350 |
一、按 a 分组,取 b 最小 / 最大时对应的 c 字段
1. 取 b 最小时对应的 c——row_number()
1
2
3
4
5
6
7
8
| select
a as group_a,b as min_b,c
from
(
select a,b,c,row_number() over(partition by a order by b asc) as rn
from abc_test
)as tmp
where rn = 1;
|
| group_a | min_b | c |
|---|
| A | 10 | 100 |
| B | 15 | 150 |
含义:
PARTITION BY a 每个 a 分组内部单独排序。
ORDER BY b ASC 表示按 b 从小到大排序。
rn = 1 表示取每组中 b 最小的那一行。
2. 取 b 最大时对应的 c
1
2
3
4
5
6
7
8
| select
a as group_a,b as maz_b,c
from
(
select a,b,c,row_number() over(partition by a order by b desc) as rn
from abc_test
)as tmp
where rn = 1;
|
| group_a | maz_b | c |
|---|
| A | 40 | 400 |
| B | 35 | 350 |
3. 如果 b 有重复值怎么办?
例如:
1
2
3
| insert into abc_test values
('A',10,999),
('A',20,999);
|
如果你用:row_number() 只会随机/按额外排序规则取其中一条。
如果你希望 所有 b 最小的记录都取出来,应该用:rank()同排名的下一个跳号, 或 dense_rank()同排名的下一个不跳号
1
2
3
4
5
6
7
8
| select
a as group_a,b as maz_b,c
from
(
select a,b,c,rank() over(partition by a order by b asc) as rn
from abc_test
)as tmp
where rn = 1;
|
| group_a | maz_b | c |
|---|
| A | 10 | 100 |
| A | 10 | 999 |
| B | 15 | 150 |
二、按 a 分组,取 b 字段第二小 / 第二大时对应的 c 字段
1. 取 b 第二小时对应的 c
1
2
3
4
5
6
7
8
| select
a as group_a,b as maz_b,c
from
(
select a,b,c,dense_rank() over(partition by a order by b asc) as rn
from abc_test
)as tmp
where rn = 2;
|
| group_a | maz_b | c |
|---|
| A | 20 | 200 |
| A | 20 | 999 |
| B | 25 | 250 |
2. 取 b 第二大时对应的 c
1
2
3
4
5
6
7
8
| select
a as group_a,b as maz_b,c
from
(
select a,b,c,dense_rank() over(partition by a order by b desc) as rn
from abc_test
)as tmp
where rn = 2;
|
| group_a | maz_b | c |
|---|
| A | 30 | 300 |
| B | 25 | 250 |
3. 第二小到底是“第二行”还是“第二个不同的 b 值”?
这是面试/实际开发中非常重要的区别。
假设数据:
| a | b | c |
|---|
| A | 10 | 100 |
| A | 10 | 101 |
| A | 20 | 200 |
| A | 30 | 300 |
情况一:取排序后的第二行
用row_number()结果可能是:
因为它只是第 2 行。
情况二:取第二个不同的 b 值
应该用:dense_rank()
结果是:
三、按 a 分组,取 b 字段前二小 / 前二大时对应的 c 字段
1. 取 b 前二小对应的 c
1
2
3
4
5
6
7
8
| select
a as group_a,b as min_b,c
from
(
select a,b,c,row_number() over(partition by a order by b asc) as rn
from abc_test
)as tmp
where rn <= 2;
|
| group_a | min_b | c |
|---|
| A | 10 | 100 |
| A | 10 | 999 |
| B | 15 | 150 |
| B | 25 | 250 |
2. 取 b 前二大对应的 c
1
2
3
4
5
6
7
8
| select
a as group_a,b as min_b,c
from
(
select a,b,c,row_number() over(partition by a order by b desc) as rn
from abc_test
)as tmp
where rn <= 2;
|
| group_a | min_b | c |
|---|
| A | 40 | 400 |
| A | 30 | 300 |
| B | 35 | 350 |
| B | 25 | 250 |
3. 如果要取前两个不同的 b 值
1
2
3
4
5
6
7
8
| select
a as group_a,b as min_b,c
from
(
select a,b,c,dense_rank() over(partition by a order by b asc) as rn
from abc_test
)as tmp
where rn <= 2;
|
| group_a | min_b | c |
|---|
| A | 10 | 100 |
| A | 10 | 999 |
| A | 20 | 200 |
| A | 20 | 999 |
| B | 15 | 150 |
| B | 25 | 250 |
四、按 a 分组,b 字段排序,对 c 做累计和
先恢复为初始数据
1
2
3
4
5
6
| insert overwrite table abc_test
select *
from interview.abc_test
where c != 999;
select * FROM abc_test;
|
| a | b | c |
|---|
| A | 10 | 100 |
| A | 20 | 200 |
| A | 30 | 300 |
| A | 40 | 400 |
| B | 15 | 150 |
| B | 25 | 250 |
| B | 35 | 350 |
这是典型的窗口聚合题。
1
2
3
4
5
6
7
8
9
10
11
| SELECT
a,
b,
c,
-- 窗口函数正确语法:聚合函数(字段) OVER(窗口定义)
SUM(c) OVER(
PARTITION BY a
ORDER BY b ASC -- 必须指定排序字段,否则累计和无意义!
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS sum_c
FROM abc_test;
|
PARTITION BY a ,每个 a 分组内计算。
ORDER BY b ASC,按照 b 从小到大定义累计顺序。
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW:窗口范围是:从当前分组第一行,到当前行。
结果:
| a | b | c | sum_c |
|---|
| A | 10 | 100 | 100 |
| A | 20 | 200 | 300 |
| A | 30 | 300 | 600 |
| A | 40 | 400 | 1000 |
| B | 15 | 150 | 150 |
| B | 25 | 250 | 400 |
| B | 35 | 350 | 750 |
五、按 a 分组,b 字段排序,对 c 做累计平均值
1
2
3
4
5
6
7
8
9
10
11
| SELECT
a,
b,
c,
-- 窗口函数正确语法:聚合函数(字段) OVER(窗口定义)
avg(c) OVER(
PARTITION BY a
ORDER BY b ASC -- 必须指定排序字段,否则累计和无意义!
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS sum_c
FROM abc_test;
|
| a | b | c | avg_c |
|---|
| A | 10 | 100 | 100.0 |
| A | 20 | 200 | 150.0 |
| A | 30 | 300 | 200.0 |
| A | 40 | 400 | 250.0 |
| B | 15 | 150 | 150.0 |
| B | 25 | 250 | 200.0 |
| B | 35 | 350 | 250.0 |
六、按 a 分组,b 字段排序,对 c 做累计排名比例
这里要先明确:累计排名比例 通常有几种理解。
1. 当前行在组内的排名比例
当前行排名 / 当前组总行数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| SELECT
a,
b,
c,
-- 以a分组,按b升序,生成行号
row_number() OVER(
PARTITION BY a
ORDER BY b ASC
) AS rn,
-- a分组的元素数
count(*) OVER (
PARTITION BY a
) AS total_cnt,
-- 计算排名比例rank_ratio=排名/总元素数
-- 在 Hive 中整数除法可能导致结果不符合预期,建议转成 double:
cast(row_number() OVER (
PARTITION BY a
ORDER BY b ASC
)AS double)
/
count(*) OVER (
PARTITION BY a
) AS rank_ratio
FROM abc_test;
|
| a | b | c | rn | total_cnt | rank_ratio |
|---|
| A | 10 | 100 | 1 | 4 | 0.25 |
| A | 20 | 200 | 2 | 4 | 0.5 |
| A | 30 | 300 | 3 | 4 | 0.75 |
| A | 40 | 400 | 4 | 4 | 1.0 |
| B | 15 | 150 | 1 | 3 | 0.3333333333333333 |
| B | 25 | 250 | 2 | 3 | 0.6666666666666666 |
| B | 35 | 350 | 3 | 3 | 1.0 |
2. Hive 内置函数:percent_rank()
percent_rank() 的公式 :(rank - 1) / (总行数 - 1),所以第一行是 0,最后一行是 1
1
2
3
4
5
6
7
8
9
10
| -- 正确写法:percent_rank() 不需要ROWS BETWEEN子句
SELECT
a,
b,
c,
percent_rank() OVER(
PARTITION BY a
ORDER BY b ASC
) AS percent_rank_value
FROM abc_test;
|
| a | b | c | percent_rank_value |
|---|
| A | 10 | 100 | 0.0 |
| A | 20 | 200 | 0.3333333333333333 |
| A | 30 | 300 | 0.6666666666666666 |
| A | 40 | 400 | 1.0 |
| B | 15 | 150 | 0.0 |
| B | 25 | 250 | 0.5 |
| B | 35 | 350 | 1.0 |
3. Hive 内置函数:cume_dist()
cume_dist() 更接近“累计分布比例” = 当前行及之前的行数 / 总行数
1
2
3
4
5
6
7
8
9
| SELECT
a,
b,
c,
cume_dist() OVER(
PARTITION BY a
ORDER BY b ASC
) AS cume_dist_value
FROM abc_test;
|
| a | b | c | cume_dist_value |
|---|
| A | 10 | 100 | 0.25 |
| A | 20 | 200 | 0.5 |
| A | 30 | 300 | 0.75 |
| A | 40 | 400 | 1.0 |
| B | 15 | 150 | 0.3333333333333333 |
| B | 25 | 250 | 0.6666666666666666 |
| B | 35 | 350 | 1.0 |
七、按 a 分组,b 字段排序,对 c 做累计和比例
当前累计 c 之和 / 当前组 c 总和
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| SELECT
a,
b,
c,
-- 窗口函数正确语法:聚合函数(字段) OVER(窗口定义)
sum(c) OVER(
PARTITION BY a
ORDER BY b asc
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS current_sum_value,
sum(c) OVER (
PARTITION BY a
) AS total_sum_value,
cast(sum(c) OVER(
PARTITION BY a
ORDER BY b asc
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)AS double)
/
sum(c) OVER (
PARTITION BY a
) as sum_percent
FROM abc_test;ercent
FROM abc_test;
|
| a | b | c | current_sum_value | total_sum_value | sum_percent |
|---|
| A | 10 | 100 | 100 | 1000 | 0.1 |
| A | 20 | 200 | 300 | 1000 | 0.3 |
| A | 30 | 300 | 600 | 1000 | 0.6 |
| A | 40 | 400 | 1000 | 1000 | 1.0 |
| B | 15 | 150 | 150 | 750 | 0.2 |
| B | 25 | 250 | 400 | 750 | 0.5333333333333333 |
| B | 35 | 350 | 750 | 750 | 1.0 |
八、按 a 分组,b 字段排序,对 c 取前后各一行的和
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| SELECT
a,
b,
c,
-- 前一行的值
lag(c,1,0) OVER(
PARTITION BY a
ORDER BY b asc
) AS pre_value,
-- 后一行的值
lead(c,1,0) OVER(
PARTITION BY a
ORDER BY b asc
) AS after_value,
-- 前一行+后一行的值
lag(c,1,0) OVER(
PARTITION BY a
ORDER BY b asc
)+
lead(c,1,0) OVER(
PARTITION BY a
ORDER BY b asc
) as sum_pre_after
FROM abc_test;
|
| a | b | c | pre_value | after_value | sum_pre_after |
|---|
| A | 10 | 100 | 0 | 200 | 200 |
| A | 20 | 200 | 100 | 300 | 400 |
| A | 30 | 300 | 200 | 400 | 600 |
| A | 40 | 400 | 300 | 0 | 300 |
| B | 15 | 150 | 0 | 250 | 250 |
| B | 25 | 250 | 150 | 350 | 500 |
| B | 35 | 350 | 250 | 0 | 250 |
如果需要包含当前值,则可以使用ROWS BETWEEN 1 PRECEDING AND 1 following
1
2
3
4
5
6
7
8
9
10
11
| SELECT
a,
b,
c,
-- 窗口函数正确语法:聚合函数(字段) OVER(窗口定义)
sum(c) OVER(
PARTITION BY a
ORDER BY b asc
ROWS BETWEEN 1 PRECEDING AND 1 following
) AS current_sum_value
FROM abc_test;
|
| a | b | c | current_sum_value |
|---|
| A | 10 | 100 | 300 |
| A | 20 | 200 | 600 |
| A | 30 | 300 | 900 |
| A | 40 | 400 | 700 |
| B | 15 | 150 | 400 |
| B | 25 | 250 | 750 |
| B | 35 | 350 | 600 |
九、按 a 分组,b 字段排序,对 c 取平均值
情况一:取整个 a 分组内 c 的平均值
每个 a 分组内,所有 c 的平均值。
1
2
3
4
5
6
7
8
| SELECT
a,
b,
c,
AVG(c) OVER (
PARTITION BY a
) AS avg_c_in
FROM abc_test;
|
| a | b | c | avg_c_in |
|---|
| A | 10 | 100 | 250.0 |
| A | 20 | 200 | 250.0 |
| A | 30 | 300 | 250.0 |
| A | 40 | 400 | 250.0 |
| B | 15 | 150 | 250.0 |
| B | 25 | 250 | 250.0 |
| B | 35 | 350 | 250.0 |
情况二:按 b 排序后,取当前行及之前所有行的累计平均值
1
2
3
4
5
6
7
8
9
10
| SELECT
a,
b,
c,
AVG(c) OVER (
PARTITION BY a
ORDER BY b asc
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS avg_pre_curr
FROM abc_test;
|
| a | b | c | avg_c_in |
|---|
| A | 10 | 100 | 100.0 |
| A | 20 | 200 | 150.0 |
| A | 30 | 300 | 200.0 |
| A | 40 | 400 | 250.0 |
| B | 15 | 150 | 150.0 |
| B | 25 | 250 | 200.0 |
| B | 35 | 350 | 250.0 |
情况三:按 b 排序后,取当前行、上一行、下一行,三行的滑动平均值
1
2
3
4
5
6
7
8
9
10
| SELECT
a,
b,
c,
AVG(c) OVER (
PARTITION BY a
ORDER BY b asc
ROWS BETWEEN 1 PRECEDING AND 1 following
) AS avg_pre_curr_follow
FROM abc_test;
|
| a | b | c | avg_pre_curr |
|---|
| A | 10 | 100 | 150.0 |
| A | 20 | 200 | 200.0 |
| A | 30 | 300 | 300.0 |
| A | 40 | 400 | 350.0 |
| B | 15 | 150 | 200.0 |
| B | 25 | 250 | 250.0 |
| B | 35 | 350 | 300.0 |
情况四:按 b 排序后,取上一行、当前行、两行的滑动平均值
1
2
3
4
5
6
7
8
9
10
| SELECT
a,
b,
c,
AVG(c) OVER (
PARTITION BY a
ORDER BY b asc
ROWS BETWEEN 1 PRECEDING AND current row
) AS avg_pre_curr
FROM abc_test;
|
| a | b | c | avg_pre_curr_follow |
|---|
| A | 10 | 100 | 100.0 |
| A | 20 | 200 | 150.0 |
| A | 30 | 300 | 250.0 |
| A | 40 | 400 | 350.0 |
| B | 15 | 150 | 150.0 |
| B | 25 | 250 | 200.0 |
| B | 35 | 350 | 300.0 |
十、核心函数总结
1. 排名类窗口函数
| 函数 | 作用 | 是否跳号 | 典型场景 |
|---|
row_number() | 每行生成唯一序号 | 不涉及并列 | 每组取第一行、第二行、TopN |
rank() | 并列同名次 | 会跳号 | 需要保留并列排名 |
dense_rank() | 并列同名次 | 不跳号 | 取第 N 个不同值 |
percent_rank() | 排名百分比 | - | 排序位置比例 |
cume_dist() | 累积分布比例 | - | 当前值及之前占比 |
2. 偏移类窗口函数
| 函数 | 作用 | 典型场景 |
|---|
lag(c, 1) | 取上一行 c | 环比、差值、前值比较 |
lead(c, 1) | 取下一行 c | 后值比较、区间结束值 |
first_value(c) | 当前窗口第一行 c | 取首个值 |
last_value(c) | 当前窗口最后一行 c | 取末尾值,注意窗口范围 |
3. 聚合类窗口函数
| 函数 | 作用 | 典型场景 |
|---|
sum(c) over (...) | 窗口内求和 | 累计和、占比 |
avg(c) over (...) | 窗口内平均 | 累计均值、滑动均值 |
count(*) over (...) | 窗口内计数 | 排名比例、总行数 |
max(c) over (...) | 窗口内最大 | 组内最大值 |
min(c) over (...) | 窗口内最小 | 组内最小值 |
十一、最重要的规律总结
规律一:只要题目里出现“按 a 分组”
大概率写:
规律二:只要题目里出现“按 b 排序”
大概率写:
1
2
3
| ORDER BY b ASC
- 或:
ORDER BY b DESC
|
规律三:只要题目里出现“第几小 / 第几大 / 前几名”
优先考虑:
1
2
3
| row_number()
rank()
dense_rank()
|
选择规则:
| 需求 | 推荐函数 |
|---|
| 只要一行,严格第 N 行 | row_number() |
| 并列同名次,并且允许跳号 | rank() |
| 按不同值排名,不跳号 | dense_rank() |
规律四:只要题目里出现“累计”
优先考虑 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
1
2
3
4
5
| SUM(c) OVER (
PARTITION BY a
ORDER BY b
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)
|
规律五:只要题目里出现“前一行 / 后一行”
优先考虑:
规律六:只要题目里出现“占比”
通常是:
当前值 / 总值 c / SUM(c) OVER (PARTITION BY a)
或者:
累计值 / 总值
1
2
3
4
5
6
7
| SUM(c) OVER (
PARTITION BY a
ORDER BY b
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)
/
SUM(c) OVER (PARTITION BY a)
|