背景
项目中有一个报表是留存率,包括日留存,周留存,月留存。其中在计算周留存率时,可能会直接想到【weekofyear】这个函数,然后简单拼接年就可以得到周数,大部分时候是对的,但在跨年的时候可能会出现bug。
hive> select concat(year('2019-08-01'),'-',weekofyear('2019-08-01'));
OK
2019-31
Time taken: 3.217 seconds, Fetched: 1 row(s)
上面的结果一看也没啥问题,但如果选极端一点的日期,比如【2018-12-31】:
hive> select concat(year('2018-12-31'),'-',weekofyear('2018-12-31'));
OK
2018-1
Time taken: 0.09 seconds, Fetched: 1 row(s)
得到的结果明显是错误的,【2018-12-31】明显是2018年最后一天,却算出来的周数是【2018-1】(也就是2018年第一周),
上面例子可以引出一个问题,可能会出现一周跨年的情况,比如例子中的【2018-12-31】所在的周横跨了2018和2019年,那这周是属于2018还是2019年的呢?
当然,从上面结论来看,hive的【weekofyear】函数是把【2018-12-31】所在的周认为是2019年的第一周了,所以才会出现【2018-12-31】是2018年第一周的bug。
Q:hive的【weekofyear】是怎么判断跨年的周到底属于哪一年的呢?
A:先看hive对【weekofyear】函数的官方定义:
hive> describe function weekofyear;
OK
weekofyear(date) - Returns the week of the year of the given date. A week is considered to start on a Monday and week 1 is the first week with >3 days.
Time taken: 0.008 seconds, Fetched: 1 row(s)
1、每周是从周一开始算起的。
2、每年的第一周必须是属于这一年的天数大于3(不包括3)的第一周。
回到上面跨年周的问题,其实hive就是认为,跨年的这这一周,有4天及以上在新的一年,这周算是新一年的第一周,否则就是旧一年的最后一周。
比较典型的两个日期【2010-01-01】和【2014-12-31】(注:错误示例)
hive> select concat(year('2010-01-01'),'-',weekofyear('2010-01-01'));
OK
2010-53
Time taken: 0.143 seconds, Fetched: 1 row(s)
hive> select concat(year('2014-12-31'),'-',weekofyear('2014-12-31'));
OK
2014-1
Time taken: 0.066 seconds, Fetched: 1 row(s)
【2010-01-01】是星期五,这一周有4天在2009年,有3天在2010年,所以这周应该是2009年的最后一周,即第53周。
【2014-12-31】是星期三,这一周有3天在2014年,有4天在2015年,所以这周应该是2015年的第一周,即第1周。
使用case when 来判断【新一年的第一天】到【指定日期所在周的周一】的天数差值,如果差值大于3,将这周判定为【所在周的周一的日期所属的年份】即旧一年,否则将这周判定为【所在周的周日的日期所属的年份】即新一年。
-- 涉及到的 hive 日期函数 next_day, date_sub, add_months, trunc, datediff
select next_day('2018-12-31', 'MO') -- 获取下一周的周一日期
select date_sub(next_day('2018-12-31', 'MO'), 7) -- 在下一周的周一日期基础上,获取前7天的日期,实际就是获取本周周一的日期
select add_months(date_sub(next_day('2018-12-31', 'MO'), 7), 1) # 获取下一个月的日期
select trunc(add_months(date_sub(next_day('2018-12-31', 'MO'), 7), 1), 'MM'); -- 获取指定日期的当月第一天的日期。
select datediff(trunc(add_months(date_sub(next_day('2018-12-31', 'MO'), 7), 1), 'MM'), date_sub(next_day('2018-12-31', 'MO'), 7)) ; -- datediff 获取两个日期间隔, 这里其实就是获取新一年的第一天到指定日期天数。
select concat(
case when datediff(trunc(add_months(date_sub(next_day('2018-12-31', 'MO'), 7), 1), 'MM'), date_sub(next_day('2018-12-31', 'MO'), 7)) > 3
then year(date_sub(next_day('2018-12-31', 'MO'), 7 ))
else year(next_day('2018-12-31', 'SU'))
end,
'-',
case when weekofyear('2018-12-31') > 9
then weekofyear('2018-12-31')
else concat('0', weekofyear('2018-12-31'))
end
) as week
执行结果,【2018-12-31】不再是2018年的第一周,而是2019年的第一周!
hive> select concat(
> case when datediff(trunc(add_months(date_sub(next_day('2018-12-31', 'MO'), 7), 1), 'MM'), date_sub(next_day('2018-12-31', 'MO'), 7)) > 3
> then year(date_sub(next_day('2018-12-31', 'MO'), 7 ))
> else year(next_day('2018-12-31', 'SU'))
> end,
> '-',
> case when weekofyear('2018-12-31') > 9
> then weekofyear('2018-12-31')
> else concat('0', weekofyear('2018-12-31'))
> end
> ) as week
> ;
OK
2019-01
Time taken: 0.083 seconds, Fetched: 1 row(s)