嗨,各位奋斗在代码一线的开发者们!👋 你是否也曾遇到过这样的场景:
“老板,我需要从这张订单表里,查出每个用户的最后一笔订单金额。”
“产品经理,我要在用户动态列表里,只显示每个用户最新发布的那一条动态。” 这些需求翻译成 SQL 语言,本质上都是同一个问题:如何在一个分组中,取出按时间排序后的最新一条记录? 很多同学的第一反应可能是这样写:
SELECT sb_id, qyb
FROM yqhs_qyb
WHERE jyz_id = #{jyzId}
ORDER BY create_time DESC;
然后,在前端代码里循环处理,或者用 GROUP BY sb_id 试试?结果发现要么数据不对,要么语法报错。🤯
别慌!今天这篇文章,就带你彻底搞懂这个问题,并为你盘点四种解决方案,从青铜到王者,总有一款适合你!
场景设定
假设我们有一张表 yqhs_qyb,记录了不同设备(sb_id)在不同时间(create_time)的某个业务值(qyb)。我们的目标是:获取每个 sb_id 对应的最新 qyb 数据。
表结构简化如下:
| id | sb_id | qyb | create_time | jyz_id |
|---|---|---|---|---|
| 1 | A | 100 | 2023-10-01 10:00:00 | 101 |
| 2 | B | 200 | 2023-10-01 10:01:00 | 101 |
| 3 | A | 150 | 2023-10-01 10:05:00 | 101 |
| 4 | C | 300 | 2023-10-01 10:02:00 | 101 |
| 5 | B | 250 | 2023-10-01 10:06:00 | 101 |
| 我们期望得到的结果是: | ||||
| sb_id | qyb | |||
| ------- | ----- | |||
| A | 150 | |||
| B | 250 | |||
| C | 300 |
👑 王者方案:窗口函数 ROW_NUMBER()
这是目前最现代、最优雅、性能也通常最好的解决方案!如果你用的是 MySQL 8.0+、PostgreSQL、SQL Server、Oracle 等现代数据库,请把它作为你的首选!
💡 思路:
我们可以想象,先给每个 sb_id 分组内的数据按时间倒序排个名,最新的那条就是第 1 名。最后,我们只需要把所有“第 1 名”的记录挑出来就行了。
✅ 代码实现:
WITH RankedData AS (
SELECT
sb_id,
qyb,
-- 核心:为每个 sb_id 分组内的数据按创建时间倒序排名
ROW_NUMBER() OVER(PARTITION BY sb_id ORDER BY create_time DESC) as rn
FROM
yqhs_qyb
WHERE
jyz_id = #{jyzId}
)
SELECT
sb_id,
qyb
FROM
RankedData
WHERE
rn = 1; -- 筛选出每组的第一名
👍 优点:
- 标准高效:现代数据库优化器对窗口函数支持极佳。
- 逻辑清晰:代码意图一目了然,可读性、可维护性极强。
- 灵活扩展:想取最新的两条?只需改成
WHERE rn <= 2,轻松搞定! 推荐指数:⭐⭐⭐⭐⭐
🥈 经典方案:子查询 JOIN
这个方法非常经典,兼容性极好,尤其适合还在使用 MySQL 5.7 等旧版本数据库的同学。 💡 思路: 分两步走:
- 先用
GROUP BY找到每个sb_id对应的最新时间点 (MAX(create_time))。 - 然后将原表和这个“最新时间点”的结果集进行关联,找出时间点匹配的完整记录。 ✅ 代码实现:
SELECT
t1.sb_id,
t1.qyb
FROM
yqhs_qyb t1
INNER JOIN (
-- 子查询:找出每个 sb_id 的最新 create_time
SELECT
sb_id,
MAX(create_time) AS max_create_time
FROM
yqhs_qyb
WHERE
jyz_id = #{jyzId}
GROUP BY
sb_id
) t2 ON t1.sb_id = t2.sb_id AND t1.create_time = t2.max_create_time
WHERE
t1.jyz_id = #{jyzId};
👍 优点:
- 兼容性好:几乎所有版本的数据库都跑得动。
- 逻辑直观:先找到目标时间,再反查记录,符合直觉。 👎 缺点:
- 如果
create_time精度不够,导致同一设备有多条记录的时间戳完全相同,JOIN后可能会返回多条。 - 写法比窗口函数稍显繁琐。 推荐指数:⭐⭐⭐⭐
🥉 专属福利:PostgreSQL DISTINCT ON
如果你是 PostgreSQL 用户,那么恭喜你!你拥有最简洁的“专属武器”。
💡 思路:
DISTINCT ON (sb_id) 语法会为每个 sb_id 只保留一行,具体保留哪一行,由 ORDER BY 子句决定。
✅ 代码实现:
SELECT DISTINCT ON (sb_id)
sb_id,
qyb
FROM
yqhs_qyb
WHERE
jyz_id = #{jyzId}
ORDER BY
sb_id, create_time DESC; -- 注意:DISTINCT ON 的列必须在 ORDER BY 的最前面
👍 优点:
- 极致简洁:代码量最少,可读性爆表。
- 性能优秀:PG 对此语法有深度优化。 👎 缺点:
- 不通用:PostgreSQL 独有,换了数据库就不行了。 推荐指数(PG用户):⭐⭐⭐⭐⭐
🚫 青铜方案:相关子查询
最后介绍一个能实现但不推荐的方案,了解一下即可,避免在性能敏感的场景中使用。
💡 思路:
对于表中的每一条记录,都去问一个问题:“是否存在另一条同 sb_id 但 create_time 比我更新的记录?” 如果答案是否定的,那我就是最新的。
✅ 代码实现:
SELECT
t1.sb_id,
t1.qyb
FROM
yqhs_qyb t1
WHERE
t1.jyz_id = #{jyzId}
AND NOT EXISTS (
SELECT 1
FROM yqhs_qyb t2
WHERE t2.sb_id = t1.sb_id
AND t2.jyz_id = #{jyzId}
AND t2.create_time > t1.create_time
);
👎 缺点:
- 性能杀手:这个查询可能会导致“笛卡尔积”式的灾难,数据量一大,数据库 CPU 就会飙升。生产环境请务必避免! 推荐指数:⭐
总结与划重点
| 方案 | 适用场景 | 推荐度 |
|---|---|---|
| 窗口函数 | MySQL 8.0+ / PG / SQL Server / Oracle | ⭐⭐⭐⭐⭐ (首选) |
| 子查询 JOIN | MySQL 5.7 等旧版数据库 | ⭐⭐⭐⭐ (备选) |
| PG DISTINCT ON | PostgreSQL 用户 | ⭐⭐⭐⭐⭐ (PG首选) |
| 相关子查询 | 了解即可,避免使用 | ⭐ |
一句话总结:
能用窗口函数就用窗口函数,旧版 MySQL 用 JOIN,PG 用户用
DISTINCT ON,相关子查询快忘掉!
希望这篇文章能帮你解决日常开发中的这个小烦恼,写出更高效、更优雅的 SQL!🚀
觉得有用?那就 点赞 👍、在看 👀、转发 🔄 就是对我们最大的支持! 关注 公众号-秋雨 ,解锁更多硬核技术干货!