【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 行分隔符
例如现在需要将如下数据组织形式的文本文件加载到指定表中:
需要使用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优化相关推荐
- mysql sql优化书籍_MySQL SQL优化的正确姿势
大家好,我是知数堂SQL 优化班老师 网名:骑龟的兔子 已经很久没写文章了 今天分享一篇优化SQL 案例 slow query 里有如下 SQL 看下执行计划如下 从执行计划可以看出C表全表扫描了 那 ...
- MySQL SQL 优化参数 引发的悲剧
大家好,我是知数堂SQL 优化班老师 网名:骑龟的兔子 今天给大家看一个案例来讨论,这个案例是真实案例,因为之前踩bug 导致数据库crash 所以临时关了优化器参数 set gloabl optim ...
- MySQL SQL优化
前言 有人反馈之前几篇文章过于理论缺少实际操作细节,这篇文章就多一些可操作性的内容吧. 注:这篇文章是以 MySQL 为背景,很多内容同时适用于其他关系型数据库,需要有一些索引知识为基础. 优化目标 ...
- mysql sql优化_浅谈mysql中sql优化
说到sql优化,一般有几个步骤呢,在网上看到了一篇很不错的帖子.在这分享一下吧,也是自己学习的一个过程. 一.查找慢查询 1.1.查看SQL执行频率 SHOW STATUS LIKE 'Com_%'; ...
- 谈谈mysql优化_浅谈MySQL SQL优化
本文首发于个人微信公众号<andyqian>,期待你的关注 前言 有好几天没有写文章了,实在不好意思.之前就有朋友希望我写写MySQL优化的文章.我迟迟没有动笔,主要是因为,SQL优化这个 ...
- 18.Mysql SQL优化
18.SQL优化 18.1 优化SQL语句的一般步骤 18.1.1 通过show status命令了解各种SQL的执行频率 show [session|global] status; -- 查看服务器 ...
- mysql sql优化与调优机制详解_MySQL之SQL优化详解(一)
目录 序言: 在我面试很多人的过程中,很多人谈到SQL优化都头头是道,建索引,explain分析,like全模糊会导致索引失效 云云,于是我问道:优化之前,需要找出数据库中比如超过2s的慢SQL,你是 ...
- mysql sql优化
事情是这样的,之前的小伙伴碰到一个sql优化的问题,,三个表联查,速度很慢要十几秒,当然这个三个表数据有个几万条的样子,想想不该呀,几万条数据而已,第一反应是索引问题,mysql完全没问题不至于这么慢 ...
- mysql sql优化_MySQL优化SQL语句的步骤
我们在执行一条SQL语句的时候,如果我们想要知道这条SQL语句查询了哪些表,有没有使用索引,获取数据的时候遍历了多少行数据,我们可以通过EXPLAIN命令来查看这些执行信息,这些执行信息统称为执行计划 ...
- MySQL SQL 优化命令行问题 SQL 抓取方式
墨墨导读:优化的道路永无止境. 对于数据库来说安装,部署几乎是一次性的.后期的管理和优化是持续性的工作. 对于MySQL来说,可以说90%问题都在SQL语句上面.从问题SQL的筛选和优化,在MySQL ...
最新文章
- #大学生活#锐捷客户端与VMWare
- 配置ORACLE 客户端连接到数据库
- 使用Memory Analyzer tool(MAT)分析内存泄漏
- 精通Android3笔记--第四章
- Tpcc-MySQL测试
- 【图像处理】MATLAB:频域处理
- mysql格式化11位时间戳_格式化MYSQL时间戳函数FROM_UNIXTIME
- Python中终端彩色打印输出
- 软件测试流程改进的几点看法
- Maven学习记录之依赖问题 Missing artifact org.aspectj:aspectjweaver:jar:1.8.0.M1
- R语言使用rbind函数向量或者dataframe数据和另外一个dataframe数据纵向合并起来(vertically)
- 电商业务容器化遇瓶颈,公有云Docker镜像P2P加速很安全
- c语言编码rna翻译,哪位大牛有哈夫曼编码的C语言源程序,麻烦帮帮忙啦!
- 搭建系统|升级选股工具,多板块个股同时提取!个股行情走势存入数据库
- 搞清楚 Python traceback
- 2020 嵌入式系统原理与应用技术(第2版) 期末复习 【整理】 习题2
- 【数据库笔记】(一)数据库模式
- 解决xbox无法登录、没有反应
- 睡眠质量不好的解决方法,每个年龄段的睡眠时间
- 关于VS打印中文出现乱码情况,可以看一下,这篇,亲测有效