这道题来自阿里巴巴淘宝事业部的用户增长数据分析岗面试。首单到第二单的间隔是衡量"用户激活速度"的关键指标——如果用户买了一次就再也不来了,说明产品体验或商品吸引力不足。反之,间隔越短说明用户越容易"上瘾"。
业务场景:运营团队在做新用户首单后的push推送时机决策时,需要知道"大多数用户隔多久下第二单",然后在那个时间窗口内推送优惠券。这道题计算的就是那个时间窗口的基准值。
现有一张用户订单表 t10_user_orders,计算用户从首次购买到第二次购买的平均间隔天数。
注意:只有下单>=2次的用户才有"前两单间隔",只下单1次的用户不参与计算。
用户订单表 t10_user_orders:
+-----------+----------+----------------------+
| order_id | user_id | order_time |
+-----------+----------+----------------------+
| O001 | u01 | 2023-03-01 10:00:00 |
| O002 | u01 | 2023-03-05 10:00:00 |
| O003 | u01 | 2023-03-10 10:00:00 |
| O004 | u02 | 2023-03-02 12:00:00 |
| O005 | u02 | 2023-03-08 12:00:00 |
+-----------+----------+----------------------+
核心是锚定每个用户的前两笔订单:
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_time) 为每笔订单编号WHERE rn <= 2,同时用 CASE WHEN 把第1笔和第2笔的时间分别放到不同列DATEDIFF(第2笔日期, 第1笔日期) 得到天数维度 | 评分 |
|---|---|
题目难度 | ⭐️⭐️⭐️ |
题目清晰度 | ⭐️⭐️⭐️⭐️ |
业务常见度 | ⭐️⭐️⭐️⭐️ |
执行SQL
select user_id, order_time,
row_number() over (partition by user_id order by order_time) as rn
from t10_user_orders
执行结果
+----------+----------------------+-----+
| user_id | order_time | rn |
+----------+----------------------+-----+
| u01 | 2023-03-01 10:00:00 | 1 |
| u01 | 2023-03-05 10:00:00 | 2 |
| u01 | 2023-03-10 10:00:00 | 3 |
| u02 | 2023-03-02 12:00:00 | 1 |
| u02 | 2023-03-08 12:00:00 | 2 |
+----------+----------------------+-----+
5 rows selected (0.399 seconds)(https://www.dwsql.com)
u01 的3笔订单分别编号为 1、2、3;u02 的2笔订单编号为 1、2。
用 CASE WHEN 将 rn=1 和 rn=2 的时间分别放入 first_order 和 second_order 列,外层 GROUP BY user_id 用 MAX 聚合(因为每列只有一个非NULL值)。
执行SQL
select user_id,
datediff(max(second_order), max(first_order)) as interval_days
from (
select user_id,
case when rn = 1 then order_time end as first_order,
case when rn = 2 then order_time end as second_order
from (
select user_id, order_time,
row_number() over (partition by user_id order by order_time) as rn
from t10_user_orders
) t1
where rn <= 2
) t2
group by user_id having interval_days is not null
执行结果
+----------+----------------+
| user_id | interval_days |
+----------+----------------+
| u01 | 4 |
| u02 | 6 |
+----------+----------------+
2 rows selected (0.37 seconds)(https://www.dwsql.com)
u01 从 03-01 首次购买到 03-05 第二次购买间隔 4 天。u02 从 03-02 到 03-08 间隔 6 天。两位用户的平均复购间隔 = (4+6)/2 = 5 天。
执行SQL
select avg(interval_days) as avg_interval_days
from (
select user_id,
datediff(max(second_order), max(first_order)) as interval_days
from (
select user_id,
case when rn = 1 then order_time end as first_order,
case when rn = 2 then order_time end as second_order
from (
select user_id, order_time,
row_number() over (partition by user_id order by order_time) as rn
from t10_user_orders
) t1
where rn <= 2
) t2
group by user_id having interval_days is not null
) t3
执行结果
+--------------------+
| avg_interval_days |
+--------------------+
| 5.0 |
+--------------------+
1 row selected (0.458 seconds)(https://www.dwsql.com)
坑1:CASE WHEN + MAX 的技巧理解
有人会疑惑为什么用 MAX() 聚合——因为同一用户的前两笔数据分布在两行,CASE WHEN 让每行只有一个非NULL的日期列,MAX() 在此处的实际作用是"取非NULL值"(因为 NULL < 任何日期值)。
坑2:LEAD 方案需要额外处理
另一种解法是 LEAD(order_time, 1) OVER (PARTITION BY user_id ORDER BY order_time) 获取下一单时间,但 LEAD 让每笔订单都能看到下一笔的时间,你需要额外取 rn=1 那一行才是"首单的下一单"。ROW_NUMBER + CASE WHEN 方案更直观。
坑3:只买了一次的用户
如果用户只下了一单,rn=2 不存在,CASE WHEN 返回 NULL,最终 DATEDIFF(NULL, *) = NULL。需要用 HAVING interval_days IS NOT NULL 或最外层过滤掉这些用户。
LEAD(order_time) OVER (PARTITION BY user_id ORDER BY order_time) 直接获取下一单时间,配合 WHERE rn=1 取首单的下一单间隔rn <= 2 改成 rn <= N,计算"首次购买到第N次购买"的累计间隔和平均每单间隔考点 | 说明 |
|---|---|
ROW_NUMBER + WHERE rn <= 2 | 精确取每个用户的前两笔订单 |
CASE WHEN + MAX 列转行 | 将两行的日期合并到同一行,便于计算差值 |
DATEDIFF | 计算两个日期的天数差 |
HAVING IS NOT NULL | 过滤只买了一单的用户 |
CREATE TABLE t10_user_orders (
order_id string COMMENT '订单ID',
user_id string COMMENT '用户ID',
order_time string COMMENT '下单时间'
) COMMENT '用户订单表';
INSERT INTO t10_user_orders VALUES
('O001','u01','2023-03-01 10:00:00'),('O002','u01','2023-03-05 10:00:00'),
('O003','u01','2023-03-10 10:00:00'),('O004','u02','2023-03-02 12:00:00'),
('O005','u02','2023-03-08 12:00:00');