用 row_number() 去重最稳,核心是 partition by 分组 + order by 排序后取 rn = 1;distinct 对整行判重无效,group by 聚合易错配字段,窗口函数确保整行一致性。

用 ROW_NUMBER() 做去重最稳,别碰 DISTINCT 套子查询
嵌套查询本身不解决去重逻辑,真正起作用的是窗口函数。很多人写个 SELECT DISTINCT * FROM (SELECT ...),结果发现重复还在——因为 DISTINCT 对整行判重,而你真正想保留的可能是“每个用户最新一条订单”,这得靠排序+编号。
-
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC)是核心:按用户分组,按时间倒序排,每组第一行就是最新记录 - 嵌套只是载体:外层筛出
rn = 1的行,内层负责算编号,不能反过来把ROW_NUMBER()放在外层 - 常见错误是漏写
PARTITION BY,导致全表只编 1 个号;或ORDER BY用了ASC却以为能取最新
MySQL 8.0+ 和 PostgreSQL 可直接用,但 SQLite 要绕路
窗口函数不是所有数据库都支持。SQLite 在 3.25.0+ 才支持 ROW_NUMBER(),老版本只能用自关联或 GROUP BY + MAX() 模拟,但容易丢字段。
- MySQL 5.7 及更早:不支持窗口函数,嵌套子查询配合
JOIN或相关子查询是唯一办法,性能差、SQL 膨胀 - PostgreSQL:支持完整窗口函数,且
WITH TIES配合FETCH FIRST能处理并列情况(比如同时间两条记录) - 注意 MySQL 8.0 默认开启窗口函数,但某些云厂商 RDS 可能默认关着,得确认
sql_mode是否含ONLY_FULL_GROUP_BY干扰
去重字段不一致时,GROUP BY + 聚合易翻车
有人用 SELECT user_id, MAX(created_at), MAX(order_amount) FROM orders GROUP BY user_id,看似去重,实则错配:最大时间那条记录的金额,未必和最大金额是同一行。
- 这种写法在 MySQL 5.7 strict 模式下会报错,在宽松模式下返回不可靠结果
- 正确做法仍是窗口函数:先编号,再过滤,确保整行数据来自同一条原始记录
- 如果只要某个字段(如最新状态),可用
FIRST_VALUE(status) OVER (PARTITION BY user_id ORDER BY updated_at DESC),避免多层嵌套
性能敏感场景必须加索引,否则嵌套+窗口=慢出天际
窗口函数本身不走索引,PARTITION BY 和 ORDER BY 字段没索引时,数据库要全表排序,嵌套一层就放大一次开销。
- 复合索引优先建在
(user_id, created_at)上,顺序不能反——PARTITION BY字段必须在前 - 大表慎用
RANK()或DENSE_RANK():它们对并列值分配相同序号,可能导致外层WHERE rn = 1漏掉本该保留的记录 - 测试时用
EXPLAIN看执行计划,重点盯Using filesort和Using temporary出现次数
真正麻烦的是业务语义模糊的时候:比如“最新”是指最后插入、最后更新,还是最后支付?窗口函数只管排序依据,填错字段比语法错误更难排查。










