以下代码演示均在linux的mysql服务中运行,我用的是CentOS9

当然依旧使用datagrip或者idea也是可以的

create database if not exists bilibili; #创建数据库bilibili use bilibili; #选择bilibili数据库,即可进行下面的代码演示 drop database if exists bilibili; #代码演示结束后删除bilibili数据库。这个小文件全部代码演示完再删 show databases; #查看是否删除完成

 

 

SQL优化 插入数据

 

insert优化

 

从这节课开始,插入多条数据的时候不能一条一条插入,因为每一次的insert都会与数据库进行建立连接、进行网络传输,性能会比较低。优化如下

 

一、插入的数据量不超过十万

 

1、批量插入,当需要插入500~1000条数据,当需要插入上万条数据时就需要分多次insert插入

 

2、手动事务提交。MySQL的事务提交方式默认是自动提交,每次insert都会重新开启和关闭事务,导致事务操作频繁

 

3、主键顺序插入。顺序插入的性能高于乱序插入,原因取决于MySQL的数据组织结构(下节课会学) (1)主键乱序插入: 2 4 3 8 6 5 7 1 9 (2)主键顺序插入: 1 2 3 4 5 6 7 8 9

 

二、插入的数据量超过10万

 

当业务需要插入几百万大量数据的时候,使用insert语句插入性能较低,此时可以使用MySQL数据库提供的load指令进行插入 使用load指令分为三个步骤,如下

 

步骤一:客户端连接服务端时,加上参数 --local-infile

 

步骤二:设置全局参数local_infile为1,表示开启从本地加载文件导入数据的开关

 

步骤三:执行load指令将准备好的数据sql.log,加载到表(tb_user)结构中

 

 

SQL优化 插入数据

 

主键优化

 

数据组织方式 在InnoDB存储引擎中,表数据都是根据主键顺序组织存放的,这种存储方式的表称为索引组织表(index organized table)简称IOT

 

页分裂 1区的大小是1M,1区有64个页,1页的大小是16k。页可以为空,也可以填充一半,也可以填充100%。每个页包含了2~n行数据,如果 一行数据过大,会导致行溢出。每个页的行数据会根据主键排列

 

聚集索引的B+树的底部节点索引是双向指针,存储的是一整行数据,并且数据大小是从左到右,即左小右大。插入的顺序可以是顺序或乱 须,但是存储进去之后必须是以左小右大的顺序来存储

 

主键顺序插入 当数据在一个页中已经写满了或写不下,就会申请第二个页继续写。这两个页之间会维护一个双向指针。同理申请更多页的话,页之间也会 有双向指针

 

主键乱序插入(页分裂) 假如当已有的两个页都写满了数据,此时需要再插入'一个数据',那么就会申请第三个页,但是由于是乱序插入,要插入的'这个数据'的大小是 在'第一个页的末尾'和'第二个页的头部'之间,那么'这个数据'怎么插入进去,申请的第三个页怎么使用,如下:####################### 第一个页的50%后的数据会全部转移到第三个页,然后要插入的'这个数据'会在第三页的转移数据后的位置插入,然后调换第一页和第三页的位置, 在调换页的时候,内部会重新设置链表指针,也就是第一页指针指向的不是第二页,第一页指针指向的是第三页。这种现象称为页分裂 总结: 当主键乱序插入的时候,就可能会出现也分裂

 

主键乱序插入可能会导致页分裂,所以在SQL优化中,我们在插入数据时,建议是顺序插入

 

删除数据(页合并) 当删除一行记录时,实际上记录并没有被物理删除,只是记录被标记(flaged)为删除并且它的空间变得允许被其他记录声明使用。当我们要删除某个页的 超过50%数据的时候,InnoDB会开始寻找最靠近的页,看看是否可以将两个页进行合并(找前面的页跟自己合并,或找后面的页跟自己合并),以优化空间使用

 

页合并(自己这个页,跟前面或后面的其中一个页合并),举例如下

1、当前面那个页只用了10%,自己的页被删了超过50%目前只用了10%,那么前面那个页就会跟自己的页合并为一个页

2、当后面那个页只用了30%,自己的页被删了超过50%目前只用了10%,那么后面那个页就会跟自己的页合并为一个页

3、当前面那个页只用了80%,自己的页被删了超过50%目前只用了30%,不可以合并,原因是80%+30%>100%

