事情是这样的。客户反馈说后台的订单列表页打开很慢,有时候转圈超过十秒。我看了下数据量——订单表一共四万多行。四万行的表查询花了八秒,这肯定有问题。
第一件事是打开MySQL的慢查询日志,把那条慢SQL抓出来。SQL长这样:
SELECT o.*, u.name AS user_name, u.phone FROM orders o LEFT JOIN users u ON o.user_id = u.id WHERE o.status IN (1,2,3) AND o.created_at > '2025-01-01' ORDER BY o.created_at DESC LIMIT 20
看起来没啥特别的,一个基础的联表查询加一个排序加一个分页。但EXPLAIN一跑,问题出来了——type列显示的是ALL,也就是全表扫描。明明有二十条分页,但MySQL先把四万多行全部扫了一遍,排了序,再挑了二十条出来。
再看possible_keys是空的。也就是说这个查询没有匹配到任何可用的索引。status、created_at、user_id三个查询条件,没有一个单独或者组合的索引。
我的优化思路是加一个联合索引,把查询条件里的字段按选择性从高到低排列。选择的逻辑是这样的:status字段只有三个值,选择性不高,但作为查询条件的前缀能帮InnoDB快速过滤。created_at是范围查询,选择性很好,放在status后面。user_id是联表字段,也放进去。最后为了覆盖排序,把created_at也加上作为索引的最后一个字段。
加的索引:ALTER TABLE orders ADD INDEX idx_status_created_user (status, created_at, user_id);
这个索引加上之后,同样的查询从8.2秒降到了0.03秒。其实原理说穿了不值一提,就是让MySQL不用扫全表了。但现实中很多系统上线的时候没经验,忘记给常用的查询字段加索引,或者加的索引和查询条件不匹配。
后来我建议开发团队把一段SQL审核的脚本集成到部署流程里,每次有新的SQL语句上线之前自动跑一遍EXPLAIN,如果type列是ALL就自动拦截并提示开发人员加索引。这个习惯养成了之后,类似的慢查询问题在后续项目中几乎没再出现过。
技术博客