2018 — 2026蒸汽教育 · 八周年

SQL 留存分析面试:分群、日期窗口与分母怎样算清楚

用可手算用户事件与SQLite查询练习第七日留存,说明注册分群、去重、行为、时区与成熟分母,加入滚动窗口和加权反例帮助解释SQL结果。

圆眼镜男主把同色圆点放进一张只有两行三格的日历条;突出分群与时间,不堆数字。
📅 时效更新核对日期:2026-10-22。案例与数字为原创教学示例;招聘安排与工具规则以当前官方说明为准。

正在准备SQL留存,先补哪一步?

如果 SQL 能写出结果,但面试时解释不清口径,加微信说一下目标数据岗和最常出错的题型,可以先把专项训练范围定下来。

文 / STEM Career 编辑部

留存分析的 SQL 题,看起来经常只有几行查询,真正容易出错的却是分群、日期和分母。同一组事件,按日历日、滚动时间或不同活跃定义计算,结果可能不同。

本文用一组可以手算的原创数据,练习注册后第七个日历日的目标行为留存,并给出 SQLite 查询。先讲清口径,再写代码,面试时才能解释结果为什么成立。

先固定本题定义

用户按注册日期分群。目标行为是 action;第七日指注册日期加七个日历日,不包含注册当天;只有第七日已完整结束的用户进入分母。所有时间已按题设统一为 UTC,观测截止为 2026 年 10 月 10 日,不包含这一天。

因此,10 月 2 日注册的用户,第七日是 10 月 9 日,可以观察;10 月 3 日注册的用户,第七日是 10 月 10 日,尚未完整进入本次窗口,应排除。

这是一种教学定义,不是唯一留存标准。真实题目应先澄清目标行为、注册还是首次活跃分群、时间区间和成熟窗口。

原创用户与事件数据

用户注册日目标行为日期
A10 月 1 日10 月 8 日两次
B10 月 1 日10 月 7 日
C10 月 2 日10 月 9 日一次
D10 月 2 日无
E10 月 3 日本次观测未成熟

A 的两次事件只算一个留存用户;B 在第六日发生行为,不算本题第七日;C 符合;D 不符合;E 排除出分母。

手算结果应为 10 月 1 日组两人中一人,第七日留存 0.5;10 月 2 日组同样为 0.5。没有成熟窗口的分群不显示为零留存。

先建一张用户粒度的结果表

最重要的中间结果是每位成熟用户一行,标记是否在目标日发生行为。不要直接连接事件后用 COUNT(*) 计算人数,否则 A 的重复行为会扩大记录数。

没有行为的用户仍应保留,因此不能用只保留匹配事件的连接方式。先固定分母人群,再计算标记,查询会更容易解释与复核。

SQLite 日期函数文档说明了 date 等函数的用法。下方示例使用其方言;换到 PostgreSQL 或其他数据库时,需要按对应日期函数调整实现。

可运行的教学查询

WITH users(user_id, signup_day) AS (
  VALUES ('A','2026-10-01'),('B','2026-10-01'),
         ('C','2026-10-02'),('D','2026-10-02'),
         ('E','2026-10-03')
),
events(user_id, event_day, event_type) AS (
  VALUES ('A','2026-10-08','action'),
         ('A','2026-10-08','action'),
         ('B','2026-10-07','action'),
         ('C','2026-10-09','action')
),
per_user AS (
  SELECT u.user_id, u.signup_day,
    CASE WHEN EXISTS (
      SELECT 1 FROM events e
      WHERE e.user_id = u.user_id
        AND e.event_type = 'action'
        AND e.event_day = date(u.signup_day, '+7 days')
    ) THEN 1 ELSE 0 END AS retained
  FROM users u
  WHERE date(u.signup_day, '+7 days') < '2026-10-10'
)
SELECT signup_day, COUNT(*) AS eligible_users,
       SUM(retained) AS retained_users,
       1.0 * SUM(retained) / COUNT(*) AS retention
FROM per_user
GROUP BY signup_day
ORDER BY signup_day;

EXISTS 只回答是否存在符合条件的事件,避免把重复事件算成多位用户。成熟条件与观察截止采用严格小于,和前面的排除定义一致。实际数据库还要按数据类型和业务时区处理。

看结果前先做三个检查

先核对用户表是否一人一行。若同一用户有多个注册时间,需要先定义主记录,不能在查询末尾用 DISTINCT 试图掩盖来源问题。

