各位朋友,大家好!我是今天的主讲人,咱们今天聊聊MySQL的EXPLAIN,这玩意儿就像医生的CT扫描,能帮咱们诊断SQL语句的健康状况,找出潜在的性能问题。别看它输出的东西密密麻麻的,掌握了它,优化SQL就像开了上帝视角,指哪儿打哪儿!
一、 EXPLAIN 是个啥?
EXPLAIN 语句是MySQL提供的一个非常有用的工具,它可以显示MySQL如何执行一条SQL语句。通过分析EXPLAIN的输出结果,我们可以了解MySQL优化器是如何工作的,并根据这些信息来优化SQL语句,从而提高查询性能。简单来说,就是让MySQL自己“坦白”,告诉我们它打算怎么干活,我们再看看它干活的姿势对不对。
二、 怎么用 EXPLAIN?
使用方法很简单,在你想要分析的SQL语句前面加上 EXPLAIN 即可。例如:
EXPLAIN SELECT * FROM users WHERE age > 25 AND city = 'Beijing';
执行这条语句后,MySQL会返回一个表格,里面包含了关于查询执行计划的各种信息。
三、 EXPLAIN 输出结果详解
EXPLAIN 的输出结果包含多个列,每个列都代表了查询执行过程中的一个方面。下面我们逐个分析这些列的含义。
| 列名 | 含义 | 可能的值 | 优化建议 |
|---|---|---|---|
id |
查询的标识符。如果查询中包含子查询,则每个子查询都会有一个唯一的 id。id 值越大,查询的优先级越高。如果 id 相同,则执行顺序从上到下。 |
正整数,NULL (用于 UNION RESULT) |
无 |
select_type |
查询的类型。它描述了查询中每个 SELECT 子句的类型。 |
SIMPLE, PRIMARY, SUBQUERY, DERIVED, UNION, UNION RESULT, DEPENDENT SUBQUERY, DEPENDENT UNION, MATERIALIZED |
尽量避免复杂的子查询,可以将子查询转换为 JOIN 操作。 |
table |
查询涉及的表名。如果查询涉及多个表,则会显示多个表名。 | 表名,别名,<derivedN> (表示 id 为 N 的查询结果),<unionM,N> (表示 id 为 M 和 N 的查询结果的 UNION) |
检查表名是否正确,是否使用了别名。 |
partitions |
查询涉及的分区。如果表是分区表,则会显示查询涉及的分区。 | 分区名,NULL (如果表不是分区表) |
如果查询没有使用分区,可以考虑添加分区。 |
type |
访问类型。这是最重要的列之一,它描述了MySQL如何查找表中的行。值越好,查询性能越高。 | system, const, eq_ref, ref, fulltext, ref_or_null, index_merge, unique_subquery, index_subquery, range, index, ALL |
尽量将 type 优化到 range 或更好。避免 ALL (全表扫描) 和 index (全索引扫描)。 |
possible_keys |
MySQL可以使用哪些索引来查找行。 | 索引名,NULL (如果没有可用的索引) |
如果 possible_keys 有值,但 key 为 NULL,说明MySQL认为使用索引不如全表扫描。可以尝试强制使用索引。 |
key |
MySQL实际选择使用的索引。 | 索引名,NULL (如果没有使用索引) |
如果 key 为 NULL,说明MySQL没有使用索引。可以考虑添加索引或优化查询条件。 |
key_len |
索引的长度,表示MySQL在索引里使用的字节数。通过这个值,可以计算具体使用了索引中的哪些列。 | 整数 | 了解索引长度有助于判断是否充分利用了索引。如果只使用了索引的一部分,可以考虑创建更合适的索引。 |
ref |
显示索引的哪一列被使用了。如果可能的话,是一个常数。 | 列名,const (表示常量),NULL |
检查 ref 列是否正确,是否使用了常量或列名。 |
rows |
MySQL估计需要扫描的行数。这个值只是一个估计值,并不一定准确。 | 整数 | 尽量减少 rows 的值,可以通过添加索引、优化查询条件等方式实现。 |
filtered |
表示返回结果的行数占需要读取行数的百分比。filtered 的值越大越好。 |
0 到 100 的整数 | 如果 filtered 的值很小,说明查询条件过滤性较差,可以考虑优化查询条件。 |
Extra |
包含MySQL解决查询的额外信息。 | 各种字符串,如 Using index, Using where, Using temporary, Using filesort, Using join buffer (Block Nested Loop), Impossible WHERE noticed after reading const tables, No tables used, Not exists |
Extra 列的信息非常重要,可以帮助我们了解查询的瓶颈。 |
下面我们来详细解释几个重要的列:
-
type(访问类型):system: 表只有一行记录(等于系统表),这是 const 类型的一个特例,平时不会出现,可以忽略不计。const: 表示通过索引一次就找到了,const用于比较primary key或者unique索引。因为只匹配一行数据,所以很快。例如:SELECT * FROM users WHERE id = 1;(假设id是PRIMARY KEY或者UNIQUE索引)。eq_ref: 唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见于主键或唯一索引的关联查询。例如:SELECT * FROM orders o JOIN users u ON o.user_id = u.id;(假设u.id是PRIMARY KEY)。ref: 非唯一性索引扫描,返回匹配某个单独值的所有行。例如:SELECT * FROM users WHERE city = 'Beijing';(假设city是一个普通索引)。range: 只检索给定范围的行,使用一个索引来选择行。key列显示使用了哪个索引,一般就是在你的WHERE语句中出现了BETWEEN、<、>、IN等的查询。例如:SELECT * FROM users WHERE age BETWEEN 20 AND 30;(假设age是索引)。index: 全索引扫描,index与ALL的区别为index类型只遍历索引树。通常比ALL快,因为索引文件通常比数据文件小。ALL: 全表扫描,MySQL 必须扫描整张表才能找到所需的行。这是最差的一种情况,应该尽量避免。
-
Extra(额外信息):Using index: 表示查询使用了覆盖索引,避免了回表查询,性能很高。覆盖索引是指查询的字段都在索引中,不需要再到数据表中查找数据。例如:SELECT id, name FROM users WHERE city = 'Beijing';(假设(city, id, name)是一个联合索引)。Using where: 表示MySQL服务器在存储引擎收到行后进行“后过滤”(Post-filter)。这意味着 MySQL 使用WHERE子句来过滤结果集。这通常发生在没有合适的索引可以使用的情况下。Using temporary: 表示MySQL需要使用临时表来存储结果集,常见于排序和分组查询。这通常是一个性能瓶颈,应该尽量避免。例如:SELECT city, COUNT(*) FROM users GROUP BY city;(如果city没有索引)。Using filesort: 表示MySQL需要对结果集进行外部排序,而不是使用索引排序。这通常是一个性能瓶颈,应该尽量避免。例如:SELECT * FROM users ORDER BY age;(如果age没有索引)。Using join buffer (Block Nested Loop): 使用连接缓存来加速JOIN操作。 这通常发生在JOIN的驱动表没有合适的索引可以使用的情况下。
四、 优化案例分析
下面我们通过几个案例来演示如何使用 EXPLAIN 来优化SQL语句。
案例 1: 全表扫描 (ALL)
假设我们有以下SQL语句:
SELECT * FROM orders WHERE order_date = '2023-10-26';
执行 EXPLAIN 后,我们发现 type 是 ALL,表示全表扫描。这意味着MySQL需要扫描整个 orders 表才能找到符合条件的行。
EXPLAIN SELECT * FROM orders WHERE order_date = '2023-10-26';
输出结果(简化):
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
|----|-------------|--------|------|---------------|------|---------|------|-------|-------|
| 1 | SIMPLE | orders | ALL | NULL | NULL | NULL | NULL | 10000 | Using where |
优化方案:
在 order_date 列上创建索引:
CREATE INDEX idx_order_date ON orders (order_date);
再次执行 EXPLAIN,我们发现 type 变成了 ref,表示使用了索引,性能大大提高。
EXPLAIN SELECT * FROM orders WHERE order_date = '2023-10-26';
输出结果(简化):
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
|----|-------------|--------|------|---------------|----------------|---------|-------|------|-------|
| 1 | SIMPLE | orders | ref | idx_order_date | idx_order_date | 3 | const | 100 | NULL |
案例 2: 使用临时表和文件排序 (Using temporary, Using filesort)
假设我们有以下SQL语句:
SELECT city, AVG(age) FROM users GROUP BY city ORDER BY AVG(age) DESC;
执行 EXPLAIN 后,我们发现 Extra 列包含 Using temporary 和 Using filesort,表示使用了临时表和文件排序。这意味着MySQL需要先创建一个临时表来存储分组结果,然后再对临时表进行排序,性能很差。
EXPLAIN SELECT city, AVG(age) FROM users GROUP BY city ORDER BY AVG(age) DESC;
输出结果(简化):
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
|----|-------------|-------|------|---------------|------|---------|-----|------|-------------------------------------------|
| 1 | SIMPLE | users | ALL | NULL | NULL | NULL | | 1000 | Using temporary; Using filesort |
优化方案:
创建覆盖索引,包含 city 和 age 列:
CREATE INDEX idx_city_age ON users (city, age);
再次执行 EXPLAIN,我们发现 Extra 列只包含 Using index,表示使用了覆盖索引,避免了临时表和文件排序,性能大大提高。但是,Using index 只是避免了回表,Using temporary 和 Using filesort 仍然存在。我们需要更进一步的优化。
为了避免 Using temporary 和 Using filesort,我们需要让MySQL可以直接使用索引进行排序和分组。这需要修改SQL语句,并创建一个更合适的索引。
ALTER TABLE users ADD COLUMN avg_age DECIMAL(10,2) GENERATED ALWAYS AS ( (SELECT AVG(age) FROM users AS u2 WHERE u2.city = users.city) ) STORED;
CREATE INDEX idx_city_avg_age ON users (city, avg_age);
EXPLAIN SELECT city, avg_age FROM users GROUP BY city ORDER BY avg_age DESC;
由于MySQL的限制,直接在GROUP BY和ORDER BY中使用函数计算的值通常很难优化,所以我们这里使用生成列(Generated Column)预先计算好平均年龄,并创建一个基于city和avg_age的索引。这样,查询就可以直接使用索引进行分组和排序,避免了临时表和文件排序。 (注意: 实际生产环境中生成列需要谨慎评估对写入性能的影响)
案例 3: 子查询 (SUBQUERY, DEPENDENT SUBQUERY)
假设我们有以下SQL语句:
SELECT * FROM orders WHERE user_id IN (SELECT id FROM users WHERE city = 'Beijing');
执行 EXPLAIN 后,我们发现 select_type 包含 SUBQUERY 或 DEPENDENT SUBQUERY,表示使用了子查询。子查询的性能通常比较差,特别是当子查询的结果集很大时。
EXPLAIN SELECT * FROM orders WHERE user_id IN (SELECT id FROM users WHERE city = 'Beijing');
输出结果(简化):
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
|----|--------------------|--------|------|---------------|------|---------|------|------|-------------|
| 1 | PRIMARY | orders | ALL | user_id | NULL | NULL | NULL | 1000 | Using where |
| 2 | DEPENDENT SUBQUERY | users | ref | idx_city | idx_city | 152 | const| 100 | Using index |
优化方案:
将子查询转换为 JOIN 操作:
SELECT o.* FROM orders o JOIN users u ON o.user_id = u.id WHERE u.city = 'Beijing';
再次执行 EXPLAIN,我们发现 select_type 变成了 SIMPLE,表示使用了 JOIN 操作,性能大大提高。
EXPLAIN SELECT o.* FROM orders o JOIN users u ON o.user_id = u.id WHERE u.city = 'Beijing';
输出结果(简化):
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
|----|-------------|--------|------|---------------|----------|---------|-----------------|------|-------------|
| 1 | SIMPLE | u | ref | idx_city | idx_city | 152 | const | 100 | Using index |
| 1 | SIMPLE | o | ref | user_id | user_id | 4 | u.id | 10 | Using where |
五、 总结
EXPLAIN 是一个强大的工具,可以帮助我们了解MySQL如何执行SQL语句,并找出潜在的性能问题。通过分析EXPLAIN的输出结果,我们可以根据实际情况进行优化,例如添加索引、优化查询条件、避免子查询、避免临时表和文件排序等,从而提高查询性能。
记住,优化SQL是一个持续的过程,需要不断地学习和实践。 掌握 EXPLAIN 只是第一步,更重要的是理解MySQL的内部工作原理,才能写出高效的SQL语句。
希望今天的分享对大家有所帮助! 咱们下次再见!