导航:
【黑马Java笔记+踩坑汇总】Java基础+进阶+JavaWeb+SSM+SpringBoot+瑞吉外卖+SpringCloud+黑马旅游+谷粒商城+学成在线+设计模式+牛客面试题
目录
2.8 没覆盖索引时,is not null、not like导致索引失效
1. 索引优化思路
都有哪些维度可以进行数据库调优?简言之:
-
索引失效、没有充分利用到索引——建立索引
-
关联查询太多JOIN(设计缺陷或不得已的需求)——SQL优化
-
服务器调优及各个参数设置(关闭慢查询日志、缓冲、线程数等)——调整my.cnf
-
数据过多——分库分表
-
定期清理垃圾:对于不再使用的表、数据、日志、缓存等,应该及时清理,避免占用过多的MySQL资源,从而提高MySQL的性能。
-
使用合适的存储引擎:MyISAM比较适合读取频繁,写入较少的场景(因为表级锁、B+树叶存地址),而InnoDB比较适合并发写入的场景(因为行级锁、B+树叶存记录)。
关于数据库调优的知识非常分散。不同的DBMS,不同的公司,不同的职位,不同的项目遇到的问题都不尽相同。这里我们分为三个章节进行细致讲解。
虽然SQL查询优化的技术有很多,但是大方向上完全可以分成物理查询优化和逻辑查询优化两大块。
-
物理查询优化:索引和表连接方式等技术来进行优化,这里重点需要掌握索引的使用。
-
逻辑查询优化:通过SQL等价变换提升查询效率,直白一点就是说,换一种查询写法效率可能更高。
2. 索引失效的11种情况
用不用索引,最终都是优化器说了算:
优化器是基于什么的优化器? 基于 cost开销(CostBaseOptimizer),它不是基于规则(Rule-Basedoptimizer),也不是基于语义。怎么样开销小就怎么来。另外,SQL语句是否使用索引,跟数据库版本、数据量、数据选择度都有关系。
2.0. 数据准备
学员表** 插 50万 条,班级表 插 1万 条。
CREATE DATABASE atguigudb2;
USE atguigudb2;
步骤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)
DETERMINISTIC
NO SQL
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)
DETERMINISTIC
NO SQL
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 ;
#假如要删除
#drop PROCEDURE insert_stu;
创建往class表中插入数据的存储过程
#执行存储过程,往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 ;
#假如要删除
#drop PROCEDURE insert_class;
步骤5:调用存储过程
class
#执行存储过程,往class表添加1万条数据
CALL insert_class(10000);
stu
#执行存储过程,往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.1 要尽量满足全值匹配
查询age and classId and name时,(age,classId,name)索引比(age,classId)快。
查询顺序不重要,例如查询age and classId and name和查询classId and name and age是一样的。
分析查询计划:
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30 AND classId=4 AND name = 'abcd';
发现没有走索引:
建立age索引
CREATE INDEX idx_age ON student(age);
再次分析查询计划,发现走索引:
建立(age,classId)索引
CREATE INDEX idx_age_classid ON student(age,classId);
再次分析查询计划,发现走索引:
建立(age,classId,name)索引
CREATE INDEX idx_age_classid_name ON student(age,classId,name);
再次分析查询计划,发现走索引:
2.2 要满足最佳左前缀法则
在MySQL建立联合索引时会遵守最佳左前缀原则,即最左优先,在检索数据时从联合索引的最左边开始匹配。
例如索引(a,b,c),只有查询(a),(a,b),(a,b,c)会走索引,而(b),(b,c),(c)都不会走索引。
举例1:
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.name = 'abcd';
举例2:
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.classId=1 AND student.name = 'abcd';
举例3:索引idx_age_classid_name还能否正常使用?
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.classId=4 AND student.age=30 AND student.name = 'abcd';
如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。
mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.name = 'abcd';
虽然可以正常使用,但是只有部分被使用到了。
mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.classId=1 AND student.name = 'abcd';
完全没有使用上索引。
结论:MySQL可以为多个字段创建索引,一个索引可以包含16个字段。对于多列索引,过滤条件要使用索引必须按照索引建立时的顺序,依次满足,一旦跳过某个字段,索引后面的字段都无法被使用。如果查询条件中没有用这些字段中第一个字段时,多列(或联合)索引不会被使用。
2.3 主键插入顺序尽量自增
对于一个使用 InnoDB 存储引擎的表来说,在我们没有显式的创建索引时,表中的数据实际上都是存储在 聚簇索引的叶子节点的。而记录又是存储在数据页中的,数据页和记录又是按照记录 主键值从小到大 的顺序进行排序所以如果我们 插入 的记录的 主键值是依次增大 的话,那我们每插满一个数据页就换到下一个数据页继续插,而如果我们插入的 主键值忽大忽小的话,就比较麻烦了,假设某个数据页存储的记录已经满了,它存储的主键值在1~100 之间:
如果此时再插入一条主键值为 9 的记录,那它插入的位置就如下图:
可这个数据页已经满了,再插进来咋办呢?我们需要把当前 页面分裂 成两个页面,把本页中的一些记录移动到新创建的这个页中。页面分裂和记录移位意味着什么?意味着: 性能损耗 !所以如果我们想尽量避免这样无谓的性能损耗,最好让插入的记录的 主键值依次递增 ,这样就不会发生这样的性能损耗了。 所以我们建议:让主键具有 AUTO_INCREMENT ,让存储引擎自己为表生成主键,而不是我们手动插入 , 比如: person_info 表:
CREATE TABLE person_info(
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
birthday DATE NOT NULL,
phone_number CHAR(11) NOT NULL,
country varchar(100) NOT NULL,
PRIMARY KEY (id),
KEY idx_name_birthday_phone_number (name(10), birthday, phone_number)
);
我们自定义的主键列 id 拥有 AUTO_INCREMENT 属性,在插入记录时存储引擎会自动为我们填入自增的主键值。这样的主键占用空间小,顺序写入,减少页分裂。
2.4 计算、函数导致索引失效
下面sql,第一条走索引,第二条没有走索引:
CREATE INDEX idx_name ON student(NAME);
#1.函数导致索引失效,没有走索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE LEFT(student.name,3) = 'abc';
#索引优化成like,走索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name LIKE 'abc%';
#2.计算导致索引失效
CREATE INDEX idx_sno ON student(stuno);
EXPLAIN SELECT SQL_NO_CACHE id, stuno, NAME FROM student WHERE stuno+1 = 900001;
测试前先删除其他索引
2.5 类型转换导致索引失效
CREATE INDEX idx_name ON student(NAME);
#1.手动类型转换,通过调用函数,导致索引失效
EXPLAIN SELECT id, stuno, name FROM student WHERE name=CAST(123 as CHAR);
#2.自动类型转换导致索引失效。name字段类型是varchar,你赋值成数字它会默认转成字符串导致索引失败
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name=123;
# 索引优化成目标字符串,走索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name='123';
测试前先删除其他索引
2.6 范围条件右边的列索引失效
例如(a,b,c)联合索引,查询条件a,b,c,如果b使用了范围查询,那么b右边的c索引失效。这里右边看的联合索引的键右边。
解决办法:新建联合索引(a,c,b)或(c,a,b),把需要范围查询的字段放在最后
范围包括:(<) (<=) (>) (>=) 和 between。
注意:模糊查询“like”不是范围查询。
案例:
CREATE INDEX idx_age_classid_name ON student(age,classId,name); #都不使用范围查询。key_len为73 EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.classId=4 AND student.name = 'abcd'; #联合索引第一个参数age使用范围查询,全部索引失效。ken_len为0,全表扫描。 EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age>30 AND student.classId=4 AND student.name = 'abcd'; #联合索引第二个参数classId使用范围查询,name索引失效。key_len为10,即只用age和classId。 EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.classId>3 AND student.name = 'abcd'; #联合索引第三个参数name使用范围查询,索引都生效。key_len为73 EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.classId=4 AND student.name > 'abcd';
应用开发中范围查询,例如:金额查询,日期查询往往都是范围查询。创建的联合索引中,务必把范围涉及到的字段写在最后,然后将范围查询条件放置where语句最后。
2.7 没覆盖索引时,“不等于”导致索引失效
因为“不等于”不能精准匹配,全表扫描二级索引树再回表效率不如直接全表扫描聚簇索引树。但使用覆盖索引时,联合索引数据量小,加载到内存所需空间比聚簇索引树小,且不需要回表,索引效率优于全表扫描聚簇索引树。覆盖索引:一个索引包含了满足查询结果的数据就叫做覆盖索引,不需要回表等操作。
不等于符号:!= 或者<>,这两个符号意义相同。
没覆盖索引的情况下,使用“不等于”导致索引失效。因为如果使用索引,则需要依次遍历非聚簇索引B+树里所有叶节点,时间复杂度O(n),找到记录后还要回表,加在一起效率不如全表扫描,所以查询优化器就选择全表扫描了。
案例:
没覆盖索引的情况下,使用“不等于”导致索引失效。因为如果使用索引,则需要依次遍历非聚簇索引B+树里所有叶节点,时间复杂度O(n),找到记录后还要回表,加在一起效率不如全表扫描,所以查询优化器就选择全表扫描了。
CREATE INDEX idx_age_name ON student(age, NAME); #查所有字段,并且使用“不等于”,索引失效 EXPLAIN SELECT * FROM student WHERE age <> 20;
覆盖索引,查的两个字段被联合索引给覆盖了,性能更高。虽然还是需要依次遍历非聚簇索引B+树里所有叶节点,时间复杂度O(n),但是不需要回表了,整体效率比不用索引更高,查询优化器就又使用索引了。
CREATE INDEX idx_age_name ON student(age, NAME); #查的两个字段正好被联合索引“idx_age_name ”覆盖了,索引成功 EXPLAIN SELECT age,name FROM student WHERE age <> 20;
案例2:
- 为name字段创建索引
CREATE INDEX idx_name ON student(NAME);
- 查看索引是否失效
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name <> 'abc';
或者
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name != 'abc';
场景举例:用户提出需求,将财务数据,产品利润金额不等于0的都统计出来。
2.8 没覆盖索引时,is not null、not like导致索引失效
因为is not null、not like不能精准匹配,全表扫描二级索引树再回表效率不如直接全表扫描聚簇索引树。但使用覆盖索引时,联合索引数据量小,加载到内存所需空间比聚簇索引树小,且不需要回表,索引效率优于全表扫描聚簇索引树。覆盖索引:一个索引包含了满足查询结果的数据就叫做覆盖索引,不需要回表等操作。
is null可以使用索引,is not null无法使用索引
原因:
在进行索引扫描时,MySQL 会优先利用索引中已经存在的值进行查询,在查询时直接跳过为 NULL 的那些行。但是,如果使用了 IS NOT NULL 条件,那么 MySQL 无法在索引中找到 NULL 值,也就是说,MySQL 无法像前面那样跳过那些为 NULL 的行,只能扫描整张表来找到符合条件的行,因此无法使用索引。
解决方案:
- 设计数据库的时候就将字段设置为 NOT NULL 约束
- 将 INT 类型的字段,默认值设置为0。
- 将字符类型的默认值设置为空字符串('')。
扩展:同理,在查询中使用not like也无法使用索引,导致全表扫描。
- IS NULL: 可以触发索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age IS NULL;
- IS NOT NULL: 无法触发索引。
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age IS NOT NULL;
2.9 没覆盖索引时,左模糊查询导致索引失效
因为字符串开头都不能精准匹配,全表扫描二级索引树再回表效率不如直接全表扫描聚簇索引树。但使用覆盖索引时,联合索引数据量小,加载到内存所需空间比聚簇索引树小,且不需要回表,索引效率优于全表扫描聚簇索引树。
覆盖索引:一个索引包含了满足查询结果的数据就叫做覆盖索引,不需要回表等操作。
没覆盖索引的情况下,like以通配符“%”或“_”开头,导致索引失效。在使用LIKE关键字进行查询的查询语句中,如果匹配字符串的第一个字符为'%',索引就不会起作用。只有'%'不在第一个位置,索引才会起作用。
原因:没覆盖索引的情况下,字符串开头都不知道是什么,走索引再回表效率不如直接全表扫描
Alibaba《Java开发手册》:【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。
没覆盖索引的情况下,左模糊查询导致索引失效
#没覆盖索引的情况下,左模糊查询导致索引失效 CREATE INDEX idx_age_name ON student(age, NAME); EXPLAIN SELECT * FROM student WHERE NAME LIKE '%abc';
覆盖索引情况下,左模糊查询索引生效
主要原因也是因为走非聚簇索引B+树遍历叶节点,不回表,效率会比全表扫描时高,查询优化器选择效率高的方案。
#有覆盖索引的情况下,左模糊查询索引生效 CREATE INDEX idx_age_name ON student(age, NAME); EXPLAIN SELECT id,age,NAME FROM student WHERE NAME LIKE '%abc';
- 使用到索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name LIKE 'ab%';
- 未使用到索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name LIKE '%ab%';
2.10 “OR”前后存在非索引列,导致索引失效
MySQL里,即使or左边条件满足,右边条件依然要进行判断。
在WHERE子句中,如果在OR前的条件列进行了索引,而在OR后的条件列没有进行索引,那么索引会失效。
index_merge:OR前后的两个条件中的列都是索引时,查询中才使用索引,索引类型是“index_merge”,将两个索引字段分别扫描,然后合并。
因为OR的含义就是两个只要满足一个即可,因此只有一个条件列进行了索引是没有意义的,只要有条件列没有进行索引,就会进行全表扫描,因此所以的条件列也会失效。
查询语句使用OR关键字的情况:
# 未使用到索引 EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 10 OR classid = 100;
因为classId字段上没有索引,所以上述查询语句没有使用索引。
#使用到索引 EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 10 OR name = 'Abel';
因为age字段和name字段上都有索引,所以查询中使用了索引。你能看到这里使用到了index_merge,简单来说index_merge就是对age和name分别进行了扫描,然后将这两个结果集进行了合并。这样做的好处就是避免了全表扫描。
2.11 不同字符集导致索引失败,建议utf8mb4
不同的字符集进行比较前需要进行转换会造成索引失效。
数据库和表的字符集统一使用utf8mb4。统一使用utf8mb4( 5.5.3版本以上支持)兼容性更好,统一字符集可以避免由于字符集转换产生的乱码。
2.12 总结
尽量全值匹配:查询age and classId and name时,(age,classId,name)索引比(age,classId)快。
考虑最左前缀:联合索引把频繁查询的列放左。索引(a,b,c),只能查(a,b,c),(a,b),(a)。
主键尽量自增:如果主键不自增,需要查找目标位置再插入,并且如果目标位置所在数据页满了就必须得分页,造成性能损耗。
计算、函数导致索引失效:计算例如where num+1=2,函数例如abs(num)取绝对值
类型转换导致索引失效:例如name=123,而不是name='123'。又例如使用了不同字符集。
范围条件右边的列索引失效:例如(a,b,c)联合索引,查询条件a,b,c,如果b使用了范围查询,那么b右边的c索引失效。建议把需要范围查询的字段放在最后。范围包括:(<) (<=) (>) (>=) 和 between。
没覆盖索引时,“不等于”导致索引失效:因为“不等于”不能精准匹配,全表扫描二级索引树再回表效率不如直接全表扫描聚簇索引树。但使用覆盖索引时,联合索引数据量小,加载到内存所需空间比聚簇索引树小,且不需要回表,索引效率优于全表扫描聚簇索引树。覆盖索引:一个索引包含了满足查询结果的数据就叫做覆盖索引,不需要回表等操作。
没覆盖索引时,左模糊查询导致索引失效:例如LIKE '%abc'。因为字符串开头都不能精准匹配。跟上面一个道理。
没覆盖索引时,is not null、not like无法使用索引:因为不能精准匹配。跟上面一个道理。
“OR”前后存在非索引列,导致索引失效:MySQL里,即使or左边条件满足,右边条件依然要进行判断。
不同字符集导致索引失败:建议utf8mb4,不同的字符集进行比较前需要进行 转换 会造成索引失效。
3. 自测练习
练习:假设:index(a,b,c)
Where语句 | 索引是否被使用 | 使用到哪些列 |
---|---|---|
where a = 3 | Y | a |
where a = 3 and b =5 | Y | a, b |
where a = 3 and b = 5 and c =4 | Y | a, b, c |
where b=3 | N | |
where b=3 and c= 4 | Y | b |
where c = 4 | Y | c |
where a = 3 and c =5 | Y | a, c |
where a = 3 and b> 4 and c= 5 | Y | a, b, c |
where a is null and b is not null | Y(isnull) | a, b |
where a <>3 | N | |
where abs(a)=3 | N | |
3 and b like 'kk%' and where a = c=4 | Y | a, b, c |
where a = 3 and b like kk' and c = 4 | Y | a, b, c |
where a = 3 and b like %kk%' and c= 4 | Y | a, b, c |
where a = 3 and b like 'k%kk% | Y | a, b, c |
一般性建议
- 对于单列索引,尽量选择针对当前query过滤性更好的索引
- 在选择组合索引的时候,当前query中过滤性最好的字段在索引字段顺序中,位置越靠前越好。
- 在选择组合索引的时候,尽量选择能够当前query中where子句中更多的索引。
- 在选择组合索引的时候,如果某个字段可能出现范围查询时,尽量把这个字段放在索引次序的最后面。
总之,书写SQL语句时,尽量避免造成索引失效的情况