开窗函数这个话题,其实最核心的价值就在于它能让我们在保留全部明细数据的同时,完成复杂的聚合计算。比如,要算每个产品在其所在分类中的销售占比,这在报表分析里太常用了。如果用传统GROUP BY再加JOIN回去,SQL写得啰嗦不说,性能也糟糕。
计算分组内占比的核心逻辑
说白了就是:用当前行的值,除以它所在那个分组的总和。关键的技巧在于,必须用SUM() OVER(PARTITION BY ...)这个开窗函数,把分组的总和“广播”到每一行上。这样,分母就变成了一个针对当前分组的、重复出现的总数值,分子则是它自己的原值。
这里的几个要点,新手最容易栽跟头:
- 分母:必须用
SUM(amount) OVER (PARTITION BY category),这才能算出每个category的总和,并把它赋给该类别下的每一行。 - 分子:直接写字段
amount就行,千万别画蛇添足,再加什么聚合或窗口函数。 - 类型陷阱:这是个大坑。尤其在PostgreSQL和SQL Server里,如果
amount是整数,直接除会得到0(整数除法截断)。通常需要显式转为DECIMAL,或者像例子中那样乘个100.0来“逼”出小数。
MySQL 8.0+ vs. PostgreSQL:写法差异与注意
虽然语法看着差不多,但不同数据库在除法上的默认行为不同,这一点需要注意。
SELECT
category,
product,
amount,
ROUND(amount * 100.0 / SUM(amount) OVER (PARTITION BY category), 2) AS pct
FROM sales;
- MySQL 8.0+:相对省心,默认会返回小数。不过要控制精度,把
100.0换成100.00是个好习惯。 - PostgreSQL:这里最典型。如果
amount是INTEGER,而你不记得乘100.0,结果出来全是0。这是个非常常见的坑。 - SQL Server:和PostgreSQL类似,必须确保除法的两个操作数中至少有一个是浮点类型,否则结果会被截断为整数。
诡异的NULL值
数据里出现NULL是家常便饭,它处理起来需要点技巧。默认情况下,SUM() OVER()会忽略NULL,这本身问题不大。但最麻烦的情况是,如果整个分组的amount都是NULL,那分子分母都是NULL,结果自然也是NULL,这就很难看了。
常见的应对策略:
- 用
COALESCE(SUM(amount) OVER (...), 0)把分母里的NULL补成0。但这样有可能引出“除零”错误,所以最好再用个CASE保护一下。 - 更稳妥的写法是:
CASE WHEN SUM(amount) OVER (PARTITION BY category) = 0 THEN 0 ELSE ... END。逻辑清晰,也不怕除零。 - 如果业务逻辑要求把
NULL当作0来处理,那就需要在计算分子和分母时,都先用COALESCE(amount, 0)把它替换掉。
多维度分组
如果分组条件不止一个,比如要算“每个城市、每一年”的销售占比,那就要把维度都放进PARTITION BY里。
- 正确写法:
PARTITION BY city, YEAR(order_date)。顺序不重要,但列必须齐全。 - 错误写法:只写了
PARTITION BY city,忘了年份。结果就是把不同年份的数据混在一起算,结果完全错误。 - 还有个小坑:窗口函数里不能直接用列别名。比如你定义了
YEAR(order_date) as yr,但在OVER()里还是得老老实实写原表达式YEAR(order_date)。
总结一下,写这类SQL时,最容易卡住的地方就是分母的类型转换和NULL值处理。特别是从MySQL迁移到PostgreSQL,或者在不同数据库之间切换时,同样的SQL跑出来全是0或者全是NULL,这种问题排查起来最让人头疼。
