最推荐用窗口函数 ROW_NUMBER() 按 user_id 分组、created_at DESC(加 id DESC 防并列)编号后取 rn=1;不支持窗口函数时用 NOT EXISTS 关联子查询;避免 GROUP BY + MAX 的错误写法,并注意 NULL 和时区处理。

用窗口函数 ROW_NUMBER() 按用户分组排序取第一条
直接在子查询或 CTE 中对每个 user_id 按 created_at 降序编号,再筛选 rn = 1 的记录——这是最通用、语义清晰且性能可控的做法。
- 必须用
PARTITION BY user_id,否则会把全表当一个组排序 -
ORDER BY created_at DESC要明确写,不能只写ORDER BY created_at(默认 ASC) - 如果存在同秒创建的多笔订单,
ROW_NUMBER()会任意分配序号;需稳定结果可追加id DESC作为次级排序 - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持,但 SQLite 3.35+ 才支持窗口函数
SELECT user_id, order_id, amount, created_at
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC, id DESC) AS rn
FROM orders
) t
WHERE rn = 1;
用 NOT EXISTS 子查询避免窗口函数依赖
当数据库不支持窗口函数(如旧版 MySQL 或某些嵌入式环境),可用关联子查询排除“存在更新订单”的记录——逻辑等价但写法更绕,且容易误写成笛卡尔积。
- 子查询里必须加
o2.user_id = o1.user_id关联条件,漏掉就变成全表比较 -
o2.created_at > o1.created_at是核心判断,不能写成>=(否则同时间订单全被排除) - 若
created_at允许 NULL,需额外处理,否则 NULL 订单永远被选中 - 索引建议:联合索引
(user_id, created_at)能显著加速该写法
SELECT o1.user_id, o1.order_id, o1.amount, o1.created_at
FROM orders o1
WHERE NOT EXISTS (
SELECT 1 FROM orders o2
WHERE o2.user_id = o1.user_id
AND o2.created_at > o1.created_at
);
别用 GROUP BY + MAX(created_at) 直接查订单详情
这是新手常见误区:先按 user_id 分组取最大时间,再试图关联原表查完整字段——结果往往错乱,因为 MAX(created_at) 不保证对应到同一行的 order_id 或 amount。
- MySQL 5.7 严格模式下会报错:
Expression #2 of SELECT list is not in GROUP BY clause - 即使开启
sql_mode宽松模式,order_id可能来自任意一条同用户订单,毫无业务意义 - 想强行用聚合函数包裹其他字段(如
MAX(order_id))也不可靠——ID 最大 ≠ 时间最新
注意时区和 NULL 值带来的隐性偏差
真实业务中 created_at 很可能存的是 UTC 时间,而业务逻辑需要本地时间最近订单;同时未支付订单可能设为 NULL,它们不该参与“最近”计算。
- 若需按用户所在时区判断,得先用
CONVERT_TZ()(MySQL)或AT TIME ZONE(PostgreSQL)转换后再排序 -
WHERE created_at IS NOT NULL必须加在最外层或子查询中,否则ROW_NUMBER()会给 NULL 排上序(取决于数据库排序规则) - 部分数据库把 NULL 排在最前(ASC)或最后(DESC),不同行为会导致 NULL 订单意外入选
(user_id, created_at) 索引是否存在——没索引时 NOT EXISTS 也可能慢得无法接受。

















