【MySQL】-【索引优化与查询优化】


在这里插入图片描述

数据准备

学员表 插 50万 条, 班级表 插 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 ;
#假如要删除
#drop function rand_string;

随机产生班级编号

#用于随机产生多少到多少的编号
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 ;
#假如要删除
#drop function rand_num;

步骤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");

索引失效案例

在这里插入图片描述

全值匹配我最爱

系统中经常出现的SQL语句如下:

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';

建立索引前执行:(关注执行时间)

SELECT SQL_NO_CACHE * FROM student WHERE age=30 AND classId=4 AND NAME = 'abcd';#0.149s

建立索引:

CREATE INDEX idx_age ON student(age);
CREATE INDEX idx_age_classid ON student(age,classId);
CREATE INDEX idx_age_classid_name ON student(age,classId,NAME);

再执行上述select语句,发现使用的是idx_age_classid_name索引,因为使用前两个索引还会进行一次回表,效率较低

结论:尽量对where条件中的所有字段建立联合索引,且索引顺序不变

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
最佳左前缀:假如你为a、b、c字段创建了联合索引,那么只有以下情况能用到该索引:

  1. 先检索a,再检索b,再检索c
  2. 先检索a,再检索b
  3. 检索a

在这里插入图片描述

请添加图片描述

计算、函数、类型转换(自动或手动)导致索引失效

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'; 
# 创建索引
CREATE INDEX idx_name ON student(NAME);

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

类型转换导致索引失效

请添加图片描述

结论:设计实体类属性时,一定要与数据库字段类型相对应,否则就会出现类型转换的情况

范围条件右边的列索引失效

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

不等于(!= 或者<>)索引失效

在这里插入图片描述

is null可以使用索引,is not null无法使用索引

在这里插入图片描述
is null相当于等于某个值,is not null相当于不等于某个值,当使用is null时数据库直接找null,使用is not null时,数据库会把数据一条条取出来看

like以通配符%开头索引失效

开头字母都不确定,我怎么去b+树中找?所以只能全表扫描
在这里插入图片描述
在这里插入图片描述

OR前后存在非索引的列,索引失效

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

数据库和表的字符集统一使用utf8mb4

统一使用utf8mb4(5.5.3版本以上支持)兼容性更好,统一字符集可以避免由于字符集转换产生的乱码。不同的字符集进行比较前需要进行转换(会使用转换函数)会造成索引失效。

练习及一般建议

在这里插入图片描述
在这里插入图片描述
一般性建议:

  1. 对于单列索引,尽量选择针对当前query过滤性更好的索引,假如你where条件中用到了很多字段,最好为这些字段建立联合索引
  2. 在选择组合索引的时候,当前query中过滤性最好的字段在索引字段顺序中,位置越靠前越好,假如你where条件中有很多条件,其中一个是:【性别=女】,如果把它排在第一个条件,那么只能过滤50%的数据,但是如果有一个条件能过滤90%的数据,建议把这个条件放在【性别=女】这一条件之前
  3. 在选择组合索引的时候,尽量选择能够包含当前query中的where子句中更多字段的索引。
  4. 在选择组合索引的时候,如果某个字段可能出现范围查询时,尽量把这个字段放在索引次序的最后面。

关联查询优化

数据准备

采用左外连接

右外连接同左外连接,此处不在再赘述
由于现在还没加索引,所以是全表扫描。假设type有20条数据,book有30条数据,先从type中取一条数据,然后根据连接条件去book表中查找符合条件的数据,会找30次,type会取20次数据,所以一共执行了600次,类似于嵌套循环。总是遍历book表,所以优化器使用join buff把数据缓存起来,提升检索速度

因为是左外连接,所以左表的数据一定全要,主要还是在右表中做筛选,所以一定要给右表的字段建立索引
请添加图片描述
请添加图片描述
在这里插入图片描述

采用内连接

结论:SELECT * FROM type INNER JOIN book ON type.card=book.card;

  1. 如果type.card和book.card都没有索引,则小表驱动大表
  2. 如果type.card没有索引,book.card有索引,则book是被驱动表
  3. 如果type.card有索引,book.card没有索引,则type是被驱动表
  4. 如果type.card和book.card都有索引,则小表驱动大表

