窗口函数替代子查询更可靠,应使用row_number()按user_id分组并按created_at倒序编号后过滤rn=1;若不支持窗口函数,则用关联子查询或left join自连接,并在(user_id, created_at)建联合索引。

用窗口函数替代子查询更可靠
直接在 WHERE 子句里套 SELECT TOP 1 ... ORDER BY time DESC 这类子查询,多数数据库会报错(比如 MySQL 5.7 或 SQL Server 在非相关子查询中不支持 ORDER BY),或者返回不可预期的结果(因未绑定分组上下文)。真正稳定的做法是先用窗口函数标出每组最新记录的序号,再过滤。
以获取每个 user_id 下 created_at 最新的订单为例:
SELECT user_id, order_id, amount, created_at
FROM (
SELECT user_id, order_id, amount, created_at,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders
) t
WHERE rn = 1;
-
ROW_NUMBER()是关键:它按user_id分组、按时间倒序编号,确保每组第一条就是最新 - 别用
RANK()或DENSE_RANK():当存在相同created_at时,它们可能给多条记录标为 1,导致重复 - MySQL 8.0+、PostgreSQL、SQL Server 2005+、Oracle 都支持;但 SQLite 3.25+ 才支持窗口函数
MySQL 5.7 或旧版必须用关联子查询
如果数据库不支持窗口函数(如 MySQL 5.7),只能用相关子查询,但必须确保子查询能正确关联外层分组字段,否则容易全表扫描或逻辑错误。
正确写法(注意 o2.user_id = o1.user_id 这个关联条件):
SELECT o1.user_id, o1.order_id, o1.amount, o1.created_at FROM orders o1 WHERE o1.created_at = ( SELECT MAX(o2.created_at) FROM orders o2 WHERE o2.user_id = o1.user_id );
- 必须用
MAX()而不是TOP 1 ORDER BY:后者在 MySQL 5.7 的子查询中语法非法 - 如果
created_at不唯一,可能返回多条;此时需加额外去重逻辑(比如再比order_id) - 这个查询在大数据量下性能差——
WHERE中的子查询对o1每行都执行一次,建议在(user_id, created_at)上建联合索引
用 LEFT JOIN 自连接避免子查询性能陷阱
当分组字段和排序字段组合存在重复值,且数据库不支持窗口函数时,自连接比子查询更可控,也更容易加索引优化。
思路:对每条记录,尝试找同组中“更新”的记录;找不到,说明它就是最新的:
SELECT o1.user_id, o1.order_id, o1.amount, o1.created_at FROM orders o1 LEFT JOIN orders o2 ON o1.user_id = o2.user_id AND o1.created_at
- 核心是
o1.created_at :只要存在一条同组、时间更晚的记录,<code>o2.user_id就不为NULL - 必须加
AND o1.created_at ,不能只写 <code>!=或,否则会误排除所有时间相同的记录 - 这个写法在 PostgreSQL 和 SQL Server 上通常比相关子查询快;但在 MySQL 中要注意是否启用
STRAIGHT_JOIN控制连接顺序
注意 NULL 和边界场景
真实数据里 created_at 可能为 NULL,而 NULL 在 ORDER BY ... DESC 中默认排最前或最后(行为因数据库而异),这会导致 ROW_NUMBER() 把 NULL 当成“最新”。
- 显式排除:
WHERE created_at IS NOT NULL再开窗 - 或强制排序优先级:
ORDER BY created_at DESC NULLS LAST(PostgreSQL/Oracle 支持;MySQL 和 SQL Server 不支持该语法,得用CASE WHEN created_at IS NULL THEN 1 ELSE 0 END, created_at DESC) - 空分组(某
user_id完全没数据)不会影响结果,但若用LEFT JOIN方式,记得确认是否要保留这些空组
真正麻烦的是时间精度不一致——比如日志存的是 DATETIME,但应用写入时毫秒部分被截断,导致多条记录时间相同。这时候仅靠时间无法唯一确定“最新”,必须引入第二排序字段,比如自增 id 或带时序的 uuid。










