mysql 连续签到天数_为啥APP里都有打卡签到?真相令人气愤!

555367feb08fe96a243417d543deb984.png

(CSDN博主:写代码也要符合基本法)

今天小刘看到一道有趣的SQL数据分析题目,准备和大家分享一下

不能只让我一个人秃头!!!

一软件中有用户每日签到功能,现在要用SQL计算出上月中各用户的最后一次连续签到的天数(只有一天的,算作连续一天)

26abdb95ba712a1245c8133fb9f4e985.png

01 数据准备

首先我们准备一下五个用户在七月份的随机签到数据

CREATE TABLE demo_checkin_history ASSELECT user_id, MAX(checkin_date) checkin_date  FROM (SELECT ceil(LEVEL / 31) user_id              ,to_date('20200701', 'yyyymmdd') + 31 * dbms_random.value checkin_date          FROM dual        CONNECT BY LEVEL <= 31 * 5) GROUP BY user_id, trunc(checkin_date);

26abdb95ba712a1245c8133fb9f4e985.png

02 题目分析

用户每次签到的记录在表中是行行分开的,想要检查日期是否连续,让人首先想到的是跨行引用,将一行前前后后的数据都引用到一行上,以便比较计算

这正是这道题迷惑人的地方,因为用户实际连续签到的天数是未知的,从而我们不能确定要跨行引用多少次才够用,相反如果是想要找到某月连续签到天数不小于N天的用户,这个思路倒也是可行的

既然实际连续签到了几天是未知数,那么小刘想到的是利用递归查询,从用户最后一次签到日期开始,每向下一层去寻找前一天的记录,直到断签,此时LEVEL还正好就是这组连续签到的天数

递归查询的根结点,选取每个用户的签到日期最大的那些行

递归的规则,即为同一用户的签到日期相差一天

另外我们只需得到连续签到的天数,即每个树最大的LEVEL值,从而我们将只筛选叶子结点的数据即可

SELECT t.user_id      ,t.checkin_date  checkin_date_from      ,connect_by_root t.checkin_date checkin_date_to      ,LEVEL           days  FROM demo_checkin_history t WHERE connect_by_isleaf = 1 START WITH t.checkin_date =            (SELECT MAX(u.checkin_date)               FROM demo_checkin_history u              WHERE u.user_id = t.user_id)CONNECT BY PRIOR trunc(t.checkin_date) = trunc(t.checkin_date) + 1       AND PRIOR t.user_id = t.user_id ORDER BY days DESC, checkin_date_from DESC;

b87b478b50b707610e6edf1eff502f87.png

对于递归查询不太熟悉的小伙伴,可以点击这里回顾

26abdb95ba712a1245c8133fb9f4e985.png

03 加大难度

前面是找出每个用户上月最后一次连续签到的数据,现在比方说老(diao)板(mao)改主意了,他想看看每个用户上月的最大连续签到天数对比

新需求对于我们上例程序的最大挑战在于,根结点定位规则变化了,而且是由相对固定变成了相对不定

当然,简单粗暴的办法是去掉START WITH子句,任由数据库从每一行分别去递归,最后找到每个客户的最大的LEVEL值就完事儿

但考虑到实际业务场景中,如果只有五个用户,那公司也就倒闭了,需要这么大费周章去计算报表的,用户量没准儿是上万的,百万行量级的表去放飞自我的反复递归,带来的必定是老板的怒火性能的浪费

所以,延续上例的思路,我们这里应当调整根结点定位规则,想办法去找到每一个断签点,从这些点开始往前捯

此时,跨行引用有了用武之地

在每一个用户的分区内,按日期排序后,排在某行前一位的日期不与本行日期连续,则为断签点

例如我们观察一下用户2的签到日期,断签点已被圈出

580e779889a946c44eea73afcd6377ca.png

观察数据我们发现,该用户不仅常常断签,而且坚持签到总是不超三日

这里的关键在于同样天数的连续签到,一个用户可能会有多组,那么我们就要在查询时考虑只取其中某一组,本例我们取最后一组

定位断签点为根结点的递归查询,我们可以这样来写

WITH checkin_his AS (SELECT t.user_id        ,t.checkin_date        ,lag(t.checkin_date) over(PARTITION BY t.user_id ORDER BY t.checkin_date DESC) next_checkin_date    FROM demo_checkin_history t),conn_rst AS (SELECT h.user_id        ,h.checkin_date checkin_date_from        ,connect_by_root h.checkin_date checkin_date_to        ,LEVEL days        ,MAX(LEVEL) over(PARTITION BY h.user_id) max_days        ,MAX(h.checkin_date) keep(dense_rank FIRST ORDER BY LEVEL DESC) over(PARTITION BY h.user_id) last_checkin_date_from    FROM checkin_his h   WHERE connect_by_isleaf = 1   START WITH h.next_checkin_date IS NULL           OR trunc(h.next_checkin_date) - trunc(h.checkin_date) > 1  CONNECT BY PRIOR h.user_id = h.user_id         AND PRIOR trunc(h.checkin_date) = trunc(h.checkin_date) + 1)SELECT c.user_id, c.checkin_date_from, c.checkin_date_to, c.days  FROM conn_rst c WHERE c.days = c.max_days   AND c.checkin_date_from = c.last_checkin_date_from ORDER BY days DESC, checkin_date_from DESC;

71a4d49bbd7d4d2b7eba5c13f7cb30e3.png

这里对于分析函数lag和dense_rank用法不太熟悉的小伙伴,请点击这里和这里回顾