join语句的原理

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
由读取记录数可知,因为a+b*a=a(b+1),所以影响比较大的是a表的数量,所以a表数量越小越好,所以小表驱动大表
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
join buffer中要放where条件的字段、select的字段
在这里插入图片描述
straight_join:优化器不要破坏我驱动表、被驱动表的顺序,straight_join左边的字段就是驱动表,右边的字段就是被驱动表
这里推荐使用第一种就是因为第一种t1表只有一个字段需要放在buffer中,但是t2表需要把表中所有字段都放在buffer中,虽然可能t1表实际比t2表字段多得多,但是在这句sql中,还是将t1作为驱动表
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

子查询优化

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

排序优化

排序优化

在这里插入图片描述

测试

在这里插入图片描述
在过程一中,还没加索引,所以肯定不会使用索引
在这里插入图片描述
过程二中,不添加limit,索引失效,因为你要查询所有字段,如果使用索引:先根据age和classid排序,然后回表,查询所有数据获取每条数据的其他字段和值,效率更低,不如直接全表查询一次到位

EXPLAIN SELECT SQL NO CACHE age, classid FROM student ORDER BY age, classid;
# 也使用上了索引,因为查询的字段就是索引,不用回表

然后添加上limit,使用了索引,先根据age和classid排序,取前十条,再对这10条回表,效率比全表查询更高
在这里插入图片描述
根据最佳左前缀原则,前两个失效,后三个有效
在这里插入图片描述
除了最后一个使用索引(倒着取数),其余都不使用索引
在这里插入图片描述
第一条和第二条,where使用索引,order by没有使用索引,因为先执行where,如果过滤了大部分数据,剩下没多少数据了,在进行order by时就不会使用索引了
第三条没有使用索引,因为classid不符合最佳左前缀原则,没有索引
第十条使用了age、classid联合索引,他先根据age排序,然后根据where过滤,然后再取前10条
在这里插入图片描述

案例实战

在这里插入图片描述
在这里插入图片描述
方案一这样建是因为stuno是考虑范围的字段,所以把它排出了,方案一的执行结果:
在这里插入图片描述
只使用了where索引
在这里插入图片描述
方案二执行结果:
在这里插入图片描述
使用的是filesort,索引只用了age和stood,这是因为where条件过滤后,数据就没几条了,没必要再使用order by的索引了
这三个方案竟然
在这里插入图片描述
可以

filesort算法:双路排序和单路排序

在这里插入图片描述
双路排序:假如某条sql根据age排序:order by age,第一次磁盘的扫描:找到age字段,将所有的age列加载到内存中,然后再内存中针对age排序,排好序后,再根据age找到表中完整的那条数据,再把完整的那条数据加载到内存中
单路排序:把所有的age列读进磁盘然后排序
建议使用index排序,但是如果一定要使用filesort,可以从以下方面优化:
在这里插入图片描述
在这里插入图片描述

group by优化

在这里插入图片描述

优化分页查询

在这里插入图片描述
在这里插入图片描述

覆盖索引

一、什么是覆盖索引:我们前面讲非聚簇索引的创建时(当时为c2列建立了非聚簇索引),在进行查找时,先根据非聚簇索引找到主键,然后再拿着主键去聚簇索引上查其余内容,此时需要使用两棵b+树,这个过程也称为回表,就是要回到聚簇索引中查找。那假如我要查的字段都在非聚出索引中,那就不用回表,这就是覆盖索引,即一个索引包含了满足查询结果的数据。

简单说就是, 索引列+主键 包含 SELECT 到 FROM之间查询的列 。

二、案例:

  1. 执行530行是不会使用索引的,这在前面我们就讲过了,执行533行却会使用索引,因为存在索引覆盖,不用回表,执行效率更高。所以我们前面讲的那些规则并不是绝对的。
    请添加图片描述
  2. 执行537行不会使用索引,执行539行会使用索引,因为存在索引覆盖,不用回表,执行效率更高
    请添加图片描述
    三、覆盖索引的利弊
    请添加图片描述
  3. 好处:避免innodb表进行索引的二次查询(回表)
  4. 把随机io变成了顺序io:已为c2列建立非聚簇索引,假如现在需要查询2<c2<20范围的数据,那肯定会在非聚簇索引的叶子节点查出很多数据,并且这些数据都是连续的(这就是顺序io),如果需要回表,那我们会拿着这一堆数据的主键去聚簇索引中查询,这一堆主键可能不是连续的,那我们在聚簇索引中查询的时候,就是随机io了。所以砍掉回表也砍掉了随机io

