在按分类统计商品销量时,很多查询看起来只差一个除法,结果却容易因为分组范围、整数截断或零值分母而算错。下面按实际写 SQL 时最常见的几个问题展开,给出窗口函数写法、兼容旧版 MySQL 的替代方案,以及占比展示时该怎么处理精度。
分类销量占比的核心写法
这类计算的关键,不是先 GROUP BY 再拼回明细,而是直接在当前行上取出“所属分类的总销量”作为分母:

sales / SUM(sales) OVER(PARTITION BY category)
其中:
sales是当前商品的销量。SUM(sales) OVER(PARTITION BY category)会对同一category下的所有行求和,但结果仍保留在每一行上。
这里最容易出错的地方,是漏掉 PARTITION BY category。一旦省略,分母就会变成全表总销量,最终得到的不是“分类内占比”,而是“全表占比”。
为什么窗口函数适合这个场景
窗口函数的优势在于:既能拿到分类汇总值,又不需要丢掉商品明细行。对于“每个商品占本分类多少”这种需求,它比先聚合、再回表拼接更直接。
整数截断和版本支持要一起看
如果 sales 是整数,直接相除在部分数据库里可能被截断,结果甚至变成 0。这个问题在 PostgreSQL 和 SQL Server 中尤其常见,因此最好显式转成浮点或定点类型。
sales::DECIMAL / SUM(sales) OVER(PARTITION BY category)
CAST(sales AS FLOAT) / SUM(sales) OVER(PARTITION BY category)
版本支持方面也要提前确认:
- MySQL 8.0+ 支持窗口函数这种写法。
- MySQL 5.7 及更早版本不支持窗口函数,需要改用子查询或关联汇总结果的方式实现。
遇到零值或空值分类,怎么避免除零错误
如果某个分类下所有 sales 都是 0,那么 SUM() OVER() 的结果也会是 0。这时直接相除,可能报错,也可能返回 NULL,不同数据库行为并不一致,所以不能依赖“业务数据一般不会这样”。

更稳妥的写法,是用 NULLIF() 把分母中的 0 转成 NULL:
sales / NULLIF(SUM(sales) OVER(PARTITION BY category), 0)
这样做的结果是:当分母为 0 时,表达式安全返回 NULL。
如果业务上更希望看到 0,可以再套一层 COALESCE():
COALESCE(sales / NULLIF(SUM(sales) OVER(PARTITION BY category), 0), 0)
这一点在跨库场景里尤其重要:
- Oracle 遇到除零会直接抛异常,通常必须显式处理。
- SQLite 默认可能返回
NULL,但这种行为不适合作为通用假设。
需要显示百分比时,先算精确值再格式化
实际报表里,经常要把占比显示成类似 12.34% 的形式。这时 ROUND(x, 4) 只能控制显示位数,解决不了浮点误差本身,更不能保证汇总后严格等于 100%。
比较稳妥的方式,是先完成比例计算,再在最终展示阶段处理小数位:
ROUND(100 * sales / NULLIF(SUM(sales) OVER(PARTITION BY category), 0), 2)
有两个细节值得单独注意:
- 不要在窗口函数相关表达式里反复嵌套多层
ROUND(),每舍入一次都会引入新的误差。 - 如果业务要求“各商品占比加总后必须严格等于 100%”,通常需要用
ROW_NUMBER()把误差集中调整到最后一行。这属于展示或结算规则,不能指望 SQL 自己自然满足。
MySQL 5.7 及更早版本的替代写法
如果环境还是 MySQL 5.7 或更早版本,由于不支持窗口函数,就需要先按分类汇总,再把结果关联回明细表。思路和窗口函数一致,只是实现方式不同。

SELECT
t.category,
t.sales,
t.sales / NULLIF(s.total_sales, 0) AS ratio
FROM your_table t
JOIN (
SELECT category, SUM(sales) AS total_sales
FROM your_table
GROUP BY category
) s ON t.category = s.category;
如果还需要避免整数截断,同样要在除法前做显式类型转换。换句话说,旧版 MySQL 只是写法不同,分母范围、除零防御和精度处理这三件事并不会少。
实战里最常踩的几个坑
- 漏写
PARTITION BY category,把分类占比算成全表占比。 - 直接用整数做除法,结果被截断。
- 忽略零值分类或脏数据,导致除零错误。
- 过早
ROUND(),把展示问题变成精度问题。
如果数据来自旧系统迁移,NULL 和 0 混在一起非常常见。此时最先检查的通常不是窗口函数本身,而是类型转换和 NULLIF 是否已经补齐。













