0
点赞
收藏
分享

微信扫一扫

05_MySQL索引优化

白衣蓝剑冰魄 2023-06-09 阅读 99

1. 性能分析(explain)

1.1 explain是什么?

模拟优化器查看执行计划

1.2 explain能干什么?

1.3 explain怎么玩?

官方文档:MySQL :: MySQL 8.0 Reference Manual :: 8.8.2 EXPLAIN Output Format

数据准备:

CREATE TABLE t1(id INT(10) AUTO_INCREMENT, content VARCHAR(100) NULL, PRIMARY KEY (id));
CREATE TABLE t2(id INT(10) AUTO_INCREMENT, content VARCHAR(100) NULL, PRIMARY KEY (id));
CREATE TABLE t3(id INT(10) AUTO_INCREMENT, content VARCHAR(100) NULL, PRIMARY KEY (id));
CREATE TABLE t4(id INT(10) AUTO_INCREMENT, content VARCHAR(100) NULL, PRIMARY KEY (id));

# 以下新增sql多执行几次,以便演示
INSERT INTO t1(content) VALUES(CONCAT('t1_',FLOOR(1+RAND()*1000)));
INSERT INTO t2(content) VALUES(CONCAT('t2_',FLOOR(1+RAND()*1000)));
INSERT INTO t3(content) VALUES(CONCAT('t3_',FLOOR(1+RAND()*1000)));
INSERT INTO t4(content) VALUES(CONCAT('t4_',FLOOR(1+RAND()*1000)));

1.4 各字段解释

1.4.1 id(重要)

三种情况:

① id相同,执行顺序由上至下。例如上图

② id不同,如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行

EXPLAIN SELECT * 
FROM t1 WHERE t1.content =(
	SELECT t2.content
	FROM t2 WHERE t2.content=(
		SELECT t3.content
		FROM t3
		WHERE t3.content=""
	)
);

③  id既有相同又有不同

1.4.2 select_type(不会用于优化)

SIMPLE

简单的 SELECT(没有 使用UNION或者 子查询(PS:单表查询))

PRIMARY

最外层的Select 作为primary 查询。(PS:含有子查询的情况,但是并不复杂)案例1

DERIVED

在from 查询语句中的(派生,嵌套很多)子查询.(PS:递归操作这些子查询)

SUBQUERY

在SELECT或WHERE列表中包含了子查询。案例2

DEPENDENT SUBQUERY

第一个查询是子查询,依赖于外部查询(相关查询)。案例3

MATERIALIZED

在非相关子查询中 并且需要进行物化时会出现MATERIALIZED关键词。案例4

UNCACHEABLE SUBQUERY

子查询结果(系统变量)不能被缓存, 而且必须重写(分析)外部查询的每一行。案例5

UNION

若第二个SELECT出现在UNION之后,则被标记为UNION。案例6

若UNION包含在FROM子句的子查询中,外层SELECT将被标记为:DERIVED

UNION RESULT

结果集是通过union 而来的。案例6

案例1:explain select * from (select t2.* from t2) s2

案例2:explain select * from t1 where t1.id=(select t2.id from t2 where t2.id=1)

案例3:explain select t1.*,(select t2.content from t2 where t2.id=t1.id) from t1 where t1.id=1;

案例4:explain select * from t1 where t1.content in (select t2.content from t2 where t2.content in (select t3.content from t3 where t3.content='')) #需要5.7以后版本演示

案例5:EXPLAIN SELECT * from t1 where t1.id=(select t2.id from t2 where t2.id=@@sort_buffer_size);

案例6:explain select * from t1 UNION select * from t2;

1.4.3 table

1.4.3 partitions

1.4.5 type(重要)

一般来说,得保证查询至少达到range级别,最好能达到ref。

① 创建索引

② index_merge

EXPLAIN SELECT * FROM t3 WHERE t3.content IS NULL OR t3.id=10;

③ ref_or_null

EXPLAIN SELECT * FROM t3 WHERE t3.content IS NULL OR t3.content='aaaa';

④ index_subquery

EXPLAIN SELECT * FROM t2 WHERE t2.content IN (SELECT t3.content FROM t3);

⑤ unique_subquery

EXPLAIN SELECT * FROM t2 WHERE t2.id IN (SELECT t3.id FROM t3);

1.4.6 possible_keys

1.4.7 key(优化重要指标)

1.4.8 key_len(重要)

如何计算

索引字段最好不要为NULL,因为NULL让统计更加复杂,并且需要额外一个字节的存储空间。

1.4.9 ref

1.4.10 rows

1.4.11 filtered

1.4.12 extra(重要)

包含不适合在其他列中显示但十分重要的额外信息,通过这些额外信息来理解MySQL到底将如何执行当前的查询语句。MySQL提供的额外信息有好几十个,这里只挑比较重要的介绍。

