LTV=ARPU×LT,即用户生命周期价值等于每用户平均收入乘以平均生命周期月数;RFM模型通过最近消费时间(R)、消费频率(F)和消费金额(M)三个维度量化用户价值,支撑分层运营与LTV提升。

GROUP BY customer_id 是打标前提,不是可选项
不按 customer_id 分组,就根本谈不上“给用户打标签”。RFM、LTV、活跃分层这些价值标签全都是用户粒度的聚合结果,不是订单或事件粒度。漏写 GROUP BY customer_id,MySQL 会强制返回单行(ERROR 1140 或静默截断),PostgreSQL 直接报错。哪怕你后面套了 CASE WHEN,只要没对齐用户维度,标签就是错的。
常见错误包括:
- 用
GROUP BY order_id或GROUP BY DATE(order_date),结果是每笔订单/每天一条,根本没法回填到用户表 - 只写
SELECT MAX(order_date), COUNT(*)却不加GROUP BY,返回的是一条“幻影汇总”,业务上毫无意义 - 在宽表 JOIN 后再
GROUP BY,但基础表字段没全包含进GROUP BY列表,MySQL 5.7 会报错,MySQL 8.0+ 可能返回不可靠结果
聚合逻辑必须显式写死,不能依赖下游补
标签值不是“算出来就行”,而是要严格匹配业务定义。比如“近30天复购用户”这个标签,不能只写 COUNT(*) > 1,得明确:
- 时间范围:必须
WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY),否则历史冷单会干扰判断 - 状态过滤:只统计
status IN ('paid', 'shipped'),退款单、测试单、未支付单必须排除 - 去重逻辑:若定义为“自然日复购”,得用
COUNT(DISTINCT DATE(order_date)) > 1;若定义为“订单数复购”,才用COUNT(*) > 1 - 金额兜底:
amount若为VARCHAR,SUM()会静默返回 0,必须提前CAST(amount AS DECIMAL(10,2))
别指望把“清洗逻辑”留到应用层——SQL 层没拦住的脏数据,后面打标、分群、推送全都会错。
标签值要用 COALESCE 显式处理 NULL,别信默认值
用户没下单、没登录、没点击,对应聚合字段就是 NULL。直接拿 MAX(last_login_time) 做判断,结果也是 NULL;用它参与 DATEDIFF(CURDATE(), MAX(last_login_time)),整列变 NULL;再拿这个结果进 CASE WHEN r_value ,<code>NULL 全被划进 ELSE 分支——表面看都进了“低活”,实际是“从未行为”的用户和“沉睡30天”的用户混作一团。
正确做法是每一步都主动兜底:
- R 值:
COALESCE(DATEDIFF(CURDATE(), MAX(order_date)), 9999) - F 值:
COALESCE(COUNT(*), 0) - M 值:
COALESCE(SUM(CASE WHEN status = 'paid' THEN amount END), 0) - 标签字段本身:
CASE WHEN COALESCE(f_value, 0) >= 3 AND COALESCE(r_value, 9999)
动态时间 + 自动更新才是真动态标签
所谓“动态”,不是手动改 SQL 里的日期常量,而是让时间范围随运行日期自动偏移。硬编码 WHERE order_date BETWEEN '2026-09-01' AND '2026-09-30',跑一周就失效。必须用函数生成相对时间:
- MySQL:
WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 90 DAY) - PostgreSQL:
WHERE order_date >= CURRENT_DATE - INTERVAL '90 days' - BigQuery:
WHERE order_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 90 DAY)
再配合系统级的“自动更新”机制(如每日凌晨跑一次),才能保证今天查出的“近7日活跃用户”,明天还是“近7日”,而不是变成“固定某7天”。否则你做的所有分群、推送、AB测试,底层数据都在漂移。
最易被忽略的一点:标签 SQL 的执行时间点,决定了“今日”的基准。如果任务在凌晨 2 点跑,而你的业务以自然日结算,那 0:00–1:59 的订单就会被算进“昨日”而非“今日”——这个偏差在做实时性要求高的召回或风控时,会直接导致策略失效。