索引下推(ICP)

一、案例:

  1. 案例1:key1是非主键
    请添加图片描述
    底层实现:假如s1表中有10000条数据,首先建立key1的非聚簇索引,然后根据key1>'z'查询非聚簇索引,假如查出1000条数据,然后根据key1 like '%a'查询这1000条数据,假如查出100条,然后对这100条数据进行回表
  2. 举例2:创建表并插入数据:
    请添加图片描述
    执行以下语句:
    请添加图片描述
    索引下推常出现在联合索引中,如本例,底层实现:假如表people中有10000条数据,先根据zipcode='000001'查找非聚簇索引,假如查出来1000条,再根据lastname like '%张%'从这1000条数据中查询,假如查出来100条,因为没有给address创建索引,所以接下来对这100条数据进行回表,查询满足条件的数据,这就是索引下推。
    显然索引下推减少了回表的次数,也减少了随机io的次数
    二、索引下推的开启与关闭:
    请添加图片描述
    最后一个单词是condition
    三、索引下推的开启/关闭的性能对比
    创建存储过程,向people表中添加1000000条数据,测试icp开启与关闭状态下的性能
    请添加图片描述
    首先请添加图片描述
    四、ICP的使用条件
    请添加图片描述
    只有当需要回表的时候,icp才有意义

其他查询优化策略

exist和in的区别

在这里插入图片描述
in是从括号中送出一条数据来给外面用,exist是从外面送一条数据到括号中执行,所以,如果外面的表小,里面的表大,则使用exist,外面的表大,里面的表小使用in。

COUNT(*)与COUNT(具体字段)效率

本题只考虑mysql统计表中有多少条数据的情况
请添加图片描述

关于SELECT(*)

请添加图片描述

LIMIT 1 对优化的影响

请添加图片描述

多使用COMMIT

请添加图片描述

淘宝数据库的主键如何设计

自增ID的问题

自增ID做主键,简单易懂,几乎所有数据库都支持自增类型,只是实现上各自有所不同而已。自增ID除了简单,其他都是缺点,总体来看存在以下几方面的问题:

  1. 可靠性不高:存在自增ID回溯的问题,这个问题直到最新版本的MySQL 8.0才修复。
  2. 安全性不高:对外暴露的接口可以非常容易猜测对应的信息。比如:/User/1/这样的接口,可以非常容易猜测用户ID的值为多少,总用户数量有多少,也可以非常容易地通过接口进行数据的爬取。
  3. 性能差:自增ID的性能较差,需要在数据库服务器端生成。
  4. 交互多:业务还需要额外执行一次类似last_insert_id()的函数才能知道刚才插入的自增值,这需要多一次的网络交互。在海量并发的系统中,多1条SQL,就多一次性能上的开销。
  5. 局部唯一性:最重要的一点,自增ID是局部唯一,只在当前数据库实例中唯一,而不是全局唯一,在任意服务器间都是唯一的。对于目前分布式系统来说,这简直就是噩梦。

业务字段做主键(了解)

为了能够唯一地标识一个会员的信息,需要为会员信息表设置一个主键。那么,怎么为这个表设置主键,才能达到我们理想的目标呢? 这里我们考虑业务字段做主键。 表数据如下:
请添加图片描述
在这个表里,哪个字段比较合适呢?

  1. 选择卡号(cardno):会员卡号(cardno)看起来比较合适,因为会员卡号不能为空,而且有唯一性,可以用来标识一条会员记录。不同的会员卡号对应不同的会员,字段“cardno”唯一地标识某一个会员。如果都是这样,会员卡号与会 员一一对应,系统是可以正常运行的。但实际情况是,会员卡号可能存在重复使用的情况。比如,张三因为工作变动搬离了原来的地址,不再到商家的门店消费了 (退还了会员卡),于是张三就不再是这个商家门店的会员了。但是,商家不想让这个会员卡空着,就把卡号是“10000001”的会员卡发给了王五。从系统设计的角度看,这个变化只是修改了会员信息表中的卡号是“10000001”这个会员信息,并不会影响到数据一致性。也就是说,修改会员卡号是“10000001”的会员信息, 系统的各个模块,都会获取到修改后的会员信息,不会出现“有的模块获取到修改之前的会员信息,有的模块获取到修改后的会员信息, 而导致系统内部数据不一致”的情况。因此,从信息系统层面上看是没问题的。但是从使用系统的业务层面来看,就有很大的问题 了,会对商家造成影响。 比如,我们有一个销售流水表(trans),记录了所有的销售流水明细。2020 年 12 月 01 日,张三在门店购买了一本书,消费了89元。那么,系统中就有了张三买书的流水记录,如下所示:
    请添加图片描述
    如果会员卡“10000001”又发给了王五,我们会更改会员信息表。导致这次查询得到的结果是:王五在 2020 年 12 月 01 日,买了一本书,消费 89 元。显然是错误的
    结论:千万不能把会员卡号当做主键。
  2. 选择会员电话 或 身份证号
    (1)在实际操作中,手机号也存在被运营商收回 ,重新发给别人用的情况。
    (2)身份证号属于个人隐私,顾客不一定愿意给你。要是强制要求会员必须登记身份证号,会把很多客人赶跑的。其实,客户电话也有这个问题,这也是我们在设计会员信息表的时候,允许身份证号和电话都为空的原因。

