hive中分组取前N个值的实现

本文介绍如何使用Hive SQL结合自定义函数row_number()实现分组取TopN的功能,包括不同场景下的具体实现方法及注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

背景

假设有一个学生各门课的成绩的表单,应用hive取出每科成绩前100名的学生成绩。

这个就是典型在分组取Top N的需求。

 

解决思路

对于取出每科成绩前100名的学生成绩,针对学生成绩表,根据学科,成绩做order by排序,然后对排序后的成绩,执行自定义函数row_number(),必须带一个或者多个列参数,如ROW_NUMBER(col1, ....),它的作用是按指定的列进行分组生成行序列。在ROW_NUMBER(a,b) 时,若两条记录的a,b列相同,则行序列+1,否则重新计数。

只要返回row_number()返回值小于100的的成绩记录,就可以返回每个单科成绩前一百的学生。

 

解决过程

成绩表结构

create table score_table (
  subject        string,
  student       string,
  score           int
)
partitioned by (date string)

 

如果要查询2012年每科成绩前100的学生成绩,sql如下

create temporary function row_number as 'com.blue.hive.udf.RowNumber';
select subject,score,student from
    (select subject,score,student from score where dt='2012'  order by subject,socre desc) order_score
where row_number(subject) <= 100;

com.blue.hive.udf.RowNumber是自定义函数,函数的作用是按指定的列进行分组生成行序列。这里根据每个科目的所有成绩,生成序列,序列值从1开始自增。

假设成绩表的记录如下:

复制代码
物理  80 张三
数学  100 李一
物理  90  张二
数学  90  李二
物理  100 张一
数学  80  李三
.....
复制代码

经过order by全局排序后,记录如下

复制代码
物理  100 张一
物理  90  张二
物理  80 张三
.....
数学  100 李一
数学  90  李二
数学  80  李三
....
复制代码

接着执行row_number函数,返回值如下

复制代码
科目  成绩 学生   row_number
物理  100 张一      1
物理  90  张二      2
物理  80  张三      3
.....
数学  100 李一      1
数学  90  李二      2
数学  80  李三      3
....
复制代码

因为hive是基于MAPREADUCE的,必须保证row_number执行是在reducer中执行。上述的语句保证了成绩表的记录,按照科目和成绩做了全局排序,然后在reducer端执行row_number函数,如果在map端执行了row_number,那么结果将是错误的。

要查看row_number函数在map端还是reducer端执行,可以查看hive的执行计划:

create temporary function row_number as 'com.blue.hive.udf.RowNumber';
explain select subject,score,student from
    (select subject,score,student from score where dt='2012'  order by subject,socre desc) order_score
where row_number(subject) <= 100;

explain不会执行mapreduce计算,只会显示执行计划。

 

只要row_number函数在reducer端执行,除了使用order by全局排序配合,也可以使用distribute by + sort by。distribute by可以让相同科目的成绩记录发送到同一个reducer,而sort by可以在reducer端对记录做排序。

而使用order by全局排序,只有一个reducer,未能充分利用资源,相比之下,distribute by + sort by在这里更有性能优势,可以在多个reducer做排序,再做row_number的计算。

sql如下:

create temporary function row_number as 'com.blue.hive.udf.RowNumber';
select subject,score,student from
    (select subject,score,student from score where dt='2012'  distribute by subject sort by subject asc, socre desc) order_score
where row_number(subject) <= 100;

 

如果成绩有学院字段college,要找出学院里,单科成绩前一百的学生,解决方法如下:

create temporary function row_number as 'com.blue.hive.udf.RowNumber';
explain select college,subject,score,student from
    (select college,subject,score,student from score where dt='2012'  order by college asc,subject asc,socre desc) order_score
where row_number(college,subject) <= 100;

 

如果成绩有学院字段college,要找出学院里,总成绩前一百的学生,解决方法如下:

