ROW_NUMBER() 可实现严格 Top 3:为每行分配唯一序号,确保每组恰好三条记录;需在 ORDER BY 中添加稳定排序键(如 id)避免结果不可复现。

用 ROW_NUMBER() 实现严格 Top 3(含并列时截断)
当需要每个分组中「恰好三条记录」,哪怕存在相同排序值也强制只取前三个(比如按时间戳取最新三条),ROW_NUMBER() 是最直接的选择。它为每行分配唯一序号,不跳过、不重复。
常见错误是误用 RANK() 或 DENSE_RANK() 导致返回超过三条——比如某组里有 4 条 score=95 的记录,RANK() 会全给 1,结果全被留下。
实操建议:
- 确保
ORDER BY子句在窗口定义中明确且稳定(例如加id作第二排序键,避免无序导致结果不可复现) - 写法示例:
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY category ORDER BY score DESC, id) AS rn FROM products ) t WHERE rn <= 3;
- 注意:MySQL 8.0+、PostgreSQL、SQL Server、Oracle 均支持;SQLite 3.25+ 也支持,但旧版不支持窗口函数
用 DENSE_RANK() 获取「并列也算一个名次」的前三名
如果业务要求「分数并列第一,下一名就是第二」,且希望所有并列第三的记录都保留(比如 top 3 分数段的所有人),就得用 DENSE_RANK()。它不会因并列而跳过后续名次。
典型场景:排行榜展示「进入前三名的所有用户」,而非「只展示三个人」。
实操建议:
-
DENSE_RANK()和RANK()都能处理并列,但RANK()会在并列后跳号(如 1,1,3),容易漏掉实际想留下的记录 - 示例:
SELECT * FROM ( SELECT *, DENSE_RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS dr FROM employees ) t WHERE dr <= 3;
- 性能上,三者差异极小;但若分组内数据量极大(如单组百万行),
ORDER BY字段必须有索引,否则窗口计算会变慢
WHERE 子句不能直接引用窗口函数别名
很多人写完 SELECT *, ROW_NUMBER() OVER (...) AS rn 后,直接在外部 WHERE rn ,结果报错:<code>column "rn" does not exist 或类似提示。
这是因为窗口函数在 SQL 执行顺序中晚于 WHERE,所以别名在 WHERE 层不可见。
实操建议:
- 必须用子查询或 CTE 包裹,再在外层过滤——没有例外
- CTE 写法更清晰(尤其多层逻辑时):
WITH ranked AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY region ORDER BY sales DESC) AS rn FROM orders ) SELECT * FROM ranked WHERE rn <= 3;
- 不要试图用
HAVING替代——HAVING只对聚合有效,不适用窗口函数
MySQL 5.7 或旧版 PostgreSQL 怎么办?
这些版本不支持窗口函数,硬要用 Top N 就得靠自连接或变量模拟,但极易出错且难以维护。
实操建议:
- MySQL 5.7 推荐升级到 8.0+;若无法升级,可用相关子查询(性能差,仅限小数据量):
SELECT * FROM products p1 WHERE ( SELECT COUNT(*) FROM products p2 WHERE p2.category = p1.category AND p2.score > p1.score ) < 3;
- PostgreSQL 9.6- 可用
LIMIT+UNION ALL模拟,但需手动为每个分组写一遍,不现实 - 真正要落地,优先推动数据库升级——窗口函数不是语法糖,是解决这类问题的基础设施
实际用的时候,先想清楚「要不要保留并列」,再选函数;然后立刻套一层子查询,别省这一步;最后检查执行计划里 ORDER BY 字段有没有走索引——没索引的 PARTITION BY ... ORDER BY 在大数据量下会明显拖慢。


















