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 只管分组,不管排序。要取最新,必须先排序再取。

相关推荐
H_HX1267 分钟前
IDEA的下载和安装
java·intellij-idea·idea
AIGS0019 分钟前
向量空间JBoltAI:本体语义如何跨越企业AI的语义鸿沟
java·大数据·人工智能·人工智能ai大模型应用
r_oo_ki_e_13 分钟前
Java 线程池
java·开发语言
日月云棠15 分钟前
Spring 源码深度剖析:DispatcherServlet 的自动化注册机制
java·后端
CyL_Cly29 分钟前
解决官网下载慢!Android Studio 最新版网盘下载
android·ide·android studio
多巴胺梦想家31 分钟前
数据库恢复技术:当灾难来临
网络·数据库·oracle
智塑未来40 分钟前
鸿蒙系统对比安卓、iOS,核心优势是什么?
android·ios·harmonyos
AI刀刀1 小时前
豆包智能体对话导出后,如何构建长期归档与高效检索体系?
android·人工智能·word·excel·ai导出鸭
北冥you鱼1 小时前
Go 语言初始化 MySQL 数据库实战指南:从零到生产环境部署
数据库·mysql·golang
Jazz_z1 小时前
通过 Java 实现 Word 转 TXT
java·c#·word