create temporary function row_number as 'com.blue.hive.udf.RowNumber';
explain select college,totalscore,student from
    (select college,student,sum(score) as totalscore from score where dt='2012'  group by college,student  order by college asc,totalscore desc) order_score
where row_number(college) <= 100;

 

row_number的源码

函数row_number(),必须带一个或者多个列参数,如ROW_NUMBER(col1, ....),它的作用是按指定的列进行分组生成行序列。在ROW_NUMBER(a,b) 时,若两条记录的a,b列相同,则行序列+1,否则重新计数。

复制代码
package com.blue.hive.udf;

import org.apache.hadoop.hive.ql.exec.UDF;

public class RowNumber extends UDF {

    private static int MAX_VALUE = 50;
    private static String comparedColumn[] = new String[MAX_VALUE];
    private static int rowNum = 1;

    public int evaluate(Object... args) {
        String columnValue[] = new String[args.length];
        for (int i = 0; i < args.length; i++) 『
            columnValue[i] = args[i].toString();
        }
        if (rowNum == 1) {
            for (int i = 0; i < columnValue.length; i++)
                comparedColumn[i] = columnValue[i];
        }

        for (int i = 0; i < columnValue.length; i++) {
            if (!comparedColumn[i].equals(columnValue[i])) {
                for (int j = 0; j < columnValue.length; j++) {
                    comparedColumn[j] = columnValue[j];
                }
                rowNum = 1;
                return rowNum++;
            }
        }
        return rowNum++;
    }
}
复制代码

编译后,打包成一个jar包,如/usr/local/hive/udf/blueudf.jar

然后在hive shell下使用,如下:

add jar /usr/local/hive/udf/blueudf.jar;
create temporary function row_number as 'com.blue.hive.udf.RowNumber';
select subject,score,student from
    (select subject,score,student from score where dt='2012'  order by subject,socre desc) order_score
where row_number(subject) <= 100;

 

参考 http://chiyx.iteye.com/blog/1559460 