Using filesort:说明mysql会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取。

MySQL中无法利用索引完成的排序操作称为“文件排序”

这类SQL语句性能极差,需要进行优化。

在一个非索引列上进行了order by,就会触发filesort,常见的优化方案是,在order by的列上添加索引,避免每次查询都全量排序(只查询索引列的值)。

Using temporary:使了用临时表保存中间结果,MySQL在对查询结果排序时使用临时表。常见于排序 order by 和分组查询 group by。

group by和order by同时存在,且作用于不同的字段时,就会建立临时表,以便计算出最终的结果集。

USING index:利用索引进行了排序或分组。表示相应的select操作中使用了覆盖索引(Covering Index),避免访问了表的数据行,效率不错!(EXPLAIN select * from t_emp where age=30 ORDER BY name)如果同时出现using where,表明索引被用来执行索引键值的查找;如果没有同时出现using where,表明索引只是用来读取数据而非利用索引执行查找。

Using where:表明使用了where过滤

using join buffer:使用了连接缓存,非主键关联(mysql8Using join buffer (hash join) 速度要好于 mysql5.7Using join buffer (Block Nested Loop) )

impossible where:where子句的值总是false,不能用来获取任何元组。(EXPLAIN select * from t_emp where false;)

select tables optimized away:在没有GROUPBY子句的情况下,基于索引优化MIN/MAX操作或者对于MyISAM存储引擎优化COUNT(*)操作,不必等到执行阶段再进行计算,查询执行计划生成的阶段即完成优化。

在innodb中:

在Myisam中:

1.5 小结

表的读取顺序:id

数据读取操作的操作类型:type

那些索引被实际使用:key

使用索引的长度:key_len

表之间的引用:table

每张表有多少行被物理查询:rows

额外优化信息:extra

1.6 Json格式的执行计划

EXPLAIN语句输出中缺少了一个衡量执行计划好坏的重要属性 —执行计划花费的成本,在EXPLAIN单词和真正的查询语句中间加上FORMAT=JSON。

EXPLAIN FORMAT=json SELECT * FROM t_emp;
{
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "1.25" // 查询耗时:单位毫秒
    },
    "table": {
      "table_name": "t_emp",
      "access_type": "ALL",
      "rows_examined_per_scan": 10,
      "rows_produced_per_join": 10,
      "filtered": "100.00",
      "cost_info": {
        "read_cost": "0.25",//io耗时
        "eval_cost": "1.00",//获取处理返回结果耗时
        "prefix_cost": "1.25",
        "data_read_per_join": "800"//读取的数据量
      },
      "used_columns": [//投影列
        "id",
        "name",
        "age",
        "deptId",
        "empno"
      ]
    }
  }
}

2. 数据准备

在做优化之前,要准备大量数据。接下来创建两张表,并往员工表里插入50W数据,部门表中插入1W条数据。

建表sql:

CREATE TABLE `dept` (
	`id` INT(11) NOT NULL AUTO_INCREMENT,
	`deptName` VARCHAR(30) DEFAULT NULL,
	`address` VARCHAR(40) DEFAULT NULL,
	ceo INT NULL ,
	PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

CREATE TABLE `emp` (
	`id` INT(11) NOT NULL AUTO_INCREMENT,
	`empno` INT NOT NULL ,
	`name` VARCHAR(20) DEFAULT NULL,
	`age` INT(3) DEFAULT NULL,
	`deptId` INT(11) DEFAULT NULL,
	PRIMARY KEY (`id`)
	#CONSTRAINT `fk_dept_id` FOREIGN KEY (`deptId`) REFERENCES `t_dept` (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

怎么快速插入50w条数据呢? 存储过程

怎么保证插入的数据不重复?函数

以部门表分析:

以员工表分析:

总结:需要产生随机字符串和区间随机数的函数。

2.1 创建函数

set global log_bin_trust_function_creators=1; 
# 随机产生字符串
DELIMITER $$
CREATE FUNCTION rand_string(n INT) RETURNS VARCHAR(255)
BEGIN    
	DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
	DECLARE return_str VARCHAR(255) DEFAULT '';
	DECLARE i INT DEFAULT 0;
	WHILE i < n DO  
		SET return_str =CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1));  
		SET i = i + 1;
	END WHILE;
	RETURN return_str;
END $$

#用于随机产生区间数字
DELIMITER $$
CREATE FUNCTION rand_num (from_num INT ,to_num INT) RETURNS INT(11)
BEGIN   
 DECLARE i INT DEFAULT 0;  
 SET i = FLOOR(from_num +RAND()*(to_num -from_num+1));
RETURN i;  
END$$

#假如要删除
#drop function rand_string;
#drop function rand_num;

2.2 存储过程

