必须先用CASE WHEN将年龄映射为分类标签再GROUP BY,否则直接在GROUP BY中计算年龄会报错或结果错乱;需处理空值、用窗口函数SUM OVER算占比更高效准确;注意跨数据库日期函数差异及年龄计算精度问题。

用 CASE WHEN + COUNT 计算各年龄段人数
直接在 GROUP BY 里写年龄计算逻辑会报错或结果错乱,必须先用 CASE WHEN 把年龄映射成分类标签,再分组。比如用户表有 birth_date 字段,需先算出年龄:FLOOR(DATEDIFF(CURDATE(), birth_date) / 365.25)(MySQL),再套一层 CASE:
SELECT
CASE
WHEN FLOOR(DATEDIFF(CURDATE(), birth_date) / 365.25) < 18 THEN 'under_18'
WHEN FLOOR(DATEDIFF(CURDATE(), birth_date) / 365.25) <= 25 THEN '18-25'
WHEN FLOOR(DATEDIFF(CURDATE(), birth_date) / 365.25) <= 35 THEN '26-35'
WHEN FLOOR(DATEDIFF(CURDATE(), birth_date) / 365.25) <= 45 THEN '36-45'
ELSE '46_plus'
END AS age_group,
COUNT(*) AS cnt
FROM users
GROUP BY age_group;注意:别在 WHERE 里提前过滤掉空 birth_date,否则统计基数不准——占比计算需要总人数,缺失值应归入“未知”组或单独处理。
加窗口函数 SUM OVER 算占比(MySQL 8.0+ / PostgreSQL)
单纯用 COUNT(*) / (SELECT COUNT(*) FROM users) 虽能算占比,但子查询执行多次、无法复用且不直观。更稳的方式是用窗口函数一次性拿到总数:
SELECT
age_group,
cnt,
ROUND(cnt * 100.0 / SUM(cnt) OVER(), 2) AS pct
FROM (
SELECT
CASE
WHEN FLOOR(DATEDIFF(CURDATE(), birth_date) / 365.25) < 18 THEN 'under_18'
WHEN FLOOR(DATEDIFF(CURDATE(), birth_date) / 365.25) <= 25 THEN '18-25'
WHEN FLOOR(DATEDIFF(CURDATE(), birth_date) / 365.25) <= 35 THEN '26-35'
WHEN FLOOR(DATEDIFF(CURDATE(), birth_date) / 365.25) <= 45 THEN '36-45'
ELSE '46_plus'
END AS age_group,
COUNT(*) AS cnt
FROM users
WHERE birth_date IS NOT NULL
GROUP BY age_group
) t;关键点:
-
SUM(cnt) OVER()不带PARTITION BY就是全量求和,比子查询快且语义清晰 - 必须用
100.0(带小数点)避免整数除法截断,否则 MySQL 会返回 0 -
WHERE birth_date IS NOT NULL放在子查询里,保证分母不含空值干扰
兼容旧版 MySQL(5.7)或 SQLite 的写法
如果数据库不支持窗口函数,就只能用 JOIN 或子查询。推荐用 JOIN,可读性略好于嵌套子查询:
SELECT
t1.age_group,
t1.cnt,
ROUND(t1.cnt * 100.0 / t2.total, 2) AS pct
FROM (
SELECT
CASE
WHEN FLOOR((julianday('now') - julianday(birth_date)) / 365.25) < 18 THEN 'under_18'
WHEN FLOOR((julianday('now') - julianday(birth_date)) / 365.25) <= 25 THEN '18-25'
-- 其他分支同理(SQLite 用 julianday,MySQL 5.7 仍用 DATEDIFF)
ELSE '46_plus'
END AS age_group,
COUNT(*) AS cnt
FROM users
WHERE birth_date IS NOT NULL
GROUP BY age_group
) t1
CROSS JOIN (SELECT COUNT(*) AS total FROM users WHERE birth_date IS NOT NULL) t2;注意不同数据库的日期函数差异:
- MySQL 5.7:用
DATEDIFF(CURDATE(), birth_date) - SQLite:用
julianday('now') - julianday(birth_date) - PostgreSQL:用
EXTRACT(YEAR FROM AGE(NOW(), birth_date))
容易被忽略的边界与精度问题
年龄计算不是简单减法,闰年、出生日未到、时区都会影响结果。例如 2000-03-01 出生的人,在 2025-02-28 还没满 25 周岁,但用 DATEDIFF / 365.25 会算成 24.99 → 向下取整得 24,误归入“18–25”。更准的做法是:
- 用数据库原生年龄函数(如 PostgreSQL 的
AGE()) - 或手动比较年份+月份:
(YEAR(NOW()) - YEAR(birth_date)) - (DATE_FORMAT(NOW(), '%m%d') < DATE_FORMAT(birth_date, '%m%d'))(MySQL) - 分组标签的区间要互斥且全覆盖,比如用
<= 25和> 25 AND <= 35比用两个<=更安全 - 占比列建议保留两位小数,但要注意四舍五入后总和可能不等于 100 —— 这是正常现象,别强行加修正逻辑

















