MySQL索引优化二
学习来源:图灵课堂
https://vip.tulingxueyuan.cn
分页优化
一般来说,我们的后台管理系统都是有翻页功能的,并且有时候还要加上一些筛选过滤条件;如果对查询没有经过特别的优化,那么就会发现翻页越往后就越慢,这是为什么呢?
因为如果单独是使用limit,例如limit 90000, 5;这个并不是从第90000条数据开始,往后查询5条数据,而是从第一条开始查,查到90005条数据,然后抛弃前90000条数据,所以效率比较差。
针对这样的查询可以进行优化的。针对的是select *
1.如果主键索引是完全自增且连续的,并且查询结果是使用主键排序的,就可以改成 where id > 90000 limit 5;如此是可以的,但是这个条件太苛刻了,实际上很难满足,那么就不用这种方式。
2.针对没有选择过滤条件的查询,那么就去主键索引查询,也可以避免频繁的回表操作。如果加上的有过滤条件,并且这些过滤条件是有联合索引的, 那么就可以修改为使用到联合索引查询到主键值,然后再通过这些主键值去回表查,回表次数也会少很多;例如:
select * from table where id in (select id from table where 联合索引字段 = 条件 order by 联合索引字段 limit xx)
或者 select * from table t inner join (select id from table where 联合索引字段 = 条件 order by 联合索引字段 limit xx) ta on t.id = ta.id
如果扫描整个二级索引无法获取到全部查询的值,并且还要频繁的回表,那么优化器就会直接全表扫描,放弃使用索引。
总结来说,分页优化,就是如果是复杂查询,尽量使用到联合索引,先过滤出来主键值,这样可以过滤掉很多数据,然后再拿这些主键值去回表查询,这样效率就会高很多。
join关联优化
CREATE TABLE `t1` ( `id` int(11) NOT NULL AUTO_INCREMENT, `a` int(11) DEFAULT NULL, `b` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_a` (`a`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; create table t2 like t1; -- 插入一些示例数据 -- 往t1表插入1万行记录 drop procedure if exists insert_t1; delimiter ;; create procedure insert_t1() begin declare i int; set i=1; while(i<=10000)do insert into t1(a,b) values(i,i); set i=i+1; end while; end;; delimiter ; call insert_t1(); -- 往t2表插入100行记录 drop procedure if exists insert_t2; delimiter ;; create procedure insert_t2() begin declare i int; set i=1; while(i<=100)do insert into t2(a,b) values(i,i); set i=i+1; end while; end;; delimiter ; call insert_t2();
注意t1和t2表结构完全一致;并且t1一万条数据,t2一百条数据。
嵌套循环连接 Nested-Loop Join(NLJ) 算法
一次一行的从第一张表(驱动表)获取数据,然后根据关联字段,去另外一张表(被驱动表)中取出相关的行,展示数据。通过explain看,如果id一致,那么第一行就是第一张表驱动表,第二行才是被驱动表;如果id不一致,那么id值大的就是驱动表。
1.也就是说t2是驱动表,t1是被驱动表;优化器针对查询进行了优化,让小表去驱动大表;所以说,排在前面的表并不一定就是小表,排在inner后面的不一定是大表。
2.当使用left join的时候,已经明确指定了left前面的是驱动表,left之后的是被驱动表;使用right join的时候,right前面的是被驱动表,right后面的是驱动表;此时不管是大表还是小表了。
EXPLAIN SELECT * FROM t1 LEFT JOIN t2 on t1.id = t2.id; EXPLAIN SELECT * FROM t1 RIGHT JOIN t2 on t1.id = t2.id;
3.使用了 NLJ算法。一般 join 语句中,如果执行计划 Extra 中未出现 Using join buffer 则表示使用的 join 算法是 NLJ。
上面的执行流程:
- 首先从驱动表中取出一条符合要求的数据;
- 然后取出关联条件,去被驱动表中查询;
- 然后从被驱动表查询出来数据,进行组装;
- 重复上述三个步骤。
整个过程中,会去t2查询一百行,同时因为关联条件是id,是主键索引,那么也可以认为去t1表也是查询一百行;加起来就是一共查询二百行;使用索引就会使用这种方式,这种方式比较高效。
针对有索引的关联查询会使用这样的方式。这种方式是磁盘扫描。
基于块的嵌套循环连接 Block Nested-Loop Join(BNL)算法
会将驱动表的数据加载到join_buffer中,然后去扫描被驱动表,将被驱动表的每一行取出来放到buffer中进行比对。buffer中的数据是无序的,极端情况下,里面的数据都会全部扫描一次。
但是要注意,在buffer中比对也是比较快的,速度要高于从磁盘中读取数据。
MySQL8.0版本的是hash join方式;5.7版本是BNL方式。
EXPLAIN select * from t1 inner join t2 on t1.b= t2.b;
如果是BNL的方式分析:
问题:为什么没有索引的要使用BNL,而不使用NLJ?
因为如果关联字段没有索引,如果使用NLJ,就要进行多次的磁盘扫描读取数据,磁盘的IO是很耗时并且消耗性能的;如果使用了BNL,虽然也是要进行多次比较,但是这个是比较过程是在内存中的,可以大幅减少磁盘IO操作。BNL会更快速。
所以针对有索引的关联查询,可以使用NLJ;没有走索引的关联查询可以使用BNL。
针对关联查询的优化总结
- 被关联的字段最好加上索引;或者是使用有索引的列进行关联;被驱动的表关联字段最好加上索引。
- 尽量保证小表驱动大表,如果已经提前确认了大小表,可以使用straight_join写法固定连接驱动方式,省去mysql优化器自己判断的时间。
straight_join解释:
- straight_join功能同join类似,但能让左边的表来驱动右边的表,能改表优化器对于联表查询的执行顺序。
- 比如:select * from t2 straight_join t1 on t2.a = t1.a; 代表指定mysql选着 t2 表作为驱动表。
- straight_join只适用于inner join,并不适用于left join,right join。(因为left join,right join已经代表指定了表的执行顺序)
- 尽可能让优化器去判断,因为大部分情况下mysql优化器是比人要聪明的。使用straight_join一定要慎重,因为部分情况下人为指定的执行顺序并不一定会比优化引擎要靠谱。
针对小表的定义:
并不是说一张表1万条数据就一定是大表,要看过滤之后的数据有多少,两张表过滤之后,符合要求数据量最少的才是小表。
in和exsits优化
原则上是尽量小标驱动大表去查询。
select * from A where id in (select id from B)
注意使用in的时候,in后面的查询先执行,也就是说小表放到in后面,大表放到in前面;如上所示,B先执行,A后执行。
select * from A where exists (select 1 from B where B.id = A.id)
使用exist的时候,exist前面的先执行,小表要放到前面,大表放到exist后面。先查询A中符合的数据,然后去B中匹配。
count(*)查询优化
EXPLAIN select count(1) from t1; EXPLAIN select count(id) from t1; EXPLAIN select count(a) from t1; EXPLAIN select count(*) from t1;
查询结果如上图所示:表明查询结果类似,并没有什么明显的差距。
注意:count(字段),如果字段有null值,是不会统计到这行数据的。这个要注意。
count(1)和count(字段)两个类似,唯一不同的就是count(1)并不会去索引或者表中获取到字段值,而是直接进行统计计算,这样就可以减少查询的数据量,所以效率比较高一些。
count(id)为什么也使用了二级索引,而不使用主键索引;因为一般来说主键索引都会带上本行的全部数据,查询的数据量较大,经过MySQL优化器之后,认为查询二级缓存可以减少查询数据量,提升查询效率,所以count(id)也是用的二级索引。
高版本的MySQL,针对count(*)专门进行了优化,不会取出来字段值,而是直接进行查询,查询效率是非常高的。
针对查询总数据量的优化
1. 如果是myisam引擎,会专门去维护一个全表数据量,count(*)查询效率非常高;为什么innodb没有呢?因为innodb为了解决不可重复读,引入了MVCC机制,当前读和快照读读取的数量可能不一致。
2. innodb引擎下如果要知道一个大概的数据量,可以使用
show table STATUS LIKE '表名';
这只是一个大概的预估值。
3. 将总数据量维护在redis中,但是这个并不能保证百分百的准确性,因为不在同一个事务中。
4. 引入另外一张表。这个看业务要求。
数据类型的选择
首先是要确认是什么大类型:数字,字符串,时间,二进制;
然后确认具体的类型:有无符号,范围大小,定长变长,精度等;
一个原则:在确保业务的情况下,要尽量选择符合的数据类型,没有必要为了什么所谓的准则去设计字段。
1. 整数类型
int从小到大分为tinyint,smallint,int,bigint;1个字节,2两个字节,4个字节,8个字节;如果没有符合,那就是从0到最大值;如果有符号,那就是从负数到整数。
- 如果整形数据没有负数,如ID号,建议指定为UNSIGNED无符号类型,容量可以扩大一倍。
- 建议使用TINYINT代替ENUM、BITENUM、SET。
- 避免使用整数的显示宽度,也就是说,不要用INT(10)类似的方法指定字段显示宽度,直接用INT。
- DECIMAL最适合保存准确度要求高,而且用于计算的数据,比如价格。但是在使用DECIMAL类型的时候,注意长度设置。
- 建议使用整形类型来运算和存储实数,方法是,实数乘以相应的倍数后再操作。
- 整数通常是最佳的数据类型,因为它速度快,并且能使用AUTO_INCREMENT。
2. 日期和时间
日期主要是date,datetime,timestamp三种;占用空间大小分别为3字节,8字节,4字节。
格式分别为YYYY-MM-DD,YYYY-MM-DD HH:MM:SS,YYYYMMDDhhmmss。
MySQL高版本进行了timestamp的优化扩容(5.6.4及之后),数据量也变大了。所以首选是timestamp;如果空间没有压力,使用datetime最好。
- 对于
DATETIME
,如果你不指定默认值,那么它的默认值是NULL
。 - 对于
TIMESTAMP
,如果你不指定默认值,那么它的默认值是当前的日期和时间。 - 在选择使用
DATETIME
还是TIMESTAMP
时,你应该根据你的具体需求来决定。如果你需要存储一个与时区无关的日期和时间,或者你需要一个更大的日期范围,那么DATETIME
可能是一个更好的选择。如果你需要自动更新日期和时间,或者你需要节省存储空间,那么TIMESTAMP
可能更适合你。
字符串类型
char:定长,0-255字节,当插入的数据不足长度时,后面会加上空格补齐,但是在进行运算的时候会去掉空格。
varchar:变长,0-65535 字节,varchar(n)中的n代表最大列长度,插入的字符串实际长度不足n时不会补充空格。
text:极大字符串,可以保存文本,长文本
- 字符串的长度相差较大用VARCHAR;字符串短,且所有值都接近一个长度用CHAR。
- CHAR和VARCHAR适用于包括人名、邮政编码、电话号码和不超过255个字符长度的任意字母数字组合。那些要用来计算的数字不要用VARCHAR类型保存,因为可能会导致一些与计算相关的问题。换句话说,可能影响到计算的准确性和完整性。
- 尽量少用BLOB和TEXT,如果实在要用可以考虑将BLOB和TEXT字段单独存一张表,用id关联。
- BLOB系列存储二进制字符串,与字符集无关。TEXT系列存储非二进制字符串,与字符集相关。BLOB和TEXT都不能有默认值。
`id` TINYINT(2) UNSIGNED ZEROFILL
这个2表示补齐长度的意思,当存入的值小于10的时候,例如是1,查出来的就是01,实际存储的值还是1,并且存储的范围也没有变,就是在MySQL输出的时候前面补齐加0.没有多少实际意义。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix