应使用窗口函数ROW_NUMBER()配合子查询获取每组最新完整记录,而非直接用MAX()聚合,因MAX()仅返回最大值不绑定整行数据。

用窗口函数 ROW_NUMBER() 配合子查询最可靠
直接在 GROUP BY 后用 MAX(created_at) 只能拿到时间戳,拿不到对应那条完整记录。真正要“每组最新一条”,本质是按分组排序后取序号为 1 的行,ROW_NUMBER() 是目前兼容性好、语义清晰的解法。
常见错误是写成 SELECT *, MAX(updated_at) FROM t GROUP BY category —— 这在 MySQL 5.7 严格模式或 PostgreSQL 中会报错,因为非聚合字段和 GROUP BY 不匹配。
实操建议:
- 先用
ROW_NUMBER() OVER (PARTITION BY category ORDER BY updated_at DESC)给每组记录按时间倒序编号 - 外层查出
rn = 1的行即可 - 注意
ORDER BY里如果有并列时间(如秒级精度),建议追加主键(如id DESC)保证结果确定性
SELECT id, category, title, updated_at
FROM (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY category
ORDER BY updated_at DESC, id DESC
) AS rn
FROM articles
) t
WHERE rn = 1;MySQL 8.0+ 支持 QUALIFY 简化写法
QUALIFY 是窗口函数的过滤语法,能省掉一层子查询,但仅限 BigQuery、Snowflake 和 MySQL 8.0+(需开启窗口函数支持)。不是所有环境都能用,别盲目套用。
使用场景:你确认数据库版本且不需兼容旧版时,可让 SQL 更紧凑。
实操建议:
- 必须搭配窗口函数使用,不能单独出现
-
QUALIFY执行时机在窗口计算之后、最终输出之前,所以能直接引用ROW_NUMBER()别名 - 老版本 MySQL 或 PostgreSQL 仍需回退到子查询写法
SELECT id, category, title, updated_at FROM articles WINDOW w AS (PARTITION BY category ORDER BY updated_at DESC, id DESC) QUALIFY ROW_NUMBER() OVER w = 1;
用关联子查询也能做,但性能容易翻车
对小表或有强索引的场景,用 NOT EXISTS 或自连接找“不存在更新记录”是可行的,逻辑直观但执行计划常出人意料。
典型错误现象:数据量一过万,查询从毫秒变秒级,EXPLAIN 显示全表扫描或嵌套循环加剧。
实操建议:
- 确保
(category, updated_at)有联合索引,否则性能不可控 - 避免在子查询中用
MAX()再关联,比如WHERE updated_at = (SELECT MAX(...) FROM ...)—— 可能因 NULL 或重复时间导致多条命中 - PostgreSQL 中可用
DISTINCT ON替代,但这是特有语法,跨数据库迁移成本高
SELECT a1.*
FROM articles a1
WHERE NOT EXISTS (
SELECT 1 FROM articles a2
WHERE a2.category = a1.category
AND a2.updated_at > a1.updated_at
);GROUP BY + 聚合函数拼接字段风险高
有人用 GROUP_CONCAT(MySQL)或 STRING_AGG(PostgreSQL)把整行转字符串再解析,或者依赖 MySQL 5.7 以前的“隐式 GROUP BY”行为——这些都不可靠。
问题在于:聚合函数不保证返回的是同一行的数据。比如 MAX(updated_at) 和 title 可能来自不同记录,结果错乱且难以复现。
实操建议:
- 绝对不要依赖 MySQL 5.7 以下的非标准
GROUP BY行为,升级后立马失效 -
GROUP_CONCAT(id ORDER BY updated_at DESC SEPARATOR ',')只能取 ID,再用额外查询捞数据,IO 成倍增加 - 如果业务真要求“每组最新 N 条”,优先考虑窗口函数的
ROW_NUMBER()或RANK(),别绕弯
最易被忽略的一点:时间字段是否允许 NULL?如果 updated_at 有 NULL 值,ORDER BY ... DESC 会让它们排最前,导致“最新”变成“未更新”。上线前务必检查数据分布,必要时加 WHERE updated_at IS NOT NULL 过滤。

















