上周接手的一个列表页,测试环境毫秒级返回,生产上稳定 3 秒起步。第一件事不是猜,是把慢日志打开,用事实说话。

第一步:让慢查询自己报到

SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.5;  -- 单位秒,按场景调
-- 日志位置
SHOW VARIABLES LIKE 'slow_query_log_file';

很快抓到元凶:一条按 user_id + status + created_at 过滤排序的分页查询。

第二步:EXPLAIN 三个关键字段

EXPLAIN SELECT id, title FROM orders
WHERE user_id = 42 AND status = 1
ORDER BY created_at DESC LIMIT 20;

重点看三列:

  • type:访问类型。ALL 是全表扫描,ref 走了非唯一索引,const 主键或唯一索引等值命中。这条查询是 ALL,八百万行的全表扫描;
  • key:实际用到的索引,这里是 NULL——表上只有一个 user_id 的单列索引,但优化器没选它(回表成本高于全扫);
  • ExtraUsing filesort,说明排序没吃到索引序,内存里又排了一次。

第三步:按最左前缀建联合索引

ALTER TABLE orders
  ADD INDEX idx_user_status_created (user_id, status, created_at);

等值条件放前面、排序字段收尾,这样过滤和 ORDER BY 都能吃到同一个索引,filesort 消失。再看 EXPLAIN:type=refrows 从八百万掉到两千出头,查询 60ms。

两个后续

一是这类问题上线前就该拦住:预发环境造一批贴近生产的匿名化数据量,EXPLAIN 纳入 code review 清单。二是注意索引也不是越多越好——这张表写入不轻,最终删掉了两个冗余的单列索引来平摊写入开销。查询快了,写入也别陪葬。