【二】HiveSQL 排名中取他值

总结 HiveSQL 中按分组排名后取对应字段值的常见写法,包括 row_number、rank 等窗口函数的使用场景

全文共计3369字 次阅读

我先给一个统一的示例表,后面所有 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;
abc
A10100
A20200
A30300
A40400
B15150
B25250
B35350

一、按 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_amin_bc
A10100
B15150

含义:

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_amaz_bc
A40400
B35350

3. 如果 b 有重复值怎么办?

例如:

abc
A10100
A10999
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_amaz_bc
A10100
A10999
B15150

二、按 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_amaz_bc
A20200
A20999
B25250

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_amaz_bc
A30300
B25250

3. 第二小到底是“第二行”还是“第二个不同的 b 值”?

这是面试/实际开发中非常重要的区别。

假设数据:

abc
A10100
A10101
A20200
A30300

情况一:取排序后的第二行

row_number()结果可能是:

abc
A10101

因为它只是第 2 行。


情况二:取第二个不同的 b 值

应该用:dense_rank()

结果是:

abc
A20200

三、按 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_amin_bc
A10100
A10999
B15150
B25250

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_amin_bc
A40400
A30300
B35350
B25250

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_amin_bc
A10100
A10999
A20200
A20999
B15150
B25250

四、按 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;
abc
A10100
A20200
A30300
A40400
B15150
B25250
B35350

这是典型的窗口聚合题。

 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:窗口范围是:从当前分组第一行,到当前行。

结果:

abcsum_c
A10100100
A20200300
A30300600
A404001000
B15150150
B25250400
B35350750

五、按 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;
abcavg_c
A10100100.0
A20200150.0
A30300200.0
A40400250.0
B15150150.0
B25250200.0
B35350250.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;
abcrntotal_cntrank_ratio
A10100140.25
A20200240.5
A30300340.75
A40400441.0
B15150130.3333333333333333
B25250230.6666666666666666
B35350331.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;
abcpercent_rank_value
A101000.0
A202000.3333333333333333
A303000.6666666666666666
A404001.0
B151500.0
B252500.5
B353501.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;
abccume_dist_value
A101000.25
A202000.5
A303000.75
A404001.0
B151500.3333333333333333
B252500.6666666666666666
B353501.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;
abccurrent_sum_valuetotal_sum_valuesum_percent
A1010010010000.1
A2020030010000.3
A3030060010000.6
A40400100010001.0
B151501507500.2
B252504007500.5333333333333333
B353507507501.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;
abcpre_valueafter_valuesum_pre_after
A101000200200
A20200100300400
A30300200400600
A404003000300
B151500250250
B25250150350500
B353502500250

如果需要包含当前值,则可以使用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;
abccurrent_sum_value
A10100300
A20200600
A30300900
A40400700
B15150400
B25250750
B35350600

九、按 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;
abcavg_c_in
A10100250.0
A20200250.0
A30300250.0
A40400250.0
B15150250.0
B25250250.0
B35350250.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;
abcavg_c_in
A10100100.0
A20200150.0
A30300200.0
A40400250.0
B15150150.0
B25250200.0
B35350250.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;
abcavg_pre_curr
A10100150.0
A20200200.0
A30300300.0
A40400350.0
B15150200.0
B25250250.0
B35350300.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;
abcavg_pre_curr_follow
A10100100.0
A20200150.0
A30300250.0
A40400350.0
B15150150.0
B25250200.0
B35350300.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 分组”

大概率写:

1
PARTITION BY 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 
)

规律五:只要题目里出现“前一行 / 后一行”

优先考虑:

1
2
lag() 
lead()

规律六:只要题目里出现“占比”

通常是:

当前值 / 总值 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)
使用 Hugo 构建
主题 StackJimmy 设计
无法复制,本站文章内容受保护