子查询法最易理解且兼容性好:先用子查询求每组MAX(created_at),再关联原表匹配;JOIN法性能更优但需注意索引与版本兼容性;窗口函数ROW_NUMBER()语义清晰、推荐现代数据库使用。

用子查询找分组内最新日期记录最直接
想按 user_id 分组,每组只取 created_at 最大的那条记录,子查询是最易理解、兼容性最好的写法。核心思路是:先在子查询里算出每个分组的 MAX(created_at),再和原表关联匹配。
常见错误是直接在 GROUP BY 后选非聚合字段,比如写 SELECT user_id, name, MAX(created_at) —— 这在 MySQL 5.7+ 严格模式或 PostgreSQL 中会报错 ERROR 1055,因为 name 不在 GROUP BY 里也不在聚合函数中。
正确写法示例:
SELECT t1.* FROM orders t1 WHERE t1.created_at = ( SELECT MAX(t2.created_at) FROM orders t2 WHERE t2.user_id = t1.user_id );
- 适用于所有主流数据库(MySQL、PostgreSQL、SQL Server、Oracle)
- 如果
created_at有重复,可能返回多条——这是行为预期,不是 bug - 注意加复合索引
(user_id, created_at),否则子查询会全表扫描
JOIN 方式性能更好但写法更绕
当数据量大、且 user_id + created_at 组合唯一时,用 JOIN 替代相关子查询能显著减少执行计划中的嵌套循环次数,尤其在 MySQL 8.0+ 或 PostgreSQL 中效果明显。
关键点在于:先用 GROUP BY 拿到每个 user_id 对应的 MAX(created_at),再和原表 JOIN,但必须确保连接条件能精准命中单行(否则仍可能多对多)。
示例:
SELECT t1.* FROM orders t1 INNER JOIN ( SELECT user_id, MAX(created_at) AS max_time FROM orders GROUP BY user_id ) t2 ON t1.user_id = t2.user_id AND t1.created_at = t2.max_time;
- 比子查询少一次对主表的逐行扫描,IO 更低
- 如果存在多个相同
created_at的记录,结果仍是多行——和子查询行为一致 - 某些旧版 MySQL(如 5.6)对派生表优化不佳,可能比子查询还慢,建议 EXPLAIN 验证
窗口函数是现代 SQL 的推荐解法
如果你用的是 MySQL 8.0+、PostgreSQL 8.4+、SQL Server 2005+ 或 Oracle,ROW_NUMBER() 是最干净、语义最明确的方式。它不依赖时间字段是否唯一,也不怕重复值干扰逻辑。
典型陷阱是误用 RANK() 或 DENSE_RANK():它们会在并列时给相同序号,导致“最大日期”对应多行;而 ROW_NUMBER() 强制分配唯一序号,哪怕时间相同也能稳定取第一条。
示例:
SELECT user_id, order_id, created_at
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders
) ranked
WHERE rn = 1;
- 可轻松扩展为取“每组第二新”(
rn = 2)或“前 N 条”(rn ) - 必须用
ORDER BY created_at DESC,升序会拿到最老记录 - 注意
PARTITION BY字段要和业务分组维度完全一致,比如按user_id, status双字段分组就得写全
别忽略 NULL 和时区带来的隐性问题
created_at 为 NULL 时,MAX() 会忽略它,但 ROW_NUMBER() 会把 NULL 排在最前(因默认 NULLS FIRST),导致结果不一致。时区差异更隐蔽:应用写入的是本地时间,数据库存的是 UTC,MAX(created_at) 算出来可能是跨天的“最大”,但业务上并不认为它是“最新”。
- 处理 NULL:在子查询或窗口函数中加
WHERE created_at IS NOT NULL - 统一时区:查询前设
SET TIME ZONE 'UTC'(PostgreSQL)或用CONVERT_TZ()(MySQL)对齐 - 业务时间字段建议用
TIMESTAMP WITH TIME ZONE类型,避免后期补救成本
真正麻烦的从来不是语法怎么写,而是“最新”这个业务定义,在数据不干净、时区没对齐、NULL 处理没共识的情况下,三种写法都会返回看似合理实则错误的结果。










