【MySQL】SQL优化

SQL优化

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

1.批量插入数据

Insert into tb_test values(1,'Tom'),(2,'Cat'),(3,'Jerry');

批量插入可以减少因为频繁访问数据库而带来的性能消耗

2.手动控制事务

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;

由于插入操作默认是自动提交事务的,在这种情况下如果我们多次插入数据,那么数据库就要频繁开启事务和关闭事务,这是相当消耗性能的,因此我们需要对事务实行手动控制

3.主键顺序插入

主键乱序插入 : 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 文件路径 into table tb_user fields terminated by 字段分隔符 lines terminated by 行分隔符 

例如现在需要将如下数据组织形式的文本文件加载到指定表中:

【MySQL】SQL优化_第1张图片

需要使用load指令如下:

load data local infile '/root/load_user_100w_sort.sql'        -- 表示文件路径为'/root/load_user_100w_sort.sql'
into table tb_user fields terminated by ','                   -- 表示一行数据中每个字段使用','来分隔   
lines terminated by '\n' ;                                    -- 表示每行数据使用'\n'来分隔

需要注意的是,使用load指令加载数据时,主键顺序插入的性能同样高于乱序插入


2 主键优化

上面我们提到主键顺序插入的性能是要高于乱序插入的。 这里我们就来介绍一下具体的原因,然后再分析一下主键又该如何设计。

2.1 数据组织方式

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

【MySQL】SQL优化_第2张图片

行数据,都是存储在聚集索引的叶子节点上的。而我们之前也讲解过InnoDB的逻辑结构图:

【MySQL】SQL优化_第3张图片

在InnoDB引擎中,数据行是记录在逻辑结构 page 页中的,而每一个页的大小是固定的,默认16K。那也就意味着, 一个页中所存储的行也是有限的,如果当前页已经无法再插入数据行,那么数据将会存储到下一个页中,页与页之间会通过指针连接。

2.2 页分裂

页可以为空,也可以填充一半,也可以填充100%。每个页包含了2-N行数据(如果一行数据过大,会行溢出),并根据主键排列。

当我们插入数据时,如果是按照主键顺序插入的,那么数据在写入时会先将第一个页写满,再写入下一个页,页与页之间通过指针连接,如下图所示

【MySQL】SQL优化_第4张图片

但是如果我们是乱序插入的,比如此时第一页和第二页中已经存在如下所示的数据:

【MySQL】SQL优化_第5张图片

如果我们此时再插入一条id为50的记录,那就很明显不是按照主键顺序插入了,那么此时会出现什么现象呢?会再次开启一个页,并将这条记录写入新的页中吗?

结果是不会,因为索引结构的叶子节点是有顺序的。按照顺序,id为50的记录应该存储在id为47的记录之后。但是id为47的记录所在的页,已经写满了,存储不了50对应的数据了,当出现这种情况时,数据库底层将会执行如下操作:

  • 开辟一个新的页 3

在这里插入图片描述

  • 将第1页后一半的数据,移动到第3页,然后在3页,插入50

【MySQL】SQL优化_第6张图片

  • 重新设置链表指针

在这里插入图片描述

上述的这种现象,称之为 “页分裂”,是比较耗费性能的操作。当我们乱序插入一条数据时,就极有可能出现页分裂,这也是为什么我们再插入数据时要尽量按主键顺序进行插入的原因

2.3 页合并

假设目前表中已有数据的索引结构(叶子节点)如下:

在这里插入图片描述

当我们对已有数据进行删除时,具体的效果如下:

当删除一行记录时,实际上记录并没有被物理删除,只是记录被标记(flaged)为删除并且它的空间变得允许被其他记录声明使用。

在这里插入图片描述

此时我们可以继续删除第二页中的记录,当被标记的记录达到这一页的50%时,InnoDB会开始寻找最靠近的页(前或后)看看是否可以将两个页合并以优化空间使用,一旦找到了可以合并的列,InnoDB会清除这些被标记的记录,并将另一页的数据迁移到当前页上,如下图所示:

【MySQL】SQL优化_第7张图片

【MySQL】SQL优化_第8张图片

上述过程中所发生的合并页的这个现象,就称之为 “页合并”。页合并的阈值(MERGE_THRESHOLD)可以由自己在创建表或者创建索引时指定。

