使用 SQL 进行队列分析

你推出了一款新应用,用户每天都在注册。但这里有一个价值百万的问题:他们会回来吗?

队列分析概览:展示不同队列(JAN 2023, MAB 2023, MAR 2023, APR 2023, MAY 2023)在不同月份的留存率变化

单一指标如"总活跃用户数"无法告诉你完整的故事。你需要知道:1月注册的用户在3月还活跃吗?他们比2月注册的用户更活跃吗?

这就是队列分析(Cohort Analysis)的用武之地------产品分析中最强大的技术之一。最棒的是?你可以完全用 SQL 来实现它。

什么是队列分析?

队列(Cohort)是一组在特定时间段内共享共同特征的用户。例如:

  • 所有在 2024年1月 注册的用户
  • 所有在 黑色星期五 首次购买的用户

队列分析 跟踪这些群体随时间的行为变化。最常见的用例是留存分析(Retention Analysis):衡量用户在首次互动后回归的百分比。

为什么队列分析很重要?

想象你正在运营一个 SaaS 产品。你注意到:

  • 1月队列:60% 的用户在3个月后仍然活跃
  • 2月队列:只有30% 的用户在3个月后仍然活跃

这告诉你1月和2月之间发生了一些变化。也许你发布了一个有 bug 的功能,或者你的入门流程出了问题。没有队列分析,你永远不会发现这一点。

队列分析的价值

队列分析帮助你回答关键的业务问题,包括产品留存率评估、功能变更影响分析、用户生命周期价值预测以及流失预警和干预。通过对比不同时期的队列表现,你可以量化产品改进的效果,识别用户流失的关键节点,并制定更精准的用户运营策略。

数据模型

让我们使用一个简单的电商示例。我们有一个 user_activity 表,跟踪用户访问网站的时间:

示例数据:user_activity 表

user_id activity_date
1 2024-01-05
1 2024-01-12
1 2024-02-03
2 2024-01-10
2 2024-01-15
3 2024-01-20
3 2024-02-10
3 2024-03-05
4 2024-02-05
4 2024-02-15

我们想回答:1月加入的用户中有多少百分比在2月回来了?

步骤 1:识别队列

首先,我们需要确定每个用户的队列------他们首次活跃的月份。

sql 复制代码
SELECT 
  user_id,
  DATE_TRUNC('month', MIN(activity_date)) AS cohort_month
FROM user_activity
GROUP BY user_id;

查询结果

user_id cohort_month
1 2024-01-01
2 2024-01-01
3 2024-01-01
4 2024-02-01

结果解读 :用户 1、2、3 属于 2024年1月队列 ,用户 4 属于 2024年2月队列

跨数据库语法

数据库 月份截断语法
PostgreSQL DATE_TRUNC('month', activity_date)
MySQL DATE_FORMAT(activity_date, '%Y-%m-01')
SQL Server DATEFROMPARTS(YEAR(activity_date), MONTH(activity_date), 1)
SQLite DATE(activity_date, 'start of month')

步骤 2:计算活动周期

接下来,对于每次活动,我们计算自用户队列月份以来已经过去了多少个月。这被称为周期数(Period Number)。

sql 复制代码
WITH cohorts AS (
  SELECT 
    user_id,
    DATE_TRUNC('month', MIN(activity_date)) AS cohort_month
  FROM user_activity
  GROUP BY user_id
)
SELECT 
  ua.user_id,
  c.cohort_month,
  DATE_TRUNC('month', ua.activity_date) AS activity_month,
  (EXTRACT(YEAR FROM ua.activity_date) - EXTRACT(YEAR FROM c.cohort_month)) * 12 
    + (EXTRACT(MONTH FROM ua.activity_date) - EXTRACT(MONTH FROM c.cohort_month)) AS period_number
FROM user_activity ua
JOIN cohorts c ON ua.user_id = c.user_id;

查询结果

user_id cohort_month activity_month period_number
1 2024-01-01 2024-01-01 0
1 2024-01-01 2024-01-01 0
1 2024-01-01 2024-02-01 1
2 2024-01-01 2024-01-01 0
2 2024-01-01 2024-01-01 0
3 2024-01-01 2024-01-01 0
3 2024-01-01 2024-02-01 1
3 2024-01-01 2024-03-01 2
4 2024-02-01 2024-02-01 0
4 2024-02-01 2024-02-01 0

