慢查询是数据库性能问题的头号杀手。一个没有索引的全表扫描可能让查询从毫秒级退化到秒级,在高并发下迅速拖垮整个数据库。MySQL提供了EXPLAIN执行计划分析工具,通过解读执行计划可以精确定位性能瓶颈,针对性优化。
EXPLAIN执行计划关键字段解读
EXPLAIN是MySQL优化器的查询执行计划展示工具。在SQL语句前加EXPLAIN即可查看。重点关注以下字段:
EXPLAIN SELECT * FROM orders
JOIN users ON orders.user_id = users.id
WHERE orders.status = 'paid' AND users.city = '上海'
ORDER BY orders.created_at DESC LIMIT 20;
+----+-------------+--------+------+---------------+---------+---------+-------------------+--------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+------+---------------+---------+---------+-------------------+--------+----------------------------------------------+
| 1 | SIMPLE | users | ref | PRIMARY,idx_city| idx_city| 102 | const | 5000 | Using index; Using temporary; Using filesort |
| 1 | SIMPLE | orders | ref | idx_user | idx_user| 8 | test.users.id | 20000 | Using where |
+----+-------------+--------+------+---------------+---------+---------+-------------------+--------+----------------------------------------------+
type字段表示访问类型,性能从好到差依次为:system > const > eq_ref > ref > range > index > ALL。ALL是全表扫描,必须优化。index是全索引扫描,通常也需要优化。range和ref是常见的合理访问类型。
key字段显示实际使用的索引。如果为NULL说明没有使用索引,需要排查原因。possible_keys列出了可能使用的索引,如果没有使用可能是因为统计信息过期或索引设计不合理。
rows字段是优化器估算的扫描行数,越少越好。如果rows很大但实际结果很少,说明索引选择性差或有更优的索引方案。
Extra字段包含额外信息。Using index表示索引覆盖查询,不需要回表,是最理想的状态。Using temporary表示用了临时表,通常出现在GROUP BY和DISTINCT场景。Using filesort表示需要额外排序,通常出现在ORDER BY字段没有索引的场景。Using temporary和Using filesort同时出现是性能危险信号。
索引设计原则与联合索引优化
索引设计遵循最左前缀原则。联合索引(a, b, c)可以用于a、(a,b)、(a,b,c)三种查询条件,但不能用于b或(b,c)查询。索引列顺序按区分度从高到低排列,区分度高的列放前面能更有效过滤数据。
-- 查看索引区分度
SELECT
COUNT(DISTINCT user_id) / COUNT(*) AS user_id_selectivity,
COUNT(DISTINCT status) / COUNT(*) AS status_selectivity,
COUNT(DISTINCT created_at) / COUNT(*) AS created_at_selectivity
FROM orders;
-- 假设结果:user_id=0.8, status=0.01, created_at=0.99
-- 联合索引应按 (status, user_id) 或 (user_id, status) 创建
-- status区分度低但常用于等值查询,放前面可以让后续索引列更有效过滤
上面的查询计划中users表出现了Using filesort,因为ORDER BY的是orders表的created_at字段,而JOIN后无法利用索引排序。优化方案是创建覆盖索引:
-- 为orders表创建联合索引,覆盖查询、排序需求
ALTER TABLE orders ADD INDEX idx_user_status_created (user_id, status, created_at);
-- 优化后的查询计划
EXPLAIN SELECT * FROM orders
JOIN users ON orders.user_id = users.id
WHERE orders.status = 'paid' AND users.city = '上海'
ORDER BY orders.created_at DESC LIMIT 20;
-- orders表执行计划变为:
-- type: ref, key: idx_user_status_created
-- Extra: Using where; Using index(索引覆盖,无需回表)
覆盖索引与索引下推优化
覆盖索引指查询的所有字段都包含在索引中,不需要回表读取数据行。InnoDB的聚簇索引结构决定了二级索引存储的是主键值,查询非索引字段需要先从二级索引取主键,再从聚簇索引取完整行数据。覆盖索引消除了回表操作,对IO密集型查询提升显著。
-- 查询只需要user_id和status两个字段
SELECT user_id, status FROM orders WHERE user_id = 123 AND status = 'paid';
-- 如果有索引idx_user_status_created(user_id, status, created_at)
-- user_id和status都在索引中,直接从索引返回,不需要回表
-- Extra: Using index 表示覆盖索引生效
索引下推(Index Condition Pushdown,ICP)是MySQL 5.6引入的优化。在没有ICP时,存储引擎根据联合索引的第一个列找到记录后返回给Server层,Server层再根据其他条件过滤。ICP将WHERE条件下推到存储引擎层,在索引遍历时就做过滤,减少回表次数。
-- 联合索引 (last_name, first_name)
SELECT * FROM employees
WHERE last_name LIKE '张%' AND first_name LIKE '三%';
-- 无ICP:存储引擎找到所有last_name以"张"开头的记录,全部回表,Server层再过滤first_name
-- 有ICP:存储引擎在索引层同时检查last_name和first_name,只对满足条件的记录回表
-- Extra: Using index condition 表示ICP生效
分页查询深度翻页优化
LIMIT偏移量过大是常见的慢查询场景。LIMIT 1000000, 20需要扫描前100万行再丢弃,效率极低。优化方案有延迟关联和游标分页两种。
延迟关联:先通过子查询用覆盖索引取出主键,再JOIN原表取完整数据:
-- 优化前:扫描100万行
SELECT * FROM orders ORDER BY created_at DESC LIMIT 1000000, 20;
-- 优化后:子查询走覆盖索引取出主键,再JOIN
SELECT t.* FROM orders t
INNER JOIN (
SELECT id FROM orders ORDER BY created_at DESC LIMIT 1000000, 20
) tmp ON t.id = tmp.id;
-- 假设created_at有索引,子查询走idx_created_at覆盖索引
-- 只需扫描索引取出20个主键,再回表20次,效率提升数百倍
游标分页:记住上一页最后一条记录的排序值,下一页从该值之后查询。完全避免OFFSET:
-- 第一页
SELECT * FROM orders ORDER BY created_at DESC LIMIT 20;
-- 第二页(假设第一页最后一条created_at = '2026-08-01 10:30:00', id = 5000)
SELECT * FROM orders
WHERE created_at < '2026-08-01 10:30:00'
OR (created_at = '2026-08-01 10:30:00' AND id < 5000)
ORDER BY created_at DESC, id DESC LIMIT 20;
-- 联合索引 (created_at, id) 让查询走索引范围扫描,恒定扫描20行
慢查询日志配置与分析
MySQL慢查询日志记录执行时间超过阈值的SQL,是发现性能问题的第一手段。配置方式如下:
-- 查看慢查询配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
SET GLOBAL long_query_time = 1; -- 超过1秒记录
SET GLOBAL log_queries_not_using_indexes = 'ON'; -- 记录未用索引的查询
-- 永久生效写入my.cnf
-- [mysqld]
-- slow_query_log = 1
-- slow_query_log_file = /var/log/mysql/slow.log
-- long_query_time = 1
-- log_queries_not_using_indexes = 1
mysqldumpslow工具可以聚合分析慢查询日志,按耗时或次数排序找出TOP SQL:
# 按总耗时排序
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
# 按返回行数排序
mysqldumpslow -s r -t 10 /var/log/mysql/slow.log
# 按次数排序
mysqldumpslow -s c -t 10 /var/log/mysql/slow.log
对于更深入的分析,pt-query-digest(Percona Toolkit)提供了更详细的统计信息,包括SQL指纹、执行时间分布、97%分位等。优化SQL时应优先处理耗时排名靠前且出现频率高的查询。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/mysql-man-cha-xun-you-hua-shi-zhan-zhi-xing-ji-hua-fen-xi/