需用row_number()窗口函数配合子查询获取每组top 3,因sql不支持group by后直接limit/top;mysql 8.0+、postgresql、sql server、oracle及sqlite 3.25+支持,但where中不可直接使用窗口函数。

用 ROW_NUMBER() 配合子查询取每组 Top 3
直接在 GROUP BY 后用 LIMIT 或 TOP 不行,SQL 不支持分组内限制行数。必须借助窗口函数生成组内序号,再在外层筛选 rn 。
关键点是:窗口函数不能出现在 WHERE 中(执行顺序早于 WHERE),所以必须套一层子查询或 CTE。
-
ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC)按 category 分组,sales 降序排,相同值也会被赋予不同序号 - 如果要处理并列(比如两个第二名),改用
RANK()或DENSE_RANK(),但要注意它们会导致实际返回行数可能 > 3 - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持;SQLite 3.25+ 也支持,但旧版不支持窗口函数
避免常见错误:WHERE 中误用窗口函数
下面这种写法会报错:SELECT * FROM orders WHERE ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) —— 因为窗口函数不能在 WHERE 子句中使用。
正确结构必须是:
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn FROM employees ) t WHERE t.rn
- 别名
t必须存在,否则外层无法引用rn - ORDER BY 在窗口定义里决定“谁排前三”,不是在外层 SELECT 后加 ORDER BY
- 如果原表没主键或排序不稳定(如 salary 相同),建议加二级排序字段(如
id)保证结果可重现:ORDER BY salary DESC, id ASC
性能注意:大表慎用,加好索引
窗口函数本身不走索引,PARTITION BY 字段 + ORDER BY 字段组合的联合索引能显著提速。
- 例如对
GROUP BY category ORDER BY created_at DESC,建索引:CREATE INDEX idx_cat_created ON table_name (category, created_at DESC) - PostgreSQL 和 SQL Server 对这类索引优化较好;MySQL 8.0 支持前缀索引但不支持 DESC 索引,可用
created_at + 0变通(不推荐),更稳妥是升级到 8.0.13+ 并确认版本支持 - 若数据量超千万且实时性要求不高,考虑物化中间结果(如每天定时存 Top 3 到汇总表)
兼容旧版本 MySQL(5.7 及以下)的替代方案
没有窗口函数时只能靠自连接或相关子查询,性能差、写法绕,仅作兜底。
典型自连接写法(按 price 取每 category 前三):
SELECT t1.* FROM products t1 WHERE ( SELECT COUNT(*) FROM products t2 WHERE t2.category = t1.category AND t2.price > t1.price )
- 这个逻辑是:“比当前记录 price 更高的同 category 记录少于 3 个”,即当前记录至少排前三
- 注意是
而不是 <code>,否则会取到前四名 - 该方式在 category 值重复多、price 高度重复时容易漏数据(并列情况处理粗糙);且无索引时可能全表扫描多次
- 强烈建议优先升级数据库,而非硬扛这种写法
窗口函数是目前最可靠、可读性最强的解法,但 PARTITION BY 字段的数据倾斜、排序字段的重复性、以及底层是否真支持——这几个点一旦忽略,要么结果错,要么慢到超时。











