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

相关推荐
小程故事多_801 小时前
Claude Code自定义workflow skills用法
数据库·人工智能·智能体
云烟成雨TD1 小时前
Spring AI Alibaba 1.x 系列【64】 ReactAgent 长期记忆
java·人工智能·spring
quan26312 小时前
20260529,日常开发-数据库主从问题
java·mysql·主从·延迟
JacksonMx2 小时前
@Transactional 最佳实践
java·spring boot·spring·性能优化
夏贰四2 小时前
数据建模工具如何筑牢数据根基?数据建模工具怎样落实标准体系?
数据库·数学建模·数据建模工具
Sincerelyplz2 小时前
【AI会议纪要实践】mapReduce、RAG 与结构化输出
java·后端·agent
过期动态2 小时前
【LeetCode 热题 100】接雨水
java·数据结构·算法·leetcode·职场和发展
zhangjw343 小时前
第15篇:Java多线程零基础入门,进程线程、线程创建方式、线程生命周期、线程安全彻底吃透
java·开发语言·面试
蝈理塘(/_\)大怨种3 小时前
类和对象 (上)
java·开发语言