4、当后面那个页只用了80%,自己的页被删了超过50%目前只用了30%,不可以合并,原因是80%+30%>100%

 

合并之后,必然会有页空闲,空闲的页不会被回收,只是这个页里面没有数据,等待着下一批数据的进来 页合并的阈值MERGE_THRESHOLD也就是50%是可以自己设置的,默认是50%,在创建表或者创建索引时指定、

 

通过上面的学习,我们知道主键插入的顺序尽量使用顺序插入,以及在删除数据时出现的页合并。 主键索引的设计原则如下:

1、满足业务需求的情况下,尽量降低主键的长度 原因:主键索引(唯一)会在二级索引(多个)的B+树底部叶子节点上存储,如果主键索引长且二级索引多,会占用大量磁盘空间,在搜索的时候还会浪费大量的磁盘IO

2、插入顺序时,尽量选择顺序插入,选择使用auto_increment自增主键

3、尽量不要使用UUID做主键或者是其他自然主键,如身份证号

4、业务操作时,避免对主键的修改*/

 

 

SQL优化 排序order by

 

准备数据

 

order by优化

 

MySQL的排序有两种方式

1、Using filesort: 通过表的索引或全表扫描,读取满足条件的数据行,然后把数据行放在排序缓冲区sort buffer中完成排序操作,所有不是通过索引直接 返回排序结果的排序都叫filesort排序

2、Using index: 通过有序索引顺序扫描直接返回有序数据,这种情况即为using index,不需要额外排序,操作效率高*/

 

查询当前tb_user表有哪些索引

 

为profession、age、status创建联合索引

 

为email创建前缀索引

 

查询tb_user表的id、name、phone字段,且返回的数据要根据年龄进行升序排序,即上小下大

 

查看上面那行的执行计划

原因是age,phone字段没有设置索引,所以返回的Extra列为Using filesort

 

为phone和age字段创建联合索引

 

创建索引后,根据age,phone进行升序排序。我们不看执行结果,直接看执行计划explain

 

根据年龄降序排序,如果年龄相同就根据手机号降序排序

上面那行的Backward index scan表示反向扫描索引,原因:idx_user_age_phone联合索引的B+树,底部叶子节点存储的是从左到右升序的age数据,

 

此时我们需要的是倒序排序的,所以就变成反向扫描索引,也就是返回的Extra列有一个为Backward index scan

根据手机号升序排序,如果手机号相同就根据年龄升序排序

分析: 创建联合索引时,age是第一个字符,而上面的排序是把phone作为第一个字段,所以没遵守最左前缀法则,也就是只有phone索引生效,没有

走age索引,就会出现返回的Extra列的前面那个为Using index,后面那个为Using filesort

 

根据年龄升序排序,如果年龄相同就根据手机号倒序排序,

分析: 创建联合索引时我们没有指定索引的排序,那么所以默认是升序,要查phone降序,没有降序的索引,索引phone不走索引

 

如何优化上面那行的Using filesort,把Using filesort优化为Using index

分析:我们再次创建关于age和phone的联合索引,注意这次我们是指定索引的排序方式。我们上面是需要age升序,phone降序,那我们就指定一下

再次查看,结果是返回的Extra列为Using index,即可实现优化

 

如果查都是升序的呢,当然也是Using index,原因:MySQL字段为我们选择了第一次创建的idx_user_age_phone联合索引,该联合索引未指定排序,默认就是升序的

 

总结

创建联合索引没有指定联合索引的排序,默认是升序,查询的时候,如下,注意左边是我们正常的查询语句,右边是执行计划返回的Extra列的值 (1)当order by后面位置的字段还没有建立索引,联合索引之间的位置随意,联合索引可以是升序或倒序。那执行计划的Extra列的值是Using filesort (2)左边的order by后面的位置是联合索引的最左索引在前面,联合索引都写成升序 order by + 升序 = Using index (3)左边的order by后面的位置是联合索引的最左索引不在前面,联合索引都写成升序 order by + 升序 = Using index + Using filesort (4)左边的order by后面的位置是联合索引,联合索引之间的位置随意,联合索引都写成倒序 order by + 倒序 = Backward index scan + Using index

上面成立的条件是使用了覆盖索引,也就是select后面要查的的字段,必须都得建立索引(建议是联合索引)

 

总结:

1、根据排序字段建立合适的索引,多字段排序时,也遵循最左前缀法则