所以,建议尽量不要用跟业务有关的字段做主键。毕竟,作为项目设计的技术人员,我们谁也无法预测在项目的整个生命周期中,哪个业务字段会因为项目的业务需求而有重复,或者重用之类的情况出现。

淘宝的主键设计

在淘宝的电商业务中,订单服务是一个核心业务。请问, 订单表的主键淘宝是如何设计的呢“是自增ID吗?打开淘宝,看一下订单信息:

经验:刚开始使用 MySQL 时,很多人都很容易犯的错误是喜欢用业务字段做主键,想当然地认为了解业 务需求,但实际情况往往出乎意料,而更改主键设置的成本非常高。
请添加图片描述
从上图可以发现,订单号不是自增ID!我们详细看下上述4个订单号:

1550672064762308113
1481195847180308113
1431156171142308113
1431146631521308113

订单号是19位的长度,且订单的最后5位都是一样的,都是08113。且订单号的前面14位部分是单调递增 的。大胆猜测,淘宝的订单ID设计应该是:订单ID = 时间 + 去重字段 + 用户ID后6位尾号。这样的设计能做到全局唯一,且对分布式系统查询及其友好。

推荐的主键设计

非核心业务:对应表的主键自增ID,如告警、日志、监控等信息。
核心业务:主键设计至少应该是全局唯一且是单调递增。全局唯一保证在各系统之间都是唯一的,单调递增是希望插入时不影响数据库性能。
这里推荐最简单的一种主键设计:UUID。 UUID的特点:全局唯一,占用36字节,数据无序,插入性能差。

认识uuid

MySQL数据库的UUID组成:UUID = 时间+UUID版本(16字节)- 时钟序列(4字节) - MAC地址(12字节),我们以UUID值e0ea12d4-6473-11eb-943c-00155dbaa39d举例:
请添加图片描述
mysql中的uuid是用字符串来存储的

  1. 为什么UUID是全局唯一的?
    在UUID中时间部分占用60位,存储的类似TIMESTAMP的时间戳,但表示的是从1582-10-15 00:00:00.00 到现在的100ns的计数。可以看到UUID存储的时间精度比TIMESTAMPE更高,时间维度发生重复的概率降 低到1/100ns。
    时钟序列是为了避免时钟被回拨导致产生时间重复的可能性。MAC地址用于全局唯一。
  2. 为什么UUID占用36个字节?
    UUID根据字符串进行存储,设计时还带有无用"-"字符串,因此总共需要36个字节。
  3. 为什么UUID是随机无序的呢?
    因为UUID的设计中,将时间低位放在最前面,而这部分的数据是一直在变化的,并且是无序。

改造UUID

若将时间高低位互换,则时间就是单调递增的了,也就变得单调递增了。MySQL 8.0可以更换时间低位和 时间高位的存储方式,这样UUID就是有序的UUID了。
MySQL 8.0还解决了UUID存在的空间占用的问题,除去了UUID字符串中无意义的"-"字符串,并且将字符 串用二进制类型保存,这样存储空间降低为了16字节。
可以通过MySQL8.0提供的uuid_to_bin函数实现上述功能,同样的,MySQL也提供了bin_to_uuid函数进行 转化:

现在常用的是雪花算法——来自弹幕

猜你喜欢

转载自blog.csdn.net/CaraYQ/article/details/129168300