周期数说明

  • Period 0 = 队列月份(用户首次活跃的月份)
  • Period 1 = 1个月后
  • Period 2 = 2个月后
  • 以此类推

工作原理

  1. CTE cohorts:为每个用户计算队列月份(首次活跃月份)
  2. JOIN:将每次活动与用户的队列月份关联
  3. period_number 计算:使用年份和月份的差值计算周期数

步骤 3:构建留存矩阵

现在我们统计每个队列在每个周期有多少用户活跃。

sql 复制代码
-- 完整的队列分析查询
WITH cohorts AS (
  SELECT 
    user_id,
    DATE(MIN(activity_date), 'start of month') AS cohort_month
  FROM user_activity
  GROUP BY user_id
),
user_periods AS (
  SELECT 
    ua.user_id,
    c.cohort_month,
    DATE(ua.activity_date, 'start of month') AS activity_month,
    (CAST(strftime('%Y', ua.activity_date) AS INTEGER) - CAST(strftime('%Y', c.cohort_month) AS INTEGER)) * 12 
      + (CAST(strftime('%m', ua.activity_date) AS INTEGER) - CAST(strftime('%m', c.cohort_month) AS INTEGER)) AS period_number
  FROM user_activity ua
  JOIN cohorts c ON ua.user_id = c.user_id
)
SELECT 
  cohort_month,
  period_number,
  COUNT(DISTINCT user_id) AS active_users
FROM user_periods
GROUP BY cohort_month, period_number
ORDER BY cohort_month, period_number;

查询结果

cohort_month period_number active_users
2024-01-01 0 3
2024-01-01 1 2
2024-01-01 2 1
2024-02-01 0 1

结果解读

  • 2024-01-01, Period 0: 3个用户(所有1月注册的用户在1月都活跃)
  • 2024-01-01, Period 1: 2个用户(3个1月注册用户中有2个在2月回来了)
  • 2024-01-01, Period 2: 1个用户(3个1月注册用户中有1个在3月回来了)
  • 2024-02-01, Period 0: 1个用户(2月注册的用户在2月活跃)

留存率计算公式

复制代码
留存率 = (Period N 的活跃用户数) / (Period 0 的活跃用户数) × 100%

对于1月队列:

  • Month 1 留存率: 2/3 = 67%
  • Month 2 留存率: 1/3 = 33%

查询解析

  1. cohorts CTE:识别每个用户的队列月份
  2. user_periods CTE:计算每次活动的周期数
  3. 最终查询:按队列月份和周期数分组,统计活跃用户数

可视化队列数据

留存热力图:使用颜色(绿色=高留存,红色=低留存)展示不同队列的留存率

在实践中,你会将这些数据导出到 Excel、Google Sheets 或 BI 平台来创建留存热力图

队列 Month 0 Month 1 Month 2
2024年1月 100% 67% 33%
2024年2月 100% 100% 100%

颜色编码

  • 绿色 = 高留存(80%+)
  • 黄色 = 中等留存(50-80%)
  • 红色 = 低留存(<50%)

热力图的价值

热力图让你一眼就能看出哪些队列表现良好,哪些队列需要关注。例如,如果你看到某个队列的留存率突然下降,你可以立即调查那个时期发生了什么(产品变更、营销活动、外部事件等)。

高级:计算队列指标

一旦你有了队列结构,你可以计算其他指标:

每个队列的平均收入

sql 复制代码
SELECT 
  c.cohort_month,
  AVG(o.order_total) AS avg_revenue
FROM orders o
JOIN cohorts c ON o.user_id = c.user_id
GROUP BY c.cohort_month;

示例数据:orders 表

user_id order_total order_date
1 100.00 2024-01-10
1 50.00 2024-02-05
2 200.00 2024-01-15
3 75.00 2024-01-25
4 150.00 2024-02-10

查询结果

cohort_month avg_revenue
2024-01-01 106.25
2024-02-01 150.00

业务洞察:2月队列的平均收入更高,可能是因为营销活动吸引了更高价值的用户。

流失率

