一、数据库调优
1.1、调优维度
- 1、索引失效、没有充分利用到索引——
索引建立
- 2、关联查询太多JOIN(设计缺陷或不得已的需求)——
SQL优化
- 3、服务器调优及各个参数设置(缓存、线程数等)——
调整 my.cnf
- 4、数据过多——
分库分表
1.2、物理查询优化
物理查询优化是通过
索引
和表连接方式
等技术来进行优化
1.3、逻辑查询优化
逻辑查询优化是通过
SQL 等价变换
提升查询效率,直白一点就是说,换一种查询写法执行效率可能更高
二、数据准备
2.1、建表
- class 表
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`)
);
- student 表
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`)
);
2.2、设置参数
- 命令开启:允许创建函数设置
# 不加global只是当前窗口有效
SET GLOBAL log_bin_trust_function_creators = 1;
2.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 ;
- 创建往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 ;
- 调用存储过程
# 执行存储过程,往class表添加1万条数据
CALL insert_class(10000);
# 执行存储过程,往stu表添加50万条数据
CALL insert_stu(100000, 500000);
- 销毁索引
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");
三、索引失效
-
MySQL中
提高性能
的一个最有效的方式是对数据表设计合理的索引
。索引提供了高效访问数据的方法,并且加快查询的速度,因此索引对查询的速度有着至关重要的影响。- 使用索引可以
快速地定位
表中的某条记录,从而提高数据库查询的速度,提高数据库的性能 - 如果查询时没有使用索引,查询语句就会
扫描表中的所有记录
。在数据量大的情况下,这样查询的速度回很慢
- 使用索引可以
大多数情况下采用
B+树
来构建索引。只是空间列类型的索引使用R-树
,并且MEMORY表还支持hash索引
其实,用不用所以,最终都是优化器说了算。优化器是基于什么的优化器?基因
cost开销(CostBaseOptimizer)
,它不是基于规则(Rule-BasedOptimizer)
,也不是基于语义
,怎么样开销小就怎么样来。SQL语句是否使用索引,跟
数据库版本
、数据量
、数据选择度
都有关系
3.1、案例1——全值匹配我最爱
3.1.1、查看student表
索引情况
- 查看命令
SHOW INDEX FROM student;
3.1.2、通过 age
来查询
- SQL
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30;
-
EXPLAIN结果EXPLAIN结果.png
3.1.2、通过 age
和 classId
来查询
- SQL
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30 AND classId=4;
-
EXPLAIN结果EXPLAIN结果.png
3.1.3、通过 age
和 classId
和NAME
来查询
- SQL
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30 AND classId=4 AND NAME = 'abcd';
-
EXPLAIN结果EXPLAIN结果.png
3.1.3、小结
由于
student表
没有索引,所以上述查询都是全表扫描
3.1.4、给age
添加索引
- SQL
CREATE INDEX idx_age ON student (age);
- 查询
EXPLAIN
SELECT SQL_NO_CACHE *
FROM student
WHERE age = 30;
EXPLAIN
SELECT SQL_NO_CACHE *
FROM student
WHERE age = 30
AND classid = 4;
EXPLAIN
SELECT SQL_NO_CACHE *
FROM student
WHERE age = 30
AND classid = 4
AND name = 'abcd';
-
EXPLAIN结果EXPLAIN结果.png
- 查询使用了索引
idx_age
3.1.5、使用age
和classid
创建联合索引
- 创建联合索引
CREATE INDEX idx_age_classid ON student(age,classId);
3.1.5.1 通过 age
查询
EXPLAIN
SELECT SQL_NO_CACHE *
FROM student
WHERE age = 30;
-
EXPLAIN结果EXPLAIN结果.png
- 小结
- 可选
idx_age,idx_age_classid
两个索引 - 最终选用了
idx_age
-
key_len=5
:age为INT类型占用4字节
+可以为NULL
- 可选
3.1.5.2 通过 age
和classid
查询
EXPLAIN
SELECT SQL_NO_CACHE *
FROM student
WHERE age = 30
AND classid = 4;
-
EXPLAIN分析 EXPLAIN分析.png
- 小结
- 可选
idx_age,idx_age_classid
两个索引 - 最终选用了
idx_age_classid
-
key_len=10
:age为INT类型占用4字节
+可以为NULL
;同理classid为INT类型占用4字节
+可以为NULL
- 可选
3.1.5.3 通过 age
和classid
和name
查询
EXPLAIN
SELECT SQL_NO_CACHE *
FROM student
WHERE age = 30
AND classid = 4
AND name = 'abcd';
-
EXPLAIN分析EXPLAIN分析.png
- 小结:使用索引情况与上面一致
3.1.6、使用age
和classid
和name
创建联合索引
- 创建联合索引
CREATE INDEX idx_age_classid_name ON student (age, classid, name);
3.1.6.1 通过 age
查询
EXPLAIN
SELECT SQL_NO_CACHE *
FROM student
WHERE age = 30;
-
EXPLAIN分析EXPLAIN分析.png
- 小结
- 可选
idx_age,idx_age_classid,idx_age_classid_name
3个索引 - 最终选用了
idx_age
-
key_len=5
:age为INT类型占用4字节
+可以为NULL
- 可选
3.1.6.2 通过 age
和classid
查询
EXPLAIN
SELECT SQL_NO_CACHE *
FROM student
WHERE age = 30
AND classid = 4;
-
EXPLAIN分析EXPLAIN分析.png
-
小结
- 可选
idx_age,idx_age_classid,idx_age_classid_name
3个索引 - 最终选用了
idx_age_classid
-
key_len=10
:age为INT类型占用4字节
+可以为NULL
;同理classid为INT类型占用4字节
+可以为NULL
- 可选
3.1.6.3 通过 age
和classid
和name
查询
EXPLAIN
SELECT SQL_NO_CACHE *
FROM student
WHERE age = 30
AND classid = 4
AND name = 'abcd';
-
EXPLAIN分析EXPLAIN分析.png
-
小结
- 可选
idx_age,idx_age_classid,idx_age_classid_name
3个索引 - 最终选用了
idx_age_classid_name
-
key_len=93
:age为INT类型占用4字节
+可以为NULL(1字节)
;同理classid为INT类型占用4字节
+可以为NULL(1字节)
+ name为varchar(20)
类型占用80字节
+可以为NULL(1字节)
+可变字符(2字节)
- 可选
3.1.7、查看索引
SHOW INDEX FROM student;
-
student表索引student表索引.png
3.1.8、查看冗余索引
SELECT *
FROM sys.schema_redundant_indexes;
3.1.9、删除冗余索引
ALTER TABLE `atguigudb2`.`student` DROP INDEX `idx_age`;
ALTER TABLE `atguigudb2`.`student` DROP INDEX `idx_age_classid`;
3.1.10、再次查看索引
SHOW INDEX FROM student;
3.1.11、再次查看上述
- 使用
age
来查询
使用`age`来查询.png - 使用
age
和classid
来查询
使用`age`和`classid`来查询.png - 使用
age
和classid
和name
来查询使用`age`和`classid`和`name`来查询.png
3.1.12、小结
- 删除索引
idx_age
和idx_age_classid
后,上述查询并不受影响 - 所以直接使用
idx_age_classid_name
就可以满足上述查询
3.2、案例2——最佳左前缀法则
在MySQL建立联合索引时会遵守最佳左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配
3.2.1、使用age
和name
来查询
- SQL
EXPLAIN
SELECT SQL_NO_CACHE *
FROM student
WHERE age = 30
AND name = 'abcd';
-
EXPLAIN结果image.png
-
分析:
使用到部分索引
- 1、可选
idx_age_classid_name
索引 - 2、最终选用
idx_age_classid_name
索引 - 3、
key_len=5
:age为INT类型占用 4字节+可以为NULL占用 1 字节; - 4、
name
字段没有使用到索引
- 1、可选
3.2.2、使用classid
和name
来查询
- SQL
EXPLAIN
SELECT SQL_NO_CACHE *
FROM student
WHERE classid = 4
AND name = 'abcd';
-
EXPLAIN结果image.png
- 分析:
完全没有使用到索引
3.2.3、小结
MySQL可以为多个字段创建索引,一个索引可以包含
16个字段
。对于多列索引,过滤条件要使用索引必须按照索引建立时的顺序,依次满足,一旦跳过某个字段,索引后面的字段都无法被使用。
如果查询条件中没有使用这些字段中第 1 个字段时,多列(联合)索引不会被使用
3.3、主键插入顺序
对于一个使用
InnoDB
存储引擎的表来说,在我们没有显示的创建索引时,表中的数据实际上都是存储在聚簇索引
的叶子节点的。而记录又是存储在数据页
中的,数据页
和记录又是按照记录主键值从小到大
的顺序进行 排序,所以如果我们插入
的记录的主键值是依次增大
的话,那每插满一个数据页
就换到下一个数据页
继续插,而如果我们插入的主键值忽大忽小
的话,就比较麻烦了,假设某个数据页
存储的记录已经满了,它存储的主键值在1~100
之间
3.3.1、主键插入顺序
3.3.2、插入一条主键值为9
的记录
要在完整数据页中插入一条数据.png
- 在数据页已经满的情况下,再插入数据改怎么办?
我们需要把当前
页面分裂
成两个页面,把本页中的一些记录移动到新创建的这个页中
。页面分裂和记录移位意味着什么?意味着:性能损耗 !
所以如果我们想尽量避免这样无谓的性能损耗
,最好让插入的记录的主键值依次递增
,这样就不会发生这样的性能损耗了。所以我们建议:让主键具有AUTO_INCREMENT
,让存储引擎自己为表生成主键,而不是我们手动插入
3.4、计算、函数、类型转换(自动或手动)导致索引失效
3.4.1、查看student
表中索引,删除多余索引
- 查看当前索引
SHOW INDEX FROM student;
- 删除索引
DROP INDEX idx_age_classid_name ON student;
3.4.2、使用name
查询
- SQL
EXPLAIN
SELECT SQL_NO_CACHE *
FROM student
WHERE name LIKE 'abc%';
-
EXPLAIN没有使用索引.png
给
name
字段创建索引
CREATE INDEX idx_name ON student(name);
- 再次使用
name
查询
EXPLAIN
SELECT SQL_NO_CACHE *
FROM student
WHERE name LIKE 'abc%';
-
EXPLAIN使用 idx_name 索引.png
3.4.3、使用name
查询,由于使用函数造成索引失效
- SQL
EXPLAIN
SELECT SQL_NO_CACHE *
FROM student
WHERE LEFT(name, 3) = 'abc';
-
EXPLAIN没有使用索引.png
3.4.4、使用计算
造成索引失效
- 为
stuno
字段创建索引
CREATE INDEX idx_sno ON student(stuno);
- SQL
EXPLAIN SELECT SQL_NO_CACHE id, stuno, NAME FROM student WHERE stuno+1 = 900001;
- 索引可用
EXPLAIN SELECT SQL_NO_CACHE id, stuno, NAME FROM student WHERE stuno = 900001 - 1;
3.4.5、使用函数
造成索引失效
- 为
stuno
字段创建索引
CREATE INDEX idx_sno ON student(stuno);
- SQL
EXPLAIN
SELECT id, stuno, name
FROM student
WHERE SUBSTRING(name, 1, 3) = 'abc';
- 索引优化
EXPLAIN
SELECT id, stuno, name
FROM student
WHERE name LIKE 'abc%';
-
EXPLAINimage.png
3.4.6、返回字段影响是否使用索引
- 创建
name
和stuno
的联合索引idx_name_stuno
CREATE INDEX idx_name_stuno ON student (name, stuno);
- 查询SQL
EXPLAIN
SELECT id, stuno, name
FROM student
WHERE SUBSTRING(name, 1, 3) = 'abc';
-
EXPLAIN image.png
- 分析
虽然使用联合索引
idx_name_stuno
使查询使用了索引但是效果并不好,rows=499086
和ALL
扫描数量一样多
3.4.7、类型转换导致索引失效
- 当前索引
SHOW INDEX FROM student;
- 使用
name
搜索
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name=123;
-
EXPLAIN类型转换导致索引失效.png
索引优化:
name
查询条件name='123'
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name='123';
-
EXPLAIN使用索引.png
3.4.8、范围条件右边的列索引失效
- 清理索引
CALL proc_drop_index('atguigudb2','student');
- 创建联合索引
idx_age_classid_name(age, classid, name)
CREATE INDEX idx_age_classid_name ON student (age, classid, name);
- 查询SQL
EXPLAIN
SELECT SQL_NO_CACHE *
FROM student
WHERE student.age = 30
AND student.classid > 20
AND student.name = 'abc';
或
EXPLAIN
SELECT SQL_NO_CACHE *
FROM student
WHERE student.age = 30
AND student.name = 'abc'
AND student.classid > 20;
-
EXPLAINimage.png
-
分析:
使用部分索引
- 1、使用了
idx_age_classid_name
部分索引 - 2、
WHERE
查询条件的先后顺序
并不影响索引的是使用情况 - 3、由于
idx_age_classid_name
索引在WHERE
中由于classid
使用了>
(还有如:<、<=、>、>=、Between
)造成索引断裂(索引失效)
- 1、使用了
优化索引
- 创建新索引
CREATE INDEX idx_age_name_classid ON student (age, name, classid);
- 再次查询
EXPLAIN
SELECT SQL_NO_CACHE *
FROM student
WHERE student.age = 30
AND student.classid > 20
AND student.name = 'abc';
-
EXPLAINimage.png
3.4.9、不等于(!= 或者<>)索引失效
- 查询SQL
EXPLAIN
SELECT SQL_NO_CACHE *
FROM student
WHERE name <> 'abc';
EXPLAIN
SELECT SQL_NO_CACHE *
FROM student
WHERE name != 'abc';
-
EXPLAIN索引失效.png
使用
=
EXPLAIN
SELECT SQL_NO_CACHE *
FROM student
WHERE name = 'abc';
-
EXPLAIN使用索引.png
3.4.10、 IS NULL
可以使用索引,IS NOT NULL
无法使用索引
- 创建索引
CREATE INDEX idx_name ON student(age);
- 3.4.10.1、IS NULL 情况
EXPLAIN
SELECT SQL_NO_CACHE *
FROM student
WHERE age IS NULL;
-
EXPLAINIS NULL可以使用索引.png
3.4.10.2、IS NOT NULL 情况
EXPLAIN
SELECT SQL_NO_CACHE *
FROM student
WHERE age IS NOT NULL;
-
EXPLAINIS NOT NULL 无法使用索引.png
小结
最好在设计数据表的时候将
字段设置为 NOT NULL 约束
,比如可以将INT
类型的字段默认值设置为0
;将字符类型
的默认值设置为空字符串('')
。
- 同理,在查询中使用
NOT LIKE
也无法使用索引,导致全表扫描
3.4.11、LIKE 以通配符%开头索引失效
在使用
LIKE
关键字进行查询的查询语句中,如果匹配字符串的第一个字符为%
,索引将不会起作用;只要%
不在第一个位置,索引才会起作用
- 创建索引
CREATE INDEX idx_name ON student (name);
- 3.4.11.1、以
%
开头匹配索引失效
EXPLAIN
SELECT SQL_NO_CACHE *
FROM student
WHERE name LIKE '%ab%';
-
EXPLAIN以`%`开头匹配索引失效.png
3.4.11.2、不以
%
开头可以使用索引
EXPLAIN
SELECT SQL_NO_CACHE *
FROM student
WHERE name LIKE 'ab%';
-
EXPLAINimage.png
小结:页面搜索禁止
左模糊
或者全模糊
,如果需要请走搜索引擎来解决
3.4.12、 OR 前后存在非索引的列,索引失效
在
WHERE
子句中,如果在OR
前的条件列进行了索引,而在OR
后的条件列没有进行索引,那么索引会失效。也就是说,OR 前后的两个条件中的列都是索引时,查询中才使用索引
。因为OR
的含义就是两个只要满足一个即可,因此只有一个条件列进行了索引是没有意义的
,只要有条件列没有进行索引,就会进行全表扫描
,因此索引的条件也会失效。
- 给
age
创建索引
CREATE INDEX idx_age ON student (age);
- 查询SQL
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 10 OR classid = 100;
-
EXPLAIN因为 classid 字段上没有索引,索引查询语句没有使用索引.png
给
classid
添加索引
CREATE INDEX idx_cid ON student (classid);
- 查询SQL
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 10 OR classid = 100;
-
EXPLAINindex_merge.png
小结:
index_merge
因为
age字段
和name字段
上都有索引,所以查询中使用了索引。可以看到这里使用到了index_merge
,简单来说index_merge
就是对age字段
和name字段
分别进行了扫描,然后将这两个结果集进行了合并。这样做的好处是避免了全表扫描
。
3.4.13、数据库和表的字符集统一使用utf8mb4
统一使用utf8mb4( 5.5.3版本以上支持)兼容性更好,统一字符集可以避免由于字符集转换产生的乱码。不同的
字符集
进行比较前需要进行转换
会造成索引失效
3.4.14、小结
对于
单列索引
,尽量选择针对当前query
过滤性更好的索引在选择
组合索引
的时候,当前query
中过滤性最好的字段在索引字段顺序中,位置越靠前越好在选择
组合索引
的时候,尽量选择能够包含当前query
中的WHERE
子句中更多字段的索引在选择
组合索引
的时候,如果某个字段可能出现范围查询时,尽量把这个字段放在索引次选的最后面