mysql 8.0+ 中用 row_number() over (partition by ... order by ...) 实现分组内编号最稳妥,需套 cte 或子查询后 where rn = 1 筛首行;旧版无此函数,变量法易出错;row_number() 不支持直接在 where 中使用,且排序字段相同时仍保证唯一编号。

MySQL 8.0+ 用 ROW_NUMBER() 最稳妥
直接在分组内编号,再筛出序号为 1 的行,逻辑清晰、结果确定。旧版 MySQL 没这个函数,强行用变量或子查询容易错位,尤其数据无严格排序依赖时。
常见错误是只写 GROUP BY 却没意识到它不保证返回哪一行——SQL 标准里,SELECT 列表中非聚合字段若不在 GROUP BY 中,MySQL 5.7 默认允许但行为不可靠,8.0+ 默认报错 ERROR 1055。
- 必须显式指定排序依据,比如按时间取最新一条:
ORDER BY created_at DESC -
ROW_NUMBER()是窗口函数,不能出现在WHERE子句,得套一层子查询或 CTE - 示例:
WITH ranked AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY category ORDER BY updated_at DESC) rn FROM products ) SELECT id, category, name, updated_at FROM ranked WHERE rn = 1;
PostgreSQL 和 SQL Server 同样适用 ROW_NUMBER()
语法一致,只是 PostgreSQL 对 NULL 排序默认更严格(NULLS LAST 需显式写),SQL Server 支持 TOP (1) WITH TIES 配合窗口函数,但可读性不如直接筛 rn = 1。
注意:如果分组键有重复值且排序字段也相同,ROW_NUMBER() 仍会强制编号,而 RANK() 或 DENSE_RANK() 可能导致多行并列第一,此时要根据业务决定是否接受“多条”还是必须“唯一一条”。
- 避免用
SELECT DISTINCT ON(PostgreSQL 特有)替代,它依赖ORDER BY顺序,且只能用于单表,关联后失效 - SQL Server 若版本 APPLY 或相关子查询,性能差、易出错
旧版 MySQL(5.7 及以下)慎用 GROUP BY + 隐式字段
虽然配置 sql_mode 去掉 ONLY_FULL_GROUP_BY 能让语句跑通,但返回的“第一条”实际是存储引擎扫描顺序决定的,换索引、加 WHERE、甚至优化器升级都可能让结果突变。
真正安全的做法是用关联子查询,但要注意性能陷阱:
- 子查询必须带
ORDER BY和LIMIT 1,否则可能返回任意匹配行 - 外层
GROUP BY字段必须和子查询WHERE条件完全对应,漏一个就变成笛卡尔积 - 示例(慢但正确):
SELECT p1.* FROM products p1 WHERE p1.id = ( SELECT p2.id FROM products p2 WHERE p2.category = p1.category ORDER BY p2.updated_at DESC LIMIT 1 );
别忽略索引对性能的实际影响
无论用哪种方法,PARTITION BY 字段 + ORDER BY 字段的联合索引几乎总是必要项。比如 (category, updated_at) 能让 ROW_NUMBER() 窗口计算跳过排序,而 (updated_at, category) 就无效。
实测中,没建对索引时,百万级表的 ROW_NUMBER() 查询可能从 200ms 涨到 8s;而子查询方式在没索引时可能直接触发全表扫描嵌套。
- 用
EXPLAIN看执行计划,确认type是range或ref,不是ALL - 如果分组键是字符串且很长,考虑前缀索引,但需确保前缀足够区分分组
窗口函数本身不难,难的是理解“分组内排序”和“物理存储顺序”根本不是一回事。很多人卡在测试数据刚好按插入顺序排列,上线后数据一更新、一归档,结果就乱了。











