sql无内置分组取最新操作,须用row_number() over(partition by 分组字段 order by 时间/id desc)编号后where rn=1筛选;需加二级排序防时间重复导致结果不可控。

用 ROW_NUMBER() 配合 PARTITION BY 实现分组取最新
直接结论:SQL 没有内置“分组去重并留最新”的原子操作,必须靠窗口函数模拟。核心是给每组数据按时间/ID倒序编号,再筛出序号为 1 的记录。
典型场景:用户表有重复 user_id,但要保留每个用户最后一条 created_at 的记录;订单表按 order_no 分组,取 updated_at 最大的那条。
关键点在于排序字段必须能明确表达“新旧”——优先用带时区的 TIMESTAMP WITH TIME ZONE,其次用自增 id(需确认插入顺序与业务时间一致)。
示例(PostgreSQL / MySQL 8.0+ / SQL Server):
SELECT user_id, email, created_at
FROM (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY created_at DESC, id DESC
) AS rn
FROM users
) t
WHERE rn = 1;
注意:ORDER BY 里加 id DESC 是防 created_at 精度不足导致并列——比如多个记录同秒插入,不加二级排序可能随机选中某条,结果不可控。
MySQL 5.7 或 SQLite 等不支持窗口函数怎么办
只能退化为关联子查询或左连接,性能差、写法绕,且容易漏数据。
常见错误写法:SELECT * FROM users GROUP BY user_id ORDER BY created_at DESC —— 这在 MySQL 5.7 是非法语法(除非开了 ONLY_FULL_GROUP_BY 关闭),即使能跑,SELECT * 中非分组字段的值也是未定义的,不能保证取到最新那条。
安全替代方案(以 MySQL 5.7 为例):
- 用相关子查询:对每条记录查同
user_id下最大created_at,再比对是否相等 - 用左连接自关联:把表连自己一次,条件是相同
user_id且对方时间更大,只取连接结果为空的行(即没有“更新”的那条)
示例(左连接法):
SELECT u1.* FROM users u1 LEFT JOIN users u2 ON u1.user_id = u2.user_id AND u1.created_at <p>⚠️ 这个写法在 <code>created_at</code> 有重复时会返回多条——因为 u1 和 u2 的“更大”关系不成立,u2.id 仍为 NULL。此时必须补上 <code>AND (u1.created_at 才能唯一确定。</code></p><h3> <code>DISTINCT ON</code> 是 PostgreSQL 的快捷方式,但有陷阱</h3><p>PostgreSQL 提供 <code>DISTINCT ON</code>,看起来更简洁:</p><pre class="brush:php;toolbar:false;">SELECT DISTINCT ON (user_id) * FROM users ORDER BY user_id, created_at DESC, id DESC;
但它强制要求 ORDER BY 开头必须和 DISTINCT ON 字段一致(这里是 user_id),否则报错 SELECT DISTINCT ON expressions must match initial ORDER BY expressions。
容易踩的坑:
- 忘写
ORDER BY后续字段,导致“最新”不准确 - 误以为
DISTINCT ON能跨数据库使用——它仅 PostgreSQL 支持,迁移到 MySQL 或 SQL Server 会直接报错 - 在复杂查询中嵌套
DISTINCT ON,结果集排序可能被外层干扰,建议封装成 CTE 或子查询再处理
性能关键:索引必须覆盖分组和排序字段
无论用哪种写法,没索引时全表扫描 + 临时文件排序,百万级数据就明显卡顿。
最优索引结构是复合索引,顺序必须匹配查询中的 PARTITION BY 和 ORDER BY 字段:
- 对
ROW_NUMBER()方案:建INDEX idx_user_created_id ON users (user_id, created_at DESC, id DESC) - 对左连接方案:同样需要这个索引,否则
u2表无法高效定位“更大的记录” - 如果经常按不同字段取最新(比如有时按
updated_at,有时按status_updated_at),就得建多个索引,权衡写入开销
特别提醒:MySQL 中 DESC 在索引定义里从 8.0 才真正生效,5.7 及以前版本即使写了 DESC 也当成 ASC 处理,得靠应用层或调整查询逻辑兜底。