2、尽量使用覆盖索引,避免使用select *

3、多字段排序,一个升序一个降序,此时需要注意联合索引在创建时的规则(ASC/DESC)

4、如果不可避免的出现filesort,大数据量排序时,可以适当增大排序缓冲区大小sort_buffer_size(默认256k)

5、执行计划的Extra列出现filesort,就要适当增大排序缓冲区大小

6、当排序缓冲区的大小不足以排序我们的大数据量,那么就会在磁盘文件当中进行排序,此时性能会差*/

 

查看排序缓冲区的大小

 

 

SQL优化 分组group by

 

#索引对于分组操作的影响

 

#查看tb_user表有哪些索引

#删除目前的联合索引idx_user_pro_age_sta;

#删除前几节课创建的索引

#执行分组操作,根据profession字段分组,在没有联合索引的情况下,看一下执行计划

#返回的执行计划的type列是ALL,表示全表扫描。Extra列为Using temporary,表示用到了临时表。性能低

 

#创建索引

#执行分组操作,根据profession字段分组,在有联合索引的情况下,看一下执行计划

#返回的执行计划的type列是index,表示用到了索引。Extra列为Using index,表示用的是索引索引扫描直接返回数据。性能高

 

#执行分组操作,根据age字段分组

#Extra列为Using index; Using temporary,虽然用到了索引,到时出现了Using temporary,表示用到了临时表。性能较低 #原因:group by后面没有使用了profession,profession是我们创建联合索引的最左索引,不满足最左前缀法则,Extra走索引但是会出现临时表

 

#执行分组操作,根据profession、age字段分组

#返回的执行计划的type列是index,表示用到了索引。Extra列为Using index,表示用的是索引索引扫描直接返回数据。性能高 #原因:group by后面使用了profession,profession是我们创建联合索引的最左索引,满足最左前缀法则,Extra直接走索引

 

#执行分组操作,根据age字段分组。分组之前先对profession字段进行过滤。只查软件工程的年龄,并对年龄进行分组操作

#返回的执行计划的type列是index,表示用到了索引。Extra列为Using index,表示用的是索引索引扫描直接返回数据。性能高 #原因:满足最左前缀法则。所以我们在where或group by后面的任意一个位置出现profession即可满足最左前缀法则,不一定非要group by后面出现

 

总结:

1、在分组操作时,可以通过索引来提供效率

2、在分组操作时,索引的使用也是需要满足最左前缀法则,否则不走索引

 

 

SQL优化 分页limit

 

limit优化 一个常见又非常头疼的问题就是limit 2000000,10。此时需要MySQL排序前2000010记录,仅仅返回2000000(不包含)~2000010(包含)的记录,其他 记录丢弃,查询排序的代价非常大 解决:覆盖索引+子查询的方式来优化 优化思路:一般分页查询时,通过创建覆盖索引能够比较好地提高性能,可以通过覆盖索引加子查询的方式进行优化

 

例如如下 原语句:select * from tb_sku limit 1000000,10; 优化后:select s.* from tb_sku as s,(select id from tb_sku limit 1000000,10) as a where s.id = a.id;

 

查询速度大概能缩短一半时间

 

-- 准备数据,重新创建我们200万数据的tb_sku表。如果这张表你还没删就不用重新建了,因为我上章节学完就删了sku表,所以才需要重新建

 

-- 为tb_sku表导入数据。下面三行需要在linux终端、xshell终端、Windows命令行其中一个才能执行,datagrip里面是不能执行的

 

查看tb_sku有多少条记录

 

对tb_user表进行分页操作。从第1条记录开始(包含第1条),返回一页,一页返回10条记录

 

对tb_user表进行分页操作。从第11条记录开始(包含第11条),返回一页,一页返回10条记录

 

上面两条分页操作的语句看似都很快,但是当我们查询从第1000001记录开始,返回一页,一页返回10条记录。就变得慢了

 

对于limit分页查询来说,往后查的数据量越大,效率越低

 

解决:通过覆盖索引+子查询的方式来优化。如下

select id from tb_sku limit 1000000,10;#覆盖索引,优点不需要回表 select * from tb_sku where id in (select id from tb_sku limit 1000000,10);#覆盖索引+子查询。当前版本不支持这种语法,解决如下

 

原因: in后面出现了limit导致当前版本不支持。分析: 我们可以把子查询返回的数据当作一张表,然后使用多表联查进行解决

 

 

