SQL167 连续签到领金币
题目描述
用户行为日志表tb_user_log
id | uid | artical_id | in_time | out_time | sign_in |
---|---|---|---|---|---|
1 | 101 | 0 | 2021-07-07 10:00:00 | 2021-07-07 10:00:09 | 1 |
2 | 101 | 0 | 2021-07-08 10:00:00 | 2021-07-08 10:00:09 | 1 |
3 | 101 | 0 | 2021-07-09 10:00:00 | 2021-07-09 10:00:42 | 1 |
4 | 101 | 0 | 2021-07-10 10:00:00 | 2021-07-10 10:00:09 | 1 |
5 | 101 | 0 | 2021-07-11 23:59:55 | 2021-07-11 23:59:59 | 1 |
6 | 101 | 0 | 2021-07-12 10:00:28 | 2021-07-12 10:00:50 | 1 |
7 | 101 | 0 | 2021-07-13 10:00:28 | 2021-07-13 10:00:50 | 1 |
8 | 102 | 0 | 2021-10-01 10:00:28 | 2021-10-01 10:00:50 | 1 |
9 | 102 | 0 | 2021-10-02 10:00:01 | 2021-10-02 10:01:50 | 1 |
10 | 102 | 0 | 2021-10-03 10:00:55 | 2021-10-03 11:00:59 | 1 |
11 | 102 | 0 | 2021-10-04 10:00:45 | 2021-10-04 11:00:55 | 0 |
12 | 102 | 0 | 2021-10-05 10:00:53 | 2021-10-05 11:00:59 | 1 |
13 | 102 | 0 | 2021-10-06 10:00:45 | 2021-10-06 11:00:55 | 1 |
(uid-用户ID, artical_id-文章ID, in_time-进入时间, out_time-离开时间, sign_in-是否签到)
场景逻辑说明:
- artical_id-文章ID代表用户浏览的文章的ID,特殊情况artical_id-文章ID为0表示用户在非文章内容页(比如App内的列表页、活动页等)。注意:只有artical_id为0时sign_in值才有效。
- 从2021年7月7日0点开始,用户每天签到可以领1金币,并可以开始累积签到天数,连续签到的第3、7天分别可额外领2、6金币。
- 每连续签到7天后重新累积签到天数(即重置签到天数:连续第8天签到时记为新的一轮签到的第一天,领1金币)
问题:计算每个用户2021年7月以来每月获得的金币数(该活动到10月底结束,11月1日开始的签到不再获得金币)。结果按月份、ID升序排序。
注:如果签到记录的in_time-进入时间和out_time-离开时间跨天了,也只记作in_time对应的日期签到了。
输出示例:
示例数据的输出结果如下:
uid | month | coin |
---|---|---|
101 | 202107 | 15 |
102 | 202110 | 7 |
解释:
101在活动期内连续签到了7天,因此获得1*7+2+6=15金币;
102在10.01~10.03连续签到3天获得5金币
10.04断签了,10.05~10.06连续签到2天获得2金币,共得到7金币。
SQL Schema
DROP TABLE IF EXISTS tb_user_log;
CREATE TABLE tb_user_log (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '自增ID',
uid INT NOT NULL COMMENT '用户ID',
artical_id INT NOT NULL COMMENT '视频ID',
in_time datetime COMMENT '进入时间',
out_time datetime COMMENT '离开时间',
sign_in TINYINT DEFAULT 0 COMMENT '是否签到'
) CHARACTER SET utf8 COLLATE utf8_bin;
INSERT INTO tb_user_log(uid, artical_id, in_time, out_time, sign_in) VALUES
(101, 0, '2021-07-07 10:00:00', '2021-07-07 10:00:09', 1),
(101, 0, '2021-07-08 10:00:00', '2021-07-08 10:00:09', 1),
(101, 0, '2021-07-09 10:00:00', '2021-07-09 10:00:42', 1),
(101, 0, '2021-07-10 10:00:00', '2021-07-10 10:00:09', 1),
(101, 0, '2021-07-11 23:59:55', '2021-07-11 23:59:59', 1),
(101, 0, '2021-07-12 10:00:28', '2021-07-12 10:00:50', 1),
(101, 0, '2021-07-13 10:00:28', '2021-07-13 10:00:50', 1),
(102, 0, '2021-10-01 10:00:28', '2021-10-01 10:00:50', 1),
(102, 0, '2021-10-02 10:00:01', '2021-10-02 10:01:50', 1),
(102, 0, '2021-10-03 11:00:55', '2021-10-03 11:00:59', 1),
(102, 0, '2021-10-04 11:00:45', '2021-10-04 11:00:55', 0),
(102, 0, '2021-10-05 11:00:53', '2021-10-05 11:00:59', 1),
(102, 0, '2021-10-06 11:00:45', '2021-10-06 11:00:55', 1);
思路分析
比较好理解的思考方式就是根据需要的结果,一步一步反推自己需要什么样格式的数据。
计算活动期间每个用户每月所获得的金币数。那么我反推一步最希望的数据是用户某一天签到时所获得的金币数,然后就可以按照用户(uid)和月份(month)进行分组,聚合(sum)一下金币数即可得到活动期间每个用户每月所获得的金币数。
用户某一天签到时所获得的金币数的数据如下表所示:
uid dt day_coin 101 2021-07-07 1 101 2021-07-08 1 101 2021-07-09 3 101 2021-07-10 1 101 2021-07-11 1 101 2021-07-12 1 101 2021-07-13 7 102 2021-10-01 1 102 2021-10-02 1 102 2021-10-03 3 102 2021-10-05 1 102 2021-10-06 1 再反推,想要获得用户某一天签到时所获得的金币数,那么我必须知道用户签到当天是连续签到的第几天。知道是连续签到的第几天之后,那么签到当天所获得的金币数 = 连续签到的第几天对 7 取余,如果余数为 0 则获得 7 枚金币,如果为3则获得3枚,其他情况为 1 枚金币。
用户签到当天是连续签到的第几天的数据如下表所示,从表中可以看到,因为用户102在 2021-10-04 那一天断签了,所以他在 2021-10-05 那一天会被重新算作连续签到的第一天。
uid dt 连续签到的第几天 101 2021-07-07 1 101 2021-07-08 2 101 2021-07-09 3 101 2021-07-10 4 101 2021-07-11 5 101 2021-07-12 6 101 2021-07-13 7 102 2021-10-01 1 102 2021-10-02 2 102 2021-10-03 3 102 2021-10-05 1 102 2021-10-06 2 其实反推到这里思路就已经非常清晰了,求用户签到当天是连续签到的第几天,也就是所谓的连续问题。
连续问题的核心就是利用签到日期与排序编号的差值相等。因为如果是连续的话,编号自增 1,日期同样自增 1 天。
如下表所示,dt 是签到日期,dt_tmp 是签到日期与排序编号的差值,可以发现用户102在 2021-10-04 那一天断签了,因此他在 2021-10-05 那天的 dt_tmp 与前面的不相同。
uid dt rn dt_tmp 101 2021-07-07 1 2021-07-06 101 2021-07-08 2 2021-07-06 101 2021-07-09 3 2021-07-06 101 2021-07-10 4 2021-07-06 101 2021-07-11 5 2021-07-06 101 2021-07-12 6 2021-07-06 101 2021-07-13 7 2021-07-06 102 2021-10-01 1 2021-09-30 102 2021-10-02 2 2021-09-30 102 2021-10-03 3 2021-09-30 102 2021-10-05 4 2021-10-01 102 2021-10-06 5 2021-10-01 求用户签到当天是连续签到的第几天。只需按照用户和签到日期与排序编号的差值进行分组,日期升序进行编号(
ROW_NUMBER() OVER (PARTITION BY uid, DATE_SUB(dt, INTERVAL rn DAY) ORDER BY dt)
)即可得到用户签到当天是连续签到的第几天。uid dt rn dt_tmp 连续签到的第几天 101 2021-07-07 1 2021-07-06 1 101 2021-07-08 2 2021-07-06 2 101 2021-07-09 3 2021-07-06 3 101 2021-07-10 4 2021-07-06 4 101 2021-07-11 5 2021-07-06 5 101 2021-07-12 6 2021-07-06 6 101 2021-07-13 7 2021-07-06 7 102 2021-10-01 1 2021-09-30 1 102 2021-10-02 2 2021-09-30 2 102 2021-10-03 3 2021-09-30 3 102 2021-10-05 4 2021-10-01 1 102 2021-10-06 5 2021-10-01 2
答案
WITH t1 AS (SELECT DISTINCT uid, -- 为了防止一天有多次签到活动,使用 DISTINCT 去重
DATE(in_time) AS `dt`,
DENSE_RANK() OVER (PARTITION BY uid ORDER BY DATE(in_time)) `rn` -- 按照用户分组,日期升序进行编号
FROM tb_user_log
WHERE DATE(in_time) BETWEEN '2021-07-07' AND '2021-10-31'
AND artical_id = 0
AND sign_in = 1),
t2 AS (SELECT uid,
dt,
rn,
-- 如果用户是连续签到的话,则每天日期-编号所得到的日期(差值)应该是相等的,如果不是连续(即中间有断签的情况)的话,则差值不相等
DATE_SUB(dt, INTERVAL rn DAY) AS `dt_tmp`,
-- 按照用户和相减所得到的日期进行分组,日期升序进行编号,如果用户中间有断签,就不会分到同一组,也就会重新编号
ROW_NUMBER() OVER (PARTITION BY uid, DATE_SUB(dt, INTERVAL rn DAY) ORDER BY dt) AS `连续签到的第几天`,
-- 计算用户当天签到时应该获得的金币数
CASE ROW_NUMBER() OVER (PARTITION BY uid, DATE_SUB(dt, INTERVAL rn DAY) ORDER BY dt) % 7
WHEN 3 THEN 3
WHEN 0 THEN 7
ELSE 1 END AS `day_coin` -- 用户当天签到时应该获得的金币数
FROM t1)
SELECT uid, DATE_FORMAT(dt, '%Y%m') AS `month`, SUM(day_coin) AS `coin`
FROM t2
GROUP BY 1, 2
ORDER BY 2, 1;