<think>我们面临的任务:在HiveSQL中根据idtime字段,将数据按照time维度分成10份(即生成10个等宽的时间区间)。注意:用户要求的是针对每个id?还是全局?从描述看是全局时间分段(minmax是全局的),但提到id字段,可能需要对每个id单独分段?然而问题描述是“将表中的数据按照time维度的最小最大分成10份”,所以是全局分段。但是用户提到“根据idtime字段”,这里可能存在歧义。根据问题上下文,我理解是:先获全局time的最小最大,然后将整个时间范围等分成10个区间,每个区间的时间跨度相同。然而,如果用户是想针对每个id单独计算其时间范围并分段,那么写法会不同。但问题描述没有明确要求按每个id分段,且初始要求是“表中time维度的最小最大作为下界上界”,所以按全局分段。步骤:1.计算全局最小时间(min_time)最大时间(max_time)。2.将时间范围(max_time-min_time)等分成10份,得到每个区间的时间长度:$$\text{interval\_length}=\frac{\text{max\_time}-\text{min\_time}}{10}$$3.生成10个区间的边界点(共11个点,包括最小最大)。在HiveSQL中,我们可以使用窗口函数或者交叉连接(CROSSJOIN)来实现。但是注意,HiveSQL中生成序列可以使用`posexplode`或者`lateralview`结合`split``space`等函数模拟。由于Hive没有直接提供generate_series,我们可以用以下方法生成0到10的数字序列:-使用`posexplode(split(space(10),''))`,其中space(10)生成10个空格,split后得到11个元素(因为10个空格会被split成11个部分,从0到10的索引)。但是更简单的方式是使用`lateralviewposexplode(split(space(10),''))`,注意这样会产生0到10的索引(索引从0开始,共11个)。另一种方法是使用`row_number()over()`,但是需要有一个已知行数的表。如果表很大,我们可以直接利用原表(但这样会产生很多重复行,效率低),所以更推荐用虚拟表。由于Hive不支持generate_series,我们可以考虑创建一个0-10的临时数字序列。具体步骤:1.计算全局最小时间最大时间(用子查询)。2.生成一个0到10的序列(11个数字)。3.计算每个分割点的时间:$$\text{min\_time}+\text{index}\times\frac{\text{max\_time}-\text{min\_time}}{10}$$但是,时间类型在Hive中通常是timestamp或string,需要转换为时间戳(unix时间戳)进行计算,然后再转回时间格式。假设time_column是timestamp类型(或者可以转换为时间戳的字符串),我们可以这样处理:-将min_timemax_time转换为unix时间戳(单位为秒)。-计算总时间差(秒数):`max_ts-min_ts`。-然后每个分割点的时间戳为:`min_ts+(max_ts-min_ts)*n/10.0`(n从0到10)。-再将时间戳转换为timestamp类型。但是,如果用户要求的是每个id单独分段,那么我们需要按id分组计算minmax,然后对每个id生成10个区间。但问题描述没有明确,我们按照全局分段处理。下面给出全局分段的HiveSQL:注意:Hive中除法默认是整数除法?为了避免整数除法,我们需要确保除法操作中至少有一个操作数是浮点数。我们使用如下方法:```sqlWITHtime_boundsAS(SELECTMIN(time_column)ASmin_time,MAX(time_column)ASmax_timeFROMyour_table),numbersAS(SELECTnFROM(SELECTposexplode(split(space(10),''))AS(n,_dummy)--n从0到10)t)SELECTn,--区间索引(0到10)--计算每个分割点的时间戳(从unix时间戳转换而来)from_unixtime(unix_timestamp(min_time)+(unix_timestamp(max_time)-unix_timestamp(min_time))*n/10.0)ASsegment_timeFROMtime_boundsCROSSJOINnumbersORDERBYn;```但是,上面的方法使用了`space(10)`来生成11个数字(0到10)。注意:`split(space(10),'')`会生成一个包含11个元素的数组(因为10个空格分割后是11个空字符串),然后`posexplode`会同时输出位置索引(从0开始)元素(空字符串)。我们只需要索引`n`。另外,如果时间类型已经是时间戳格式(比如bigint,表示毫秒或秒),那么需要调整:-如果时间戳是毫秒,则计算时也要用毫秒:`min_time+(max_time-min_time)*n/10.0`但是,如果原始时间字段是字符串,我们使用`unix_timestamp`转换时要注意格式匹配,或者先转换为timestamp。重要:在Hive中,`unix_timestamp`函数默认格式是'yyyy-MM-ddHH:mm:ss'。如果不是这个格式,需要指定格式。另一种方案:使用Hive的数字序列生成方法:利用`stack`函数生成11行。我们可以这样写:```sqlSELECTnfrom(select0asn)tlateralviewposexplode(split(space(10),''))dummyasn_index,n_value```其实上面已经可以生成0-10的序列,但是更简洁的方式是使用`lateralview`直接生成序列:不过,上面的方法已经可以了。下面是一个完整例子:假设表名为`your_table`,时间字段为`event_time`(string类型,格式为'yyyy-MM-ddHH:mm:ss'):```sqlWITHtime_boundsAS(SELECTMIN(event_time)ASmin_time,MAX(event_time)ASmax_timeFROMyour_table),number_sequenceAS(SELECTposexplode(split(space(10),''))AS(n,dummy))SELECTn,from_unixtime(unix_timestamp(min_time)+(unix_timestamp(max_time)-unix_timestamp(min_time))*n/10.0)assegment_timeFROMtime_boundsCROSSJOINnumber_sequence```注意:这个查询会返回11个时间点(从第0个到第10个),对应10个区间:[0,1),[1,2),...[9,10](但最后一个区间是闭区间,因为第10个点就是max_time)。但是,如果用户希望获每个时间区间内的数据分布,那么我们需要将数据分配到这些区间中。然而用户目的问题只是要求生成分段的边界点。如果用户需要将数据按照这10个区间进行分组统计,我们可以使用这些边界点进行分组。但是,生成分组查询需要用到窗口函数(比如NTILE)或者使用CASEWHEN。不过NTILE是等分数据的,而这里要求等分时间范围(等宽分箱),所以使用边界点进行分配。例如,统计每个区间的数据量:```sqlWITHtime_boundsAS(...),--同上segmentsAS(...)--同上生成11个点SELECTbucket,COUNT(*)ascntFROM(SELECTt.*,--使用生成的区间点,找到每个event_time所属的区间(用区间左端点标记)--方法:找到第一个大于event_time的分割点,那么上一个区间就是所属区间--这里我们使用数组lateralviewexplode来分配,或者使用窗口函数--更高效的方式是计算每个时间所属的区间索引:floor((unix_timestamp(event_time)-min_ts)/((max_ts-min_ts)/10))--注意:这样计算需要全局min_ts,max_ts--我们可以在time_bounds中计算总时间跨度区间长度,然后计算每个时间落在哪个桶--但是,由于可能超出范围,所以需要限制在0-9--另一种方法:使用UDF进行分配,但是为了纯SQL,我们这样:--步骤:--1.将全局边界区间长度计算出来--2.对每条记录,计算:(unix_timestamp(event_time)-min_ts)/((max_ts-min_ts)/10),然后整数部分(向下整),得到索引(0-9)--3.但是注意:当event_time等于max_time时,索引应该是9(因为最大时间属于最后一个区间),但是计算可能会得到10,所以需要处理:如果索引>=10,则设为9。--所以:WITHglobalAS(SELECTunix_timestamp(MIN(event_time))asmin_ts,unix_timestamp(MAX(event_time))asmax_ts,(unix_timestamp(MAX(event_time))-unix_timestamp(MIN(event_time)))asrange_secFROMyour_table),bucketsAS(SELECTyour_table.*,--计算桶索引:floor((unix_timestamp(event_time)-min_ts)/(range_sec/10))--避免除零,当range_sec=0时,所有数据都在一个桶CASEWHENrange_sec=0THEN0ELSECAST(LEAST(FLOOR((unix_timestamp(event_time)-min_ts)/(range_sec/10.0)),9)ASINT)ENDasbucket_idFROMyour_tableCROSSJOINglobal)SELECTbucket_id,COUNT(*)ascntFROMbucketsGROUPBYbucket_idORDERBYbucket_id;```注意:以上方法避免了生成11个边界点,直接计算每个记录所属的桶。但是用户原始问题是要求生成边界点,所以上面的生成边界点的SQL已经完成用户要求。根据用户后续问题,我们可能还需要回答每个区间的分布情况。回到问题:用户要求的是“将数据分成10份”,但是怎么分?是生成10个区间边界点,还是将数据划分到10个区间?从问题描述看是生成边界点。因此,我们先给出生成10份(11个边界点)的HiveSQL。如果用户需要将数据划分到区间,那么我们可以使用上面桶分配的方法。现在,我们按照用户问题,给出生成边界点的HiveSQL(兼容Hive,且处理时间类型):注意:如果时间字段是bigint类型(如毫秒时间戳),则不需要使用unix_timestamp转换,但是要注意单位。假设时间字段是timestamp或string(标准格式),我们使用上述方法。完整代码:```sqlWITHtime_boundsAS(SELECTMIN(time_column)ASmin_time,MAX(time_column)ASmax_timeFROMyour_table),--生成0到10的数字序列(11个数)numbersAS(SELECTtmp.nASnFROM(SELECT1)tLATERALVIEWposexplode(split(space(10),''))tmpASn,dummy)SELECTn,from_unixtime(unix_timestamp(min_time)+(unix_timestamp(max_time)-unix_timestamp(min_time))*(n/10.0))ASsegment_timeFROMtime_boundsCROSSJOINnumbers;```注意:`space(10)`生成10个空格,`split(space(10),'')`生成一个包含11个元素的数组(每个元素都是空字符串)。`posexplode`返回每个元素的位置索引(从0开始)(空字符串)。我们使用位置索引`n`作为序列。另外,当`min_time``max_time`相等时,每个分割点的时间都是同一个时间。但是,Hive的`unix_timestamp`函数如果传入的是timestamp类型,在Hive中timestamp类型在内部是以时间戳存储的,所以也是可以直接用unix_timestamp转换的。如果时间字段是bigint类型(毫秒时间戳),则调整为:```sqlSELECTn,--时间戳单位是毫秒,所以整个计算用毫秒--但from_unixtime要求秒,所以需要除以1000from_unixtime((min_time_ms+(max_time_ms-min_time_ms)*n/10.0)/1000)ASsegment_time```所以,我们需要根据实际表的时间字段类型进行调整。最后,如果生成的分割点需要用于其他查询,可以将该查询保存为一个临时视图。我们假设时间字段为字符串(标准时间格式)的情况下,给出最终答案。</think> ### HiveSQL 实现时间维度数据分段方案 #### 解决方案 在 HiveSQL 中,可以通过以下步骤实现全局时间维度的等分切割: ```sql WITH time_bounds AS ( SELECT MIN(time_column) AS min_time, MAX(time_column) AS max_time, (unix_timestamp(MAX(time_column)) - unix_timestamp(MIN(time_column))) AS total_seconds FROM your_table ), segments AS ( SELECT n, from_unixtime( unix_timestamp(min_time) + (n * total_seconds / 10.0) ) AS time_tick FROM time_bounds LATERAL VIEW posexplode(split(space(10), ' ')) t AS n, dummy -- 生成0-10的序列 ) SELECT time_tick FROM segments ORDER BY time_tick; ``` #### 核心原理 1. **时间分割公式**: $$ \text{time\_tick}_n = \text{min\_time} + n \times \frac{\text{total\_seconds}}{10} \times 1\text{秒} $$ 其中 $n \in [0, 10]$ 2. **关键组件**: - `unix_timestamp()`:将时间转为秒级时间戳 - `posexplode(split(space(10), ' '))`:生成0-10的数字序列 - `from_unixtime()`:将时间戳转回标准时间格式 3. **特殊处理**: - 当 min_time = max_time 时,所有时间点重叠 - 使用`10.0`确保浮点数除法(避免整数除法截断) - 生成**11个边界点**(10个区间) #### 分段结果应用示例 统计各时间区间内的数据分布: ```sql WITH bounds AS ( SELECT MIN(event_time) AS min_t, MAX(event_time) AS max_t, unix_timestamp(MAX(event_time)) - unix_timestamp(MIN(event_time)) AS sec FROM user_logs ), segments AS ( SELECT n, from_unixtime(unix_timestamp(min_t) + n*sec/10.0) AS start_time, from_unixtime(unix_timestamp(min_t) + (n+1)*sec/10.0) AS end_time FROM bounds LATERAL VIEW posexplode(split(space(9), ' ')) t AS n, dummy -- 生成0-9 ) SELECT s.n AS segment_id, s.start_time, s.end_time, COUNT(l.user_id) AS user_count FROM segments s JOIN user_logs l ON unix_timestamp(l.event_time) BETWEEN unix_timestamp(s.start_time) AND unix_timestamp(s.end_time) GROUP BY s.n, s.start_time, s.end_time ORDER BY segment_id; ``` #### 优化建议 1. **处理跨年数据**: ```sql -- 添加时间范围过滤 WHERE time_column BETWEEN '2023-01-01' AND '2023-12-31' ``` 2. **提升精度**: ```sql -- 使用毫秒级精度 SELECT from_unixtime( (unix_timestamp(min_time)*1000 + n*(unix_timestamp(max_time)*1000 - unix_timestamp(min_time)*1000)/10.0 ) / 1000 ) ``` 3. **动态分区**(预计算): ```sql CREATE TABLE time_segments AS SELECT ... -- 上述分段逻辑 ``` --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值