2.4 索引设计原则

  • 满足业务需求的情况下,尽量降低主键的长度。因为二级索引的叶子节点存放的是主键
  • 插入数据时,尽量选择顺序插入,选择使用AUTO_INCREMENT自增主键。
  • 尽量不要使用UUID做主键或者是其他自然主键,如身份证号。
  • 业务操作时,避免对主键的修改。

【MySQL】SQL优化_第9张图片


3 order by优化

MySQL的排序,有以下两种方式:

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

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

对于以上的两种排序方式,Using index的性能高,而Using filesort的性能低,我们在优化排序操作时,尽量要优化为 Using index。

接下来,我们来做一个测试:

准备一张user表,user表结构如下所示:

【MySQL】SQL优化_第10张图片

索引结构如下所示:

在这里插入图片描述

执行如下排序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 都没有索引(不满足索引idx_user_pro_age_sta的最左前缀法则),所以此时在排序时会出现Using filesort, 排序性能较低。

此时我们可以根据这两个字段再额外建立一个索引

create index idx_user_age_phone_aa on tb_user(age,phone);

创建索引后,我们再尝试进行如下操作:

1.根据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,性能就比较高的了。

2.根据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版本中,支持降序索引,我们也可以创建降序索引。

3.根据phone,age进行升序排序,phone在前,age在后:

explain select id,age,phone from tb_user order by phone , age;

在这里插入图片描述

排序时,也需要满足最左前缀法则,否则也会出现Using filesort。因为在创建索引的时候, age是第一个字段,phone是第二个字段,所以排序时,也就该按照这个顺序来,否则就会出现 Using filesort。

4.根据age,phone进行排序,一个升序,一个降序

explain select id,age,phone from tb_user order by age asc , phone desc ;

在这里插入图片描述

在创建索引时,如果我们未指定顺序,索引会默认按照升序排序的,这种情况下,如果一个排序条件为升序,另一个排序条件为降序,就会出现Using filesort。

【MySQL】SQL优化_第11张图片

为了解决这个问题,我们可以再创建一个age与phone的联合索引,并指定排序顺序为 age 升序排序,phone 倒序排序

create index idx_user_age_phone_ad on tb_user(age asc ,phone desc);

【MySQL】SQL优化_第12张图片

然后再次执行如下SQL:

explain select id,age,phone from tb_user order by age asc , phone desc ;

在这里插入图片描述

5.根据age、phone进行升序排序,但是查询结果返回*

explain select * from tb_user order by age , phone;

在这里插入图片描述

之前几条sql语句我们查询的字段为id,age,phone,这几个字段的值我们都是能通过联合索引idx_user_age_phone拿到的,而现在我们需要查询所有字段,需要进行回表查询,这时也会出现就会出现 Using filesort。

由上述的测试,我们得出order by优化原则:

  • 根据排序字段建立合适的索引,多字段排序时,需要注意遵循最左前缀法则。
  • 尽量使用覆盖索引。
  • 多字段排序, 一个升序一个降序,此时需要注意联合索引在创建时的规则(ASC/DESC)。
  • 如果不可避免的出现filesort,大数据量排序时,可以适当增大排序缓冲区大小sort_buffer_size(默认256k),因为一旦数据的大小超过了缓冲区的大小,就会使用磁盘文件,效率较低。

升序/降序联合索引结构图示:

【MySQL】SQL优化_第13张图片


4 group by优化

分组操作中主要需要关注的是索引对于分组操作的影响

同样使用上面的user表作为例子,我们先将user表中所有的索引清空

在这里插入图片描述

接下来,在没有索引的情况下,执行如下SQL,查询执行计划:

explain select profession , count(*) from tb_user group by profession ;

在这里插入图片描述

Using temporary表示对表的操作并没有走索引,而是创建了一张临时表,这个操作时比较消耗时间的

为了解决上述问题,我们可以我们在针对 profession , age, status 创建一个联合索引(直接创建一个针对profession的单列索引也是可以的,但是这里为了后续操作方便,直接创建一个联合索引)

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,并查看其执行计划:

explain select profession , count(*) from tb_user group by profession,age ;
explain select profession , count(*) from tb_user group by age ;

【MySQL】SQL优化_第14张图片

我们发现,如果仅仅根据age分组,就会出现 Using temporary ;而如果是 根据profession,age两个字段同时分组,则不会出现 Using temporary。原因是因为对于分组操作,在联合索引中,是需要符合最左前缀法则的。

