[MySQL数据库] SQL优化
🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343
🏵️热门专栏:
🧊 Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm=1001.2014.3001.5482
🍕 Collection与数据结构 (93平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482
🧀线程与网络(97平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482
🍭MySql数据库(95平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482
🍬算法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12676091.html?spm=1001.2014.3001.5482
🍃 Spring(97平均质量分)https://blog.csdn.net/2301_80050796/category_12724152.html?spm=1001.2014.3001.5482
🎃Redis(97平均质量分)https://blog.csdn.net/2301_80050796/category_12777129.html?spm=1001.2014.3001.5482
🐰RabbitMQ(97平均质量分) https://blog.csdn.net/2301_80050796/category_12792900.html?spm=1001.2014.3001.5482
感谢点赞与关注~~~
目录
- 1. 插入数据
- 1.1 Insert
- 1.2 大批量插入数据
- 2. 主键优化
- 3. order by优化
- 4. group by优化
- 5. limit优化
- 6. count优化
- 6.1 概述
- 6.2 count用法
- 7. update优化
1. 插入数据
1.1 Insert
如果我们需要一次性往数据库表中插入多条数据,可以从下面三个方面进行优化.
insert into tb_test values(1,'tom');
insert into tb_test values(2,'cat');
insert into tb_test values(3,'jerry');
.....
- 优化方案一:
批量插入数据
Insert into tb_test values(1,'Tom'),(2,'Cat'),(3,'Jerry');
- 减少网络开销,单批次插入比多批次插入减少了客户端和服务器之间的往返通信.
- 降低SQL解析成本,MySQL只需要在服务层解析一次SQL语句,而不是多次.
- 减少日志写入,InnoDB的事务日志(redo log)可以批量写入
- 优化方案二:
手动控制事务
start transaction;
insert into tb_test values(1,'Tom'),(2,'Cat'),(3,'Jerry');
insert into tb_test values(4,'Tom'),(5,'Cat'),(6,'Jerry');
insert into tb_test values(7,'Tom'),(8,'Cat'),(9,'Jerry');
commit;
- 减少事务开销,如果不是手动提交的话,默认每条语句都是一条事务,这样就会导致不停的刷盘写日志的操作.批量操作可以减少磁盘IO的压力.
- 减少锁的持有时间,表锁和行锁在整个事务中只需要获取一次.
- 总的来说: 自动提交,每条语句都会占用一个完整的事务开销,多条Insert会共享一个事务的开销.
- 优化方案三:
主键顺序插入,性能要高于乱序插入:
主键乱序插入 : 8 1 9 21 88 2 4 15 89 5 7 3
主键顺序插入 : 1 2 3 4 5 7 8 9 15 21 88 89
优化方案三的具体原因我们下面会介绍.
1.2 大批量插入数据
如果一次性需要插入大批量数据.使用insert语句插入新能比较低,==此时可以使用MySQL数据库提供load指令进行插入.操作如下:
可以执行如下的指令,将数据脚本文件中的加载到表结构中:
-- 客户端连接服务端时,加上参数 -–local-infile
mysql –-local-infile -u root -p
-- 设置全局参数local_infile为1,开启从本地加载文件导入数据的开关
set global local_infile = 1;
-- 执行load指令将准备好的数据,加载到表结构中
load data local infile '/root/sql1.log' into table tb_user fields
terminated by ',' lines terminated by '\n' ;
2. 主键优化
在上面,我们提到,主键顺序插入的性能要高于乱序插入,下面,我们就来具体介绍一下原因,然后分析一下主键应该如何设计.
- 数据组织方式
在InnoDB存储引擎中,表数据都是根据主键顺序组织存放的,这种存储方式的表称为索引组织表
行数据,都是存储在聚集索引的叶子结点上的,而我们之前也讲过InnoDB的逻辑结构图:
在InnoDB引擎中,数据行时记录在逻辑结构page页中的,而每个页的大小是固定的,默认16KB.那也就意味着,一个页中所存储的行也是有限的,如果插入的行数据row在该页中存储不小,将会存储到下一个页中,页与页之间会通过指针连接. - 页分裂
页可以为空,也可以填充一半,也可以填充100%,每个页包含了2-N行数据,根据主键排列.
- 主键顺序插入效果
从磁盘中申请页,主键顺序插入
第一个页没有满,继续往第一页插入
当第一个页写满之后,再写入第二个页,页与页之间会通过指针连接
当第二个页写满之后,再往第三页写入
- 逐渐乱序插入效果
第一页和第二页都已经写满了,存放如图所示的数据
此时再插入id为50的记录,我们来看看会发生什么现象,会再次开启一个页,写入新的页中吗?
不会,因为,索引结构的叶子结点是有顺序的,按照顺序,应该存储在47之后.
但是47所在的第一页已经满了,存储不了50对应的数据了,那么此时会开辟一个新的页.
但是并不会直接将50存入第三页,而是会将第一页的一半数据移动到第三页,然后在第三页插入50.
移动数据,并插入id为50的数据之后,那么此时,这三个页之间的数据顺序是有问题的.第一页的下一个页应该是第三页,第三页的下一页应该是第二页,所以此时需要重新设置链表指针.
上述的这种现象,称之为"页分裂",是比较耗费性能的操作.
3. 页合并
目前表中已有数据的索引结构(叶子结点)如下:
当我们对已有的数据进行删除的时候,具体的效果如下:
当删除一行记录时,实际上并没有被物理删除,只是被标记为删除并且它的空间变得允许被其他的记录声明使用.
当我们继续删除第二页的数据记录
当页中删除的记录达到
MERGE_THRESHOLD
(默认为页的50%),InnoDB会开始寻找最靠近的页(前或后)看看是否可以将两个页合并以优化空间使用.
删除数据,并将页合并之后,再次插入新的数据21,则直接插入第三页.
这里面所发生的合并也的这个现象,就称之为"页合并".
4. 索引设计原则
- 满足业务需求的情况下,尽量降低主键的长度.迫不得已的时候可以用其他的,比如分布式系统,可以选择使用雪花算法.
- 插入数据时,尽量选择顺序插入,选择使用
AUTO_INCREMENT
自增主键. - 尽量不要使用UUID做主键或者是其他的自然主键,避免出现页分裂的现象,比如身份证号.
- 业务操作时,避免对主键的修改.
3. order by优化
MySQL的排序,有两种方式:
Using filesort: 通过表的索引或者全表扫描,读取满足条件的数据行,然后在排序缓冲区sort buffer
中完成排序操作,所有不是通过索引直接返回的排序结果都叫FileSort排序.
Using index: 通过有序索引扫描直接返回有序数据,这种情况即为using index,不需要额外排序,操作效率高.
对于以上的两种排序方式,using index的性能高,而using filesort的性能很低,我们在优化排序操作的时候,尽量要优化为using index.
接下来,我们来做一个测试:
- 准备数据
把之前为tb_user
表所建立的部分索引直接删除掉
drop index idx_user_phone on tb_user;
drop index idx_user_phone_name on tb_user;
drop index idx_user_name on tb_user;
2. 执行排序SQL
explain select id,age,phone from tb_user order by age ;
explain select id,age,phone from tb_user order by age, phone ;
由于age,phone都没有索引,所以此时再排序时,出现using filesort,排序性能较低.
- 创建索引
-- 创建索引
create index idx_user_age_phone_aa on tb_user(age,phone);
- 创建索引之后,根据age,phone进行升序排序
explain select id,age,phone from tb_user order by age;
explain select id,age,phone from tb_user order by age , phone;
建立索引之后,再次进行排序查询,就由原来的using filesort,变为了using index,性能就比较高了.
- 创建索引之后,根据age,phone进行降序排序.
explain select id,age,phone from tb_user order by age desc , phone desc ;
也出现了using index,但是此时extra出现了backward index scan
,这个代表反向扫描索引,因为在MySQL中我们创建的索引,默认索引的叶子结点是从小到大排序的,而此时我们查询排序时,是从大到小,所以在扫描时,就是反向扫描,就会出现backward index scan
.在MySQL8版本中,支持降序索引,我们也可以创建降序索引.
- 根据age,phone进行排序,一个降序,一个升序.
explain select id,age,phone from tb_user order by age asc , phone desc ;
因为创建索引时,如果未指定顺序,默认都是按照升序排序的,而查询时,一个升序,一个降序,此时就会出现using filesort.
为了解决上述的问题,我们可以创建一个索引,这个联合索引中age升序排序,phone倒序排序.
- 联合创建索引(age升序排序,phone倒序排序)
create index idx_user_age_phone_ad on tb_user(age asc ,phone desc);
8. 然后再次执行如下sql
explain select id,age,phone from tb_user order by age asc , phone desc ;
升序,降序联合索引结构如图所示:
总结: 由上上述原则,我们可以得出order by优化原则:
- 根据排序字段建立合适的索引,多字段排序时,也遵循最左前缀法则.
- 尽量使用覆盖索引
- 多字段排序,一个升序,一个降序,此时需要注意联合索引在创建时的规则(asc/desc).
- 如果不可避免出现filesort,大量数据排序时,可以适当增大排序缓冲区的大小.
4. group by优化
分组操作,我们主要来看索引对分组操作的影响.首先我们将tb_user表中的索引全部删除.
接下来,在没有索引的情况下,执行如下sql,查询执行计划:
explain select profession , count(*) from tb_user group by profession ;
然后,我们在针对与profession,age,status创建一个联合索引.
create index idx_user_pro_age_sta on tb_user(profession , age , status);
紧接着,再执行前面相同的sql查看执行计划:
explain select profession , count(*) from tb_user group by profession ;
再执行如下分组查询sql,查看执行计划:
我们发现,如果仅仅根据age分组,就会出现using temporary
,而如果是根据profession,age两个字段同时分组,则不会出现using temporary
.原因是因为对于分组操作,在联合索引中,也是符合最左前缀法则的.如果分组中跳过了联合索引中的其中一个字段时,后面字段的索引都会失效.
所以在分组操作中,我们需要通过以下两点进行优化,以提升性能:
- 在分组操作时,可以通过索引来提高效率.
- 分组操作时,索引的使用也是满足最左前缀法则的.
5. limit优化
在数据量比较大,如果进行limit分页查询,在查询时,越往后,分页查询效率也越低.
我们一起来看看limit分页查询耗时对比:
通过测试我们会看到,越往后,分页查询效率越低,这就是分页查询的问题所在.
因为,当在进行分页查询时,如果执行limit 2000000,10,此时需要MySQL排序前2000010记录,仅仅返回2000000 - 2000010的记录,将其他记录丢弃,查询的代价非常大.
优化思路: 一般分页查询时,通过创建覆盖索引能够比较好的提高性能,可以通过覆盖索引加子查询的形式进行优化.
6. count优化
6.1 概述
select count(*) from tb_user ;
在之前的测试中,我们发现,如果数据量很大,在执行count操作的时候,非常耗时.
- MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高.
- InnoDB 引擎就麻烦了,它执行 count(*) 的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数.
如果要说大幅提升InnoDB表的count效率,主要的优化思路: 自己计数(可以借助Redis这样的数据库进行).
6.2 count用法
count()
是一个聚合函数,对于返回的结果集,一行一行的判断,如果count函数参数不是null,累计值就加1,否则不加,最后返回累计值.
用法:count(*)、count(主键)、count(字段)、count(数字)
按照效率排序的话: count(字段) < count(主键 id) < count(1) ≈ count(*),所以尽量使用 count(*)
7. update优化
我们主要需要注意一下update语句执行时的注意事项.
update course set name = 'javaEE' where id = 1 ;
当我们在执行更新的sql语句时,会锁定id为1这一行数据,然后事务提交之后,行锁释放.
但是当我们开启多个事务,在执行上述的sql时,我们发现行锁升级为了表锁,导致该update的语句性能大大降低.
InnoDB的行锁是针对索引加的锁,而不是对记录加的锁,并且该索引不能失效,否则就会从行锁自动升级为表锁.