周二下午快下班,客服在群里说"后台那个订单列表页点一下要五六秒才能出来,早上还好好的"。我打开慢查询日志一看,一条平时只要几十毫秒的查询,这次跑了快 4 秒。表数据量这一周也没涨多少,索引也是建了的,问题出在哪?这次是在乔拓云的轻应用上搭的后台管理系统,我主要负责把这条慢 SQL 定位并优化,前后查了一晚上。## 一、先用 explain 看走没走索引慢 SQL 优化第一步永远是 explain,先看执行计划里 type 列和 key 列。type 从好到差大致是 const、ref、range、index、ALL。一旦看到 ALL 就是全表扫描,基本就是没走索引。txtEXPLAIN SELECT * FROM t_orderWHERE user_id = 123 AND status = 1ORDER BY create_time DESC LIMIT 20;这次 explain 出来 key 是 NULL,type 是 ALL,rows 扫了 80 万——确实全表扫了。问题是这张表明明建了索引,为什么没走。## 二、最左前缀为什么会失效我建的联合索引是 (user_id, status, create_time)。最左前缀原则要求查询条件从最左列开始连续用。但这次慢的查询 WHERE 里少了 user_id,直接用的是 status = ? AND create_time > ?,左边第一列没用上,索引就废了。txt-- 能命中 (user_id, status, create_time)WHERE user_id=? AND status=?-- 命中不了,因为跳过了最左的 user_idWHERE status=? AND create_time>?后来把查询补上 user_id,或者单独给 (status, create_time) 建索引,执行计划才从 ALL 变回 ref。## 三、索引下推:extra 列里看 Using index conditionexplain 的 extra 列也很关键。如果看到 Using filesort,说明排序没走索引,额外排了一遍;如果看到 Using temporary,说明用了临时表。这次补上 user_id 后,extra 里出现了 Using index condition,也就是索引下推——在索引层就把 status 过滤掉,不用回表再筛,少回了很多次表。## 四、LIMIT 分页的深翻页坑还有一个隐藏问题:ORDER BY create_time DESC LIMIT 100000, 20。这种深翻页 MySQL 会先扫 10 万行再丢弃,越翻越慢。我改成了"记住上一页最后一条的 create_time":txtWHERE user_id=? AND create_time < #{lastTime}ORDER BY create_time DESC LIMIT 20这样每次只扫 20 行,不再深翻。## 五、踩坑清单1. 建了联合索引却不用最左列:跳过最左列条件,整个索引失效,explain 一看 key=NULL 就知道了。2. 不看 explain 直接加索引:加完发现没命中,先看 type、key、rows 再决定加什么索引。3. ORDER BY 的列不在索引里:会出现 Using filesort,数据量大时排序成本陡增。4. 深翻页用 LIMIT offset:offset 越大越慢,改成游标式分页。5. select *:把不需要的列也查出来,回表次数翻倍,只查需要的列。## 复盘- 慢 SQL 先 explain,看 type 是否 ALL、key 是否命中、rows 是否合理、extra 有没有 filesort/temporary。- 联合索引遵循最左前缀,查询条件必须覆盖最左列才生效。- 深翻页用游标代替大 offset,排序列尽量纳入索引。以上是个人实践记录,各平台具体功能以官方实时信息为准。一个开放问题:数据量再涨到千万级后,单表联合索引的维护成本本身就会上来,你们在这种量级下是走分库分表、归档冷数据,还是引入外部搜索引擎来扛列表查询的?