# 插入员工存储过程
DELIMITER $$
CREATE PROCEDURE  insert_emp(START INT, max_num INT)
BEGIN  
	DECLARE i INT DEFAULT 0;   
	#set autocommit =0 把autocommit设置成0  
	SET autocommit = 0;    
	REPEAT  
		SET i = i + 1;  
		INSERT INTO emp (empno, NAME, age, deptid ) VALUES ((START+i) ,rand_string(6), rand_num(30,50), rand_num(1,10000));  
		UNTIL i = max_num  
	END REPEAT;  
	COMMIT;  
END$$
 
#删除
# DELIMITER ;
# drop PROCEDURE insert_emp;

 
#往dept表添加随机数据
DELIMITER $$
CREATE PROCEDURE `insert_dept`(max_num INT)
BEGIN  
	DECLARE i INT DEFAULT 0;   
	SET autocommit = 0;    
	REPEAT  
		SET i = i + 1;  
		INSERT INTO dept ( deptname,address,ceo ) VALUES (rand_string(8),rand_string(10),rand_num(1,500000));  
		UNTIL i = max_num  
	END REPEAT;  
	COMMIT;  
END$$
 
#删除
# DELIMITER ;
# drop PROCEDURE insert_dept;

2.3 调用存储过程

#执行存储过程,往dept表添加1万条数据
DELIMITER ;
CALL insert_dept(10000); 

#执行存储过程,往emp表添加50万条数据
DELIMITER ;
CALL insert_emp(100000,500000); 

2.4 批量删除表索引

批量删除某个表上的所有索引

DELIMITER $$
CREATE PROCEDURE `proc_drop_index`(dbname VARCHAR(200),tablename VARCHAR(200))
BEGIN
	DECLARE done INT DEFAULT 0;
	DECLARE ct INT DEFAULT 0;
	DECLARE _index VARCHAR(200) DEFAULT '';
	DECLARE _cur CURSOR FOR SELECT index_name FROM information_schema.STATISTICS WHERE table_schema=dbname AND table_name=tablename AND seq_in_index=1 AND index_name <>'PRIMARY'  ;
	DECLARE CONTINUE HANDLER FOR NOT FOUND set done=2 ;      
	OPEN _cur;
		FETCH _cur INTO _index;
		WHILE  _index<>'' DO 
			SET @str = CONCAT("drop index ",_index," on ",tablename ); 
			PREPARE sql_str FROM @str ;
			EXECUTE sql_str;
			DEALLOCATE PREPARE sql_str;
			SET _index=''; 
			FETCH _cur INTO _index; 
		END WHILE;
	CLOSE _cur;
END$$

执行批量删除:  

CALL proc_drop_index("dbname","tablename"); # 库名称和表名称

3. 单表优化

3.1 索引优化原则

案例:

① 以下两个sql,那个写法更好?

# 创建索引
create index idx_emp_age on emp(age);
create index idx_emp_name on emp(name);

EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE emp.name LIKE 'abc%';
EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE LEFT(emp.name,3)='abc';

② 把第一个sql的like查询条件改成‘%abc%’,会怎样呢?  

③ 再来看这两个sql:不等于(!=或者<>)

④ is not null和is null

⑤ 字符串加引号

3.2 组合索引原则

① 首先删除之前创建的索引

CALL proc_drop_index("mydb","emp");

② 全值匹配我最爱  

③最左匹配原则

④ OR关联

⑤ 范围条件右边的列

3.3 小结

一般性建议:

假设index(a,b,c) 重要

Where语句索引是否被使用
where a = 3Y,使用到a
where a = 3 and b = 5Y,使用到a,b
where a = 3 and b = 5 and c = 4Y,使用到a,b,c
where b = 3 或者 where b = 3 and c = 4 或者 where c = 4N
where a = 3 and c = 5使用到a, 但是c不可以,b中间断了
where a = 3 and b > 4 and c = 5使用到a和b, c不能用在范围之后,b断了
where a is null and b is not nullis null 支持索引 is not null 类似范围查询,ab能使用,b右边的会失效
where a <> 3不能使用索引
where abs(a) =3不能使用 索引
where a = 3 and b like 'kk%' and c = 4Y,使用到a,b,c
where a = 3 and b like '%kk' and c = 4Y,只用到a
where a = 3 and b like '%kk%' and c = 4Y,只用到a
where a = 3 and b like 'k%kk%' and c = 4Y,使用到a,b,c

4. 关联查询优化

接下来再次创建两张表,并分别导入20条数据:

