应使用关联子查询或窗口函数;MIN()或ORDER BY...LIMIT 1无法保证返回对应行的完整字段,如最早时间对应的姓名、ID等。

子查询怎么写才能拿到每组第一条?
直接用 MIN() 或 ORDER BY ... LIMIT 1 在子查询里是错的——它们不保证返回对应行的完整字段,比如时间最早但姓名、ID 拿不到。真正能拿到整行数据的,得靠关联子查询或窗口函数,而多数老版本 MySQL(
MySQL 5.7 怎么用关联子查询取每组最早记录?
核心思路:对每条记录,查出同组中是否存在更早的记录;如果不存在,它就是最早的。用 NOT EXISTS 最稳,比 = (SELECT MIN(...)) 更安全(避免多条同时间时重复返回)。
- 假设表叫
orders,字段有user_id、created_at、order_id - 子查询要和外层共享分组字段:
WHERE o2.user_id = o1.user_id AND o2.created_at - 必须给
created_at加索引,否则性能崩得很快
SELECT o1.*
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
);PostgreSQL / MySQL 8.0+ 用窗口函数更简单
ROW_NUMBER() 是最直观的选择,但要注意排序字段必须唯一或加足够粒度的二级排序,否则 ROW_NUMBER() 和 RANK() 行为不同——前者强制编号,后者会并列跳号。
- 用
PARTITION BY user_id ORDER BY created_at, id避免时间相同时结果不确定 -
ROW_NUMBER() OVER (...) = 1筛出每组首行,比子查询快且可读性强 - 别漏写
OVER的括号,语法错误常出在这里
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (
PARTITION BY user_id ORDER BY created_at, id
) rn
FROM orders
) t WHERE rn = 1;为什么 GROUP BY + MIN(created_at) 不行?
这是最常见误区:SELECT user_id, MIN(created_at), order_id FROM orders GROUP BY user_id —— order_id 值是随机的,不是对应最早时间那条的 order_id。MySQL 5.7 默认开启 ONLY_FULL_GROUP_BY,这种写法直接报错:Expression #3 of SELECT list is not in GROUP BY clause。
- 即使关掉严格模式,返回的
order_id也不可靠,引擎随便挑一条 - 想混用聚合和非聚合字段,必须用子查询或窗口函数兜底
- 有些 ORM 自动生成这类 SQL,上线前务必检查执行计划和实际结果
真实业务里,时间字段精度、空值、时区偏移都会影响“最早”的判断,别只盯着语法——先确认 created_at 是否有 NULL,是否统一了时区,再决定要不要加 WHERE created_at IS NOT NULL。

