sql 复制代码
-- 在 Period 0 活跃但在 Period 1 不活跃的用户
SELECT 
  cohort_month,
  COUNT(DISTINCT CASE WHEN period_number = 0 THEN user_id END) AS period_0_users,
  COUNT(DISTINCT CASE WHEN period_number = 1 THEN user_id END) AS period_1_users,
  1.0 - (COUNT(DISTINCT CASE WHEN period_number = 1 THEN user_id END) * 1.0 / 
         COUNT(DISTINCT CASE WHEN period_number = 0 THEN user_id END)) AS churn_rate
FROM user_periods
GROUP BY cohort_month;

查询结果

cohort_month period_0_users period_1_users churn_rate
2024-01-01 3 2 0.33
2024-02-01 1 1 0.00

流失率解读

  • 1月队列:33% 的用户在第一个月后流失
  • 2月队列:0% 的用户流失(所有用户都回来了)

流失率 vs 留存率

  • 流失率 = 1 - 留存率
  • 流失率关注离开的用户,留存率关注留下的用户

用户生命周期价值(LTV)

sql 复制代码
SELECT 
  c.cohort_month,
  SUM(o.order_total) / COUNT(DISTINCT c.user_id) AS ltv
FROM cohorts c
LEFT JOIN orders o ON c.user_id = o.user_id
GROUP BY c.cohort_month;

查询结果

cohort_month ltv
2024-01-01 141.67
2024-02-01 150.00

LTV 解读:每个1月队列用户平均贡献 141.67 的收入,而2月队列用户平均贡献 150.00。

最佳实践

1. 选择正确的队列窗口

月度队列很常见,但你也可以使用周度或日度队列,具体取决于产品的节奏。

队列窗口选择指南

产品类型 推荐队列窗口 原因
快速迭代的移动应用 日度或周度 用户行为变化快,需要快速反馈
SaaS 产品 月度 用户订阅周期通常是月度
电商平台 月度或季度 购买周期较长
社交媒体 周度 用户互动频繁

2. 明确定义"活跃"

用户"活跃"的定义是什么?登录?购买?与内容互动?保持一致。

活跃定义示例

产品类型 活跃定义
SaaS 产品 登录并使用核心功能
电商平台 浏览商品或购买
社交媒体 发布内容或互动(点赞、评论)
内容平台 观看视频或阅读文章

错误示例

sql 复制代码
-- ❌ 不一致的活跃定义
SELECT * FROM user_activity WHERE login_count > 0  -- 有时用登录
SELECT * FROM user_activity WHERE purchase_count > 0  -- 有时用购买

正确示例

sql 复制代码
-- ✅ 一致的活跃定义
SELECT * FROM user_activity WHERE engaged = TRUE  -- 统一的活跃标志

3. 使用 CTE 提高可读性

队列查询可能很复杂。用 CTE(Common Table Expressions)分步骤处理。

不推荐(一个巨大的嵌套查询):

sql 复制代码
-- ❌ 难以阅读和维护
SELECT cohort_month, period_number, COUNT(DISTINCT user_id)
FROM (
  SELECT ua.user_id, c.cohort_month, ...
  FROM user_activity ua
  JOIN (SELECT user_id, MIN(activity_date) AS cohort_month FROM user_activity GROUP BY user_id) c
  ON ua.user_id = c.user_id
) sub
GROUP BY cohort_month, period_number;

推荐(使用 CTE):

sql 复制代码
-- ✅ 清晰、易读、易维护
WITH cohorts AS (...),
     user_periods AS (...)
SELECT cohort_month, period_number, COUNT(DISTINCT user_id)
FROM user_periods
GROUP BY cohort_month, period_number;

4. 使用视图自动化

创建视图以便分析师轻松查询队列逻辑。

sql 复制代码
CREATE VIEW cohort_retention AS
WITH cohorts AS (
  SELECT user_id, DATE_TRUNC('month', MIN(activity_date)) AS cohort_month
  FROM user_activity
  GROUP BY user_id
),
user_periods AS (
  SELECT 
    ua.user_id,
    c.cohort_month,
    DATE_TRUNC('month', ua.activity_date) AS activity_month,
    (EXTRACT(YEAR FROM ua.activity_date) - EXTRACT(YEAR FROM c.cohort_month)) * 12 
      + (EXTRACT(MONTH FROM ua.activity_date) - EXTRACT(MONTH FROM c.cohort_month)) AS period_number
  FROM user_activity ua
  JOIN cohorts c ON ua.user_id = c.user_id
)
SELECT 
  cohort_month,
  period_number,
  COUNT(DISTINCT user_id) AS active_users
