SQL 排序与分组实战:解决“分组后取最新数据“

SQL 排序与分组实战:解决"分组后取最新数据"

这是 SQL 面试里的高频题,也是工作中的真实痛点。

"按用户分组,取每个用户最近一次下单记录。"

听着简单,写起来全是坑。


一、问题到底难在哪

先看需求:

订单id 用户id 金额 下单时间
1 A 100 2026-05-01
2 A 200 2026-05-03
3 B 300 2026-05-02
4 B 150 2026-05-05

目标:取每个用户最新的一笔订单。

期望结果:

用户id 订单id 金额 下单时间
A 2 200 2026-05-03
B 4 150 2026-05-05

二、❌ 错误写法(90%的人踩过)

复制代码

sql

复制代码
`SELECT user_id, order_id, amount, order_time
FROM orders
GROUP BY user_id
ORDER BY order_time DESC;
`

结果:报错。

MySQL 的 ONLY_FULL_GROUP_BY 模式下,SELECT 里的非聚合字段必须出现在 GROUP BY 中。

就算关了这个模式,结果也是随机的------数据库随便挑了一行给你,不一定是最新的。

记住:GROUP BY 不保证任何排序,它只负责分组。


三、✅ 正确写法,共 5 种

按推荐程度排序。


方法1:窗口函数 ROW_NUMBER(推荐 ⭐⭐⭐⭐⭐)

复制代码

sql

复制代码
`SELECT user_id, order_id, amount, order_time
FROM (
    SELECT *,
           ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_time DESC) AS rn
    FROM orders
) t
WHERE rn = 1;
`

原理

  • PARTITION BY user_id → 按用户分组
  • ORDER BY order_time DESC → 组内按时间倒序
  • ROW_NUMBER() → 给每组内的行编号,最新的是 1
  • 外层 WHERE rn = 1 → 只取第一条

优点 : 逻辑清晰,性能好,支持取前 N 条(改 rn <= 3 就是取最近3条)。


方法2:子查询 + MAX(适合简单场景 ⭐⭐⭐⭐)

复制代码

sql

复制代码
`SELECT o.*
FROM orders o
INNER JOIN (
    SELECT user_id, MAX(order_time) AS max_time
    FROM orders
    GROUP BY user_id
) t ON o.user_id = t.user_id AND o.order_time = t.max_time;
`

原理

  • 先用子查询找出每个用户的最大时间
  • 再 join 回去,取出完整行

缺点: 如果同一用户有两条完全相同时间的记录,会都返回。需要去重的话改成:

复制代码

sql

复制代码
`SELECT o.*
FROM orders o
INNER JOIN (
    SELECT user_id, MAX(order_time) AS max_time
    FROM orders
    GROUP BY user_id
) t ON o.user_id = t.user_id AND o.order_time = t.max_time
GROUP BY o.user_id;  -- MySQL 特有写法,强制去重
`

方法3:相关子查询(直观但慢 ⭐⭐⭐)

复制代码

sql

复制代码
`SELECT o.*
FROM orders o
WHERE order_time = (
    SELECT MAX(order_time)
    FROM orders
    WHERE user_id = o.user_id
);
`

原理: 对每一行,都去查一次该用户的最大时间。

缺点: 数据量大时极慢,因为子查询执行了 N 次。


方法4:LEFT JOIN 自连接(经典老方法 ⭐⭐⭐)

复制代码

sql

复制代码
`SELECT o1.*
FROM orders o1
LEFT JOIN orders o2
  ON o1.user_id = o2.user_id
  AND o1.order_time < o2.order_time
WHERE o2.order_id IS NULL;
`

原理

  • 用 o1 左连接 o2,条件是"同一用户且 o2 时间更晚"
  • 如果 o2 为 NULL,说明没有比 o1 更晚的记录 → o1 就是最新的

优点: 不需要窗口函数,老版本 MySQL 也能用。

缺点: 逻辑绕,性能一般,数据量大时 JOIN 开销高。


方法5:GROUP_CONCAT 拼接(取巧 ⭐⭐)

复制代码

sql

复制代码
`SELECT user_id,
       SUBSTRING_INDEX(GROUP_CONCAT(order_id ORDER BY order_time DESC), ',', 1) AS latest_order_id,
       SUBSTRING_INDEX(GROUP_CONCAT(amount ORDER BY order_time DESC), ',', 1) AS latest_amount
FROM orders
GROUP BY user_id;
`

原理: 把组内数据按时间倒序拼成字符串,取第一个。

缺点: 只适合取单个字段,取多字段写起来很丑,而且有长度限制。


四、5 种方法对比

方法 性能 可读性 取前N条 兼容性
ROW_NUMBER ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ✅ 轻松 MySQL 8.0+
子查询+MAX ⭐⭐⭐⭐ ⭐⭐⭐⭐ ❌ 需改写 通用
相关子查询 ⭐⭐ ⭐⭐⭐⭐⭐ 通用
LEFT JOIN ⭐⭐⭐ ⭐⭐ 通用
GROUP_CONCAT ⭐⭐⭐ ⭐⭐ 通用

五、一个真实踩坑案例

需求:取每个用户最近3笔订单。

很多人第一反应是改 WHERE rn = 1WHERE rn <= 3

对,就是这么简单。 窗口函数的优势在这里体现得淋漓尽致:

复制代码

sql

复制代码
`SELECT user_id, order_id, amount, order_time
FROM (
    SELECT *,
           ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_time DESC) AS rn
    FROM orders
) t
WHERE rn <= 3;
`

换成其他方法?要么改起来很麻烦,要么根本不支持。


六、总结一句话

场景 用什么
取每组最新1条 ROW_NUMBER() 首选
取每组最新N条 ROW_NUMBER() 唯一靠谱选择
老版本MySQL 子查询 + MAX
面试手写 LEFT JOIN 自连接

核心记住一句:GROUP BY 只管分组,不管排序。要取最新,必须先排序再取。

相关推荐
行者全栈架构师41 分钟前
IDEA 中 Maven 项目的 15 个红色报错快速解决方法
java·后端
令人头秃的代码0_044 分钟前
mac(m5)平台编译openjdk
java
倔强的石头_1 天前
《Kingbase护城河》——数据库存储空间全景探测与精细化瘦身实战
数据库
唐青枫1 天前
Java JDBC 实战指南:从 Connection 到事务和连接池
java
一个做软件开发的牛马1 天前
MyBatis-Plus 从零实战:完整搭建可运行 Demo,BaseMapper 零 SQL、Wrapper 条件构造、分页插件与代码生成器详解
java·后端
用户3721574261351 天前
Java 处理 PDF 图片:提取 PDF 中的图片,并压缩 PDF 图片体积
java
逐光老顽童1 天前
Java 与 Kotlin 混合开发避坑指南:30 个真实案例实录
android·kotlin
用户3721574261351 天前
Java 打印 Word 文档:从基础打印到高级设置
java
冬奇Lab2 天前
每日一个开源项目(第134篇):Zvec - 阿里开源的嵌入式向量数据库,向量搜索界的 SQLite
数据库·人工智能·llm
爱勇宝2 天前
鸿蒙生态的下半场:开发者不只要能开发,还要能赚钱
android·前端·程序员