<p>最干净的办法是用COUNT() OVER()作分母:SELECT status, COUNT() AS cnt, ROUND(COUNT() 100.0 / COUNT() OVER(), 2) AS pct FROM orders GROUP BY status;需用100.0防整除截断,COUNT() OVER()获取全表总行数,且NULL分组字段会被单独成组但不计入COUNT(*)。</p>

直接用 COUNT() 和窗口函数算分组占比
想算每个分组占总数的百分比,最干净的办法是用窗口函数 COUNT(*) OVER() 当分母,避免写子查询或自连接。比如统计每种 status 在订单表里的占比:
SELECT status, COUNT(*) AS cnt, ROUND(COUNT(*) * 100.0 / COUNT(*) OVER(), 2) AS pct FROM orders GROUP BY status;
注意三点:
• 必须用 100.0(带小数点)而不是 100,否则整数除法会截断为 0;
• COUNT(*) OVER() 是全表总行数,不受 GROUP BY 影响;
• 如果表里有 NULL 的分组字段,它会被单独归为一组,但不会计入 COUNT(*)(因为 COUNT 不统计 NULL),这点和 SUM(1) 行为一致。
按条件分组再算占比时,CASE WHEN 要套在聚合里
比如要算「已支付」订单占所有「非取消」订单的百分比,不能在外面套 WHERE,否则分母就变小了。正确做法是把条件逻辑放进聚合函数:
SELECT
ROUND(
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) * 100.0 /
COUNT(*),
2
) AS paid_pct
FROM orders
WHERE status != 'cancelled';常见错误:
• 把 CASE 放在 WHERE 里过滤后再算,导致分母不是你想要的基准;
• 忘记 WHERE 过滤后,分母已经是缩小后的集合,这时如果还想算“占全部订单的比”,就得把 COUNT(*) OVER() 拉回来;
• 用 AVG(CASE ...) 替代 SUM/COUNT,虽然等价,但可读性差,且容易误用 AVG 的空值处理逻辑。
GROUP BY 多字段时,百分比分母容易搞错层级
比如按 region 和 product_type 分组,想算每个组合占所在 region 的比例,就不能用 COUNT(*) OVER() —— 那是全表总数。得用带 PARTITION BY 的窗口函数:
SELECT region, product_type, COUNT(*) AS cnt, ROUND(COUNT(*) * 100.0 / COUNT(*) OVER(PARTITION BY region), 2) AS pct_in_region FROM sales GROUP BY region, product_type;
关键点:
• COUNT(*) OVER(PARTITION BY region) 对每个 region 单独求和,作为该区域内的分母;
• 如果误写成 OVER(PARTITION BY product_type),结果就变成按品类归一化,和业务需求对不上;
• 窗口函数必须出现在 SELECT 或 HAVING 中,不能在 WHERE 或 GROUP BY 里引用(会报错)。
MySQL 8.0 以前没窗口函数?用变量或自连接硬扛
老版本 MySQL 只能靠用户变量模拟,但顺序不可靠,生产环境慎用。更稳的办法是自连接或子查询:
SELECT t1.status, t1.cnt, ROUND(t1.cnt * 100.0 / t2.total, 2) AS pct FROM ( SELECT status, COUNT(*) AS cnt FROM orders GROUP BY status ) t1 CROSS JOIN ( SELECT COUNT(*) AS total FROM orders ) t2;
问题很明显:
• 子查询执行两次全表扫描,大数据量时慢;
• 如果外层加了 WHERE 条件,两个子查询都得同步加上,容易漏改;
• PostgreSQL/SQL Server/Oracle 都支持窗口函数,优先升级或换引擎;
• SQLite 3.25+ 也支持 OVER(),别被旧文档误导。
百分比计算看着简单,真正卡住人的往往是分母该取哪一层——是全集、过滤后集合,还是某个维度的子集。确认清楚这个,再选对应语法,比死记模板管用。

















