登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  MySQL

MySQL 窗口函数怎样取每组最新一条

来源:17golang原创

时间:2026-10-14 08:48:35 275浏览 收藏

MySQL 用窗口函数取每组最新一条,常用写法是 ROW_NUMBER() OVER (PARTITION BY 分组字段 ORDER BY 时间 DESC, 唯一键 DESC),再在外层查询筛选 rn=1。时间倒序决定“最新”,唯一键负责在时间相同的时候稳定选一条。窗口函数保留原行,因此可以返回状态、金额等完整字段,不必把 MAX(时间) 和不确定的普通字段拼在一起。

以下以 MySQL 8.4 官方文档为基线,示例未运行,不包含实测执行计划或性能数据。读者需要在支持窗口函数的 MySQL 环境使用,不能直接把该语法套到 MySQL 5.7。

官方文档:https://dev.mysql.com/doc/refman/8.4/en/window-functions-usage.html

把最新定义成可以排序的规则

设想设备事件记录,每行含 id、device_id、occurred_at 和 status。要求是“每台设备取最近发生的事件”,分组字段就是 device_id,时间字段就是 occurred_at;如果时间并列,本例约定取 id 较大的一行。id 必须在候选集中唯一。这个约定只是并列裁决,不能把自增 id 直接解释成事件发生时间,补录数据可能打乱两者关系。

这里有三条独立规则:分组决定哪些行互相比较,排序决定谁排在组内首位,外层条件决定保留哪些编号。若按租户与设备共同识别对象,PARTITION BY 应包含 tenant_id、device_id;否则不同租户的同名设备可能被混在一起。

不要只在最终 SELECT 末尾加 ORDER BY occurred_at DESC。最终排序影响展示顺序,不负责告诉窗口函数如何分配组内编号。窗口内必须有自己的 ORDER BY;排序字段仍然相同的行会获得不同 ROW_NUMBER,但被选中的那一行可能不稳定。

事件记录与分组排序编号的结构关系
静态结构说明图:device_id 绑定分组,occurred_at 和 id 绑定排序,rn 绑定组内编号;不是查询结果截图。

内层编号,外层选出完整记录

下面用 CTE 构造少量示例记录,无需建立真实业务表。设备 31 有两个并列时间,设备 32 的最新事件是失败状态。先保留全部候选行,并在 ranked 中加入编号,再在最外层选首行。

WITH events AS (
    -- 教学数据:同一设备可以有相同时间,id 用于并列裁决。
    SELECT 501 AS id, 31 AS device_id,
           CAST('2026-09-10 09:00:00' AS DATETIME) AS occurred_at,
           'success' AS status
    UNION ALL
    SELECT 502, 31, CAST('2026-09-10 10:00:00' AS DATETIME), 'success'
    UNION ALL
    SELECT 503, 31, CAST('2026-09-10 10:00:00' AS DATETIME), 'failed'
    UNION ALL
    SELECT 601, 32, CAST('2026-09-10 08:00:00' AS DATETIME), 'success'
    UNION ALL
    SELECT 602, 32, CAST('2026-09-10 11:00:00' AS DATETIME), 'failed'
), ranked AS (
    SELECT id, device_id, occurred_at, status,
           ROW_NUMBER() OVER (
               PARTITION BY device_id
               ORDER BY occurred_at DESC, id DESC
           ) AS rn
    FROM events
)
-- rn 是内层结果列,在外层筛选;最后排序只控制展示。
SELECT id, device_id, occurred_at, status
FROM ranked
WHERE rn = 1
ORDER BY device_id;

按规则推导,设备 31 应取 503,设备 32 应取 602。503 与 502 时间相同,但 503 的 id 更大;602 的发生时间更晚,状态失败不影响这里的“最新”定义。这是预期结果说明,而不是已经执行的输出。

实际使用时,把 events 换成业务表即可。窗口函数只能出现在 SELECT 列表和 ORDER BY 等允许的位置,不能在同层 WHERE 中直接写 ROW_NUMBER() OVER (...) = 1,也不能把同层刚定义的 rn 当成可用的普通列。CTE 或派生表提供了清晰的查询层边界。

筛选条件的位置会改变问题

“每组最新成功记录”和“每组最新记录是否成功”不是同一个需求。前者应先限制候选集合为 success,再编号;后者要对所有事件编号,选出最新行后再检查 status。以刚才的样本为例,前者返回 502 和 601,后者不返回任何行,因为两台设备的最新事件都是 failed。