再核对事件是否已按目标时区转为日期。UTC 午夜附近的事件,换到当地日期可能落在相邻一天。不要对混合时区的原始字符串直接截取日期。

最后确认观测数据是否完整。事件延迟到达时,某个日历日结束不代表数据已经齐全,成熟条件还可能需要考虑数据延迟。

变体一 滚动窗口会改变什么

如果题目要求注册后第 168 至 192 小时发生行为,用户注册时刻就重要了。仅有日期的教学数据不足以计算,需要原始时间戳与明确边界。

这时不能简单把 date 加七天的查询改个标题。应重新定义上下界、使用时间类型,并测试恰好落在边界的事件。对面试官说明缺少哪些字段,也是分析能力的一部分。

变体二 取消或删除用户怎样处理

如果某用户在第七日前取消账号,是否仍保留在原注册分群,要由业务定义决定。删除后直接从用户表消失,可能让历史分母变化,引入选择偏差。

可以保存用于分析的适当历史状态,并遵守数据管理要求。面试回答先明确问题,再讨论可用数据,不能为了得到更高留存而把不活跃用户排除。

变体三 某一组没有成熟用户

没有成熟观察窗口时,结果可以不出现该组,或显示人数为零、留存未知,取决于报告约定。未知与零是不同信息,需要在展示里让使用者看懂。

若业务需要连续日期,可以先生成日期骨架再连接结果,但不要把缺失率统一填零。还要处理分母为零,避免查询报错或产生错误解释。

业务结论要匹配小样本

本例每组只有两位成熟用户,无法据此判断产品或渠道长期表现。教学数据用于复核逻辑,真实分析还需观察样本结构、规模与不确定性。

如果某次改版后留存升高,先检查分群与行为口径是否变化、用户构成是否不同,再讨论可能原因。相关变化不能直接写成改版的因果效果。

可配合数据分析 SQL 与指标练习,或从产品面试的用户与指标继续研究业务含义。

把边界事件放进手算表

可以再加入三条事件:A 在 10 月 9 日发生 action,B 在 10 月 8 日发生 view,E 在 10 月 10 日发生 action。按本题定义,第一条不改变 A 的第七日标记;第二条不是目标行为;第三条所在观察日尚未完整纳入,因此 E 仍不进分母。

这三个变化检验日期、行为和成熟窗口是否分别正确。若代码结果改变,先检查筛选条件,而不是立即怀疑聚合函数。保留中间的一人一行表,通常更容易定位问题。

成熟窗口会影响渠道比较

假设一个渠道用户集中在最近几天,另一个渠道主要是较早注册。直接把所有注册用户作为分母,会让较新渠道看起来更低,原因却可能只是还没有观察机会。

应先保证比较对象都有相同窗口,再说明渠道、用户类型和样本结构。若要比较不同日期组的整体率,可以用成熟用户总数与留存用户总数计算加权结果,不能随意平均大小不同的分群百分比。

本例两个分群人数相同,平均率恰好也是 0.5;人数不同后就需要重新加权。面试时给一个三人组与一人组的反例,可以帮助说明为什么分母比最后显示的小数更重要。

再看一个加权反例:一个三人组留存两人,另一个一人组留存零人。整体是四人中两人,即 0.5;两个组率的简单平均约为 0.333。先明确要回答整体用户表现还是平均分群表现,再选择计算。

把查询解释成一分钟回答

先说分群与行为,再说成熟条件和一人一行的中间表,最后展示结果与边界。面试官应能沿着你的说明手算一条用户,而不只看到查询运行成功。

如果你写得出 SQL,却常在分母和日期追问中卡住,可以加微信说明目标数据岗位和最容易出错的题型。通过专项面试准备先补口径、手算与边界,再练更复杂查询,会让能力更稳定。

常见问题(FAQ)

重复事件会重复计算留存用户吗?

本题先按用户判断是否存在目标行为,一人只计一次。

最近注册组没有完整窗口算零吗?

应区分未成熟与零留存,按明确定义排除或显示未知。

各组留存率能直接平均吗?

要先明确问题;整体用户率通常需按人数计算。

参考资料与官方说明
正在准备SQL留存,先补哪一步?

如果 SQL 能写出结果,但面试时解释不清口径,加微信说一下目标数据岗和最常出错的题型,可以先把专项训练范围定下来。