最可靠方法是用ROW_NUMBER()窗口函数:先PARTITION BY分组、ORDER BY明确排序(如created_at DESC取最新),再嵌套子查询筛选rn=1;GROUP BY无法保证顺序,MIN/MAX仅返回值不保留整行,FIRST_VALUE/LAST_VALUE需显式指定窗口帧才准确。

用 ROW_NUMBER() 按分组排序取首尾最可靠
直接用 GROUP BY 无法保证“第一条”或“最后一条”,因为 SQL 标准不定义行序,除非显式排序。真正可行的方式是借助窗口函数——ROW_NUMBER() 是最通用、语义最清晰的选择。
常见错误是试图在 GROUP BY 后用 MIN()/MAX() 套字段(比如 SELECT dept, MIN(name)),这返回的是按字典序最小的 name,不是原始插入顺序或业务时间顺序的第一条记录。
- 必须搭配
ORDER BY子句明确排序依据,例如按created_at或id - 对同一分组,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY id ASC)给每行编号,1 就是该组“第一条” - 同理,
ORDER BY id DESC下编号为 1 的就是“最后一条” - 注意:若存在相同
id或排序字段重复,ROW_NUMBER()仍会强制分配唯一序号(可能和业务预期不符),此时可改用RANK()或加二级排序(如ORDER BY id DESC, created_at DESC)
PostgreSQL / MySQL 8.0+ / SQL Server 可直接用 FIRST_VALUE() 和 LAST_VALUE()
这些数据库支持更简洁的写法,但要注意默认窗口帧范围——LAST_VALUE() 在未显式指定 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING 时,实际只作用于当前行到分区开头(即“到当前行为止”的 last),结果常为空或错乱。
-
FIRST_VALUE(name) OVER (PARTITION BY dept ORDER BY created_at)返回每组最早时间对应的name -
LAST_VALUE(name) OVER (PARTITION BY dept ORDER BY created_at ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)才能拿到真正最后一条 - MySQL 5.7 及更早版本不支持窗口函数,只能靠关联子查询或变量模拟,性能差且不可靠
SQLite 和旧版 MySQL 怎么办?用相关子查询兜底
没有窗口函数时,只能靠自连接或子查询筛选。核心思路是:对每条记录,查出同组中满足“比它更早/更晚”的记录数为 0 的那条。
- 取每组第一条(按
id最小):SELECT t1.* FROM orders t1 WHERE NOT EXISTS (SELECT 1 FROM orders t2 WHERE t2.dept = t1.dept AND t2.id < t1.id)
- 取最后一条(
id最大):把改成 <code>> - 缺点明显:数据量大时性能急剧下降;若存在重复
id,可能返回多行;不能灵活切换排序字段(比如想按时间而非 ID)
别忽略 NULL 和排序字段重复带来的陷阱
真实业务里,created_at 字段经常为 NULL,或者多个记录时间戳完全一致。这时 ORDER BY created_at 会导致排序不稳定,ROW_NUMBER() 分配的序号每次执行都可能不同。
- 务必补上
IS NULLS FIRST或IS NULLS LAST(PostgreSQL/SQL Server 支持),或用COALESCE(created_at, '1970-01-01')统一处理 - 排序字段重复时,加一个唯一字段兜底,例如
ORDER BY created_at DESC, id DESC - 如果业务上“第一条”定义为“最早插入的”,而表没存插入时间,仅靠
id推断有风险——自增 ID 中间可能被跳过,或使用 UUID

