下面假设已有 device_events 表,字段与示例一致。这两段 SQL 不创建表,也不修改数据。

-- 需求一:只在成功候选中比较,取每台设备最近一次成功。
WITH ranked AS (
    SELECT id, device_id, occurred_at, status,
           ROW_NUMBER() OVER (
               PARTITION BY device_id
               ORDER BY occurred_at DESC, id DESC
           ) AS rn
    FROM device_events
    WHERE status = 'success'
)
SELECT id, device_id, occurred_at, status
FROM ranked
WHERE rn = 1;

-- 需求二:先对全部事件排名,再只保留最新事件成功的设备。
WITH ranked AS (
    SELECT id, device_id, occurred_at, status,
           ROW_NUMBER() OVER (
               PARTITION BY device_id
               ORDER BY occurred_at DESC, id DESC
           ) AS rn
    FROM device_events
)
SELECT id, device_id, occurred_at, status
FROM ranked
WHERE rn = 1 AND status = 'success';

时间区间也一样:内层限制最近七天,就是在最近七天的候选中取最新,而不是无条件取全历史最新。若没有七天内事件,该设备不会出现。不要为了减少扫描而把任意条件移动到内层;它会改变参与排名的数据范围。

候选集合过滤和首行结果过滤的边界
静态结构说明图:内层状态条件约束候选集合,外层状态条件约束首行结果;两种语义对应不同问题,不是执行时间轴。

把并列规则和性能边界写清楚

只要每组恰好选一条,用 ROW_NUMBER 并提供唯一的排序组合。如果要保留所有最大时间并列记录,可改用 RANK 或 DENSE_RANK,窗口只按 occurred_at DESC 排序,然后在外层取排名 1。此时不要加入唯一 id 作为排名排序,否则原本的时间并列会被拆开。对于首名并列,RANK 和 DENSE_RANK 都能保留并列行;它们的差别主要体现在后续名次是否留空档。

有复合索引也不能承诺整个窗口查询无需排序。读者可评估 (device_id, occurred_at DESC, id DESC) 这类与分组及排序对应的索引,同时考虑实际内层过滤条件、读取列与写入成本。是否选用、是否减少排序,仍由执行计划和数据分布决定,不应该把索引定义当成性能保证。

MySQL 8.4 文档说明,包含窗口函数的派生子查询不会按普通方式合并,而会物化。大候选集仍可能带来排序及临时结果开销。先用语义正确的条件缩小候选范围,明确选择必要列,再由读者在自己的环境观察计划。优化细节可查 https://dev.mysql.com/doc/refman/8.4/en/window-function-optimization.html。

上线前用边界样本核对结果

建议读者准备这些样本:每组单行、不同时间多行、并列时间、全部失败、旧成功新失败、指定时间范围内没有记录。检查的不只是行数,还应检查返回的 id、状态和时间是否属于同一条完整记录。这份样本清单用于读者自己的回归,本文没有执行数据库验证。

需求关键设置
每组恰好一条最新行ROW_NUMBER + 时间及唯一键排序 + 外层 rn=1
最新的成功行内层过滤成功,再排名
最新行必须成功全部候选排名,外层过滤成功
保留最大时间并列行RANK 或 DENSE_RANK,只按时间排名
结果显示顺序稳定最外层显式 ORDER BY

没有事件的设备会出现吗?

不会。事件表中没有候选行,就没有对应分组。若设备主表也要完整展示,应从设备主表 LEFT JOIN 已经选好的每组首行结果,并把首行条件放在结果子查询或连接条件中,避免在外层 WHERE 排除未匹配行。

时间允许 NULL 怎么处理?

先定义业务含义。若未知时间不能参加“最新事件”比较,可在内层排除 occurred_at IS NULL;这会使全部时间未知的分组消失。不要随意把 NULL 替换成当前时间,那会把未知记录误标成最新。

加 LIMIT 能得到每组一条吗?

单独 LIMIT 1 只限制整个结果,不负责分组。应先筛选 rn=1,再按最终展示顺序分页;并发写入时跨页结果还可能变化,稳定排序不能替代一致的数据快照。

记住分组、排序、候选过滤与结果过滤这四项,ROW_NUMBER 的写法就不难。函数行为说明:https://dev.mysql.com/doc/refman/8.4/en/window-function-descriptions.html。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>