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 行分隔符

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

需要使用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)。

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

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

2.2 页分裂

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

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

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

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

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

  • 开辟一个新的页 3

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

  • 重新设置链表指针

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

2.3 页合并

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

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

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

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

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

2.4 索引设计原则

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


3 order by优化

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

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

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

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

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

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

索引结构如下所示:

执行如下排序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。

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

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

然后再次执行如下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),因为一旦数据的大小超过了缓冲区的大小,就会使用磁盘文件,效率较低。

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


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 ;

我们发现,如果仅仅根据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分页查询耗时对比:

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

因为,当在进行分页查询时,如果执行 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优化相关推荐

  1. mysql sql优化书籍_MySQL SQL优化的正确姿势

    大家好,我是知数堂SQL 优化班老师 网名:骑龟的兔子 已经很久没写文章了 今天分享一篇优化SQL 案例 slow query 里有如下 SQL 看下执行计划如下 从执行计划可以看出C表全表扫描了 那 ...

  2. MySQL SQL 优化参数 引发的悲剧

    大家好,我是知数堂SQL 优化班老师 网名:骑龟的兔子 今天给大家看一个案例来讨论,这个案例是真实案例,因为之前踩bug 导致数据库crash 所以临时关了优化器参数 set gloabl optim ...

  3. MySQL SQL优化

    前言 有人反馈之前几篇文章过于理论缺少实际操作细节,这篇文章就多一些可操作性的内容吧. 注:这篇文章是以 MySQL 为背景,很多内容同时适用于其他关系型数据库,需要有一些索引知识为基础. 优化目标 ...

  4. mysql sql优化_浅谈mysql中sql优化

    说到sql优化,一般有几个步骤呢,在网上看到了一篇很不错的帖子.在这分享一下吧,也是自己学习的一个过程. 一.查找慢查询 1.1.查看SQL执行频率 SHOW STATUS LIKE 'Com_%'; ...

  5. 谈谈mysql优化_浅谈MySQL SQL优化

    本文首发于个人微信公众号<andyqian>,期待你的关注 前言 有好几天没有写文章了,实在不好意思.之前就有朋友希望我写写MySQL优化的文章.我迟迟没有动笔,主要是因为,SQL优化这个 ...

  6. 18.Mysql SQL优化

    18.SQL优化 18.1 优化SQL语句的一般步骤 18.1.1 通过show status命令了解各种SQL的执行频率 show [session|global] status; -- 查看服务器 ...

  7. mysql sql优化与调优机制详解_MySQL之SQL优化详解(一)

    目录 序言: 在我面试很多人的过程中,很多人谈到SQL优化都头头是道,建索引,explain分析,like全模糊会导致索引失效 云云,于是我问道:优化之前,需要找出数据库中比如超过2s的慢SQL,你是 ...

  8. mysql sql优化

    事情是这样的,之前的小伙伴碰到一个sql优化的问题,,三个表联查,速度很慢要十几秒,当然这个三个表数据有个几万条的样子,想想不该呀,几万条数据而已,第一反应是索引问题,mysql完全没问题不至于这么慢 ...

  9. mysql sql优化_MySQL优化SQL语句的步骤

    我们在执行一条SQL语句的时候,如果我们想要知道这条SQL语句查询了哪些表,有没有使用索引,获取数据的时候遍历了多少行数据,我们可以通过EXPLAIN命令来查看这些执行信息,这些执行信息统称为执行计划 ...

  10. MySQL SQL 优化命令行问题 SQL 抓取方式

    墨墨导读:优化的道路永无止境. 对于数据库来说安装,部署几乎是一次性的.后期的管理和优化是持续性的工作. 对于MySQL来说,可以说90%问题都在SQL语句上面.从问题SQL的筛选和优化,在MySQL ...

最新文章

  1. #大学生活#锐捷客户端与VMWare
  2. 配置ORACLE 客户端连接到数据库
  3. 使用Memory Analyzer tool(MAT)分析内存泄漏
  4. 精通Android3笔记--第四章
  5. Tpcc-MySQL测试
  6. 【图像处理】MATLAB:频域处理
  7. mysql格式化11位时间戳_格式化MYSQL时间戳函数FROM_UNIXTIME
  8. Python中终端彩色打印输出
  9. 软件测试流程改进的几点看法
  10. Maven学习记录之依赖问题 Missing artifact org.aspectj:aspectjweaver:jar:1.8.0.M1
  11. R语言使用rbind函数向量或者dataframe数据和另外一个dataframe数据纵向合并起来(vertically)
  12. 电商业务容器化遇瓶颈,公有云Docker镜像P2P加速很安全
  13. c语言编码rna翻译,哪位大牛有哈夫曼编码的C语言源程序,麻烦帮帮忙啦!
  14. 搭建系统|升级选股工具,多板块个股同时提取!个股行情走势存入数据库
  15. 搞清楚 Python traceback
  16. 2020 嵌入式系统原理与应用技术(第2版) 期末复习 【整理】 习题2
  17. 【数据库笔记】(一)数据库模式
  18. 解决xbox无法登录、没有反应
  19. 睡眠质量不好的解决方法,每个年龄段的睡眠时间
  20. 关于VS打印中文出现乱码情况,可以看一下,这篇,亲测有效

热门文章

  1. FileSystemWatcher 用法
  2. 云计算——Google云计算原理与应用(Google文件系统GFS)
  3. Magic value如何解决?
  4. ffmpeg 命令学习
  5. Dragger2初体验 -- @Inject @Component 使用
  6. static静态变量 与 常量
  7. SQL联合查询 join
  8. 我希望我们在Java中拥有十大锡兰语言功能
  9. 一文读懂区块链与大数据的关系
  10. Ubuntu Server 20.04 安装桌面(图形界面) 以及 远程桌面