曾经线上某个产品在发布日上线查询接口突然慢了10倍。
我打开监控一看,一个分页SQL响应时间从200ms飙到了3.5秒。
同事说:“加个索引不就好了?”
我说:“先别急,我们看看执行计划。”
这已经不是第一次靠EXPLAIN救命了。今天,我想把这几年踩过的坑、总结的经验,毫无保留地分享给你——不是理论堆砌,而是真正能用在生产环境的实战指南。全文内容较长,建议收藏,以后慢慢看。
很多开发者觉得:“SQL能跑就行,慢了就加索引。” 但现实是:
加了索引,查询反而更慢
索引用了,但只用了部分字段
明明有索引,MySQL却视而不见
其根本原因:你没看执行计划。
查看执行计划不是DBA的专利,它是每个写SQL的人的“X光机”——让你看清MySQL到底怎么执行你的查询。也能够透视数据库内部的工作机制,发现潜在的性能瓶颈。在高性能要求的系统中,理解执行计划是确保数据库高效运行的基本技能。
在MYSQL中可以使用EXPLAIN或DESC获取预估的执行计划,即只需在SQL语句前加上EXPLAIN(或DESC)关键字即可:
mysql> EXPLAIN SELECT * FROM customers WHERE name = '张三' AND age = 30;+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | customers | NULL | ref | idx_name_age_level | idx_name_age_level | 78 | const,const | 1 | 100.00 | NULL |+1 row in set, 1 warning (0.00 sec)mysql> DESC SELECT * FROM customers WHERE name = '张三' AND age = 30;+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | customers | NULL | ref | idx_name_age_level | idx_name_age_level | 78 | const,const | 1 | 100.00 | NULL |+1 row in set, 1 warning (0.00 sec)
MySQL 8.0及以上版本还支持更详细的EXPLAIN ANALYZE,它会实际执行查询并返回详细的执行统计信息:
mysql> EXPLAIN ANALYZE SELECT * FROM customers WHERE name = '张三' AND age = 30;+| EXPLAIN |+| -> Index lookup on customers using idx_name_age_level (name='张三', age=30) (cost=0.35 rows=1) (actual time=0.0158..0.0158 rows=0 loops=1) |+1 row in set (0.01 sec)
注意: EXPLAIN ANALYZE在查看DML(INSERT,DELETE,UPDATE)时慎用,建议开启事务,查看后回滚,以免真实修改了数据。需要查看执行计划时可以考虑转为select方式进行查看执行计划。
可以指定不同的输出格式获取更详细的执行计划信息,例如传统表(默认),JSON格式,树状格式(MYSQL8.0+支持),例如:
1)传统表格格式(默认)
mysql>
mysql> EXPLAIN SELECT * FROM products WHERE price > 1000;+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | products | NULL | range | idx_price | idx_price | 5 | NULL | 539 | 100.00 | Using index condition |+1 row in set, 1 warning (0.01 sec)
2)JSON格式(可以提供更详细信息)
mysql> EXPLAIN FORMAT=JSON SELECT * FROM products WHERE category_id = 3;+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+| EXPLAIN |+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+| { "query_block": { "select_id": 1, "cost_info": { "query_cost": "82.60" }, "table": { "table_name": "products", "access_type": "ref", "possible_keys": [ "idx_category_price" ], "key": "idx_category_price", "used_key_parts": [ "category_id" ], "key_length": "4", "ref": [ "const" ], "rows_examined_per_scan": 236, "rows_produced_per_join": 236, "filtered": "100.00", "cost_info": { "read_cost": "59.00", "eval_cost": "23.60", "prefix_cost": "82.60", "data_read_per_join":
"40K" }, "used_columns": [ "id", "product_name", "category_id", "price", "stock", "create_time" ] } }} |+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+1 row in set, 1 warning (0.00 sec)
3)树状格式(MySQL 8.0+支持)
mysql> EXPLAIN FORMAT=TREE SELECT * FROM orders WHERE order_date > '2025-01-01';+| EXPLAIN |+| -> Filter: (orders.order_date > TIMESTAMP'2025-01-01 00:00:00') (cost=30221 rows=149460) -> Table scan on orders (cost=30221 rows=298920) |+1 row in set (0.00 sec)
以常用的表格格式的执行计划为例,我们来解释一下MYSQL执行计划中核心字段的意义和作用。
mysql> EXPLAIN SELECT * FROM products WHERE price > 1000;+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | products | NULL | range | idx_price | idx_price | 5 | NULL | 539 | 100.00 | Using index condition |+1 row
in set, 1 warning (0.00 sec)
id 是SELECT查询的序列号,体现查询的执行顺序,即按照如下顺序执行:
select_type表示查询的类别,常见的有:
这是最理想的情况之一,通常性能较好。
mysql> EXPLAIN SELECT * FROM users WHERE user_id = 1;+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | no matching row in const table |+1 row in set, 1 warning (0.00 sec)
mysql> explain SELECT * FROM (SELECT 1) AS t;+| id | select_type |
table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | PRIMARY | <derived2> | NULL | system | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL || 2 | DERIVED | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | No tables used |+2 rows in set, 1 warning (0.00 sec)
mysql> EXPLAIN -> SELECT * FROM customers -> WHERE id = ( -> SELECT customer_id FROM orders WHERE id = 100 -> );+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | PRIMARY | customers | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL || 2 | SUBQUERY | orders | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL |+2 rows in set, 1 warning (0.00 sec)
注: 此类子查询通常只执行一次,结果可以被缓存。
另外,如果子查询依赖外层变量(如 WHERE u.id = o.user_id),则不是 SUBQUERY,而是 DEPENDENT SUBQUERY。
mysql> EXPLAIN -> SELECT -> t1.name, -> (SELECT t2.val -> FROM t2 -> WHERE t2.t1_id = t1.id -> ORDER BY t2.val DESC -> LIMIT 1) AS max_val -> FROM t1;+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | PRIMARY | t1 | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 100.00 | NULL || 2 | DEPENDENT SUBQUERY | t2 | NULL | ALL | NULL | NULL | NULL | NULL | 5 | 20.00 | Using where; Using filesort |+2 rows in set, 2 warnings (0.00 sec)
还有一种更极端的UNCACHEABLE SUBQUERY,不过几乎很难出现,也不好复现,本文就不举例了。
例子同primary:
mysql> explain SELECT * FROM (SELECT 1) AS t;+| id | select_type | table | partitions | type | possible_keys |
key | key_len | ref | rows | filtered | Extra |+| 1 | PRIMARY | <derived2> | NULL | system | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL || 2 | DERIVED | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | No tables used |+2 rows in set, 1 warning (0.00 sec)
在MySQL8.0之前DERIVED表无法使用索引,性能较差;MySQL 8.0 引入了CTE(公共表表达式)和物化优化,有所改善。
mysql> EXPLAIN -> SELECT name FROM customers WHERE age > 30 -> UNION -> SELECT name FROM customers WHERE name like '赵%';+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | PRIMARY | customers | NULL | index | idx_name_age_level | idx_name_age_level | 140 | NULL | 49989 | 33.33 | Using where; Using index || 2 | UNION | customers | NULL | range | idx_name_age_level | idx_name_age_level | 74 | NULL | 1 | 100.00 | Using where; Using index || 3 | UNION RESULT
| <union1,2> | NULL | ALL | NULL | NULL | NULL | NULL | NULL | NULL | Using temporary |+3 rows in set, 1 warning (0.00 sec)
UNION: UNION 或UNION ALL出现在 UNION 或 UNION ALL 的第二条及以后的 SELECT 中。
UNION RESULT: UNION 操作的最终结果合并阶段。MySQL 需要将多个 SELECT 的结果合并,并去重(UNION)或直接合并(UNION ALL)NION)或直接合并(UNION ALL)。
mysql> EXPLAIN -> SELECT name FROM customers u -> WHERE EXISTS ( -> SELECT 1 FROM orders o WHERE o.customer_id = u.id AND o.amount > 1000 -> );+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | u | NULL | index | PRIMARY | idx_name_age_level | 140 | NULL | 49989 | 100.00 | Using index || 1 | SIMPLE | <subquery2> | NULL | eq_ref | <auto_distinct_key> | <auto_distinct_key> | 4 | testdb.u.id | 1 | 100.00 | NULL || 2 | MATERIALIZED | o | NULL | ALL | idx_customer_date,idx_customer_status | NULL | NULL | NULL | 298920 | 33.33 | Using where
|+3 rows in set, 2 warnings (0.00 sec)
partitions字段显示的是查询实际访问的表分区。如果表没有分区,这个字段就是NULL。
通过它,你可以快速判断查询是否有效命中了目标分区,尽量直接读取指定分区的数据,避免全分区扫描
mysql> EXPLAIN SELECT * FROM goods_partitions;+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | goods_partitions | p0,p1,p2 | ALL | NULL | NULL | NULL | NULL | 3 | 100.00 | NULL |+1 row in set, 1 warning (0.00 sec)
type列表示MySQL在表中找到所需行的方式,是判断查询效率的最重要指标,直接决定了查询性能的高低。
下面按照性能从好到坏排序进行介绍:
mysql> EXPLAIN SELECT * FROM myisam_single_row;+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | myisam_single_row | NULL | system | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |+1 row in set, 1 warning (0.00 sec)
mysql> EXPLAIN SELECT * FROM customers WHERE id = 1001;+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | customers | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL |+1 row in set, 1 warning (0.00 sec)
mysql> EXPLAIN SELECT * FROM orders o, customers c WHERE o.customer_id = c.id;+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | o | NULL | ALL | idx_customer_date,idx_customer_status | NULL | NULL | NULL | 298920 | 100.00 | NULL || 1 | SIMPLE | c | NULL | eq_ref | PRIMARY | PRIMARY | 4 | testdb.o.customer_id | 1 | 100.00 | NULL |+2 rows in set, 1 warning (0.00 sec)
mysql> EXPLAIN SELECT * FROM customers WHERE name = '张三';+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | customers | NULL | ref | idx_name_age_level | idx_name_age_level | 74 | const | 1 | 100.00 | NULL |+1 row in
set, 1 warning (0.00 sec)
mysql> explain SELECT * FROM customers WHERE name like '赵%';+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | customers | NULL | range | idx_name_age_level | idx_name_age_level | 74 | NULL | 1 | 100.00 | Using index condition |+1 row in set, 1 warning (0.00 sec)
index:全索引扫描(索引覆盖),只遍历索引树,无需回表
mysql> EXPLAIN SELECT user_id FROM users ORDER BY user_id;+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | users | NULL | index | NULL | PRIMARY | 4 | NULL | 1 | 100.00 | Using index |+1 row in set, 1 warning (0.00 sec)
ALL:全表扫描,性能最差,应尽量避免,主要是因缺少索引等引起
mysql> EXPLAIN SELECT * FROM customers WHERE age > 20;+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | customers | NULL | ALL | NULL | NULL | NULL | NULL | 49989 | 33.33 | Using where |+1 row in set, 1 warning (0.00 sec)
从高到低,各个type的性能排序如下:
| | | |
|---|
| system | | | SELECT * FROM (SELECT 1) AS t; |
| const | | | SELECT * FROM user WHERE id = 1; |
| eq_ref | | | SELECT * FROM user JOIN order ON user.id = order.user_id; |
| ref | | | SELECT * FROM user WHERE name = '小明'; |
| range | | | SELECT * FROM user WHERE age > 20; |
|
index | | | SELECT id FROM user WHERE status = 'active'; |
| ALL | | | SELECT * FROM user WHERE name LIKE '%小明%'; |
possible_keys:查询可能使用的索引
key:查询实际使用的索引
如果possible_keys有值而key为NULL,说明可能是因为SQL写法无法走索引(例如函数计算、隐式转换等)问题,或者全表扫描的代价比索引低。
mysql> explain select * from orders where customer_id<>1000 ;+| id | select_type | table | partitions | type | possible_keys | key | key_len
| ref | rows | filtered | Extra |+| 1 | SIMPLE | orders | NULL | ALL | idx_customer_date,idx_customer_status | NULL | NULL | NULL | 298920 | 87.99 | Using where |+1 row in set, 1 warning (0.00 sec)
key_len表示查询实际使用的索引的长度(单位是字节)。它能帮忙精确测量出一次查询到底用了多少索引资源,尤其是在组合索引中,可以看出一次查询进行索引检索时用到了哪些字段。
例如:
#SQL1:mysql> explain select * from customers where name='张三';+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | customers | NULL | ref | idx_name_age_level | idx_name_age_level | 74 | const | 1 | 100.00 | NULL |+1 row in set, 1 warning (0.00 sec)
#SQL2:mysql> explain select * from customers where name='张三' and age>20;+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+
| 1 | SIMPLE | customers | NULL | range | idx_name_age_level | idx_name_age_level | 78 | NULL | 1 | 100.00 | Using index condition |+1 row in set, 1 warning (0.00 sec)
#SQL3:mysql> explain select * from customers where name='张三' and age>20 and vip_level='普通';+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | customers | NULL | range | idx_name_age_level,idx_vip_level | idx_name_age_level | 78 | NULL | 1 | 50.00 | Using index condition |+1 row in set, 1 warning (0.00 sec)
#SQL4:mysql> explain select * from customers where name='张三' and age=20 and vip_level='普通';+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | customers | NULL | ref | idx_name_age_level,idx_vip_level | idx_name_age_level | 140 | const,const,const | 1 | 100.00 | NULL |+1 row in set, 1 warning (0.00 sec)
例子中的4个SQL,可以看出4个SQL都走了idx_name_age_level,但是key_len不同,这说明不同的查询走上索引查询字段是不一样的:
不同数据类型和约束下的key_len基本计算方式如下:
数据类型 | 允许NULL | 字符集 | 计算示例 | 大致key_len值 |
|---|
INT NOT NULL | 否 |
- | 4字节 | 4 |
INT DEFAULT NULL | 是 | - | 4字节 + 1字节(NULL标记) | 5 |
CHAR(10) NOT NULL | 否 | utf8mb4 | 10字符 × 4字节/字符 |
40 |
| 是 | utf8mb4 | 10字符 × 4字节/字符 + 1字节 | 41 |
VARCHAR(100) NOT NULL | 否 | utf8mb4 | 100字符 × 4字节/字符 + 2字节(变长开销) | 402 |
VARCHAR(100) DEFAULT NULL |
是 | utf8mb4 | 100字符 × 4字节/字符 + 2字节 + 1字节 | 403 |
MySQL估计需要扫描的行数。这个值不一定准确(与统计信息也有关系),但可以作为性能判断的重要参考——值越小越好。
另外rows小不代表就很快,例如关联的时候,关联字段是主键时,rows显示很可能就是1,但是不代表整个查询就很快。
filtered字段预估了存储引擎返回的数据在经过服务器层所有剩余条件过滤后,最终满足查询条件的行数所占的百分比。这个值介于0到100之间,值越大(越接近100),意味着存储引擎有效地返回了数据,并且索引得到了很好的利用,服务器层不需要再做大量的额外筛选,性能也往往越好。
Extra字段包含查询的详细信息,上述案例基本也都包含,常见值及优化建议如下:
| | | |
|---|
| Using where | | | |
| Using filesort | | | |
| Using index | | | |
|
Using temporary | 需要临时表(如GROUP BY、DISTINCT) | | |
| Impossible WHERE | | | |
-- 客户表(customers)CREATE TABLE `customers` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(24) NOT NULL DEFAULT '' COMMENT '客户姓名', `age` int(11) NOT NULL DEFAULT '0' COMMENT '年龄', `vip_level` varchar(20) NOT NULL DEFAULT '' COMMENT 'VIP等级', `registration_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间', `last_login` timestamp NULL DEFAULT NULL COMMENT '最后登录时间', PRIMARY KEY (`id`), KEY `idx_name_age_level` (`name`,`age`,`vip_level`), KEY `idx_registration_time` (`registration_time`), KEY `idx_vip_level` (`vip_level`)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='客户信息表';-- 商品表(products)CREATE TABLE `products` (
`id` int(11) NOT NULL AUTO_INCREMENT, `product_name` varchar(50) NOT NULL COMMENT '商品名称', `category_id` int(11) NOT NULL COMMENT '分类ID', `price` decimal(10,2) NOT NULL COMMENT '价格', `stock` int(11) NOT NULL COMMENT '库存', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), KEY `idx_category_price` (`category_id`,`price`), KEY `idx_price` (`price`), KEY `idx_create_time` (`create_time`)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品信息表';-- 订单表(orders)CREATE TABLE `orders` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_number` varchar(20) NOT NULL COMMENT '订单号', `customer_id` int(11) NOT NULL COMMENT '客户ID', `product_id` int(11) NOT NULL COMMENT '商品ID', `quantity` int(11) NOT NULL COMMENT '数量', `amount` decimal(10,2) NOT NULL COMMENT '金额', `order_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '订单日期', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '订单状态', PRIMARY KEY (`id`), KEY `idx_customer_date` (`customer_id`,`order_date`), KEY `idx_order_date` (`order_date`), KEY `idx_customer_status` (`customer_id`,`status`), KEY `idx_product_id` (`product_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='订单表';
测试数据大家可以自己造一些。
MySQL索引遵循最左前缀原则,即查询必须从索引的最左列开始。
有效使用索引的查询(上面案例也演示过):
mysql> mysql> EXPLAIN SELECT * FROM customers WHERE name = '张三';+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | customers | NULL | ref | idx_name_age_level | idx_name_age_level | 74 | const | 1 | 100.00 | NULL |+1 row in set, 1 warning (0.00 sec)mysql> mysql> EXPLAIN SELECT * FROM customers WHERE name = '张三' AND age = 30;+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | customers | NULL | ref | idx_name_age_level | idx_name_age_level | 78 | const,const | 1 | 100.00 | NULL |+1 row in set, 1 warning (0.00 sec)
索引失效的查询:
mysql> mysql> EXPLAIN SELECT * FROM customers WHERE age = 30;+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | customers | NULL | ALL | NULL | NULL | NULL | NULL | 49989 | 10.00 | Using where |+1 row in set, 1 warning (0.00 sec)mysql> mysql> EXPLAIN SELECT * FROM customers WHERE name = '张三' AND vip_level = '黄金';+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | customers | NULL | ref | idx_name_age_level,idx_vip_level | idx_name_age_level | 74 | const | 1 | 5.00 | Using index condition |+1 row in set, 1 warning (0.00 sec)
查询VIP等级为'黄金'的客户并按注册时间降序排列:
mysql> EXPLAIN SELECT * FROM customers WHERE vip_level = '黄金' ORDER BY registration_time DESC;+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | customers | NULL | ref | idx_vip_level | idx_vip_level | 62 | const | 1502 | 100.00 | Using filesort |+1 row in set, 1 warning (0.00 sec)
Extra中出现"Using filesort",表示需要额外的排序操作。
优化方案:
ALTER TABLE customers ADD
INDEX idx_vip_regtime (vip_level, registration_time DESC);
mysql> EXPLAIN SELECT id, name, vip_level, registration_time -> FROM customers -> WHERE vip_level = '黄金' -> ORDER BY registration_time DESC;+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | customers | NULL | ref | idx_vip_level,idx_vip_regtime | idx_vip_regtime | 62 | const | 1502 | 100.00 | NULL |+1 row in set, 1 warning (0.00 sec)
优化后,Extra应该显示"NULL",表示已经没有在使用文件排序了。
覆盖索引是指查询的字段都包含在索引中,无需回表查询数据行。
mysql> EXPLAIN SELECT * FROM customers WHERE name = '张三';+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | customers | NULL | ref | idx_name_age_level | idx_name_age_level | 74 | const | 1 | 100.00 | NULL |+1
row in set, 1 warning (0.00 sec)
mysql> EXPLAIN SELECT name, age, vip_level FROM customers WHERE name = '张三';+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | customers | NULL | ref | idx_name_age_level | idx_name_age_level | 74 | const | 1 | 100.00 | Using index |+1 row in set, 1 warning (0.00 sec)
第二个查询的Extra会显示"Using index",效率更高,因为不需要回表访问数据行。
4、索引失效的常见场景及解决方案
1)在索引列上使用函数或计算
①索引失效
mysql> EXPLAIN SELECT * FROM customers WHERE YEAR(registration_time) = 2024;+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | customers | NULL | ALL | NULL | NULL | NULL | NULL | 49989 | 100.00 | Using where |+1 row in set, 1 warning (0.00 sec)
②优化后(使用范围查询)
mysql> EXPLAIN SELECT * FROM customers WHERE registration_time BETWEEN '2024-01-01 00:00:00' AND '2024-12-31 23:59:59';+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | customers | NULL | range | idx_registration_time | idx_registration_time | 4 | NULL | 1096 | 100.00 | Using index condition |+1 row in set, 1 warning (0.00 sec)
2)隐式类型转换
mysql> desc customers2 id ;+| Field | Type | Null | Key | Default | Extra |+| id | varchar(20) | NO | PRI | NULL | |+1 row in set (0.00 sec)
①id是字符串类型,如果使用数字查询会导致索引失效
mysql> EXPLAIN SELECT * FROM customers2 WHERE id = 1001;+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | customers2 | NULL | ALL | PRIMARY | NULL
| NULL | NULL | 53014 | 10.00 | Using where |+1 row in set, 7 warnings (0.00 sec)
②应使用正确类型
mysql> EXPLAIN SELECT * FROM customers2 WHERE id = '1001';+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | customers2 | NULL | const | PRIMARY | PRIMARY | 62 | const | 1 | 100.00 | NULL |+1 row in set, 1 warning (0.00 sec)
3)LIKE模糊查询
①前缀模糊查询导致索引失效
mysql> EXPLAIN SELECT * FROM customers WHERE name LIKE '%三';+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | customers | NULL | ALL | NULL | NULL | NULL | NULL | 49989 | 11.11 | Using where |+1 row
in set, 1 warning (0.00 sec)
②后缀模糊查询可以使用索引
mysql> EXPLAIN SELECT * FROM customers WHERE name LIKE '张%';+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | customers | NULL | range | idx_name_age_level | idx_name_age_level | 74 | NULL | 1 | 100.00 | Using index condition |+1 row in set, 1 warning (0.00 sec)
③覆盖索引优化前缀模糊查询
mysql> EXPLAIN SELECT name, age FROM customers WHERE name LIKE '%三';+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+| 1 | SIMPLE | customers | NULL | index | NULL | idx_name_age_level | 140 | NULL | 49989 | 11.11 | Using where; Using index |+1 row in set, 1 warning (0.00 sec)
MySQL执行计划是数据库性能优化的核心工具,通过系统性地分析执行计划,我们可以:
通过理解执行计划,我们能够将MySQL性能优化从"艺术"变为"科学",真正掌握数据库查询调优的核心能力。在执行计划分析的指导下,我们可以构建出高性能、高可用的数据库应用系统。