那么问题来了,如果profession是作为查询条件出现在sql中而非作为排序条件出现在sql中,这种情况下仍然满足最左前缀法则吗?

我们可以试着查看如下sql的执行计划

explain select age , count(*) from tb_user where profession = '软件工程' group by age ;

在这里插入图片描述

经过测试,结果仍然是满足的。

所以,在分组操作中,我们可以通过索引来提高效率。并且在分组操作时,索引的使用要符合最左前缀法则。


5 limit优化

在数据量比较大时,如果进行limit分页查询,在查询时,越往后,分页查询效率越低。我们一起来看看执行limit分页查询耗时对比:

【MySQL】SQL优化_第15张图片

通过测试我们会看到,越往后,分页查询效率越低,这就是分页查询的问题所在。

因为,当在进行分页查询时,如果执行 limit 2000000,10 ,此时需要MySQL排序前2000010 记录,仅仅返回 2000000 - 2000010 的记录,其他记录丢弃,查询排序的代价非常大 。

针对上述问题,我们可以进行如下优化:

  • 查询时尽量使用覆盖索引,例如:

    select id from tb_sku order by id limit 9000000,10;
    
  • 如果一定要select * 的话,我们可以通过子查询的形式来优化,例如:

    explain select * from tb_sku t , (select id from tb_sku order by id limit 9000000,10) a where t.id = a.id;
    

6 count优化

在数据量很大的情况下,执行count操作是非常耗时的,这与InnoDB底层实现count的方式有关:

  • MyISAM 引擎把一个表的总行数存在了磁盘上,当我们在执行 count(*) 操作时,MyISAM 引擎会直接返回总行数,效率很高; 但是如果我们使用的是带有查询条件的count语句,在数据量很大的情况下,MyISAM引擎也很慢。
  • InnoDB 引擎就比较麻烦,它在执行 count(*) 的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数。

如果我们要大幅度提升InnoDB表的count效率,主要的优化思路为通过redis这样的数据库进行计数,当添加数据时计数+1,当删除数据时计数-1,但是这种方式同样不能适用于带有查询条件的count语句。

接下来我们主要介绍一下使用count的几种方式:

count用法 含义
count(主键) InnoDB 引擎会遍历整张表,把每一行的 主键id 值都取出来,返回给服务层。服务层拿到主键后,直接按行进行累加(主键不可能为null)
count(非主键字段) 没有not null 约束 : InnoDB 引擎会遍历整张表把每一行的字段值都取出来,返回给服务层,服务层判断是否为null,不为null,计数累加。有not null 约束:InnoDB 引擎会遍历整张表把每一行的字段值都取出来,返回给服务层,直接按行进行累加。
count(1) InnoDB 引擎遍历整张表,但不取值。服务层对于返回的每一行,放一个数字“1”,进去,直接按行进行累加。
count(*) InnoDB引擎并不会把全部字段取出来,而是专门做了优化,不取值,服务层直接按行进行累加

按照计数效率排序的话,count(字段) < count(主键) < count(1) ≈ count(*)。

所以我们在进行计数时,尽量使用 count(*),如果一定需要使用count(字段)的形式,尽量给计数字段加上非空约束


7 update优化

我们主要需要注意一下update语句执行时的注意事项。

当我们在执行如下形式的update语句时:

update course set name = 'javaEE' where id = 1 ;    -- 此时id为主键

InnoDB会锁定id为1的这一行的数据,然后直至事务提交之前,其他所有事务都不能对这一行数据进行操作,只有当事务提交,行锁释放后,其他事务才能操作这一行数据

但是当我们在执行如下形式的sql时:

update course set name = 'SpringBoot' where name = 'PHP' ;    -- 此时数据表中没有针对name字段建立索引

InnoDB会锁定course整张表的数据,直至事务提交之前,其他所有事务都不能对这张表的数据进行操作,效率大大降低。

那么为什么InnoDB在执行上述sql语句时会锁表呢?

原因是因为InnoDB的行锁是针对索引加的锁,而不是针对记录加的锁 ,并且该索引不能失效,否则会从行锁升级为表锁 。

这种情况下,我们可以针对name字段建立一个索引,这样就能防止锁升级的问题。

你可能感兴趣的:(#,MySQL,sql,数据库,mysql)