row_number()是窗口函数,必须配合over子句使用,不能直接用于group by后;正确做法是用partition by实现逻辑分组,再通过外层where过滤行号。

为什么直接在GROUP BY后用ROW_NUMBER会报错
因为 ROW_NUMBER() 是窗口函数,必须配合 OVER 子句使用,而 GROUP BY 会折叠行、破坏原始行结构——两者语义冲突。你不能写 SELECT ..., ROW_NUMBER() OVER (...) FROM t GROUP BY ...,SQL 引擎会直接报错 "window function is not allowed in GROUP BY" 或类似提示。
真正需要的不是“在 GROUP BY 里用 ROW_NUMBER”,而是“先按某维度分组,再在每组内编号并截取前 N 条”。这本质是分组内排序+限行,和聚合无关。
正确做法:用窗口函数替代GROUP BY进行分组内编号
把分组字段放进 OVER(PARTITION BY ...),用 ORDER BY 控制组内顺序,再用外层过滤。这是最常用且兼容性最好的方式。
-
PARTITION BY user_id相当于按user_id“逻辑分组”,但保留所有原始行 -
ORDER BY created_at DESC决定每组内哪条排第一(比如最新记录) - 外层
WHERE rn 实现“每组取前 3 条”
SELECT user_id, order_id, amount
FROM (
SELECT user_id, order_id, amount,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders
) t
WHERE rn
<h3>注意ORDER BY字段必须可排序,且NULL值会影响结果</h3>
<p>如果 <code>ORDER BY</code> 字段含大量 <code>NULL</code>(如 <code>created_at IS NULL</code>),不同数据库对 <code>NULLS FIRST/LAST</code> 默认处理不同:PostgreSQL 默认 <code>NULLS LAST</code>,MySQL 8.0+ 默认 <code>NULLS FIRST</code>,SQL Server 视版本而定。这会导致同一语句在不同环境截取的“前 N 条”不一致。</p>
- 显式声明更可靠:
ORDER BY created_at DESC NULLS LAST(PostgreSQL/Oracle 支持) - MySQL 8.0+ 可用
ORDER BY created_at DESC, id DESC补充二级排序,避免 NULL 干扰 - 若业务上
created_at必须非空,建议加约束或用COALESCE(created_at, '1970-01-01')填充
性能关键:PARTITION BY字段一定要有索引
ROW_NUMBER() 的 PARTITION BY + ORDER BY 组合,实际依赖排序操作。若 (user_id, created_at) 没有联合索引,大表上可能触发全表扫描+临时文件排序,耗时陡增。
- 建索引优先考虑覆盖:例如
CREATE INDEX idx_user_created ON orders(user_id, created_at DESC); - 如果只查最新 1 条,且数据量极大,可考虑物化视图或定时刷新的汇总表,避免每次实时计算
- 别在子查询里对超大中间结果集(如未加 WHERE 过滤的千万级表)直接套
ROW_NUMBER(),先缩小范围再编号
分组截取看着简单,但一旦涉及千万级数据或高并发,PARTITION BY 字段的选择、索引设计、NULL 处理这三点漏掉任何一个,都可能让查询从毫秒级变成分钟级。










