需用ROUND()配合COUNT()窗口函数计算分组占比:分子为COUNT() OVER(PARTITION BY category, item),分母为COUNT(*) OVER(PARTITION BY category),乘100.0并ROUND(…, 2)得百分比;PERCENT_RANK()不适用此场景;NULL值须用COALESCE或WHERE提前处理。

用 ROUND() 配合 COUNT(*) OVER() 算分组内占比
直接除法会得到小数,但多数场景需要保留两位小数的百分比(比如 33.33%),必须显式用 ROUND() 控制精度。窗口函数本身不处理四舍五入,COUNT(*) OVER (PARTITION BY category) 只负责算每组总行数,真正占比要靠手动相除再修约:
SELECT
category,
item,
ROUND(100.0 * COUNT(*) OVER (PARTITION BY category, item)
/ COUNT(*) OVER (PARTITION BY category), 2) AS pct_in_group
FROM sales;注意三点:
• 分子用 COUNT(*) OVER (PARTITION BY category, item) 算每组内各子类数量;
• 分母用 COUNT(*) OVER (PARTITION BY category) 算该组总数量;
• 100.0 强制转为浮点运算,避免整数除法截断(如 PostgreSQL/MySQL 中 1/3 得 0)。
PostgreSQL 和 MySQL 8.0+ 的 PERCENT_RANK() 不适用这个需求
PERCENT_RANK() 是按排序位置给值打分(最小值为 0,最大值趋近 1),不是“某值在分组中占多少比例”。它和你想要的“苹果销量占水果组 45%”完全不是一回事:
- 如果你查的是“每个订单金额在所属客户中的相对排名”,才用
PERCENT_RANK() OVER (PARTITION BY customer_id ORDER BY amount); - 如果你查的是“每个商品种类在品类中的销量占比”,就得老实用
COUNT窗口除法; - 误用
PERCENT_RANK()会导致结果全为 0 或 1,尤其当组内只有 1–2 条记录时,数值完全失真。
GROUP BY + 子查询也能做,但窗口函数更稳、更少出错
有人习惯先 GROUP BY category, item 汇总,再用子查询拉取组总数来除。这在数据量小时可行,但隐患明显:
- 若主表有重复字段或 NULL 值,子查询的
GROUP BY和外层逻辑容易对不上; - 多层嵌套后,MySQL 5.7 或旧版 SQLite 不支持相关子查询,直接报错
This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'; - 窗口函数写法一次性完成计算,执行计划更清晰,也更容易加
WHERE过滤后再算占比(比如只看销售额 > 1000 的订单分布)。
NULL 值会让占比计算变成 NULL,得提前处理
只要 category 或 item 任一字段含 NULL,对应行的 COUNT(*) OVER (PARTITION BY ...) 就不会计入任何组——不是归入 “NULL 组”,而是彻底被忽略。结果就是:该行 pct_in_group 显示为 NULL,且分母变小,其他非 NULL 行的占比会被意外拉高。
稳妥做法是统一补空:
SELECT
COALESCE(category, 'unknown') AS category,
COALESCE(item, 'unknown') AS item,
ROUND(100.0 * COUNT(*) OVER (PARTITION BY COALESCE(category, 'unknown'), COALESCE(item, 'unknown'))
/ COUNT(*) OVER (PARTITION BY COALESCE(category, 'unknown')), 2) AS pct_in_group
FROM sales;或者更干脆,在 WHERE 中排除:WHERE category IS NOT NULL AND item IS NOT NULL —— 具体选哪种,取决于业务是否允许把 NULL 当作有效分类。
真正麻烦的是隐式 NULL:比如 LEFT JOIN 后未匹配的右表字段,或者 JSON 解析失败返回的 NULL。这些不会报错,但会让占比悄悄偏移,很难排查。

