CREATE TABLE IF NOT EXISTS `class` (
	`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
	`card` INT(10) UNSIGNED NOT NULL,
	PRIMARY KEY (`id`)
);
CREATE TABLE IF NOT EXISTS `book` (
	`bookid` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
	`card` INT(10) UNSIGNED NOT NULL,
	PRIMARY KEY (`bookid`)
);
 
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
 
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));

4.1 关联案例

explain分析一下两个sql:

EXPLAIN SELECT * FROM class LEFT JOIN book ON class.card = book.card;
EXPLAIN SELECT * FROM class RIGHT JOIN book ON class.card = book.card;
EXPLAIN SELECT * FROM class INNER JOIN book ON class.card = book.card;

给book.card创建索引:

ALTER TABLE `book` ADD INDEX idx_card ( `card`);

然后explain分析:  

删除旧索引,添加新索引:

# 删除旧索引 + 新建 + 第3次explain
drop index idx_card on book;
ALTER TABLE class ADD INDEX index_class_card (card);

再次explain分析:  

同时给两张表的card字段添加索引:(class(card)索引已有:index_class_card,只需给book(card)添加索引)

ALTER TABLE `book` ADD INDEX idx_card ( `card`);

最后explain分析:

4.2 优化建议

4.3 三种实现的比较

5. 子查询优化

6. 排序优化

以下三种情况不走索引:

create index idx_age_deptid_name on emp (age,deptid,name)

# 以下  是否能使用到索引,能否去掉using filesort

explain  select SQL_NO_CACHE * from emp order by age,deptid; 

explain  select SQL_NO_CACHE * from emp order by age,deptid limit 10; 
 
 # 无过滤 不索引  观察extra的值
 
explain  select * from emp where age=45 order by deptid;
 
 
explain  select * from emp where age=45 order by   deptid,name; 
 
explain  select * from emp where age=45 order by  deptid,empno;
 
explain  select * from emp where age=45 order by  name,deptid;
 
explain select * from emp where deptid=45 order by age;
 
# 顺序错,不索引
explain select * from emp where age=45 order by  deptid desc, name desc ;
# 方向反 不索引
explain select * from emp where age=45 order by  deptid asc, name desc ;

6.1 优化演示

ORDER BY子句,尽量使用Index方式排序,避免使用FileSort方式排序

执行案例前先清除emp上的索引,只留主键

# 查询 年龄为30岁的,且员工编号小于101000的用户,按用户名称排序
SELECT SQL_NO_CACHE * FROM emp WHERE age =30 AND empno <101000 ORDER BY NAME;

结论:很显然,执行时间为0.477s,type 是 ALL,即最坏的情况。Extra 里还出现了 Using filesort,也是最坏的情况。优化是必须的。

优化思路: 尽量让where的过滤条件和排序使用上索引。

现在过滤条件使用了两个字段(age,empno)排序使用了name。

我们建一个三个字段的组合索引可否?

CREATE INDEX idx_age_empno_name ON emp(age,empno,NAME);

再次explain测试:

我们发现using filesort 依然存在,所以name 并没有用到索引。

原因是因为empno是一个范围过滤,所以索引后面的字段不会再使用索引了。

所以我们建一个3值索引是没有意义的 那么我们先删掉这个索引:DROP INDEX idx_age_empno_name ON emp

为了去掉filesort我们可以把索引建成

CREATE INDEX idx_age_name ON emp(age,NAME);

也就是说empno 和name这个两个字段只能二选其一。 这样我们优化掉了 using filesort。

执行一下sql:

速度果然提高了4倍。

假如:选择创建age和empno会速度会怎样呢,自己试试有惊喜!

结果竟然有 filesort的 sql 运行速度,超过了已经优化掉 filesort的 sql ,而且快了好多倍。何故?

原因:是所有的排序都是在条件过滤之后才执行的,所以如果条件过滤了大部分数据的话,几百几千条数据进行排序其实并不是很消耗性能,即使索引优化了排序但实际提升性能很有限。 相对的 empno<101000 这个条件如果没有用到索引的话,要对几万条的数据进行扫描,这是非常消耗性能的,所以索引放在这个字段上性价比最高,是最优选择。

结论: 当范围条件和group by 或者 order by 的字段出现二选一时 ,优先观察条件字段的过滤数量,如果过滤的数据足够多,而需要排序的数据并不多时,优先把索引放在范围字段上。反之,亦然。

6.2 了解filesort算法

6.2.1 双路排序

6.2.2 单路排序

6.2.3 优化策略

7. 分组优化

group by 使用索引的原则几乎跟order by一致 ,唯一区别是groupby 即使没有过滤条件用到索引,也可以直接使用索引。

只要对分组列创建索引即可

8. 覆盖索引  

最后使用索引的手段:覆盖索引

什么是覆盖索引?简单说就是,select 到 from 之间查询的列 <=使用的索引列+主键

explain select * from emp where name like '%abc';

使用覆盖索引后  

9. 索引无效说明

举报

相关推荐

0 条评论