应使用 row_number() 窗口函数配合外层 where rn = 1 筛选每组首条记录:先以 partition by category 分组,order by created_at asc(建议补 id 防并列),再过滤序号为 1 的行;group by 或 first_value() 均无法可靠获取整行。

用 ROW_NUMBER() 配合 WHERE 筛选首条最直接
想按某个字段(比如 category)分组,取每组中时间最早/ID最小的那条记录?别用 GROUP BY + 聚合函数硬凑——MIN(id) 或 MIN(created_at) 只能返回值,拿不到整行数据。正确做法是用窗口函数 ROW_NUMBER() 先打序号,再外层过滤 rn = 1。
常见错误是写成:SELECT category, MIN(created_at), name FROM orders GROUP BY category —— 这里的 name 值不确定,MySQL 5.7+ 默认报错,PostgreSQL 和 SQL Server 直接拒绝执行。
- 必须用子查询或 CTE 包裹
ROW_NUMBER(),不能在同一个SELECT中直接引用别名rn - 排序依据要明确:按时间升序得写
ORDER BY created_at ASC,漏掉ASC一般不影响,但显式写出更安全 - 如果存在并列(如同一秒多条),
ROW_NUMBER()仍保证唯一编号;若想随机取其一,可在ORDER BY后加, RANDOM()(PostgreSQL)或, NEWID()(SQL Server)
MySQL 8.0+ 和 PostgreSQL 支持标准写法
这两者对窗口函数支持完整,语法一致,可直接复用:
WITH ranked AS (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY created_at ASC) AS rn
FROM orders
)
SELECT id, category, name, created_at
FROM ranked
WHERE rn = 1;
注意点:
-
PARTITION BY是分组逻辑,不是GROUP BY,它不压缩行数,只是为窗口函数划分计算范围 - MySQL 5.7 或更低版本不支持窗口函数,强行运行会报错
ERROR 1064: You have an error in your SQL syntax - PostgreSQL 中若表无主键且排序字段有重复,建议加
id作为第二排序条件,避免结果不稳定
SQL Server 和旧版 MySQL 的兼容方案
SQL Server 同样支持 ROW_NUMBER(),写法相同;但若用的是 MySQL 5.7 或更早,只能靠自连接或相关子查询,性能较差:
SELECT o1.* FROM orders o1 WHERE o1.created_at = ( SELECT MIN(o2.created_at) FROM orders o2 WHERE o2.category = o1.category );
这个写法的问题很实际:
- 无法处理
created_at完全相同的情况——可能返回多条,而非“首条” - 没有索引时,子查询会为每一行触发一次扫描,数据量大时明显变慢
- 必须确保
(category, created_at)有联合索引,否则执行计划大概率走全表扫描
为什么不用 FIRST_VALUE()?
FIRST_VALUE() 确实能取分组内第一个值,但它返回的是标量,不能直接还原整行。例如:FIRST_VALUE(name) OVER (PARTITION BY category ORDER BY created_at) 只能得到 name 字段,其他字段还得额外写一遍,既啰嗦又容易出错。
更关键的是,不同数据库对 FIRST_VALUE() 的默认窗口范围理解不同:PostgreSQL 默认是 UNBOUNDED PRECEDING TO CURRENT ROW,而 SQL Server 默认是整个分区。不显式指定 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING,结果可能和预期不符。
真正需要“各分组首条记录”时,ROW_NUMBER() + 过滤是最稳、最易读、移植性最好的方案。复杂点在于排序字段的唯一性保障——如果业务上允许,建议在 ORDER BY 子句里补一个主键字段兜底。











