流失率=已流失客户数÷期初活跃客户总数;需用CASE WHEN将是否流失转为0/1后SUM计算分子,分母须通过子查询或CTE获取统计期初的客户集合,不可直接COUNT(*)或依赖状态字段。

怎么用 COUNT 和 CASE WHEN 算流失率
流失率本质是「已流失客户数 ÷ 期初活跃客户总数」,不能直接用 AVG 或 SUM 硬套。关键在把「是否流失」转成 0/1,再聚合。
常见错误是写成 COUNT(*) WHERE status = 'churned' —— 这会漏掉分母,且无法和分组对齐。
- 用
CASE WHEN status = 'churned' THEN 1 ELSE 0 END构造指标列,确保每行有值 - 分母必须是「统计周期开始时的客户数」,不是当前表里所有客户;通常需先用子查询或 CTE 拿出期初客户集合
- 如果用户表带
last_active_date,流失常定义为「过去 90 天无行为」,别直接用状态字段硬编码
SELECT COUNT(*) AS total_beginning, SUM(CASE WHEN last_active_date < CURRENT_DATE - INTERVAL '90 days' THEN 1 ELSE 0 END) AS churned_count, ROUND(100.0 * churned_count / NULLIF(total_beginning, 0), 2) AS churn_rate_pct FROM customers WHERE first_active_date <= CURRENT_DATE - INTERVAL '90 days';
GROUP BY 用户状态时为什么结果总少几类
因为 GROUP BY 只返回实际存在的数据行对应的状态,比如没人在表里标记为 'at_risk',那这组就完全不出现——但业务上你可能需要补零显示。
典型场景:运营要看各状态(active / at_risk / churned / new)的客户数分布,缺一组就容易误判漏覆盖。
- 用
LEFT JOIN或RIGHT JOIN配合状态字典表(哪怕只是 VALUES 行),强制保留空组 - PostgreSQL 可用
UNNEST(ARRAY['active','at_risk','churned','new'])构造维度;MySQL 8.0+ 建议建临时状态表 - 别依赖应用层补零,SQL 层没返回的组,下游很难判断是真为 0 还是数据缺失
用 AVG 算留存率时精度丢失怎么办
AVG(status = 'active') 在 MySQL 里看似简洁,但本质是布尔转整数再平均,隐式类型转换易出错;PostgreSQL 则直接报错,因为 boolean 不支持 AVG。
更麻烦的是:如果某天只有 1 个客户且状态是 active,AVG 返回 1.0;但如果有 1000 个客户、其中 1 个 active,它返回 0.001 —— 小数位不够看不清趋势。
- 统一用
ROUND(100.0 * COUNT(CASE WHEN ... THEN 1 END) / NULLIF(COUNT(*), 0), 2),显式控精度 -
NULLIF必须加,否则分母为 0 时整个表达式变NULL,而不是报错或跳过 - 避免在
WHERE里提前过滤状态,否则分母变小,算出来的是「活跃用户中某子集占比」,不是真正留存率
时间窗口选错导致流失率忽高忽低
流失率对时间切片极其敏感。用「按月统计」但没对齐自然月(比如取 created_at >= '2024-01-01' 却忽略 last_active_date 的更新延迟),会导致当月刚注册的用户被误判为流失。
另一个坑是跨时区:数据库用 UTC,但业务要求按用户本地时区算「90 天未登录」,直接用 CURRENT_DATE 会偏移。
- 流失判定逻辑必须独立于注册时间,专注行为终点:以
MAX(event_time)或最新日志时间为准 - 如需按用户时区计算,别在 SQL 里做
AT TIME ZONE转换(性能差),建议前置把local_last_active存为衍生字段 - 对比多期数据时,务必确认窗口长度一致(都是滚动 90 天?还是固定起止?),否则同比毫无意义
最常被忽略的一点:流失定义本身要和业务对齐。技术上能算出 5.3%,但如果产品团队把「连续 30 天未打开 App」才算流失,而你用了 90 天,这个数字就只是数学正确,不是业务可用。










