ROW_NUMBER() 必须配合 PARTITION BY 才能实现每组取一条的去重效果,仅用 ORDER BY 会生成全局序号;分页需在外层用 WHERE 过滤 rn,索引应以 PARTITION BY 字段为前导。

ROW_NUMBER() 不能直接去重,必须配合 PARTITION BY 使用
很多人以为 ROW_NUMBER() 自带去重能力,其实它只是按排序生成连续序号,重复数据照样各得一个编号。真正实现“每组取一条”的关键,是用 PARTITION BY 划分逻辑组,再对组内排序并编号。比如按 user_id 分组、按 created_at DESC 排序,就能拿到每个用户的最新记录。
常见错误是只写 ORDER BY 不写 PARTITION BY,结果全表排一遍序,ROW_NUMBER() 变成全局自增 ID,和去重完全无关。
- 正确写法:
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) - 错误写法:
ROW_NUMBER() OVER (ORDER BY created_at DESC)(无分区,不解决去重) - 注意:MySQL 8.0+ 才支持窗口函数,5.7 及更早版本会报错
FUNCTION xxx does not exist
分页必须在窗口函数之后用 WHERE 过滤,不能在子查询里 LIMIT
窗口函数执行顺序晚于 WHERE 和 GROUP BY,但早于 ORDER BY 和 LIMIT。所以必须把 ROW_NUMBER() 放在子查询或 CTE 中,外层再用 WHERE rn BETWEEN ? AND ? 实现分页。直接在含 ROW_NUMBER() 的主查询加 LIMIT,会导致先截断再编号,漏掉本该保留的首条记录。
例如想取第 2 页(每页 10 条),不能写 LIMIT 10 OFFSET 10 在窗口语句里,而要:
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
WITH ranked AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn FROM orders ) SELECT * FROM ranked WHERE rn = 1 ORDER BY created_at DESC LIMIT 10 OFFSET 10;
-
rn = 1表示每组只取最新一条(去重) - 分页条件(
LIMIT/OFFSET)必须在外层,否则窗口计算不完整 - 如果用
OFFSET偏移量很大(如 >10w 行),性能会明显下降,此时应改用游标分页(基于上一页最后的created_at值)
去重 + 分页的性能瓶颈主要在 PARTITION BY 字段的索引缺失
PARTITION BY 字段如果没有索引,MySQL 无法高效分组,窗口函数会触发临时表 + 文件排序,尤其在千万级表上可能慢到超时。即使 ORDER BY 字段有索引,PARTITION BY 字段没索引也白搭。
- 最优索引组合:
(user_id, created_at)(前导列必须是PARTITION BY字段) - 不要建单列
created_at索引——它对窗口分组无效 - 用
EXPLAIN检查执行计划,确认type是ref或range,而不是ALL或index - 如果
PARTITION BY字段基数极高(如 UUID),分组开销大,考虑是否真需要实时去重,还是可走异步归档+物化视图
替代方案:用 GROUP BY + ANY_VALUE() 更轻量,但不保证取哪一条
如果业务允许“任意取一条”而非“明确取最新/最旧”,GROUP BY 比窗口函数快得多,且 MySQL 8.0 默认允许 ANY_VALUE() 绕过 ONLY_FULL_GROUP_BY 限制。
SELECT user_id, ANY_VALUE(order_id) AS order_id, MAX(created_at) AS latest_time FROM orders GROUP BY user_id ORDER BY latest_time DESC LIMIT 10 OFFSET 10;
- 比窗口函数少一次全表扫描,适合大数据量快速分页
- 但
ANY_VALUE(order_id)不保证对应MAX(created_at)那条记录,可能跨行错配 - 若必须字段强一致(如取最新订单的完整行),只能用窗口函数,别省这个事
实际用的时候,最易忽略的是索引设计与分页位置——PARTITION BY 字段没索引,再正确的 SQL 也跑不动;OFFSET 超过几万,响应时间就不可控。这两点不提前压测,上线后就是慢查询告警。










