游客发表
昨天半夜,最新一个做后端的记录朋友发来消息:“MySQL里想拿最新的一条记录,我写了 SELECT * FROM orders ORDER BY create_time DESC LIMIT 1,最新跑起来慢得离谱,记录是最新不是我SQL写错了?”我让他把表结构发过来,一看,记录create_time 上没索引,最新数据量快800万行。记录问题不在SQL语法,最新在索引。记录
取最新一条记录,最新最直觉的记录写法就是按时间字段倒序,再 LIMIT 1。最新这条语句本身没毛病,记录但如果没有索引,最新MySQL会做一次全表扫描,再把所有行放进排序缓冲区做filesort。800万行数据,每次查询都这么干,不慢才怪。加一个 create_time 的普通索引,情况立刻不同——B+树的最右端就是最大值,优化器可以直接反向扫描索引,读一行就返回。
ALTER TABLE orders ADD INDEX idx_create_time (create_time); 但这里有个坑。如果 create_time 精度不够,比如只精确到秒,同一秒内有多条记录,ORDER BY create_time DESC LIMIT 1 返回哪一条是不确定的。你可能会说,那我再加一个自增主键 id 做次级排序。ORDER BY create_time DESC, id DESC LIMIT 1,逻辑上没问题。可索引得跟着改,建一个 (create_time, id) 的联合索引。单列索引在这种情况下不够用,因为MySQL在索引里找到最大 create_time 后,还得回表或者继续扫描才能确定同时间内的最大 id。
另一个常见场景是分组取最新。比如一张 user_login 表,要查每个用户最近一次登录记录。很多人第一反应是 GROUP BY user_id 配合 MAX(login_time),然后发现拿不到完整行——MySQL的 GROUP BY 不保证返回哪一行的其他字段。这时候子查询或者窗口函数就派上用场了。MySQL 8.0之前没有窗口函数,常用的写法是自连接:
SELECT t1.* FROM user_login t1 INNER JOIN ( SELECT user_id, MAX(login_time) AS max_time FROM user_login GROUP BY user_id ) t2 ON t1.user_id = t2.user_id AND t1.login_time = t2.max_time; 这种写法在数据量大时同样依赖索引。(user_id, login_time) 的联合索引能让子查询走覆盖索引,外层再回表拿完整行。如果 login_time 有重复,还得加上 id 去重,否则一个用户可能返回多条。
MySQL 8.0之后,窗口函数干净很多:
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_time DESC) AS rn FROM user_login ) t WHERE rn = 1; 可读性上去了,但执行计划未必比自连接快。窗口函数需要物化子查询结果,内存和临时表的开销得算进去。我自己的经验是,单表取全局最新,ORDER BY ... LIMIT 1 加对索引最省事;分组取最新,数据量在百万级以内,自连接和窗口函数差距不大,超过千万行,得看具体执行计划再定。
还有一种取巧办法,用 MAX(id) 代替时间字段。前提是 id 自增且业务上“最新”等同于“最大id”。SELECT * FROM orders WHERE id = (SELECT MAX(id) FROM orders),主键索引直接定位,速度极快。但别滥用,时间字段和自增id的顺序不一定永远一致,比如补录数据、分库分表、或者业务上时间被手动修改过。
最后提一个容易忽略的点:ORDER BY create_time DESC LIMIT 1 在 create_time 有索引时,EXPLAIN 会显示 Backward index scan。这是MySQL 8.0才有的优化,5.7版本虽然也能用索引,但执行计划里是 Using index 加 Using filesort 的混合状态,性能差异在数据量大时才明显。升级版本或者改写SQL,得实测。
取最新一条记录,核心就一句话:让排序字段走索引,别让数据库做无谓的排序。索引建对了,怎么写都快;索引没建,窗口函数也救不了。
友情链接