26abdb95ba712a1245c8133fb9f4e985.png 04 转换思路

这个题目,无论是原题还是增加难度后的情况,事实上题干都可以回归到最基本的一个点——数(shǔ)数(shù)

SQL中有一个很朴素的实现计数的功能COUNT,但是我们看到题目后基本上第一个排除的就是用使用COUNT函数,因为使用GROUP BY分组的话,一定是要一致的值才能分到一组,而在本题中,连续签到对应的日期是变化的,所以不能分到一组,进而无法简单的计数

这就跟做选择题一样,往往我们首先排除的选项恰恰就是正确答案(猛男落泪)

此时此刻此情此景,我们需要运用一些数学课上学来的知识,数学中常用的一种思想就是

--=  化  变  元  为  常  量  =--

这里我们仍以用户2的数据来举例

以日期为横轴,累积签到天数作为纵轴,可以得到下图折线

441e3ca7f6816f7dec5dcd041d844cf6.png

图中线形水平的地方,是用户没有签到的日子,它们在数据表中是不存在的,这里只在图中补充出来

而线形上升的地方,正是用户连续签到的阶段,由于每天最多签到一次,所以这些上升的地方斜率都是1

这时我们再虚构一条折线,这条折线代表着某个勤奋的用户,坚持每天签到

1760cbbb6d1222381bec880b2eeb99af.png

不积跬步无以至千里,虚构用户其线形的斜率无非也是1,但坚持的力量让用户2不能望其项背

喝完鸡汤,我们用纯粹的数学的眼光观察这两条折线,如果将紫线设为f1,红线设为f2,那么f1-f2,就能得到黑线,设为f3

ed73982120191a677095584842c2c5b3.png

这个黑线妙就妙在,红线水平的地方,它是上升的,而红线上升的地方,它却是水平的

(朋友们,我们休息的时候,正是别人和我们拉开差距的时候啊!)

至此,我们巧妙地构造了一个变元,与原来的变元相减后,得到了常量

换句话说,我们得以把连续的日期分到一组当中了!

第一步,我们需要用表中的数据,计算出每一行上的累积天数,可以使用

COUNT(1) OVER(PARTITION BY user_id ORDER BY checkin_date)

因为聚合函数在分析模式下默认的窗口正好是第一行到当前行,所以无需考虑窗口问题

不过,计算第一行到当前行有多少行,不正是计算排名吗?所以可以用排名函数来取代计数,尽管没有资料支持,但小刘觉得取名次比计数运算能让数据库少做些事情?

本题中同一用户的签到日期不会出现排名并列的情况,故选择ROW_NUMBER函数即可

ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY checkin_date)

第二步,实现构造函数f1,由于其表达式为f1(x)=x,换成人话就是日期

EXTRACT(DAY FROM checkin_date)

第三步,得到初代版本

WITH history AS (SELECT h.user_id        ,h.checkin_date        ,extract(DAY FROM h.checkin_date) f1        ,row_number() over(PARTITION BY h.user_id ORDER BY h.checkin_date) f2    FROM demo_checkin_history h),group_rst AS (SELECT h.user_id        ,MIN(h.checkin_date) checkin_date_from        ,MAX(h.checkin_date) checkin_date_to        ,COUNT(1) days    FROM history h   GROUP BY h.user_id, h.f1 - h.f2)SELECT g.user_id      ,MAX(g.checkin_date_from) keep(dense_rank FIRST ORDER BY days DESC) checkin_date_from      ,MAX(g.checkin_date_to) keep(dense_rank FIRST ORDER BY days DESC) checkin_date_to      ,MAX(g.days) max_days  FROM group_rst g GROUP BY g.user_id ORDER BY max_days DESC, checkin_date_from DESC;

第四步,上例是基于仅统计一个月份内的前提,如果时间范围放大就会出问题,所以需要把构造函数f1再还原到签到日期本身

WITH history AS (SELECT h.user_id        ,h.checkin_date        ,row_number() over(PARTITION BY h.user_id ORDER BY h.checkin_date) delta    FROM demo_checkin_history h),group_rst AS (SELECT h.user_id        ,MIN(h.checkin_date) checkin_date_from        ,MAX(h.checkin_date) checkin_date_to        ,COUNT(1) days    FROM history h   GROUP BY h.user_id, trunc(h.checkin_date) - h.delta)SELECT g.user_id      ,MAX(g.checkin_date_from) keep(dense_rank FIRST ORDER BY days DESC) checkin_date_from      ,MAX(g.checkin_date_to) keep(dense_rank FIRST ORDER BY days DESC) checkin_date_to      ,MAX(g.days) max_days  FROM group_rst g GROUP BY g.user_id ORDER BY max_days DESC, checkin_date_from DESC;

04502cf8e5e8783cfa789df32fe3e402.png

26abdb95ba712a1245c8133fb9f4e985.png

05 对比总结

递归查询法思路相对更简洁清晰,构造变元法则略微烧脑了一丢丢

但是从性能角度出发,后者更优,尤其是在数据量较大的情形下,避免递归是很必要的

18166b53c9776891ed718282f15c63ff.png

今天的脱发就到这里吧,相信各位看官各位大佬还有更好的方法解决这个问题,敬请私信赐教!


今天的分享就到这里了,第十四次发推没有经验,不会排版,行文也没有条理,以后估计也不会有什么长进,感谢朋友们的鼓励与支持,以后我会坚持下去,求求你们不要取关504fd05449dd6f70013792b083024f45.png

37baa9a81e8ba17ff6046ab66104eb4b.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值