SQL优化 总数据量的聚合函数count

 

count优化

 

MyISAM引擎: 把一个表的总行数存在了磁盘上,因此执行count()的时候会直接返回这个数,效率很高(前提条件是查询的时候不能有where条件) InnoDB引擎: 比较麻烦,执行count()的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数

 

所以对于MySQL的InnoDB引擎来说,目前没有比较好的优化方法,但是有肯定是有的,优化思路: 自己计数

 

解决: 借助可以value形式的内存级别的数据库,例如redis,当我们执行插入数据时,把这个计数+1,当我们要从表中删除数据时,把这个计数-1 由我们自己去维护计数。由于过程比较繁琐,想学的就考虑去redis相关教程学哦

 

count()是一个聚合函数,对于返回的结果集,一行行地判断,如果count函数的参数不是null,累计值就+1,否则不加,最后返回累计值 count的几种使用方式,以及这几种方式之间的性能差异,如下

 

1、count(*)

分析如下 InnoDB引擎并不会把全部字段取出来,而是专门做了优化,不取值,服务层直接按行进行累加

 

2、count(主键)

分析如下 InnoDB引擎会遍历整张表,把每一行的主键id值都取出来,返回给服务层。服务层拿到主键后,直接按行进行累加,不用判断是不是null(主键不可能为null)

 

3、count(字段)

分析如下 (1)该字段有not null约束。InnoDB引擎遍历整张表把每一行的字段值都取出来,返回给服务层,服务层判断是否为null,如果不为null,就计数累加 (2)该字段没有not null约束。InnoDB引擎遍历整张表把每一行的字段值都取出来,返回给服务层,直接按行进行计数累加

 

4、count(任意数字)

分析如下 InnoDB引擎遍历整张表,但不取值。服务层对于返回的每一行,放一个数字“1”进去,直接按行进行累加

 

总结:按照效率给上面四种使用方式进行评估: count(字段) < count(主键) < count(任意数字) ≈ count(),所以尽量使用count()*/

 

 

SQL优化 update

 

-- 准备数据

 

查看course表里面的数据

 

InnoDB引擎的三大特性: 事务、外键、行级锁(默认的事务隔离级别是行锁)。如下是开启事务,模拟事务操作

#开启事务有两种方式,分别是start transaction、begin

 

如下是正常情况

#在当前会话开启事务,为了方便区分,我们叫这个会话为服务端 begin;

再开启一个会话(需要在linux终端、xshell、windows命令行其中之一才能进行),并且开启事务,为了方便区分,我们叫这个会话为客户端

#然后回到服务端,执行一条update语句

#此时id为1的一行就会被锁住,因为InnoDB引擎默认使用的是行锁,只要事务没提交,行锁就不会被释放,也就是一直锁住 #接着去客户端,也执行一条update语句

#然后去服务端执行一下事务提交

#然后去客户端执行一下事务提交

#在服务端或客户端查询一下当前course表的数据

 

 

如下是不正常情况

#在服务端重新开启事务

#然后在服务端,执行一条update语句

#此时name为PHP的一行就会被锁住,因为InnoDB引擎默认使用的是行锁,只要事务没提交,行锁就不会被释放,也就是一直锁住 #接着去客户端并重新开启事务

#在客户端执行一条update语句

#但是,会发现,上面那条语句被堵塞了,执行不出结果。原因:是服务端在执行update语句的时候,name字段没有设置索引,就会使原来的行锁变成表锁 #在客户端未提交事务时,course表是被锁住的,其他会话无法对这个表进行修改 #然后去服务端执行一下事务提交

#此时客户端那条被堵塞的语句才会正常执行 #最后在客户端提交一下事务,数据就可以同步了

 

如何解决不正常的情况

#在任意会话,为name字段设置索引

#去服务端开启事务,并执行一条更新语句

#前去客户端开启事务,并执行一条更新语句,发现此时不堵塞了,执行的这条更新语句可以执行成功

#在服务端和客户端都执行一次提交事务,即可同步数据

 

 

总结

#在执行update语句的时候,要根据索引字段进行更新,否则进行非索引字段更新之后,原本的行锁就会升级为表锁,锁表会降低并发性能 #也就是更新语句的where条件要是索引字段,并且索引不能失效,如果索引失效了一样会升级为表锁