ROW_NUMBER() 是去重最稳方案:在支持窗口函数的数据库中,用 ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY time DESC, id DESC) 配合外层筛选 rn=1,可确保每组取最新且有序;NULL 值需显式处理;不支持窗口函数时可用左连接模拟最大值过滤。

用 ROW_NUMBER() 窗口函数去重最稳
直接结论:在支持窗口函数的数据库(PostgreSQL、SQL Server、Oracle、MySQL 8.0+)里,ROW_NUMBER() 是最可靠、语义最清晰的解法。它能明确按时间或 ID 排序后取每组第一条,不依赖唯一性假设,也不怕时间字段重复。
常见错误是用 GROUP BY 配合 MAX(time) 再连表——看似合理,但一旦同一组内有两条记录时间相同,就会漏数据或结果不可控。
- 写法核心:
ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY time DESC, id DESC),用id DESC保序兜底,避免时间相同时随机取 - 别忘了外层套一层查询,只取
rn = 1的行,不能在窗口函数所在层直接加WHERE rn = 1(语法报错) - 如果时间字段允许 NULL,记得加
NULLS LAST(PostgreSQL)或用COALESCE(time, '1970-01-01')统一处理,否则 NULL 可能被排到最前
MySQL 5.7 或 SQLite 这类不支持窗口函数的怎么办
只能靠相关子查询或左连接模拟“最大值过滤”,性能差但兼容性强。本质是:对每条记录,查同组里有没有时间更大的记录;如果没有,就是最新的。
典型写法是左连接自表:LEFT JOIN table t2 ON t1.group_col = t2.group_col AND t1.time ,再 <code>WHERE t2.id IS NULL。注意这里用 而不是 <code><=,否则会把所有记录都排除掉。
- 必须确保
(group_col, time)有联合索引,否则全表扫描极慢 - 如果存在时间相同的情况,这个方法会返回多条——它只保证“没有更大时间”,不保证“唯一一条”。真要唯一,得额外加
AND t1.id = (SELECT MAX(id) FROM ...),嵌套更深 - SQLite 不支持子查询中引用外层表别名,得改用
NOT EXISTS形式
GROUP BY + 聚合函数为什么经常出错
很多人写 SELECT group_col, MAX(time), other_col FROM t GROUP BY group_col,以为 other_col 会自动对应到 MAX(time) 那一行——这是错的。MySQL 在 sql_mode 不严格时会随机选值,PostgreSQL 直接报错 column "other_col" must appear in the GROUP BY clause or be used in an aggregate function。
- 即使 MySQL 允许执行,
other_col和MAX(time)很可能来自不同行,数据错位 - 想强行用聚合,得写
MAX(CASE WHEN time = max_time THEN other_col END),但先得算出每组max_time,需要两层子查询,可读性和维护性都差 - 除非
other_col本身就是聚合结果(比如SUM(amount)),否则别碰这种写法
ORDER BY + LIMIT 1 在分组场景下完全不适用
ORDER BY time DESC LIMIT 1 只能取全局最新一条,根本没法按组划分。有人试图用 GROUP_CONCAT 拼接再截取,但字符长度限制、排序稳定性、NULL 处理全是坑,生产环境慎用。
真正需要“每组最新”的时候,逻辑上就必须做分组内的排序和筛选,绕不开窗口函数或自连接这类显式表达分组内比较的操作。
复杂点在于时间字段的语义是否绝对唯一——如果业务上允许同一秒多条记录,就得引入第二排序键(比如自增 id 或 created_at 带毫秒),否则“最新”本身就不确定。这点容易被忽略,但直接影响结果可靠性。

