FROM user_periods
GROUP BY cohort_month, period_number;

使用视图

sql 复制代码
-- 分析师可以轻松查询
SELECT * FROM cohort_retention WHERE cohort_month = '2024-01-01';

5. 处理数据质量问题

确保数据质量,避免重复记录和异常值。

sql 复制代码
-- 去重
WITH clean_activity AS (
  SELECT DISTINCT user_id, DATE(activity_date) AS activity_date
  FROM user_activity
  WHERE activity_date IS NOT NULL  -- 过滤 NULL 值
    AND user_id IS NOT NULL
)
SELECT * FROM clean_activity;

实际应用场景

1. SaaS 产品留存分析

场景:你运营一个项目管理工具,想知道不同月份注册的用户的留存情况。

查询

sql 复制代码
WITH cohorts AS (
  SELECT user_id, DATE_TRUNC('month', MIN(signup_date)) AS cohort_month
  FROM users
  GROUP BY user_id
),
user_periods AS (
  SELECT 
    l.user_id,
    c.cohort_month,
    DATE_TRUNC('month', l.login_date) AS activity_month,
    (EXTRACT(YEAR FROM l.login_date) - EXTRACT(YEAR FROM c.cohort_month)) * 12 
      + (EXTRACT(MONTH FROM l.login_date) - EXTRACT(MONTH FROM c.cohort_month)) AS period_number
  FROM logins l
  JOIN cohorts c ON l.user_id = c.user_id
)
SELECT 
  cohort_month,
  period_number,
  COUNT(DISTINCT user_id) AS active_users
FROM user_periods
GROUP BY cohort_month, period_number
ORDER BY cohort_month, period_number;

业务洞察:如果你发现某个月的队列留存率特别低,可以调查那个月发生了什么(产品变更、营销活动、竞争对手动作等)。

2. 电商平台购买队列

场景:你想知道首次购买用户的复购率。

查询

sql 复制代码
WITH cohorts AS (
  SELECT user_id, DATE_TRUNC('month', MIN(order_date)) AS cohort_month
  FROM orders
  GROUP BY user_id
),
user_periods AS (
  SELECT 
    o.user_id,
    c.cohort_month,
    DATE_TRUNC('month', o.order_date) AS activity_month,
    (EXTRACT(YEAR FROM o.order_date) - EXTRACT(YEAR FROM c.cohort_month)) * 12 
      + (EXTRACT(MONTH FROM o.order_date) - EXTRACT(MONTH FROM c.cohort_month)) AS period_number
  FROM orders o
  JOIN cohorts c ON o.user_id = c.user_id
)
SELECT 
  cohort_month,
  period_number,
  COUNT(DISTINCT user_id) AS repeat_buyers
FROM user_periods
GROUP BY cohort_month, period_number
ORDER BY cohort_month, period_number;

业务洞察:复购率是电商的关键指标。如果某个队列的复购率低,可能需要改进产品质量或客户服务。

3. 内容平台用户参与度

场景:你运营一个视频平台,想知道不同队列的用户观看频率。

查询

sql 复制代码
WITH cohorts AS (
  SELECT user_id, DATE_TRUNC('month', MIN(watch_date)) AS cohort_month
  FROM video_watches
  GROUP BY user_id
),
user_periods AS (
  SELECT 
    vw.user_id,
    c.cohort_month,
    DATE_TRUNC('month', vw.watch_date) AS activity_month,
    (EXTRACT(YEAR FROM vw.watch_date) - EXTRACT(YEAR FROM c.cohort_month)) * 12 
      + (EXTRACT(MONTH FROM vw.watch_date) - EXTRACT(MONTH FROM c.cohort_month)) AS period_number,
    vw.watch_duration_minutes
  FROM video_watches vw
  JOIN cohorts c ON vw.user_id = c.user_id
)
SELECT 
  cohort_month,
  period_number,
  COUNT(DISTINCT user_id) AS active_users,
  AVG(watch_duration_minutes) AS avg_watch_time
