left join 是计算留存率的默认选择,因其能保留基准日全部活跃用户(分母不丢失),回访为空时字段为 null 且被 count(distinct...) 自动忽略;需严格限定 user_id 相等 + 目标日期精确匹配,并提前去重防笛卡尔积。

直接用 JOIN 计算留存率可行,但必须严格控制连接条件和去重逻辑,否则分母失真、分子漏计、结果偏高或偏低都是大概率事件。
为什么 LEFT JOIN 是计算留存率的默认选择
因为留存率的分母是「基准日活跃用户集合」,这个集合里的每个用户都必须参与计算,哪怕他后续一天都没回来。用 INNER JOIN 会自动过滤掉未回访用户,导致分母变小、比率虚高。
-
LEFT JOIN保证基准用户全量保留,回访行为为空时字段为NULL,配合COUNT(DISTINCT ...)自然忽略 - 连接条件里必须同时限定:
user_id相等 + 目标日期精确匹配(如DATE(event_time) = DATE_ADD(base_date, INTERVAL 1 DAY)) - 如果行为表有重复记录(同一用户同天多次登录),
LEFT JOIN会导致笛卡尔膨胀,必须在子查询中提前DISTINCT或用窗口函数去重
DATE_ADD 和 DATEDIFF 在不同数据库中的陷阱
MySQL 的 DATE_ADD 和 DATEDIFF 看似简单,但跨年、闰年、时区不一致时容易出错;ClickHouse/StarRocks 的 dateDiff 函数参数顺序相反,Hive 的 datediff 有伪计算缺陷。
- Hive 中
datediff('2023-01-01', '2022-12-31')返回2而非1,不能用于精确间隔判断 - MySQL 中
DATE_ADD('2025-02-28', INTERVAL 1 DAY)正确返回'2025-03-01',但若字段是DATETIME且含时分秒,需先CAST(event_time AS DATE) - 推荐统一用
DATE(event_time)截断时间,再做加减,比依赖函数更可控
多日留存不要堆砌多个 LEFT JOIN
写 7 个 LEFT JOIN 算 d1–d7 留存,SQL 可读性差、执行计划臃肿、容易超内存,尤其在大宽表场景下。
- 正确做法是用单次自连接 + 条件聚合:
CASE WHEN DATEDIFF(login_date, first_date) = 1 THEN user_id END - 必须确保
first_date是每个用户的首次行为日(不是注册日),否则老用户会被误计入新 cohort - 如果业务要求“某日新增用户”,分母必须来自
register_info表且带WHERE DATE(register_time) = '2026-06-10',不能从行为表反推
最容易被忽略的时区与去重时机
时区错位会让「当天注册」变成「次日注册」,去重晚一步会让一个用户被重复计入分母——这两个问题不会报错,但结果偏差可能超过 20%。
- 注册时间字段是 UTC?业务口径按东八区?必须先
CONVERT_TZ(register_time, '+00:00', '+08:00')再DATE() - 去重不能只在最终
COUNT时做:子查询中就要SELECT DISTINCT user_id, DATE(event_time),否则 JOIN 前已膨胀 - 毫秒级时间戳务必
CAST(event_time AS DATE),避免同日多次行为因精度差异被当成不同日期











