上周接手的一个列表页,测试环境毫秒级返回,生产上稳定 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的单列索引,但优化器没选它(回表成本高于全扫); - Extra:
Using filesort,说明排序没吃到索引序,内存里又排了一次。
第三步:按最左前缀建联合索引
ALTER TABLE orders
ADD INDEX idx_user_status_created (user_id, status, created_at);
等值条件放前面、排序字段收尾,这样过滤和 ORDER BY 都能吃到同一个索引,filesort 消失。再看 EXPLAIN:type=ref,rows 从八百万掉到两千出头,查询 60ms。
两个后续
一是这类问题上线前就该拦住:预发环境造一批贴近生产的匿名化数据量,EXPLAIN 纳入 code review 清单。二是注意索引也不是越多越好——这张表写入不轻,最终删掉了两个冗余的单列索引来平摊写入开销。查询快了,写入也别陪葬。