FROM user_periods
GROUP BY cohort_month, period_number
ORDER BY cohort_month, period_number;

业务洞察:观看时长是参与度的重要指标。如果某个队列的观看时长下降,可能需要改进内容推荐算法。

4. 移动应用会话队列

场景:你想知道不同队列的用户会话频率。

查询

sql 复制代码
WITH cohorts AS (
  SELECT user_id, DATE_TRUNC('month', MIN(session_start)) AS cohort_month
  FROM app_sessions
  GROUP BY user_id
),
user_periods AS (
  SELECT 
    s.user_id,
    c.cohort_month,
    DATE_TRUNC('month', s.session_start) AS activity_month,
    (EXTRACT(YEAR FROM s.session_start) - EXTRACT(YEAR FROM c.cohort_month)) * 12 
      + (EXTRACT(MONTH FROM s.session_start) - EXTRACT(MONTH FROM c.cohort_month)) AS period_number
  FROM app_sessions s
  JOIN cohorts c ON s.user_id = c.user_id
)
SELECT 
  cohort_month,
  period_number,
  COUNT(DISTINCT user_id) AS active_users,
  COUNT(*) AS total_sessions,
  COUNT(*) * 1.0 / COUNT(DISTINCT user_id) AS sessions_per_user
FROM user_periods
GROUP BY cohort_month, period_number
ORDER BY cohort_month, period_number;

业务洞察:会话频率反映用户粘性。如果某个队列的会话频率低,可能需要改进推送通知或应用内提示。

跨数据库实现

不同数据库的队列分析实现略有不同,主要是日期函数的差异。

PostgreSQL

sql 复制代码
WITH cohorts AS (
  SELECT 
    user_id,
    DATE_TRUNC('month', MIN(activity_date)) AS cohort_month
  FROM user_activity
  GROUP BY user_id
),
user_periods AS (
  SELECT 
    ua.user_id,
    c.cohort_month,
    DATE_TRUNC('month', ua.activity_date) AS activity_month,
    (EXTRACT(YEAR FROM ua.activity_date) - EXTRACT(YEAR FROM c.cohort_month)) * 12 
      + (EXTRACT(MONTH FROM ua.activity_date) - EXTRACT(MONTH FROM c.cohort_month)) AS period_number
  FROM user_activity ua
  JOIN cohorts c ON ua.user_id = c.user_id
)
SELECT 
  cohort_month,
  period_number,
  COUNT(DISTINCT user_id) AS active_users
FROM user_periods
GROUP BY cohort_month, period_number
ORDER BY cohort_month, period_number;

MySQL

sql 复制代码
WITH cohorts AS (
  SELECT 
    user_id,
    DATE_FORMAT(MIN(activity_date), '%Y-%m-01') AS cohort_month
  FROM user_activity
  GROUP BY user_id
),
user_periods AS (
  SELECT 
    ua.user_id,
    c.cohort_month,
    DATE_FORMAT(ua.activity_date, '%Y-%m-01') AS activity_month,
    TIMESTAMPDIFF(MONTH, c.cohort_month, ua.activity_date) AS period_number
  FROM user_activity ua
  JOIN cohorts c ON ua.user_id = c.user_id
)
SELECT 
  cohort_month,
  period_number,
  COUNT(DISTINCT user_id) AS active_users
FROM user_periods
GROUP BY cohort_month, period_number
ORDER BY cohort_month, period_number;

SQL Server

sql 复制代码
WITH cohorts AS (
  SELECT 
    user_id,
    DATEFROMPARTS(YEAR(MIN(activity_date)), MONTH(MIN(activity_date)), 1) AS cohort_month
  FROM user_activity
  GROUP BY user_id
),
user_periods AS (
  SELECT 
    ua.user_id,
    c.cohort_month,
    DATEFROMPARTS(YEAR(ua.activity_date), MONTH(ua.activity_date), 1) AS activity_month,
    DATEDIFF(MONTH, c.cohort_month, ua.activity_date) AS period_number
  FROM user_activity ua
  JOIN cohorts c ON ua.user_id = c.user_id
)
SELECT 
  cohort_month,
  period_number,
  COUNT(DISTINCT user_id) AS active_users
