MySQL执行计划分析指南
一、概述
MySQL的执行计划(EXPLAIN)是一个强大的工具,用于分析查询语句的执行方式和性能特征。通过执行计划,我们可以了解MySQL如何处理SQL查询,从而识别并解决性能瓶颈。
官方文档链接:
- MySQL 5.7: https://dev.mysql.com/doc/refman/5.7/en/explain-output.html
- MySQL 8.0: https://dev.mysql.com/doc/refman/8.0/en/explain-output.html
执行计划提供的关键信息包括:
- 查询中表的读取顺序
- 数据访问操作类型
- 可用的索引
- 实际使用的索引
- 表之间的关联关系
- 优化器预估需要扫描的行数
版本演进
- MySQL 5.6.3之前,EXPLAIN仅支持SELECT语句;5.6.3之后扩展到支持UPDATE和DELETE语句
- 在MySQL 5.7之前,需要使用EXPLAIN PARTITIONS显示分区信息,使用EXPLAIN EXTENDED显示过滤信息;5.7版本后,EXPLAIN默认显示这些信息
基本语法
EXPLAIN SELECT select_options
或
DESCRIBE SELECT select_options
环境准备:
CREATE DATABASE performance_test CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE performance_test;
CREATE TABLE employees(id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100));
CREATE TABLE departments(id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100));
CREATE TABLE projects(id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100));
CREATE TABLE assignments(id INT PRIMARY KEY AUTO_INCREMENT, employee_id INT, project_id INT);
INSERT INTO employees(name) VALUES('Alice'),('Bob'),('Charlie'),('David'),('Eve');
INSERT INTO departments(name) VALUES('HR'),('IT'),('Finance'),('Marketing');
INSERT INTO projects(name) VALUES('Project A'),('Project B'),('Project C'),('Project D');
INSERT INTO assignments(employee_id, project_id) VALUES(1,1),(2,2),(3,3),(4,4),(1,2);
二、基本用法
执行计划的使用方法很简单:在SELECT语句前加上EXPLAIN或DESC关键字,即可获取该查询的执行计划信息。
EXPLAIN SELECT * FROM employees, departments WHERE employees.id = 1;
DESC SELECT * FROM employees, departments WHERE employees.id = 1;
| 字段 | 说明 |
|---|---|
| id | 查询标识符,表示SELECT操作的顺序 |
| select_type | 查询类型,如SIMPLE、PRIMARY、UNION等 |
| table | 正在访问的表名 |
| partitions | 匹配的分区信息,非分区表为NULL |
| type | 访问类型,从最佳到最差排序:system→const→eq_ref→ref→range→index→ALL |
| possible_keys | 可能使用的索引列表 |
| key | 实际使用的索引 |
| key_len | 使用的索引字节数 |
| ref | 索引列与哪个列或常量进行比较 |
| rows | 预估需要扫描的行数 |
| filtered | 根据查询条件过滤的行百分比 |
| Extra | 额外信息,如是否使用临时表、文件排序等 |
三、字段详解
3.1、id字段
id表示查询标识符,决定了SELECT操作的执行顺序。
- 当id值相同时,执行顺序从上到下
EXPLAIN SELECT * FROM employees e, departments d, projects p WHERE e.id = d.id AND d.id = p.id;
- 当id值不同时,表示存在子查询,id值越大,优先级越高,越先执行
EXPLAIN
SELECT *
FROM departments
WHERE id = (SELECT id
FROM employees
WHERE id = (SELECT project_id
FROM projects
WHERE name = 'Project A'));
3.2、select_type与table字段
select_type表示查询的类型,用于区分普通查询、联合查询、子查询等复杂查询场景。
- SIMPLE:简单的SELECT查询,不包含子查询或UNION
EXPLAIN SELECT * FROM employees;
- PRIMARY:最外层查询,包含复杂子部分
EXPLAIN
SELECT *
FROM departments
WHERE id = (SELECT id
FROM employees
WHERE id = (SELECT project_id
FROM projects
WHERE name = 'Project B'));
- SUBQUERY:在SELECT或WHERE列表中包含的子查询
EXPLAIN
SELECT *
FROM departments
WHERE id = (SELECT id
FROM employees
WHERE name = 'Alice');
- DERIVED:FROM列表中的子查询,MySQL会递归执行并放入临时表
- UNION:UNION中的第二个或后续SELECT
- UNION RESULT:UNION的结果集
EXPLAIN
SELECT *
FROM employees
UNION
SELECT *
FROM departments;
3.3、partitions字段
partitions显示查询匹配到的分区信息,对于非分区表值为NULL。
对于非分区表:
EXPLAIN SELECT * FROM employees WHERE id = 1;
创建分区表示例:
CREATE TABLE employees_partitioned (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100)
)
PARTITION BY RANGE (id) (
PARTITION p0 VALUES LESS THAN (2),
PARTITION p1 VALUES LESS THAN (4),
PARTITION p2 VALUES LESS THAN (6)
);
INSERT INTO employees_partitioned(name) VALUES('Alice'),('Bob'),('Charlie'),('David'),('Eve');
查询分区表时,partitions字段会显示命中分区:
EXPLAIN SELECT * FROM employees_partitioned WHERE id = 1;
EXPLAIN SELECT * FROM employees_partitioned WHERE id BETWEEN 1 AND 5;
3.4、type字段
type表示表的访问类型,是性能分析的重要指标。
访问类型从最佳到最差的排序:
system > const > eq_ref > ref > range > index > ALL
- SYSTEM:表仅有一行(系统表),极少出现
- CONST:通过索引一次找到,用于比较主键或唯一索引
EXPLAIN SELECT * FROM employees WHERE id = 1;
- EQ_REF:唯一性索引扫描,每个索引键只匹配一行
EXPLAIN SELECT * FROM employees e, departments d WHERE e.id = d.id;
- REF:非唯一性索引扫描,返回匹配某个单独值的所有行
-- 无索引
EXPLAIN SELECT * FROM employees, departments WHERE employees.name = departments.name;
-- 添加索引后
CREATE INDEX idx_name ON departments(name);
EXPLAIN SELECT * FROM employees, departments WHERE employees.name = departments.name;
- RANGE:检索给定范围的行,使用索引选择行
EXPLAIN SELECT * FROM employees WHERE id > 2;
EXPLAIN SELECT * FROM employees WHERE id IN (1,3);
- INDEX:使用了索引,但没有通过索引进行过滤,通常用于排序或分组
EXPLAIN SELECT * FROM employees ORDER BY id;
- ALL:完整表扫描,性能最差
EXPLAIN SELECT * FROM employees;
3.5、possible_keys与key字段
- possible_keys:可能应用到表上的索引列表,不一定实际使用
- key:实际使用的索引,NULL表示未使用索引
- 理论上没有使用索引,但实际上使用了(覆盖索引)
EXPLAIN SELECT id FROM employees;
- 理论和实际上都没有使用索引
EXPLAIN SELECT * FROM employees WHERE name = 'Alice';
- 理论和实际上都使用了索引
EXPLAIN SELECT * FROM departments WHERE name = 'HR';
3.6、key_len字段
key_len表示索引中使用的字节数,用于判断索引使用是否充分。
不同数据类型的key_len计算:
| 列类型 | 是否为空 | 长度 | key_len |
|---|---|---|---|
| tinyint | 允许NULL | 1 | 1 + 1 |
| tinyint not null | 不允许NULL | 1 | 1 |
| int | 允许NULL | 4 | 4 + 1 |
| int not null | 不允许NULL | 4 | 4 |
| char(10) | 允许NULL | utf8=3 | 10*3 + 2 + 1 |
| char(10) not null | 不允许NULL | utf8=3 | 10*3 + 2 |
| varchar(20) | 允许NULL | utf8=3 | 20*3 + 2 + 1 |
示例:
CREATE TABLE user_profile (
id bigint NOT NULL AUTO_INCREMENT,
name varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
age int NULL DEFAULT NULL,
sex char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (id) USING BTREE,
INDEX idx_name(name) USING BTREE,
INDEX idx_age(age) USING BTREE
);
EXPLAIN SELECT * FROM user_profile WHERE id = 1;
EXPLAIN SELECT * FROM user_profile WHERE name = 'Alice';
联合索引key_len计算:
ALTER TABLE user_profile DROP INDEX idx_name, DROP INDEX idx_age;
ALTER TABLE user_profile ADD INDEX idx_name_age(name, age);
- 部分索引生效
EXPLAIN SELECT * FROM user_profile WHERE name = 'Alice';
- 联合索引完全使用
EXPLAIN SELECT * FROM user_profile WHERE name = 'Alice' AND age = 25;
3.7、ref字段
ref表示索引的哪一列被使用,以及哪些列或常量用于查找索引列的值。
- 常量引用
EXPLAIN SELECT * FROM employees WHERE id = '1';
- 关联表引用
EXPLAIN SELECT * FROM employees e LEFT JOIN departments d ON e.id = d.id WHERE e.name = 'Alice';
驱动表概念: 在多表关联查询中,第一个被处理的表是驱动表,使用驱动表去关联其他表。驱动表的选择应遵循在不影响最终结果的前提下,优先选择结果集最小的表作为驱动表。
3.8、rows字段
rows表示MySQL根据统计信息和索引选用情况,估算的所需记录行数;越少越好。
- 使用LIKE查询,产生全表扫描
EXPLAIN SELECT * FROM employees WHERE name LIKE '%A%';
- 使用等值查询,直接找到记录
EXPLAIN SELECT * FROM employees WHERE name = 'Alice';
3.9、filtered字段
filtered表示返回结果的行占需要读到的行(rows列的值)的百分比。
3.10、extra字段
Extra显示MySQL查询过程中的额外信息。
- Using filesort:需要文件排序,性能较差
CREATE TABLE test_data (id INT, value VARCHAR(10));
INSERT INTO test_data VALUES(1, 'A'), (2, 'C'), (3, 'B');
EXPLAIN SELECT * FROM test_data ORDER BY value;
优化方案:在ORDER BY的列上添加索引。
- Using temporary:需要创建临时表,性能消耗大
EXPLAIN SELECT COUNT(*), name FROM employees GROUP BY name;
优化方案:确保WHERE和GROUP BY使用相同的索引字段。
- Using where:使用WHERE条件过滤数据
EXPLAIN SELECT * FROM employees WHERE age > 30;
注意:即使使用了WHERE条件,仍需配合type字段综合判断性能。
- Using index:使用覆盖索引,无需回表
CREATE INDEX idx_name ON employees(name);
EXPLAIN SELECT id, name FROM employees WHERE name = 'Alice';
- Using join buffer (Block Nested Loop):使用连接缓冲区执行嵌套循环连接
EXPLAIN SELECT *
FROM employees e1
LEFT JOIN
(SELECT * FROM employees WHERE department_id = 1) e2
ON e1.id = e2.id;
优化方案:在关联字段上添加索引。
- Using index condition:索引条件下推优化
CREATE TABLE products (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50),
category VARCHAR(50),
price DECIMAL(10, 2),
stock INT
);
INSERT INTO products (name, category, price, stock) VALUES
('Laptop', 'Electronics', 999.99, 10),
('Phone', 'Electronics', 699.99, 15),
('Desk', 'Furniture', 199.99, 5),
('Chair', 'Furniture', 89.99, 20);
CREATE INDEX idx_category_price ON products(category, price);
EXPLAIN SELECT * FROM products WHERE category = 'Electronics' AND price > 500 AND stock > 10;
这个例子展示了Index Condition Pushdown (ICP)优化,MySQL在存储引擎层面应用部分WHERE条件,减少需要从表中读取的行数。
总结:通过分析EXPLAIN执行计划,我们可以了解SQL查询的执行方式,识别性能瓶颈,并针对性地进行优化。重点关注表的访问类型(type)、实际使用的索引(key)以及额外信息(extra),这些是判断查询性能的关键指标。