不能直接用 WHERE 过滤分组后的百分比;需先计算排名或累计占比,再用 HAVING 或子查询筛选前 80% 记录并求和。

WHERE 不能直接过滤分组后的百分比,得先算排名或累计占比
想用 WHERE 过滤“前 80% 的记录再求和”,直接写 WHERE percentile_rank 会报错——因为 percentile_rank() 是窗口函数,不能出现在 WHERE 子句里。WHERE 执行在聚合和窗口计算之前,此时根本没有分组内的排序或占比数据。
真正可行的路径是:先用窗口函数算出每行在组内的相对位置(比如用 ROW_NUMBER()、CUME_DIST() 或 PERCENT_RANK()),再用子查询或 CTE 把它拎出来,最后在外层加 WHERE 过滤。
-
CUME_DIST()更适合“截断到某个累计占比”,它返回 ≤ 当前行值的行数占比,结果范围是 (0, 1],且对重复值处理更稳定 -
PERCENT_RANK()返回的是 (rank-1)/(n-1),首尾固定为 0 和 1,中间分布可能稀疏,不适合严格按百分比截断 - 如果要截“金额最大的前 80% 记录”,必须先按金额降序排列,否则
CUME_DIST()算的是小值占比
用 CTE + CUME_DIST() 实现分组内截断求和
假设表 sales 有字段 region、amount,目标是:每个 region 内,取 amount 累计占比 ≤ 0.8 的记录,对其 amount 求和。
ORDER BY amount DESC
- PostgreSQL / SQL Server / Oracle / DuckDB 都支持
CUME_DIST() - MySQL 8.0+ 也支持,但 MySQL 5.7 不行,得用变量模拟,误差大、不可靠
- 如果
amount有 NULL,默认排在最前(DESC下),影响占比计算;建议加WHERE amount IS NOT NULL预过滤
替代方案:用 NTILE(5) 近似截 80%,但边界不精确
如果数据库不支持窗口函数,或想绕过浮点精度问题,可用 NTILE(5) 把每组分成 5 档,取前 4 档(≈80%)。但它只保证行数均分,不保证数值累计占比真为 0.8。
NTILE(5)
-
NTILE()在 SQLite 中不支持,MySQL 5.7 也不支持 - 别用
ROW_NUMBER() / COUNT(*)手动算比例——COUNT(*) 必须用窗口形式COUNT(*) OVER (PARTITION BY ...),否则聚合报错
WHERE 放错位置导致全表扫描或逻辑错误
最容易踩的坑,是把过滤条件写在聚合后、却幻想它能控制参与聚合的原始行。例如:
SELECT region, SUM(amount) FROM sales GROUP BY region HAVING SUM(amount) > 1000; -- 这是过滤分组结果,不是截断原始行
或者更隐蔽的错误:
SELECT region, SUM(amount) FROM sales WHERE amount > (SELECT PERCENTILE_CONT(0.2) WITHIN GROUP (ORDER BY amount) FROM sales) GROUP BY region;
这个 WHERE 是全表范围的 20% 分位数,不是“每个 region 自己的 20%”。它没做 PARTITION BY,根本不是分组内截断。
- 所有“分组内截断”逻辑,必须依赖窗口函数 + 子查询/CTE 结构
- 别在外部
WHERE里引用任何未出现在SELECT中的窗口别名,SQL 引擎不认 - 大数据量下,
CUME_DIST()窗口计算本身有开销,确保PARTITION BY字段上有索引(如(region, amount)复合索引)
实际执行时,CUME_DIST() 的精度、NULL 处理、重复值排序行为,在不同数据库间略有差异,上线前务必用真实数据样本验证截断边界是否符合业务预期。