FROM user_periods
GROUP BY cohort_month, period_number
ORDER BY cohort_month, period_number;

SQLite

sql 复制代码
WITH cohorts AS (
  SELECT 
    user_id,
    DATE(MIN(activity_date), 'start of month') AS cohort_month
  FROM user_activity
  GROUP BY user_id
),
user_periods AS (
  SELECT 
    ua.user_id,
    c.cohort_month,
    DATE(ua.activity_date, 'start of month') AS activity_month,
    (CAST(strftime('%Y', ua.activity_date) AS INTEGER) - CAST(strftime('%Y', c.cohort_month) AS INTEGER)) * 12 
      + (CAST(strftime('%m', ua.activity_date) AS INTEGER) - CAST(strftime('%m', c.cohort_month) AS INTEGER)) AS period_number
  FROM user_activity ua
  JOIN cohorts c ON ua.user_id = c.user_id
)
SELECT 
  cohort_month,
  period_number,
  COUNT(DISTINCT user_id) AS active_users
FROM user_periods
GROUP BY cohort_month, period_number
ORDER BY cohort_month, period_number;

性能优化

队列分析查询可能很慢,特别是对于大型数据集。以下是一些优化建议:

1. 添加索引

sql 复制代码
-- 为 user_id 和 activity_date 添加索引
CREATE INDEX idx_user_activity_user_date ON user_activity(user_id, activity_date);

2. 使用物化视图

sql 复制代码
-- 创建物化视图(PostgreSQL)
CREATE MATERIALIZED VIEW cohort_retention_mv AS
WITH cohorts AS (...),
     user_periods AS (...)
SELECT * FROM user_periods;

-- 定期刷新
REFRESH MATERIALIZED VIEW cohort_retention_mv;

3. 分区表

sql 复制代码
-- 按月份分区(PostgreSQL)
CREATE TABLE user_activity (
  user_id INT,
  activity_date DATE,
  ...
) PARTITION BY RANGE (activity_date);

CREATE TABLE user_activity_2024_01 PARTITION OF user_activity
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

4. 限制时间范围

sql 复制代码
-- 只分析最近 12 个月的数据
WITH cohorts AS (
  SELECT user_id, DATE_TRUNC('month', MIN(activity_date)) AS cohort_month
  FROM user_activity
  WHERE activity_date >= CURRENT_DATE - INTERVAL '12 months'
  GROUP BY user_id
)
...

结论

队列分析对于理解用户随时间的行为至关重要。通过根据用户加入时间对用户进行分组并跟踪他们的活动,你可以:

  • 衡量留存率并识别流失点
  • 比较队列以查看产品变更的影响
  • 预测流失并在用户离开之前采取行动

而且使用 SQL,你可以完全控制分析------无需等待第三方工具添加你需要的功能。

关键要点

  1. 队列分析的核心是将用户按首次互动时间分组,跟踪他们的后续行为
  2. 三步法:识别队列 → 计算周期数 → 构建留存矩阵
  3. 留存率是最重要的指标,但你也可以计算收入、流失率、LTV 等
  4. 最佳实践:选择合适的队列窗口、明确定义"活跃"、使用 CTE、创建视图
  5. 跨数据库:主要差异在于日期函数,核心逻辑相同
  6. 性能优化:索引、物化视图、分区表、限制时间范围

现在去构建一些队列吧!📊


相关文章推荐


版权声明 :本文转载自 SQL Boy,原文链接:https://www.hisqlboy.com/blog/cohort-analysis-with-sql

相关推荐
Project_Observer2 小时前
项目管理中如何跟踪工时?
数据库·深度学习·机器学习
一点多余.2 小时前
openGauss 企业版安装全流程指南
linux·数据库·opengauss·企业版
发现你走远了2 小时前
MySQL(Windows)压缩包安装与配置指南(超详细版)
数据库·mysql
涛涛讲AI2 小时前
Django Admin模块的都有哪些表以及之间的关系
数据库·django·sqlite
青衫码上行2 小时前
Redis持久化 (快速入门)
数据库·redis·缓存
数据知道3 小时前
PostgreSQL:详解 PostGIS 地理信息数据处理
数据库·postgresql
小草儿7993 小时前
pg18权限篇章
数据库