本篇文章是对Mysql索引的创建以及优化进行一个介绍,关于索引的底层原理可以看我另一篇文章:Mysql详解——索引详解
文章目录
一、索引的创建和设计原则
1. 索引的声明和使用
1.1 索引的分类:
-
从功能逻辑上讲,索引主要有四种:普通索引,唯一索引,主键索引,全文索引。
-
按照物理实现方式,索引主要有两种:聚簇索引和非聚簇索引。
-
按照作用字段个数进行划分:单列索引和多列索引。
1.2 索引的创建:
MySQL支持在单个或多个列上创建索引。
在创建表时创建索引:
隐式索引:主键,Unique修饰的字段,外键,MySQL都会自动这些字段创建索引。
CREATE TABLE dept(
dept_id INT PRIMARY KEY AUTO_INCREMENT,
dept_name VARCHAR(20)
);
CREATE TABLE emp(
emp_id INT PRIMARY KEY AUTO_INCREMENT,
emp_name VARCHAR(20) UNIQUE,
dept_id INT,
CONSTRAINT emp_dept_id_fk FOREIGN KEY(dept_id) REFERENCES dept(dept_id)
);
显示创建索引:
CREATE TABLE table_name [col_name data_type]
[UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY] [index_name] (col_name [length]) [ASC | DESC]
- UNIQUE 、 FULLTEXT 和 SPATIAL 为可选参数,分别表示唯一索引、全文索引和空间索引;
- INDEX 与 KEY 为同义词,两者的作用相同,用来指定创建索引;
- index_name 指定索引的名称,为可选参数,如果不指定,那么MySQL默认col_name为索引名;
- col_name 为需要创建索引的字段列,该列必须从数据表中定义的多个列中选择;
- length 为可选参数,表示索引的长度,只有字符串类型的字段才能指定索引长度;
- ASC 或 DESC 指定升序或者降序的索引值存储。
示例:
//创建普通索引
CREATE TABLE book(
book_id INT ,
book_name VARCHAR(100),
authors VARCHAR(100),
info VARCHAR(100) ,
comment VARCHAR(100),
year_publication YEAR,
INDEX(year_publication)
);
//创建唯一索引
CREATE TABLE test1(
id INT NOT NULL,
name varchar(30) NOT NULL,
UNIQUE INDEX uk_idx_id(id)
);
//创建组合索引
CREATE TABLE test3(
id INT(11) NOT NULL,
name CHAR(30) NOT NULL,
age INT(11) NOT NULL,
info VARCHAR(255),
INDEX multi_idx(id,name,age)
);
在已经存在的表上创建索引:
在已经存在的表中创建索引可以使用 ALTER TABLE 语句或者 CREATE INDEX 语句。
ALTER TABLE table_name ADD [UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY]
[index_name] (col_name[length],...) [ASC | DESC]
CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX index_name
ON table_name (col_name[length],...) [ASC | DESC]
1.3 索引的删除:
使用ALTER TABLE删除索引:
ALTER TABLE table_name DROP INDEX index_name;
使用DROP INDEX语句删除索引:
DROP INDEX index_name ON table_name;
2. MySQL8.0中索引新特性
2.1 支持降序索引
CREATE TABLE ts1(
a int,
b int,
index idx_a_b(a asc,b desc)
);
在某些时候我们可能需要对某个字段进行降序,这时候如果支持降序索引的话,将会大大提高查询效率。
测试:
分别在MySQL 5.7版本和MySQL 8.0版本的数据表ts1中插入800条随机数据,执行语句如下:
DELIMITER //
CREATE PROCEDURE ts_insert()
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i < 800
DO
insert into ts1 select rand()*80000,rand()*80000;
SET i = i + 1;
END WHILE;
commit;
END //
DELIMITER ;
#调用
CALL ts_insert();
在MySQL 5.7版本中查看数据表ts1的执行计划,结果如下:
EXPLAIN SELECT * FROM ts1 ORDER BY a,b DESC LIMIT 5;
从结果可以看出,执行计划中扫描数为799,而且使用了Using filesort。
Using filesort是MySQL中一种速度比较慢的外部排序,能避免是最好的。多数情况下,管理员可以通过优化索引来尽量避免出现Using filesort,从而提高数据库执行速度。
而在MySQL 8.0版本中查看数据表ts1的执行计划。从结果可以看出,执行计划中扫描数为5,而且没有使用Using filesort。
2.2 支持隐藏索引
在MySQL 5.7版本及之前,只能通过显式的方式删除索引。此时,如果发现删除索引后出现错误,又只能通过显式创建索引的方式将删除的索引创建回来。如果数据表中的数据量非常大,或者数据表本身比较大,这种操作就会消耗系统过多的资源,操作成本非常高。
从MySQL 8.x开始支持 隐藏索引,只需要将待删除的索引设置为隐藏索引,使查询优化器不再使用这个索引(即使使用force index(强制使用索引),优化器也不会使用该索引),确认将索引设置为隐藏索引后系统不受任何响应,就可以彻底删除索引。
这种通过先将索引设置为隐藏索引,再删除索引的方式就是软删除 。
隐藏索引的另一个好处就是可以通过通过设置隐藏索引的可见性可以查看索引对调优的帮助。
需要注意的是:当索引被隐藏时,它的内容仍然是和正常索引一样实时更新的。如果一个索引需要长期被隐藏,那么可以将其删除,因为索引的存在会影响插入、更新和删除的性能。
创建隐藏索引和创建普通索引一样,只不过多加了个INVISIBLE关键字。
CREATE TABLE tablename(
propname1 type1[CONSTRAINT1],
propname2 type2[CONSTRAINT2],
......
propnamen typen,
INDEX [indexname](propname1 [(length)]) INVISIBLE
);
ALTER TABLE tablename ADD INDEX indexname (propname [(length)]) INVISIBLE;
ALTER TABLE tablename ALTER INDEX index_name INVISIBLE; #切换成隐藏索引
ALTER TABLE tablename ALTER INDEX index_name VISIBLE; #切换成非隐藏索引
在MySQL 8.x版本中,为索引提供了一种新的测试方式,**可以通过查询优化器的一个开关(use_invisible_indexes)来打开某个设置,使隐藏索引对查询优化器可见。**如果 use_invisible_indexes设置为off(默认),优化器会忽略隐藏索引。如果设置为on,即使隐藏索引不可见,优化器在生成执行计划时仍会考虑使用隐藏索引。
select @@optimizer_switch \G
在输出结果中可以看到 use_invisible_indexes=off
将隐藏索引设置成对查询优化器可见:
set session optimizer_switch="use_invisible_indexes=on";
3. 索引的设计原则
3.1 数据准备:
CREATE DATABASE atguigudb1;
USE atguigudb1;
#1.创建学生表和课程表
CREATE TABLE `student_info` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`student_id` INT NOT NULL ,
`name` VARCHAR(20) DEFAULT NULL,
`course_id` INT NOT NULL ,
`class_id` INT(11) DEFAULT NULL,
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
CREATE TABLE `course` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`course_id` INT NOT NULL ,
`course_name` VARCHAR(40) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
#函数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
#函数2:创建随机数函数
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 //
DELIMITER ;
这里有可能会报错,由于开启过慢查询日志bin-log, 我们就必须为我们的function指定一个参数。主从复制,主机会将写操作记录在bin-log日志中。从机读取bin-log日志,执行语句来同步数据。如果使用函数来操作数据,会导致从机和主键操作时间不一致。所以,默认情况下,mysql不开启创建函数设置。
查看MySQL是否允许创建函数:
show variables like 'log_bin_trust_function_creators';
通过命令行开启:
set global log_bin_trust_function_creators=1;
创建插入模拟数据的存储过程:
# 存储过程1:创建插入课程表存储过程
DELIMITER //
CREATE PROCEDURE insert_course( max_num INT )
BEGIN
DECLARE i INT DEFAULT 0;
SET autocommit = 0; #设置手动提交事务
REPEAT #循环
SET i = i + 1; #赋值
INSERT INTO course (course_id, course_name ) VALUES
(rand_num(10000,10100),rand_string(6));
UNTIL i = max_num
END REPEAT;
COMMIT; #提交事务
END //
DELIMITER ;
# 存储过程2:创建插入学生信息表存储过程
DELIMITER //
CREATE PROCEDURE insert_stu( max_num INT )
BEGIN
DECLARE i INT DEFAULT 0;
SET autocommit = 0; #设置手动提交事务
REPEAT #循环
SET i = i + 1; #赋值
INSERT INTO student_info (course_id, class_id ,student_id ,NAME ) VALUES
(rand_num(10000,10100),rand_num(10000,10200),rand_num(1,200000),rand_string(6));
UNTIL i = max_num
END REPEAT;
COMMIT; #提交事务
END //
DELIMITER ;
调用存储过程:
CALL insert_course(100);
CALL insert_stu(1000000);
3.2 哪些情况适合创建索引
1. 字段值有唯一性的限制
如果表中某个字段是唯一的,即使是组合字段,强烈建议创建唯一性索引或者主键索引。
不要以为唯一索引影响了insert速度,这个速度是可以忽略的,但提高查询速度确实明显的。
2. 频繁作为WHERE查询条件的字段
某个字段在SELECT,UPDATE,DELETE语句中经常被使用到,尤其是在数据量大时,那么就需要为它建立索引了。
3. 经常GROUP BY和ORDER BY的列
索引本身就是让数据按照某种顺序进行存储或检索,因此对经常需要GROUP BY和ORDER BY的创建索引,能够大大提高查询效率。如果待排序有多个,那么可以考虑创建组合索引。
4. DISTINCT字段需要创建索引
有时候我们需要对某个字段进行去重,使用DISTINCT,那么对这个字段创建索引,也会提升查询效率。
这个也比较容易理解,通过创建索引,相同的数都挨在一起,那么去重也就更快了。
SELECT DISTINCT(student_id) FROM student_info
5. 多表 JOIN连接操作时,创建索引的注意事项
首先,在连接表时尽量不要超过3张,因为每增加一张表就相当于增加了一次嵌套的循环,数量级增长会非常快,严重影响查询效率。
其次,对WHERE条件创建索引,因为WHERE才是对数据的过滤,在数据量大时没有索引是很可怕的。
最后,对连接的字段创建索引。
SELECT course_id, name, student_info.student_id, course_name
FROM student_info JOIN course
ON student_info.course_id = course.course_id
WHERE name = '462eed7ac6e791292a79';
6. 使用列的类型小的创建索引
这里说的类型小指的是该类型表示的数据范围的大小。
当我们要定义某一列的类型时,以整数类型为例,有TINYINT,MEDIUMINT,INT,BIGINT等,他们表示的范围依次递增,占用的空间也是依次递增。如果要对该列创建索引时,在数据范围允许的情况下,尽量选用较小的类型,也就是能用INT就不要用BIGINT,能够MEDIUMINT就不要用INT,原因如下:
- 数据类型越小,在查询时进行的比较操作就越快。
- 数据类型越小,索引占用的空间就越小,在一个数据页内就可以存放更多的记录,整颗B+树就更矮,从而减少IO次数。
这对于表的主键更加适用,因为不仅是聚簇索引还是非聚簇索引,各节点都会存放一份主键值,如果主键选用更小的类型,也就意味着节省更多的存储空间和更高效的IO。
7. 使用字符串的前缀创建索引
当我们为一个存储字符串的列创建索引时,如果字符串很长,那么会导致两个问题:
- 字符串太长,在索引中占用的空间就越大。
- 字符串越长,比较时花费的时间就越多。
因此我们可以通过截取字符串的前面一部分内容建立索引,这称为前缀索引。
这样在查找记录时虽然无法定位到具体的位置,但是可以定位到相应前缀的位置,然后根据前缀相同的主键值回表查询完整的字符串。即节约空间,又减少了字符串的比较时间,还大体解决排序问题。
alter table shop add index(address(12));
如何确定前缀的长度,截少了区分度不高,导致有很多数据要回表查询,截多了又达不到节约索引空间的目的。
先看一下字段在全部数据中的选择度:
select count(distinct address) / count(*) from shop
通过不同长度去计算,与全表的选择性对比
count(distinct left(列名, 索引长度))/count(*)
例如:
select count(distinct left(address,10)) / count(*) as sub10, -- 截取前10个字符的选择度
count(distinct left(address,15)) / count(*) as sub11, -- 截取前15个字符的选择度
count(distinct left(address,20)) / count(*) as sub12, -- 截取前20个字符的选择度
count(distinct left(address,25)) / count(*) as sub13 -- 截取前25个字符的选择度
from shop
在阿里巴巴开发手册中,强制要求对于varchar字段建立索引时必须指定索引长度,没必要对全字段建立索引。
一般来说,对字符串类型数据,长度为 20 的索引,区分度会高达90%以上 。
8. 区分度高的列作为索引
某一列的值中不重复个数越多,越适合作为索引,同时在联合索引时更适合放在前面。
可以使用select count(distinct a) / count(*) from t
,计算区分度,一般超过33%就算是比较高的区分度了。
9. 使用最频繁的列放在联合索引的左侧
最左前缀原则。
10. 在多个列需要创建索引时,联合索引优于单值索引
3.3 限制索引的数目
在实际工作中,我们也需要平衡,索引的数目不是越多越好。建议每张表的索引数量不超过6个。原因:
- 每个索引需要占用空间,索引越多,需要的磁盘空间就越多。
- 索引会影响每次增删改的性能。
- 优化器在选择如何查询优化时,如果有多个索引都可以用于查询,会增加MySQL优化器生成执行计划的时间,降低查询性能。
3.4 哪些情况不适合创建索引
1. 在where语句中用不到的条件,不要创建索引
2. 数据量小的表最好不要设置索引
3. 有大量重复数据的列上不要创建索引
4. 避免对经常需要更新的表创建过多的索引
5. 不建议用无序的值作为索引
例如身份证,UUID,由于这些都是无序的,在插入时不太可能按照顺序插入,导致插入的位置在中间,可能造成页分裂
6. 删除不再使用或者很少使用的索引
7. 不要定义冗余或者重复的索引
-
冗余索引
有时候可能无意对同一个列创建了多个索引,例如创建了索引index(a,b),后续又创建了索引index(a)。
-
重复索引
例如id本身是主键,我们又为id创建了个索引。
二、索引优化和查询优化
数据库优化的角度:
- 索引失效,没有充分利用上索引 —— 建立索引
- 关联查询太多JOIN(设计缺陷或者不得已的需求)——SQL优化,数据库添加冗余字段
- 服务器调优及优化各个参数设置(缓冲,线程数等) —— 调整my.cnf
- 数据过多 —— 分库分表
1. 数据准备
步骤1:建表
CREATE TABLE `class` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`className` VARCHAR(30) DEFAULT NULL,
`address` VARCHAR(40) DEFAULT NULL,
`monitor` INT NULL ,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
CREATE TABLE `student` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`stuno` INT NOT NULL ,
`name` VARCHAR(20) DEFAULT NULL,
`age` INT(3) DEFAULT NULL,
`classId` INT(11) DEFAULT NULL,
PRIMARY KEY (`id`)
#CONSTRAINT `fk_class_id` FOREIGN KEY (`classId`) REFERENCES `t_class` (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
步骤2:允许创建函数
set global log_bin_trust_function_creators=1; # 不加global只是当前窗口有效。
步骤3:创建函数
随机产生字符串
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
#用于随机产生多少到多少的编号
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 //
DELIMITER ;
步骤4:创建存储过程
#创建往stu表中插入数据的存储过程
DELIMITER //
CREATE PROCEDURE insert_stu( START INT , max_num INT )
BEGIN
DECLARE i INT DEFAULT 0;
SET autocommit = 0; #设置手动提交事务
REPEAT #循环
SET i = i + 1; #赋值
INSERT INTO student (stuno, name ,age ,classId ) VALUES
((START+i),rand_string(6),rand_num(1,50),rand_num(1,1000));
UNTIL i = max_num
END REPEAT;
COMMIT; #提交事务
END //
DELIMITER ;
#执行存储过程,往class表添加随机数据
DELIMITER //
CREATE PROCEDURE `insert_class`( max_num INT )
BEGIN
DECLARE i INT DEFAULT 0;
SET autocommit = 0;
REPEAT
SET i = i + 1;
INSERT INTO class ( classname,address,monitor ) VALUES
(rand_string(8),rand_string(10),rand_num(1,100000));
UNTIL i = max_num
END REPEAT;
COMMIT;
END //
DELIMITER ;
步骤5:调用存储过程
#执行存储过程,往class表添加1万条数据
CALL insert_class(10000);
#执行存储过程,往stu表添加50万条数据
CALL insert_stu(100000,500000);
步骤6:创建删除某表上的索引的存储过程
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=1来控制游标的结束
DECLARE CONTINUE HANDLER FOR NOT FOUND set done=2 ;
#若没有数据返回,程序继续,并将变量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 //
DELIMITER ;
当想要删除索引时执行
CALL proc_drop_index("dbname","tablename");
2. 索引失效的案例
2.1 最佳左前缀法则
MySQL可以为多个字段创建索引,一个索引可以包含多个字段,对于多列索引,过滤条件要使用索引必须按照索引创建的顺序,一次满足,一旦跳过某个字段,索引后面的字段事实上并没有用上索引。
举例:创建联合索引
CREATE INDEX idx_name_age ON student(name,age);
查询,where中包含name和age:
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name = 'abcd' AND student.age = 30;
结果:可以看到它使用上了联合索引
查询:where中不包含name
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age = 30;
结果:可以看到由于匹配条件中没有name字段,不符合最左匹配选择,所以没有用上联合索引
2.2 主键插入顺序
建议主键具有AUTO_INCREMENT(不严谨,对于某些场合不适用,具体看下面详细讲解),否则很有可能会出现新的值的插入位置在B+树的中间,并且所插入的数据页已经满了,这将导致需要将页分裂成两个页面,并且需要将原先页中的数据进行移动到新的页上,影响性能。
2.3 使用函数将导致索引失效
创建索引:
CREATE INDEX idx_name ON student(name);
两条查询语句:
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name LIKE 'abc%';
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE LEFT(student.name,3) = 'abc';
可以发现,没有使用函数的语句用上了索引,而使用了函数的语句没有用上索引
2.4 计算将导致索引失效
创建索引
CREATE INDEX idx_age ON student(age);
两条查询语句:
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age = 31;
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age - 1 = 30;
可以发现,这两条语句所表达的意思其实是一样的,但是索引列有进行运算的话会导致索引失效。
2.5 类型转换将导致索引失效
查询条件中name字段(varchar类型)等于一个整数,此时会自动进行类型转换,导致索引失效。
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name = 123
2.6 范围条件右边的列索引失效
创建索引:
create index idx_age_classid_name on student(age,classId,name);
执行查询语句:
EXPLAIN SELECT SQL_NO_CACHE * FROM student
WHERE student.age=30 AND student.classId>20 AND student.name = 'abc' ;
结果发现,虽然用上了上面的索引,但是通过key_len发现该联合索引只用到了age和classId,并没有加上name,原因是因为在查询时classId是范围查找,导致后面列的索引失效了。
解决办法:创建新的索引,将classId和name的顺序调换
create index idx_age_name_classid on student(age,name,classId);
此时发现,联合索引完全使用。
2.7 不等于会导致索引失效
如果索引列的条件是不等于(!= 或者 <>)的话,会导致该索引失效。
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name <> "abc";
2.8 is null可以使用索引,is not null无法使用索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age IS NULL;
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age IS NOT NULL;
可以看到,当索引列判断条件是is null时,是可以使用到索引的,当条件是is not null时,就无法使用上索引了。
2.9 LIKE以通配符%开头导致索引失效
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name LIKE '%abc'
2.10 OR前后存在非索引的列,索引失效
给age列创建索引:
CREATE INDEX idx_age ON student(age);
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 10 OR classid = 100;
可以看到,虽然age有索引,但是由于classid没有索引,并且条件是通过OR连接起来的,因此导致索引失效。这个其实很容易理解,虽然age有索引,可以通过索引来进行查找,但是由于classid没有索引,因此也还是需要进行全表扫描,那最后就相当于要进行全表扫描,因此也就没必要使用age的索引了。
2.11 数据库和表的字符集统一使用utf8mb4
统一使用utf8mb4(5.5.3版本以上支持)兼容性更好,统一字符集可以避免由于字符集转换产生的乱码。
不同的字符集进行比较前需要进行转换会造成索引失效。
3. 关联查询优化
3.1 数据准备
# 创建两张表
CREATE TABLE IF NOT EXISTS `type`(
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)
)
#分别插入20条数据
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(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)));
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)));
3.2 外连接
以左外连接为例:
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;
由于没有创建索引,因此这两个表的type都是all,并且rows都是20,这是由于联表查询事实上是一个笛卡儿乘积的过程,类似于两层嵌套的for循环
创建索引:
ALTER TABLE book ADD INDEX Y ( card); #【被驱动表】,可以避免全表扫描
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;
可以看到第二行的type变成了ref,使用上了索引,并且rows变为了1。
这里想说的可能表达的不是很好,就是右边是我们的关键点,一定需要建立索引 ,因为左连接,左边是一定要全表扫描的,而右边是内层循环,只需要挑出符合左边的数据即可,因此右边建立索引可以大大提高检索效率。
以左连接为例,左边的表称为驱动表,右边的表称为被驱动表,我们要在被驱动表上加索引。
3.3 内连接
内连接和外连接不同的是:内连接两张表的地位相同,哪张为驱动表,哪张为被驱动表由优化器决定,很多情况下数据量少的为驱动表(驱动表要全表扫描,数据量少意味着扫描规模小)。
删除上面创建的索引:
drop index Y on book;
EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card=book.card;
查询结果可以看到,在没有创建索引,默认情况下,优化器选择了type作为驱动表。
紧接着我们向type表中再插入20条数据:
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card) VALUES(FLOOR(1 + (RAND() * 20)));
.....
此时再查询:
EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card=book.card;
可以发现此时这两条记录调换了,book为驱动表了,原因上面也说了,由于type新添加了20条数据,比book多了20条,所以优化器权衡之后觉得book数据量相对少点,作为全表扫描的驱动表更适合一点。
3.4 JOIN语句
1. Simple Nested-Loop Join
简单嵌套循环连接,算法很简单,也就是从表A中取出一条数据1,然后遍历表B,将匹配到的数据放到result中,以此类推。
这种效率是非常低的。当然MySQL并不会这么粗暴去进行表的连接,所以就出现了后面的两种优化算法。
2. Index Nested-Loop Join
索引嵌套循环连接,其优化的思路利用索引减少内层表数据的匹配次数,所以要求被驱动表必须要有索引。
3. Block Nested-Loop Join
块嵌套循环连接。
如果没有用上索引,每次访问被驱动表,都需要将其表中的记录都加载到内存中,然后再从驱动表中取一条进行匹配,匹配结束后清除内存,然后再从驱动表中加载一条记录,然后将被驱动表的数据再次加载到内存中进行匹配。这样周而复始,大大浪费了IO的次数。
为了解决这个问题,获取驱动表中的数据,不再是一条一条地获取,而是一块一块地获取,引入了join buffer缓冲区,将驱动表中join相关的部分数据(大小受join buffer限制)缓冲到join buffer中,然后全表扫描被驱动表,将简单嵌套循环的多次比较合并成一次,降低了被驱动表的加载次数。
参数设置:
- block_nested_loop:通过
show variables like '%optimizer_switch%'
查看其是否开启,默认是开启的。 - join_buffer_size:设置join buffer的大小,默认是256K。
效率比较:INLJ > BNLJ > SNLJ
前面提到的小表驱动大表是不严谨的,应该是小的结果集驱动大的结果集。这里评判结果集大小是符合条件的表的行数 * 每行的大小。
例如有表A 连接 表B,表A有1000条数据,表B有100条数据,但是在where条件中过滤掉了表A很多数据,导致最终符合条件的表A只有50条数据,并且在join buffer中表A每行数据的大小 小于 表B每行数据的大小,那这时应该是表A驱动表B。
select t1.b, t2.* from t1 straight join t2 on (t1.b = t2.b); #推荐
select t1.b, t2.* from t2 straight join t2 on (t1.b = t2.b); #不推荐
对于上面的例子,由于t2要查询的字段是全部(假设t2有很多字段),而t1只查询一个字段,那么t1相较于t2每行数据占用的空间更少,t1作为驱动表的话,join buffer中能容纳更多的行数,因此应该让t1驱动t2。
因此从上面例子也可以总结,在查询时按需查询,减少不必要的字段的查询。
4. Hash JOIN
从MySQL8.0.20开始废弃了BNLJ(块嵌套循环连接),因为从MySQL8.0.18开始引入了Hash JOIN,并作为默认。
具体底层原理这里先挖个坑。
4. 子查询优化
子查询是MySQL的一项重要功能,可以帮助我们实现比较复杂的查询操作。但是,子查询一般效率都不高,原因如下:
- **执行子查询时,MySQL需要为内层查询语句的查询结果创建一个临时表,**然后外层查询从临时表中再进行查询。临时表的创建和撤销,都会消耗CPU和IO资源,产生大量的慢查询。
- 子查询产生的临时表,是不存在索引的,因此当临时表越大,对查询性能的影响也就越大。
因此,建议将子查询拆开成多条查询,或者使用 JOIN 来代替子查询。
5. 排序优化
在MySQL中,支持两种排序方式,分别是 FileSort 和 Index 排序:
- Index排序中,索引可以保证数据的有序性,不再需要进行排序,效率更高。
- **File Sort排序则一般是在内存中进行的,占用CPU较多。**如果待排结果较大,会产生临时文件IO到磁盘中进行排序的情况,效率较低。
优化建议:尽量对ORDER BY后的字段创建索引,这样可以避免File Sort。
接下来进行一些案例分析:
首先我们给age和classid创建索引
CREATE INDEX idx_age_classid_name ON student(age,classid,name)
进行查询:会发现索引失效
EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age, classid
加上limit 10,索引又能够用了
EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age, classid LIMIT 10;
原因:这其实是优化器进行权衡判断的结果,由于查询的是所有字段,这意味着需要进行回表操作,又因为表的数据很多,优化器认为通过索引查出来后再回表 并没有直接在内存中排序更快,因此选择不使用索引。
因此,如果将 * 改成 age,classid的话,就不用进行回表操作,那肯定还是用上索引更快。
EXPLAIN SELECT SQL_NO_CACHE age,classid FROM student ORDER BY age, classid;
FileSort算法
排序的字段如果不再索引列上,那么就会进行FileSort,FileSort有两种算法:双路排序 和 单路排序。
(1)双路排序(慢):
- 在MySQL4.1之前是使用双路排序的,字面意思就是进行两次扫描磁盘。
- 从磁盘中读取排序字段,然后在buffer中进行排序,然后按照排好的顺序再从磁盘中取其他字段。
从磁盘中进行两次IO操作,是很耗时的,因此出现了第二种改进的算法。
(2)单路排序(快):
从磁盘中读取查询需要的列,按照order by的字段在buffer中进行排序,最后进行输出。由于只需要进行一次IO,因此效率更快一点,但是需要更多的空间,因为它将每一行都保存在内存中了。
(3)优化策略:
-
尝试提高 sort_buffer_size:
增大buffer的容量,在MySQL5.7中,InnoDB存储引擎默认值是1MB
SHOW VARIABLES LIKE '%sort_buffer_size%'
-
尝试提高 max_length_for_sort_data:
提高这个参数,会增加用改进算法的概率。
SHOW VARIABLES LIKE '%max_length_for_sort_data%'
6. GROUP BY 优化
- GROUP BY 使用索引的规则和 ORDER BY 大体一致。
- 当无法使用索引列时,增加 max_length_for_sort_data 和 sort_buffer_size 参数能够有很好的效果。
- where效率高于having,能写在where限定的条件就不要写在having中了。
- 减少使用ORDER BY ,和业务沟通能不排序就不排序,或者将排序操作放到程序中解决。ORDER BY ,GROUP BY,DISTINCT这些语句很耗费CPU,数据库的CPU资源是很宝贵的。
- 包含了ORDER BY,GROUP BY,DISTINCT的语句,where条件过滤出来的结果集尽量保持在1000行以内,否则SQL会很慢。
7. Limit 优化
假设此时有 LIMIT 2000000,10,这意味着MySQL需要排序前2000010条记录,然后仅仅返回2000000-2000010的记录,其他记录会被丢弃,查询排序的代价非常大。
优化思路一:利用主键索引查出2000000-2000010的记录的id,然后再回表查出其他列的内容,这同样也利用到了索引。
EXPLAIN SELECT * FROM student t,(SELECT id FROM student ORDER BY id LIMIT 2000000,10) a
WHERE t.id = a.id;
优化思路二:该方案适用于主键自增的表,可以把LIMIT查询转化成某个位置的查询(同样是利用索引)
EXPLAIN SELECT * FROM student WHERE id > 2000000 LIMIT 10;
8. 覆盖索引
如果索引中的值包含了我们想要查询的所有列,这个索引就叫做覆盖索引。
简单来说,索引列 + 主键 包含了SELECT 到 FROM 之间查询的列。
好处:
- 避免InnoDB表进行索引的二次查询(回表)。
- 可以将随机IO变成顺序IO加快查询效率。
缺点:
- 索引字段的维护是需要代价的,因此,为了支持覆盖索引而建立冗余索引需要仔细权衡利弊。
这里来看一个例子:
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age <> 20;
这是我们之前提到的关于不等于会导致索引失效,结结果也符合我们的预期,没有用上索引。
但当我们将查询列改成索引中有的值时,发现索引用上了。
EXPLAIN SELECT SQL_NO_CACHE age, name FROM student WHERE age <> 20;
因此,上面讲的索引失效的规则并不是绝对的,优化器会进行判断来决定最终是否使用索引。这里由于查询列是索引本身包含的值,因此优化器认为不需要回表,效率比直接查询整张表要高,就使用上了索引。
9. 索引下推
索引下推(Index Condition Pushdown),简称ICP,是MySQL5.6中的一个新特性,是一种在存储引擎层使用使用索引过滤数据的一种优化方式**。ICP可以减少回表次数。**
例如:
EXPLAIN SELECT * FROM s1 WHERE key1 > 'z' AND key1 LIKE '%a';
可以看到Extra中显示Using index condition,这里就是指使用到了索引下推。
对于上面例子,key1 > ‘z’ 使用上了索引idx_key1,但是后面由于模糊查询是以%开头,所以无法使用上索引,但是存储引擎此时会进行一种优化:索引下推。也就是通过索引idx_key1查询出数据后,并不急着回表,而是先通过后面的模糊查询过滤掉不符合的数据,最后再回表查询所有的列数据,这就叫做索引下推。
如果没有索引下推,那么在通过索引 idx_key1查询出数据后,就立刻进行回表查出符合的数据,最后才判断是否 符合 LIKE ‘%a’ 的条件,这样增多了回表的次数。
对于联合索引也是如此:虽然name字段无法用上索引,但是存储引擎在回表之前会对该字段先进行过滤,减少回表时的数据量,提高查询效率。
EXPLAIN SELECT * FROM student WHERE age = 20 and classid = 10 and name LIKE '%a';
默认情况下ICP是开启的,可以通过设置 optimizer_swith 变量来控制:
#关闭索引下推
SET optimizer_switch = 'index_condition_pushdown=off';
#打开索引下推
SET optimizer_switch = 'index_condition_pushdown=on';
10. 其他优化策略
10.1 EXISTS 和 IN
SELECT * FROM A WHERE cc IN (SELECT cc FROM B)
SELECT * FROM A WHERE EXISTS (SELECT cc FROM B WHERE B.cc = A.cc)
对于第一条语句使用的是IN,可以理解成会先查询B,也就是先查询子语句,然后将每一条查询结果去A表中找是否有符合条件的。因此适用于A的cc列有索引,并且B是小表。(换个角度理解,可以将 IN 看成 多个 = 号 )
对于第二条语句使用的是EXISTS,可以理解成会从A表中取出每一条数据,然后去B表中找是否有符合条件的。因此适用于B表的cc列有索引,并且A表是小表。
10.2 COUNT(*) 和 COUNT(具体字段)
区别:
- 性能:
count(*) = count(1) > count(主键字段) > count(字段)
- 对于NULL值是否统计:COUNT(*) 和 COUNT(1) 会计算NULL值,COUNT(具体字段)不会计算NULL值。
对于count(主键字段):
如果表里只有主键字段,没有二级索引时。InnoDB循环遍历聚簇索引,然后判断主键值是否为NULL,如果不为NULL就会被计数,知道所有的记录被读完。如果表里有二级索引,InnoDB循环遍历的对象就不是聚簇索引,而是二级索引,这是因为二级索引比主键索引占用更少的空间,能够减少IO次数。
对于count(1):
如果表里只有主键索引,没有二级索引时,InnoDB会循环遍历聚簇索引,但是不会读取记录的任何字段的值,也就是不会去判断是否为空,因此会说count(1)执行效率比count(主键字段)更高一点。
对于count(*):
count(*)其实等价于count(0),也就是说当你使用count(*),MySQL会自动将 * 转化成 0 来处理。所以,count(*)执行过程和count(1)执行过程基本一样,性能没有什么差异。
对于count(字段):
count(字段)的执行效率是最差的。对于下面这个查询来说,会采用全表扫描的方式来计数,这是很慢的查询效率。
如果非要统计该字段不为NULL的记录个数,建议给这个字段建立一个二级索引,这样就可以用上索引来统计。
EXPLAIN SELECT COUNT(gender) FROM student;
前面讲的都是基于InnoDB存储引擎来说的,对于MyISAM存储引擎来说,执行count函数只需要O(1)复杂度,这是因为每张MyISAM的表都有一个meta信息存储了row_count值,由表级锁保证一致性,所以直接读取row_count的值就是count函数的执行结果了。
而由于InnoDB存储引擎是支持事务的,同一时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB应该返回多少行是不确定的,因此需要扫描表来统计具体的记录数。不过带上where条件语句后,MyISAM和InnoDB就没有区别了,都需要扫描表来进行统计。
当数据量很大时,count(*)是很耗时的,那么有什么办法可以进行优化吗?
-
如果业务对于统计个数并不是需要很精确的话,比如搜索引擎在搜索关键词时给出的搜索结果数只是一个大概值,那么我们可以使用show table status 或者 explain 命令进行估算,执行explain命令效率是很高的,因为它并不会真正进行查询,下图中的rows就是 explain 命令对表student的记录数的估算。
-
额外表保存计数值
可以将计数值保存到另外一张表中,这样,在新增和删除时我们就需要进行相应的更新。
10.3 LIMIT 1
如果你确定结果集只有1条,那么加上LIMIT 1时,当找到一条结果后就不会再继续进行扫描了,能加快查询速度。 当然,如果本身对字段已经加上了唯一索引,那么也就不需要加上LIMIT 1了。
10.4 SELECT *
尽量不要使用SELECT *来查询:
- MySQL在解析过程中,会通过数据字典将 * 按序转换成所有列名,这会大大浪费资源和时间。
- 无法使用覆盖索引,需要进行回表。
10.5 多使用COMMIT
在程序中尽量进行COMMIT,这样程序的性能得到提高。
COMMIT所释放的资源有:
- 回滚段上用于恢复数据的信息
- 被程序语句获得的锁
- redo / undo log buffer 中的空间
- 管理上述3种资源中的内部花费
11. 主键该如何设置
UUID + 有序
参考淘宝订单
这里详细的有时间再整理。。挖坑。