应使用SUM(CASE WHEN...)配合COUNT()或COUNT() OVER()计算各状态占比,注意处理NULL、避免整数除法、确保精度,并根据数据库版本选择合适语法。

用 COUNT() 和 SUM() 配合 CASE 算各状态占比
直接用 COUNT() 除以总数就行,但要注意分母不能为 0,且需保证精度(避免整数除法截断)。最稳妥的方式是把分子或分母转成浮点数,再结合 CASE 按状态分别计数。
- MySQL / PostgreSQL / SQL Server 都支持
CASE WHEN status = 'active' THEN 1 END这种写法,SUM()会自动忽略NULL - 别用
COUNT(status = 'active')—— 这在 MySQL 里看似能用,但在其他数据库报错,也不符合标准 SQL - 如果状态值含空格或特殊字符(比如
'pending review'),记得用单引号包裹,别漏掉 - 示例:统计
order_status中'paid'、'shipped'、'cancelled'的占比
SELECT ROUND(SUM(CASE WHEN order_status = 'paid' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2) AS paid_pct, ROUND(SUM(CASE WHEN order_status = 'shipped' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2) AS shipped_pct, ROUND(SUM(CASE WHEN order_status = 'cancelled' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2) AS cancelled_pct FROM orders;
用窗口函数 COUNT(*) OVER() 避免重复扫描表
当表很大、状态种类多,或后续还要加其他维度聚合时,用窗口函数更高效——只扫一遍表,分母用 COUNT(*) OVER() 提前算好,不用嵌套子查询或 CTE。
-
COUNT(*) OVER()返回全表行数,和当前行无关,适合做基准总数 - 注意:不能写成
COUNT(*) OVER(PARTITION BY ...),那会按分组算局部总数,不是你要的全局占比 - 如果 WHERE 条件过滤了数据(比如只查
created_at > '2024-01-01'),窗口函数的OVER()里不加条件,它自动响应外层 WHERE
SELECT order_status, COUNT(*) AS cnt, ROUND(COUNT(*) * 100.0 / COUNT(*) OVER(), 2) AS pct FROM orders WHERE created_at > '2024-01-01' GROUP BY order_status;
遇到 NULL 状态要单独处理
很多表的状态字段允许 NULL,而 COUNT(status) 会跳过 NULL,但 COUNT(*) 不会。如果不显式处理,NULL 状态的占比就“消失”了,看起来总和不到 100%。
- 用
CASE WHEN status IS NULL THEN 1单独计数,别依赖ELSE覆盖所有情况 - 如果业务上
NULL表示“未初始化”,而你只想统计有效状态,那就加WHERE status IS NOT NULL,但得在需求确认清楚后再加 - PostgreSQL 中
COUNT(NULL)永远是 0,MySQL 同理 —— 所以靠COUNT(status)算分母是错的
导出到 Excel 或前端展示时小数位数要对齐
ROUND(x, 2) 是常用做法,但注意:有些数据库(如 SQLite)对浮点运算精度控制弱,可能出 19.999999999;另外,前端 JS 做数字格式化时,toFixed(2) 会四舍五入,但可能引发 0.1 + 0.2 !== 0.3 类问题。
- SQL 层建议统一用
ROUND(..., 2),别交给应用层算百分比 - 如果要求“显示为 12.5% 而非 12.50%”,就用
CAST(... AS DECIMAL(5,1))或数据库对应格式函数(如 PostgreSQL 的TO_CHAR(x, 'FM90D0')) - 百分比总和偶尔不是 100.00 —— 是浮点舍入误差,属正常现象,不用强行校正
实际写的时候,先确认状态字段是否可空、有没有非法值、要不要排除测试数据,再决定用 GROUP BY 还是手动枚举。窗口函数看着简洁,但老版本 MySQL(

















