导航:
【黑马Java笔记+踩坑汇总】Java基础+进阶+JavaWeb+SSM+SpringBoot+瑞吉外卖+SpringCloud+黑马旅游+谷粒商城+学成在线+设计模式+牛客面试题
目录
1. 索引优化思路
2. 索引失效的11种情况
2.0. 数据准备
2.1 要尽量满足全值匹配
2.2 要满足最佳左前缀法则
2.3 主键插入顺序尽量自增
2.4 计算、函数导致索引失效
2.5 类型转换(手动或自动)导致索引失效
2.6 范围条件右边的列索引失效
2.7 “不等于”导致索引失效
2.8 is not null、not like无法使用索引
2.9 左模糊查询导致索引失效
2.10 “OR”前后存在非索引列,导致索引失效
2.11 不同字符集导致索引失败,建议utf8mb4
3. 自测练习
1. 索引优化思路
都有哪些维度可以进行数据库调优?简言之:
-
索引失效、没有充分利用到索引——建立索引
-
关联查询太多JOIN(设计缺陷或不得已的需求)——SQL优化
-
服务器调优及各个参数设置(缓冲、线程数等)——调整my.cnf
-
数据过多——分库分表
关于数据库调优的知识非常分散。不同的DBMS,不同的公司,不同的职位,不同的项目遇到的问题都不尽相同。这里我们分为三个章节进行细致讲解。
虽然SQL查询优化的技术有很多,但是大方向上完全可以分成物理查询优化和逻辑查询优化两大块。
-
物理查询优化:索引和表连接方式等技术来进行优化,这里重点需要掌握索引的使用。
-
逻辑查询优化:通过SQL等价变换提升查询效率,直白一点就是说,换一种查询写法效率可能更高。
2. 索引失效的11种情况
MySQL中 提高性能 的一个最有效的方式是对数据表 设计合理的索引。索引提供了高效访问数据的方法,并且加快查询的速度,因此索引对查询的速度有着至关重要的影响
- 使用索引可以 快速地定位 表中的某条记录,从而提高数据库查询的速度,提高数据库的性能。
- 如果查询时没有使用索引,查询语句就会 扫描表中的所有记录。在数据量大的情况下,这样查询的速度会很慢。
大多数情况下都(默认)采用 B+树 来构建索引。只是空间列类型的索引使用 R-树,并且MEMORY表还支持 hash索引。
用不用索引,最终都是优化器说了算:
优化器是基于什么的优化器? 基于 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,classId,name)时,(age,classId,name)索引比(age,classId)快。
查询顺序不重要,例如查询(age,classId,name)和查询查询(classId,name,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 SUBSTRING(name, 1,3)='abc';
#索引优化成like,走索引
EXPLAIN SELECT id, stuno, NAME FROM student WHERE NAME LIKE 'abc%';
#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
案例:
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 “不等于”导致索引失效
不等于符号:!= 或者<>,这两个符号意义相同。
- 为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 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关键字进行查询的查询语句中,如果匹配字符串的第一个字符为'%',索引就不会起作用。只有'%'不在第一个位置,索引才会起作用。
原因:字符串开头都不知道是什么,B+树自然不能走索引,必须全表扫描。
Alibaba《Java开发手册》:【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。
- 使用到索引
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”前后存在非索引列,导致索引失效
在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版本以上支持)兼容性更好,统一字符集可以避免由于字符集转换产生的乱码。
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语句时,尽量避免造成索引失效的情况