mysql 5.7及更早版本不支持row_number()和first_value(),须先用select version()确认≥8.0.0才能用窗口函数;否则需用子查询、自连接或变量法。

确认 MySQL 版本再决定用什么方案
MySQL 5.7 及更早版本不支持 ROW_NUMBER() 和 FIRST_VALUE(),直接执行会报错 FUNCTION xxx.ROW_NUMBER does not exist 或语法错误。必须先运行 SELECT VERSION(); 确认结果 ≥ 8.0.0 才能用窗口函数。低于该版本,只能用子查询、自连接或变量法——别硬套窗口函数示例,否则查不出数据还浪费调试时间。
用 ROW_NUMBER() 取整行记录的写法和陷阱
窗口函数本身不能出现在 WHERE 子句里,所以不能写 WHERE ROW_NUMBER() OVER (...) = 1。必须用 CTE 或子查询包裹后再过滤:
WITH ranked AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY category ORDER BY created_at ASC, id ASC) AS rn FROM orders ) SELECT id, category, amount, created_at FROM ranked WHERE rn = 1;
-
PARTITION BY字段要和业务分组逻辑完全一致,比如按user_id分组就不能漏掉tenant_id(如果多租户) -
ORDER BY必须包含足够确定性的字段组合:仅用created_at可能因精度相同导致rn分配不稳定,补上id作为第二排序键可避免 - 如果表数据量大,务必在
(category, created_at, id)上建联合索引,否则排序会触发临时文件,慢得明显
没窗口函数时,子查询 JOIN 是最稳的替代方案
适用于 MySQL 5.7 及所有旧版本,语义清晰、结果确定,但要注意字段唯一性问题:
SELECT t1.* FROM orders t1 INNER JOIN ( SELECT category, MIN(id) AS min_id FROM orders GROUP BY category ) t2 ON t1.category = t2.category AND t1.id = t2.min_id;
- 这个方案依赖
id是主键或唯一标识符;如果想按时间取“最早”,得把MIN(id)换成MIN(created_at),但必须确保(category, created_at)组合唯一,否则可能匹配到多行 - 如果
created_at有重复且无其他唯一字段,建议加AND t1.id = (SELECT MIN(id) FROM orders t3 WHERE t3.category = t1.category AND t3.created_at = t1.created_at)做二次限定 - 性能上,子查询里的
GROUP BY会走索引(如INDEX(category, created_at)),外层JOIN也靠id主键快速定位,整体比全表扫描靠谱
FIRST_VALUE() 不适合取整行,强行用容易字段错位
FIRST_VALUE() 是标量函数,只返回单个值,而且它不会减少行数——原表 1000 行,结果还是 1000 行,只是多了一列“每组第一个 price”。常见误写是:
SELECT FIRST_VALUE(id) OVER (PARTITION BY category ORDER BY created_at), FIRST_VALUE(name) OVER (PARTITION BY category ORDER BY created_at) FROM orders;
这看似取了“第一条的 id 和 name”,但两个 FIRST_VALUE() 可能来自不同行(尤其当 created_at 相同时),导致 id 和 name 对不上。真要拼整行,必须用 ROW_NUMBER() 或子查询,别贪图少写几行代码。











