系列文章目录
一、MySQL数据结构选择
二、MySQL性能优化explain关键字详解
三、MySQL索引优化
文章目录
- 系列文章目录
- 一、explain是什么?
- 二、explain字段详解
- 2.1、ID
- 2.2、select_type
- 2.3、table
- 2.4、partitions
- 2.5、type(重点)
- 2.6、key
- 2.7、key_len
- 2.8、ref
- 2.9、rows
- 三、索引失效解析
- 3.1、索引失效场景一:在索引列上进行计算
- 3.2、索引失效场景二:使用索引中范围条件右边的列
- 3.3、索引失效场景三:使用< 、> 、 <=、>=、!=
- 3.4、索引失效场景四:使用'%字段'或'%字段%'
- 3.5、索引失效场景五:字符串未加单引号
一、explain是什么?
EXPLAIN 关键字在MySQL中,主要用于分析查询语句的性能,以及执行过程:
explain select * from actor;
通过explain select 语句,可以得到一个结果集。
二、explain字段详解
以下面的三张表为例:
-- 删除并重新创建 actor 表
DROP TABLE IF EXISTS `actor`;
CREATE TABLE `actor` (`id` int(11) NOT NULL,`name` varchar(45) DEFAULT NULL,`update_time` datetime DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- 插入数据到 actor 表
INSERT INTO `actor` (`id`, `name`, `update_time`)
VALUES (1, 'a', '2017-12-22 15:27:18'), (2, 'b', '2017-12-22 00:00:00');-- 删除并重新创建 film 表
DROP TABLE IF EXISTS `film`;
CREATE TABLE `film` (`id` int(11) NOT NULL AUTO_INCREMENT,`name` varchar(10) DEFAULT NULL,PRIMARY KEY (`id`),KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- 插入数据到 film 表
INSERT INTO `film` (`id`, `name`)
VALUES (3, 'film1'),(1, 'film1'),(2, 'film2');-- 删除并重新创建 film_actor 表
DROP TABLE IF EXISTS `film_actor`;
CREATE TABLE `film_actor` (`id` int(11) NOT NULL AUTO_INCREMENT,`film_id` int(11) NOT NULL,`actor_id` int(11) NOT NULL,`remark` varchar(255) DEFAULT NULL,PRIMARY KEY (`id`),KEY `idx_film_actor_id` (`film_id`, `actor_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2.1、ID
查询的标识符。每个查询(尤其是多表连接查询)都有一个唯一的 id,表示查询的执行顺序。id越大,优先级越高,如果id相同,则按照先后顺序。
若是简单查询,id 只会有一个值。
2.2、select_type
查询类型。表示查询的不同组成部分,例如简单查询、联合查询、子查询等。其常见的值:
- SIMPLE:简单查询,没有使用子查询。
explain select * from actor;
- PRIMARY:复杂查询中最外层的查询。
- SUBQUERY:子查询中的查询。
- DERIVED:派生表(即在 FROM 子句中嵌套的查询)。
explain select (select 1 from actor where id = 1) from (select * from film where id = 1) der
即上面的sql语句,依照优先级,最先执行的一定是from后面的子句,因为前面的查询条件都是依赖此。from后的子句即是DERIVED
类型。然后执行的是最外层select后的子查询,因为最外层select的目标字段即是子查询的结果,为SUBQUERY
类型。最后执行的是最外层的select语句,为PRIMARY
类型。
- UNION:联合查询的第二个及以后的部分。
- UNION ALL:与 UNION 类似,但不去重结果。
explain select 1 union all select 1;
2.3、table
正在访问的表的名称。如果是联合查询或者多表查询,table 会列出每个表。当 from 子句中有子查询时,table列是 格式,表示当前查询依赖 id=N 的查询,于是先执行 id=N 的查询。
2.4、partitions
如果表使用了分区,这一列会显示正在扫描的分区。分区是将表分成多个物理部分,以便更高效地管理和查询。注意,实际开发中很少用到分区,而是使用分库分表。
2.5、type(重点)
连接类型,表示 MySQL 执行查询时访问表的方式。连接类型是评估查询效率的重要指标,性能越差的连接类型排序越靠后,是explain中的关键信息,sql优化正是基于此字段的值:
- ALL:全表扫描,表示没有使用索引,性能最差。
explain select * from actor
actor表只有一个id主键索引
,*中不仅包含了id,还有其他两个字段,由于 actor 表的其他字段没有索引,查询需要扫描整个表来返回所有记录。
- index:索引扫描,扫描整个索引,但不需要访问表中的数据行。
explain select * from film;
- range:范围扫描,扫描索引中的某个范围(如 BETWEEN 或 > 操作符)。
explain select * from actor where id > 1;
- ref:通过非唯一索引查找匹配的记录。
explain select * from film where name = 'film1';
- eq_ref:通过唯一索引查找记录。对于每个表的记录,都只返回一行。
explain select * from film_actor left join film on film_actor.film_id = film.id;
- const:常量表访问,通常是常量查询,只有一行结果。非常高效。
explain SELECT * FROM actor WHERE id = 1 AND name = 'a';
- system:单行表扫描,类似于 const,但表大小为 1 行。
2.6、key
实际使用的索引。这个字段告诉你在查询执行过程中,MySQL 实际上选择了哪个索引。如果值为 NULL,表示没有使用索引,可能导致全表扫描。
2.7、key_len
表示使用的索引的长度。这个值帮助判断 MySQL 是否使用了索引的全部部分。越大的值表示索引的使用越完整。
2.8、ref
显示查询中索引列与常量值、其他表的列或表达式的匹配方式。通常是通过列与列、列与常量匹配。
- const:表示索引查找的常量值。
- func:通过函数操作进行的匹配。
- table_name.column_name:表示某个表的列与另一个表的列进行比较。
2.9、rows
表示 MySQL 估算查询需要扫描的行数。这个数字是估算值,表示为了找到查询结果,MySQL 需要扫描的行数。(非准确值)
此外还有filtered:表示查询在扫描过程中应用了多少过滤器,通常是以百分比的形式表示。例如,50 表示查询中 50% 的行被筛选掉。extra也是一个关键的指标,常见的如下所示:
- Using index:表示查询是通过索引访问的,且没有回表。
explain SELECT id, name FROM film
- Using where:表示使用了 WHERE 子句进行额外的过滤,
并且查询的列未被索引覆盖
:
explain select * from actor where name = 'a';
- Using temporary:表示 MySQL 创建了临时表来处理查询。
explain select distinct name from actor;
- Using filesort:表示 MySQL 在查询结果上进行了
文件排序
:
explain select * from actor order by name;
- no matching row in const table:表示 WHERE 子句的条件永远无法满足,查询会返回空结果。
explain select * from film where id = 10000
三、索引失效解析
首先我们创建了一张表,表中有一个id主键索引,以及name,age,position组成的联合索引:
CREATE TABLE `employees` (`id` int(11) NOT NULL AUTO_INCREMENT,`name` varchar(24) NOT NULL DEFAULT '' COMMENT '姓名',`age` int(11) NOT NULL DEFAULT '0' COMMENT '年龄',`position` varchar(20) NOT NULL DEFAULT '' COMMENT '职位',`hire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间',PRIMARY KEY (`id`),KEY `idx_name_age_position` (`name`, `age`, `position`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='员工记录表';INSERT INTO employees(name, age, position, hire_time) VALUES('LiLei', 22, 'manager', NOW());
INSERT INTO employees(name, age, position, hire_time) VALUES('HanMeimei', 23, 'dev', NOW());
INSERT INTO employees(name, age, position, hire_time) VALUES('Lucy', 23, 'dev', NOW());
联合索引在B+树中的结构大致如下:
导致联合索引失效的场景,最主要的是没有遵守最左前缀法则
。为何跳过中间列会导致索引失效?还是回到前篇中的那句话,索引是帮助MySQL高效获取数据的排好序的数据结构
。如果跳过中间某一列,那后续的字段必然是没有排序完成的。联合索引中,后列的排序是依赖前列的排序结果的。除此之外,还有一些常见的索引失效场景:
3.1、索引失效场景一:在索引列上进行计算
例如:
EXPLAIN SELECT * FROM employees WHERE left(name,3) = 'LiLei';
如果对索引列进行函数计算,例如截取,转换,也就是索引字段发生了改变,则在B+树的数据结构中就无法匹配,导致索引失效。也就是索引依赖于列的原始值
。并且如果在查询中使用了算术计算或函数(如 +, -, *, DIV, YEAR(), DATE() 等),则 MySQL 必须对每一行数据进行计算
,才能与查询条件进行匹配,因为函数使初始B+树维护的索引中的值/类型发生了改变。这使得索引失去了作用,因为索引的目的是避免访问表中的每一行
,快速定位到符合条件的行。如果需要计算索引列的值,MySQL就无法直接使用索引,而是需要扫描所有数据行。
3.2、索引失效场景二:使用索引中范围条件右边的列
例如:
EXPLAIN SELECT * FROM employees WHERE name= 'LiLei' AND age > 22 AND position ='manager';
虽然没有违背最左前缀法则
,但是范围查询放在了中间的位置,也是导致了position 索引的失效(观察key_len字段的值,如果联合索引生效,则应该为140),在执行这条sql时:
- MySQL 会先根据 name = ‘LiLei’ 来定位到符合的记录(如果 name 上有索引)。
- 然后,它会应用 age > 22 条件,这时会扫描符合 name = ‘LiLei’ 条件的所有记录,并对 age 列进行范围筛选。
- 由于 position = ‘manager’ 是等值查询,并且出现在范围条件之后,它无法在 索引扫描阶段 利用 position 列。
因为一旦范围条件被处理,索引的扫描只能停止在范围条件的位置,因此后面的列(如 position)的条件必须通过其他方式处理,可能会导致索引的部分失效或者需要回表扫描。
3.3、索引失效场景三:使用< 、> 、 <=、>=、!=
例如:
EXPLAIN SELECT * FROM employees WHERE name != 'LiLei';
但是此条并非绝对,mysql内部优化器会根据检索比例、表大小等多个因素整体评估是否使用索引。
3.4、索引失效场景四:使用’%字段’或’%字段%’
例如:
EXPLAIN SELECT * FROM employees WHERE name like '%Lei'
原因在于,在B+树中,索引依赖于从左到右的顺序匹配
,% 表示任意数量的
字符,MySQL 无法利用索引从开始位置快速定位到符合条件的数据行,而是必须逐行扫描表中的每一行以进行模糊匹配。
为什么使用’字段%'索引不会失效?因为最左边的部分已经确定了。
3.5、索引失效场景五:字符串未加单引号
例如:
SELECT * FROM employees WHERE name = John;
在 SQL 中,字符串 值必须被 单引号包围。这是 SQL 的语法要求,用来明确区分字符串
和列名
或其他数据类型
。如果不加单引号,MySQL 会尝试将它们解析为列名、表名或其他可能的数据类型。