题目描述

✅ 1107. 每日新用户统计

题意分析

统计指定日期区间内,每天有多少用户第一次登录。一个用户只在其全部历史中的首次登录日算作新用户,之后再次登录不能重复计入;其他类型的活动也不能当作登录。

本篇查询以 2019-06-30 为截止日,使用向前推九十天到当日的闭区间,即 2019-04-01 至 2019-06-30,两端都包含。输出日期 login_date 和当天新用户数 user_count,没有新用户的日期不需要补零行。

解法:SQL 查询建模

核心思路

[!blue]

先把“首次登录”确定下来,再判断这个首次日期是否落在统计区间。内部查询只筛选 activity = 'login',但保留这些登录行为的全部历史;按 user_id 分组,对日期取 MIN,得到每个用户唯一的首登日期。

行为条件可以提前应用,因为其他活动本来就不参与首次登录的定义;日期条件却不能提前截断原始记录。若删掉区间之前的登录历史,老用户在区间内的一次重登就可能变成剩余记录中的最早日期,被错误识别为新用户。

内部聚合完成后,一行已经代表一个登录用户及其真正首登日期。外层将日期条件作用于这个聚合结果,只保留首登日处于闭区间的用户,既排除首登更早的用户,也排除截止日之后才首登的记录。

最后按 login_date 分组,使用 COUNT(*) 统计每一天留下的行数。因为内层已经压成每用户一行,同一用户多次或同日重复登录都不会重复计数,不需要在外层再做一次用户去重。

整个查询经历了两次不同粒度的聚合:先按用户归并历史,再按首登日期汇总用户。两层不能交换,也不能用每天的登录去重人数代替新用户数。

解题步骤

  1. 从完整历史中筛出登录行为。
  2. 按用户分组,取最早登录日期,形成一用户一行的结果。
  3. 对首登日期应用截止日及前推九十天的闭区间条件。
  4. 按首登日分组,对保留的用户行计数并输出指定列。

代码实现

SELECT
    login_date,
    COUNT(*) AS user_count
FROM (
    -- 先对完整登录历史取最早日期,每个用户只保留一行。
    SELECT user_id, MIN(activity_date) AS login_date
    FROM Traffic
    -- 先筛登录行为,其他活动不参与首次登录日期统计。
    WHERE activity = 'login'
    GROUP BY user_id
) first_login
-- 日期条件作用于首登日,不能提前裁掉历史记录。
WHERE login_date BETWEEN DATE_SUB('2019-06-30', INTERVAL 90 DAY) AND '2019-06-30'
-- 按首登日期汇总,内层的一行对应一个用户。
GROUP BY login_date;

复杂度分析

  • 时间复杂度:取决于数据库聚合计划。设原记录数为 n,采用哈希聚合时可按期望 $O(n)$ 理解;若执行器需要排序或额外物化,还需计入相应开销。
  • 空间复杂度:哈希聚合需记录每个登录用户的最早日期,约为 $O(u)$,u 是不同登录用户数;日期汇总及数据库中间结果另计。

关键点总结

[!green]

  • 先按行为筛选,再按用户确定全历史首登日,最后按日期筛选和计数。
  • 内层一行对应一个用户,外层 COUNT(*) 因而就是新增用户数。
  • 日期条件应用于首次日期,不能先让历史范围变短。

易错点总结

[!yellow]

  • 直接统计每天去重后的登录用户,得到的是当天登录人数,不是首次登录人数。
  • 在原始表上先限制日期再求最小值,会把老用户的区间内重登误当首登。
  • 非登录行为也参与最小日期计算,会把其他活动时间误当成首次登录时间。
  • 不按用户先聚合,重复登录记录会在按天计数时重复贡献。
  • 忽略查询上界,只判断与截止日的天差不大于九十,可能把截止日之后的日期也纳入;当前 SQL 用闭区间同时限制两端。

相似题目

题目 难度 关联与区别
550. 游戏玩法分析 IV 中等 同样先确定用户的首次登录时间,原题统计次日留存,本题按首次登录日期统计新增用户。
1070. 产品销售分析 III 中等 同样先在完整登录或销售历史中找每组最早日期,再做日期筛选或统计,不能先截断日期范围导致首次时间改变。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2021/85949354
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!