SQL182 连续两次作答试卷的最大时间窗

描述

现有试卷作答记录表exam_record(uid用户ID, exam_id试卷ID, start_time开始作答时间, submit_time交卷时间, score得分):

|----|------|---------|---------------------|---------------------|-------|
| id | uid | exam_id | start_time | submit_time | score |
| 1 | 1006 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:21:02 | 84 |
| 2 | 1006 | 9001 | 2021-09-01 12:11:01 | 2021-09-01 12:31:01 | 89 |
| 3 | 1006 | 9002 | 2021-09-06 10:01:01 | 2021-09-06 10:21:01 | 81 |
| 4 | 1005 | 9002 | 2021-09-05 10:01:01 | 2021-09-05 10:21:01 | 81 |
| 5 | 1005 | 9001 | 2021-09-05 10:31:01 | 2021-09-05 10:51:01 | 81 |

请计算在2021年至少有两天作答过试卷的人中,计算该年连续两次作答试卷的最大时间窗days_window,那么根据该年的历史规律他在days_window天里平均会做多少套试卷,按最大时间窗和平均做答试卷套数倒序排序。由示例数据结果输出如下:

|------|-------------|--------------|
| uid | days_window | avg_exam_cnt |
| 1006 | 6 | 2.57 |

解释:用户1006分别在20210901、20210906、20210907作答过3次试卷,连续两次作答最大时间窗为6天(1号到6号),他1号到7号这7天里共做了3张试卷,平均每天3/7=0.428571张,那么6天里平均会做0.428571*6=2.57张试卷(保留两位小数);

用户1005在20210905做了两张试卷,但是只有一天的作答记录,过滤掉。

复制代码
WITH t2 AS (
SELECT 
	uid,
	COUNT(start_time) total, -- 用户2021年作答的次数
	DATEDIFF(MAX(start_time),MIN(start_time))+1  diff_time, -- 头尾作答时间窗 
	MAX(DATEDIFF(next_time,start_time))+1 days_window -- 最大间隔天数
FROM (
	SELECT uid,start_time,
	LEAD(start_time,1)OVER(PARTITION BY uid ORDER BY start_time) AS next_time -- 第二次作答时间
	FROM exam_record
    WHERE YEAR(start_time)=2021 -- 2021年的数据
	) t1
GROUP BY uid
)
SELECT uid,days_window,ROUND(total* days_window/diff_time,2) avg_exam_cnt
FROM t2
WHERE diff_time>1
ORDER BY days_window DESC,avg_exam_cnt DESC;

🔑 关键代码 & 逐行解释

1. 筛选年份:锁定 2021 年

复制代码
WHERE YEAR(start_time) = 2021
  • ✅ 只保留 2021 年的答题记录
  • 💡 YEAR() 提取年份,常用于时间分析

2. LEAD():找"下一次"答题时间(核心!)

复制代码
LEAD(start_time, 1) OVER (PARTITION BY uid ORDER BY start_time) AS next_time
  • LEAD(列, n):取后面第 n 行的值
  • PARTITION BY uid:按用户分组
  • ORDER BY start_time:按时间排序
  • 📌 作用:让每一行都知道"我下次什么时候答题"
  • ⚠️ 最后一次答题的 next_timeNULL

示例:

start_time next_time
2021-01-01 2021-01-03
2021-01-03 2021-01-08
2021-01-08 NULL

3. 计算最大间隔天数(含首日)

复制代码
MAX(DATEDIFF(next_time, start_time)) + 1 AS days_window
  • DATEDIFF(后, 前):两个日期相差多少天
  • +1:因为"1号到3号"是2天差,但实际跨了3天(1、2、3),所以加1
  • 📌 days_window = 用户最长一次"隔了几天才答题"

4. 计算总活跃天数(首尾跨度)

复制代码
DATEDIFF(MAX(start_time), MIN(start_time)) + 1 AS diff_time
  • ✅ 从第一次答题到最后一次,总共经历了多少天(含首尾)
  • 📌 比如:1月1日到1月8日 → diff_time = 8

5. 计算总答题次数

复制代码
COUNT(start_time) AS total
  • ✅ 统计每个用户答题多少次
  • 💡 注意:COUNT 不统计 NULL,所以 next_timeNULL 不影响

6. 最终公式:调整后的日均答题估算

复制代码
ROUND(total * days_window / diff_time, 2) AS avg_exam_cnt
  • ✅ 公式含义: "如果用户每次都像他最懒的那次一样间隔,那他平均每天答多少题?"
  • 💡 本质是一个归一化的活跃度评分
  • 📌 ROUND(..., 2):保留两位小数

7. 过滤条件:只看跨天用户

复制代码
WHERE diff_time > 1
  • ✅ 排除那些只在同一天答题的用户
  • 💡 因为他们没有"间隔"可分析,行为太单一

8. 排序逻辑

复制代码
ORDER BY days_window DESC, avg_exam_cnt DESC
  • ✅ 先看谁"偷懒最久"(days_window 大)
  • ✅ 再看谁在这种节奏下答得更多(avg_exam_cnt 高)

🎯 输出字段总结

字段 含义
uid 用户ID
days_window 最大答题间隔天数(含首日)
avg_exam_cnt 调整后的日均答题频率(评分)

✅ 复习时重点记这几点

  1. LEAD() 是关键:用来计算"下一次时间"
  2. +1 别忘了DATEDIFF 要 +1 才是实际跨度天数
  3. days_window = 最大间隔,反映"最长偷懒时间"
  4. diff_time = 首尾跨度,反映总活跃期
  5. 公式 total × days_window / diff_time 是一个"保守估计"的活跃度
  6. WHERE diff_time > 1 是为了排除单日用户

📌 一句话总结

LEAD 找下一次时间 → 算最大间隔 → 结合总次数和总天数 → 得出一个反映用户"断续学习"行为的评分。