给定订单明细表,计算每个用户在 2025 年期间,最长连续有多少天进行了下单购买
面试真题:计算 2025 年每个用户最长连续下单天数
题目描述:
给定一张订单明细表 order_details,记录了用户的购买行为。请编写 SparkSQL 语句,计算出每个用户在 2025 年 期间,最长连续 有多少天进行了下单购买。
示例数据
订单明细表:order_details
| order_id | user_id | order_date | amount |
|---|---|---|---|
| o001 | 101 | 2024-12-31 | 100.0 |
| o002 | 101 | 2025-01-01 | 150.0 |
| o003 | 101 | 2025-01-02 | 200.0 |
| o004 | 101 | 2025-01-03 | 50.0 |
| o005 | 101 | 2025-01-05 | 300.0 |
| o006 | 101 | 2025-01-06 | 120.0 |
| o007 | 102 | 2025-01-01 | 80.0 |
| o008 | 102 | 2025-01-01 | 90.0 |
| o009 | 102 | 2025-01-02 | 110.0 |
| o010 | 102 | 2025-01-04 | 150.0 |
| o011 | 103 | 2025-01-01 | 200.0 |
| o012 | 103 | 2025-01-03 | 250.0 |
样例说明:
- 用户 101:在 2025 年的下单日期有
2025-01-01,2025-01-02,2025-01-03(连续 3 天)和2025-01-05,2025-01-06(连续 2 天)。2024-12-31 属于 2024 年,予以排除。因此,其最长连续天数为 3。 - 用户 102:在
2025-01-01下了两次单(需去重),2025-01-02下了一次单,累计连续 2 天。2025-01-04下单 1 天。最长连续天数为 2。 - 用户 103:下单日期为
2025-01-01和2025-01-03,不连续。最长连续天数为 1。
期望输出
| user_id | max_consecutive_days |
|---|---|
| 101 | 3 |
| 102 | 2 |
| 103 | 1 |
SparkSQL 解决方案
sql
WITH temp_dedup AS (
-- 1. 过滤 2025 年数据,并对【用户+日期】进行去重(同一天多次下单只算一天)
SELECT DISTINCT
user_id,
CAST(order_date AS DATE) AS order_date
FROM order_details
WHERE order_date >= '2025-01-01' AND order_date <= '2025-12-31'
),
temp_rn AS (
-- 2. 按照用户分组,日期升序排列,生成连续的序号
SELECT
user_id,
order_date,
ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY order_date) AS rn
FROM temp_dedup
),
temp_group AS (
-- 3. 利用【日期 - 序号】差值相同的原理,构建连续下单的逻辑分组键 (diff_date)
SELECT
user_id,
order_date,
DATE_SUB(order_date, rn) AS diff_date
FROM temp_rn
),
temp_consecutive AS (
-- 4. 统计每个用户在每个连续区间内的下单天数
SELECT
user_id,
diff_date,
COUNT(1) AS consecutive_days
FROM temp_group
GROUP BY user_id, diff_date
)
-- 5. 聚合获取每个用户最长的连续下单天数
SELECT
user_id,
MAX(consecutive_days) AS max_consecutive_days
FROM temp_consecutive
GROUP BY user_id;
核心考点与深度解析
在 SparkSQL / HiveSQL 面试中,“最长连续登录/下单天数”是一道经典的高频金牌题。考官通过此题主要考察候选人对窗口函数、日期函数以及复杂逻辑抽象能力的掌握。
1. 核心解题思路:DATE_SUB(date, rn) 差值法
这是解决“连续性”问题的通用模板,其核心数学原理如下:
- 如果日期是连续的(如:
1号, 2号, 3号),而我们生成的窗口行号row_number()也是连续的(如:1, 2, 3)。 - 当我们将 【日期 - 行号】 时,他们会得到一个完全相同的日期基准值(即
diff_date)。 - 若日期发生中断(如:
1号, 2号, 4号),对应的行号为1, 2, 3,相减后的差值分别为(31号, 31号, 1号)。此时差值不同,自然分成了不同的组。
图解说明(以用户 101 为例):
| order_date | rn | DATE_SUB(order_date, rn) [diff_date] | 逻辑含义 |
|---|---|---|---|
| 2025-01-01 | 1 | 2024-12-31 | 连续组 A |
| 2025-01-02 | 2 | 2024-12-31 | 连续组 A |
| 2025-01-03 | 3 | 2024-12-31 | 连续组 A |
| 2025-01-05 | 4 | 2025-01-01 | 连续组 B |
| 2025-01-06 | 5 | 2025-01-01 | 连续组 B |
通过 GROUP BY user_id, diff_date,我们能轻易计算出 A 组有 3 天,B 组有 2 天,最后取 MAX 即可得到 3。
2. 面试易错防坑点(Deduplication)
- 同一天多次下单去重:用户可能在同一天内多次下单(例如数据中的用户 102 在
2025-01-01下单 2 次)。如果不做去重直接算row_number(),行号会累加,从而导致差值计算错乱。因此,第一步必须进行DISTINCT或GROUP BY去重。 - 严格的时间过滤:题目要求是“2025 年期间”。必须在第一步就切断跨年数据的干扰,例如本例中的
2024-12-31必须被过滤掉。
3. SparkSQL 性能调优探讨(高分加分项)
在实际的 Spark 分布式生产环境中,直接运行上述 SQL 可能会遇到性能瓶颈。如果你能在面试中主动提及以下优化点,会极大增加通过率:
- 数据倾斜(Data Skew):
ROW_NUMBER() OVER(PARTITION BY user_id ...)会导致相同user_id的所有数据被 Shuffle 到同一个 Reduce 任务中。如果存在“大卖家”或“热点用户”(某个user_id的订单量极大),就会导致 Executor OOM 或运行极慢。- 解决方案:如果只需计算连续天数,且已知数据源非常大,可以先将
user_id加上随机前缀进行两阶段聚合,或者利用 Spark 3.x 的 Adaptive Query Execution (AQE) 自动倾斜处理。
- 解决方案:如果只需计算连续天数,且已知数据源非常大,可以先将
- 避免全表扫描:
在temp_dedup中,order_date的过滤应该尽量利用分区剪裁(Partition Pruning)。如果order_details表是以dt(日期)作为分区字段的,应该优先对分区字段进行过滤,避免扫描全表,从而大幅提升 Spark 读取 HDFS/S3 的 I/O 效率。
右滑查看面试常问