mysql 8.0+ 应用窗口函数实现先排序后分组:用 row_number() 按 user_id 分组并按 created_at desc 排序编号,外层 where rn = 1 取每组首行;5.7 及更早需用关联子查询匹配每组最大时间的完整记录。

MySQL 8.0+ 用窗口函数替代 GROUP BY 实现“先排序后分组”
MySQL 默认的 GROUP BY 不保证每组内取哪一行,尤其配合 ORDER BY 时,ORDER BY 实际作用于最终结果集,而非分组前的行。想按某字段排序后取每组第一条(如最新记录、最高分),不能靠 GROUP BY + ORDER BY 组合实现。
正确做法是:先用窗口函数排序打标,再外层过滤。例如取每个 user_id 下 created_at 最大的那条记录:
SELECT * FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders
) t WHERE rn = 1;
-
ROW_NUMBER()按user_id分组,组内按created_at DESC排序并编号 - 外层
WHERE rn = 1精确拿到每组“排序后第一行” - 若需保留并列(如多个同分最高),改用
RANK()或DENSE_RANK() - 注意:该写法要求 MySQL ≥ 8.0;5.7 及更早版本不支持窗口函数
MySQL 5.7 及更早版本:用关联子查询或自连接模拟“先排序后分组”
老版本没有窗口函数,常见错误写法是:SELECT *, MAX(created_at) FROM orders GROUP BY user_id —— 这会导致非聚合字段(如 order_id、amount)的值来自任意匹配行,不可控。
安全做法是显式关联出“每组最大时间对应完整行”:
SELECT o1.* FROM orders o1 WHERE o1.created_at = ( SELECT MAX(o2.created_at) FROM orders o2 WHERE o2.user_id = o1.user_id );
- 子查询确保只匹配到每组
MAX(created_at)对应的行 - 如果存在多行
created_at相同(即并列),会全部返回;需唯一结果可加LIMIT 1并配合ORDER BY在子查询中进一步限定 - 性能敏感场景务必给
(user_id, created_at)加联合索引,否则子查询可能全表扫描 - 避免用
IN (SELECT ...)匹配主键,MySQL 5.7 对这类写法优化差,易触发临时表和文件排序
为什么不能依赖 GROUP BY + ORDER BY 的组合?
执行顺序决定了它无效:GROUP BY 先发生,此时每组只剩一条(由服务器随机选),ORDER BY 再对这“一条/组”的结果排序——根本没机会在分组前排序。
- 即使开启
sql_mode中的ONLY_FULL_GROUP_BY,也只是阻止语法错误,不改变执行逻辑 - 某些旧版本允许
SELECT a, b FROM t GROUP BY a ORDER BY b,但b值仍不确定,属未定义行为 - 官方文档明确说明:
GROUP BY后非聚合列的值是“隐式分组结果”,不可预测
容易被忽略的边界情况:NULL 值与排序稳定性
当排序字段含 NULL,不同 MySQL 版本默认处理不同(NULLS FIRST / NULLS LAST 不被原生支持),可能导致预期外的“第一行”被选中。
- 显式控制
NULL位置:用ORDER BY IF(created_at IS NULL, 1, 0), created_at DESC - 若排序字段有重复值且无第二排序键,
ROW_NUMBER()结果不稳定(相同排序值下编号顺序不确定);建议补一个唯一字段(如主键)作为次要排序条件:ORDER BY created_at DESC, id DESC - 使用
GROUP_CONCAT(... ORDER BY ...)做字符串聚合时也受此影响,但它只影响拼接顺序,不解决“取整行”问题











