应使用TIMESTAMPDIFF或EXTRACT计算年龄并用CASE WHEN分桶,避免函数索引失效;需LEFT JOIN保证分母完整,用NULLIF和COALESCE防除零;千万级表须预计算age_group字段或建汇总表。

用 DATE_PART 或 TIMESTAMPDIFF 算年龄再分组
数据库里用户生日通常是 birth_date 字段(类型为 DATE 或 TIMESTAMP),但 SQL 没有直接的 AGE() 函数(PostgreSQL 除外),得手动算。MySQL 用 TIMESTAMPDIFF(YEAR, birth_date, CURDATE()),PostgreSQL 用 EXTRACT(YEAR FROM AGE(CURRENT_DATE, birth_date)),SQLite 得靠 (strftime('%Y', 'now') - strftime('%Y', birth_date)) 再校准闰年误差。
注意别用 CURDATE() - birth_date 这类数值相减——结果是天数,不是整岁;也别在 WHERE 里用函数包裹 birth_date,否则索引失效。
按年龄段分桶得用 CASE WHEN,别依赖 GROUP BY 原始年龄值
真实场景中没人要“27岁用户活跃度”,而是“25-34岁”这种区间。硬 GROUP BY 年龄值会生成几百个分组,没意义。必须用 CASE WHEN 显式归桶:
SELECT
CASE
WHEN age BETWEEN 18 AND 24 THEN '18-24'
WHEN age BETWEEN 25 AND 34 THEN '25-34'
WHEN age BETWEEN 35 AND 44 THEN '35-44'
ELSE '45+'
END AS age_group,
COUNT(*) AS user_count,
COUNT(login_time) AS active_count
FROM (
SELECT
TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) AS age,
login_time
FROM users u
LEFT JOIN user_logins ul ON u.user_id = ul.user_id
WHERE u.birth_date IS NOT NULL
) t
GROUP BY age_group;关键点:LEFT JOIN 保证未登录用户也计入分母(COUNT(*));COUNT(login_time) 自动忽略 NULL,只统计有登录行为的;WHERE u.birth_date IS NOT NULL 防止年龄计算出错。
COUNT、AVG、MAX 的语义必须和业务对齐
“活跃度”不是固定指标,不同团队定义不同:
- 如果是“人均登录次数”,用
AVG(login_count)(需先按用户聚合) - 如果是“该年龄段登录用户占比”,用
100.0 * COUNT(login_time) / COUNT(*) - 如果是“最近一次登录距今时长”,用
AVG(DATEDIFF(CURDATE(), MAX(login_time))),但要注意MAX(login_time)在子查询里才有效
别直接写 AVG(login_time)——时间类型不能直接平均;也别漏掉 COALESCE 处理全 NULL 分组的除零风险,比如 COALESCE(100.0 * COUNT(login_time) / NULLIF(COUNT(*), 0), 0)。
性能陷阱:年龄计算无法走索引,大表必须预计算
只要 WHERE 或 GROUP BY 里出现函数调用(如 TIMESTAMPDIFF),就无法使用 birth_date 上的索引。千万级用户表跑一次要几十秒。
线上方案只有两个:
- 加一个
age_group字段,应用层或定时任务更新(推荐) - 建物化视图或汇总表,每天凌晨跑
INSERT INTO user_age_summary ...
临时查可以加 LIMIT 1000 快速验证逻辑,但别把它当线上接口用。另外,user_logins 表如果没复合索引 (user_id, login_time),JOIN 会更慢。

















