有朋友疑问到,SQL优化真的有这么重要么?SQL优化在提升系统性能中是:(成本最低 && 优化效果最明显) 的途径。如果你的团队在SQL优化这方面搞得很优秀,对你们整个大型系统可用性方面无疑是一个质的跨越,真的能让你们老板省下不止几沓子钱。

所有的sql优化基本上都是围绕以下3点来进行的:
这一篇记录了索引的所有相关知识,以及怎么查看索引是否生效等等:https://blog.csdn.net/weixin_43888891/article/details/126073266
一定要学会读sql的执行计划,要不然优化工作根本无法进行,所有的sql优化工作基本上全是围绕索引来进行优化的!!!
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)优化方案三:主键顺序插入,性能要高于乱序插入(尽量使用雪花算法id,或者自增id,尽可能的避免使用uuid)。
主键乱序插入 : 8 1 9 21 88 2 4 15 89 5 7 3
主键顺序插入 : 1 2 3 4 5 7 8 9 15 21 88 89
2. 大批量导入数据优化
如果一次性需要插入大批量数据(比如: 几百万的记录),使用insert语句插入性能较低,此时可以使用MySQL数据库提供的load指令进行插入。操作如下:
mysql不止可以通过正规的insert语句可以添加到数据库,load他就是通过数据的规律性,然后就可以将数据导入到数据库当中,如下所示:

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' ;
主键顺序插入性能高于乱序插入
示例演示:
(1)创建表结构
CREATE TABLE `tb_user` (
`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
`username` VARCHAR ( 50 ) NOT NULL,
`password` VARCHAR ( 50 ) NOT NULL,
`name` VARCHAR ( 20 ) NOT NULL,
`birthday` DATE DEFAULT NULL,
`sex` CHAR ( 1 ) DEFAULT NULL,
PRIMARY KEY ( `id` ),
UNIQUE KEY `unique_user_username` ( `username` )
) ENGINE = INNODB DEFAULT CHARSET = utf8;
(2)设置参数
-- 客户端连接服务端时,加上参数 -–local-infile
mysql –-local-infile -u root -p
-- 设置全局参数local_infile为1,开启从本地加载文件导入数据的开关
set global local_infile = 1;
(3)load加载数据
load data local infile 'C:\\Users\\gxs\\Desktop\\load_user_100w_sort.sql' into table tb_user fields terminated by ',' lines terminated by '\n' ;

我们看到,插入100w的记录,17s就完成了,性能很好。
在上面我们提到,主键顺序插入的性能是要高于乱序插入的。 这一小节,就来介绍一下具体的原因,然后再分析一下主键又该如何设计。
(1)数据组织方式
在InnoDB存储引擎中,表数据都是根据主键顺序组织存放的,这种存储方式的表称为索引组织表
(index organized table IOT)。

行数据,都是存储在聚集索引(聚簇索引)的叶子节点上的。InnoDB的逻辑结构图如下:

在InnoDB引擎中,数据行是记录在逻辑结构 page 页中的,而每一个页的大小是固定的,默认16K。那也就意味着, 一个页中所存储的行也是有限的,如果插入的数据行row在该页存储不下,将会存储到下一个页中,页与页之间会通过指针连接。
存储引擎讲解:https://blog.csdn.net/weixin_43888891/article/details/125958409
(2)页分裂
页可以为空,也可以填充一半,也可以填充100%。每个页包含了2-N行数据(如果一行数据过大,会行
溢出),根据主键排列。
A. 主键顺序插入效果
1.从磁盘中申请页, 主键顺序插入

2.第一个页没有满,继续往第一页插入

3.当第一个也写满之后,再写入第二个页,页与页之间会通过指针连接

4.当第二页写满了,再往第三页写入

B. 主键乱序插入效果
1.假如1#,2#页都已经写满了,存放了如图所示的数据

2.此时再插入id为50的记录,我们来看看会发生什么现象,想一下会再次开启一个页,写入新的页中吗?

不会。因为,索引结构的叶子节点是有顺序的。按照顺序,应该存储在47之后。

但是47所在的1#页,已经写满了,存储不了50对应的数据了。 那么此时会开辟一个新的页 3#。

但是并不会直接将50存入3#页,而是会将1#页后一半的数据,移动到3#页,然后在3#页,插入50。

移动数据,并插入id为50的数据之后,那么此时,这三个页之间的数据顺序是有问题的。 1#的下一个页,应该是3#, 3#的下一个页是2#。 所以,此时,需要重新设置链表指针。

上述的这种现象,称之为 “页分裂”,是比较耗费性能的操作。
页分裂往往就是不按照顺序插入导致的,同时修改索引id也会导致这种情况的出现!
(3)页合并
目前表中已有数据的索引结构(叶子节点)如下:

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

当我们继续删除2#的数据记录

当页中删除的记录达到 MERGE_THRESHOLD(默认为页的50%),InnoDB会开始寻找最靠近的页(前或后)看看是否可以将两个页合并以优化空间使用。

删除数据,并将页合并之后,再次插入新的数据21,则直接插入3#页

这个里面所发生的合并页的这个现象,就称之为 “页合并”。
知识小贴士:MERGE_THRESHOLD:合并页的阈值,可以自己设置,在创建表或者创建索引时指定。
(4)主键设计原则
(5)UUID、自增id、雪花id选哪个作为主键比较好?
插入数据的性能比较:UUID < 雪花有序算法id < 自增id (自增id性能最高)
使用自增id的缺点:
uuid的缺点:
雪花算法id: snowflake是Twitter开源的分布式ID生成算法,结果是64bit的Long类型的ID,有着全局唯一和有序递增的特点。
如果小的系统,使用自增id完全可以,如果系统较大,或者说以后可能会成为大的系统,那么就有可能会涉及到数据迁移、分表等等操作,我强烈推荐使用雪花算法id。
MySQL的排序,有两种方式:
对于以上的两种排序方式,Using index的性能高,而Using filesort的性能低,我们在优化排序操作时,尽量要优化为 Using index。
接下来,我们来做一个测试:
CREATE TABLE tb_user (
id INT PRIMARY KEY auto_increment COMMENT '主键',
NAME VARCHAR ( 50 ) NOT NULL COMMENT '用户名',
phone VARCHAR ( 11 ) NOT NULL COMMENT '手机号',
email VARCHAR ( 100 ) COMMENT '邮箱',
profession VARCHAR ( 11 ) COMMENT '专业',
age TINYINT UNSIGNED COMMENT '年龄',
gender CHAR ( 1 ) COMMENT '性别 , 1: 男, 2: 女',
STATUS CHAR ( 1 ) COMMENT '状态',
createtime datetime COMMENT '创建时间'
) COMMENT '系统用户表';
INSERT INTO tb_user ( NAME, phone, email, profession, age, gender, STATUS, createtime )
VALUES ( '吕布', '17799990000', 'lvbu666@163.com', '软件工程', 23, '1', '6', '2001-02-02 00:00:00' );
INSERT INTO tb_user ( NAME, phone, email, profession, age, gender, STATUS, createtime )
VALUES ( '曹操', '17799990001', 'caocao666@qq.com', '通讯工程', 33, '1', '0', '2001-03-05 00:00:00' );
INSERT INTO tb_user ( NAME, phone, email, profession, age, gender, STATUS, createtime )
VALUES ( '赵云', '17799990002', '17799990@139.com', '英语', 34, '1', '2', '2002-03-02 00:00:00' );
INSERT INTO tb_user ( NAME, phone, email, profession, age, gender, STATUS, createtime )
VALUES ( '孙悟空', '17799990003', '17799990@sina.com', '工程造价', 54, '1', '0', '2001-07-02 00:00:00' );
INSERT INTO tb_user ( NAME, phone, email, profession, age, gender, STATUS, createtime )
VALUES ( '花木兰', '17799990004', '19980729@sina.com', '软件工程', 23, '2', '1', '2001-04-22 00:00:00' );
INSERT INTO tb_user ( NAME, phone, email, profession, age, gender, STATUS, createtime )
VALUES ( '大乔', '17799990005', 'daqiao666@sina.com', '舞蹈', 22, '2', '0', '2001-02-07 00:00:00' );
INSERT INTO tb_user ( NAME, phone, email, profession, age, gender, STATUS, createtime )
VALUES ( '露娜', '17799990006', 'luna_love@sina.com', '应用数学', 24, '2', '0', '2001-02-08 00:00:00' );
INSERT INTO tb_user ( NAME, phone, email, profession, age, gender, STATUS, createtime )
VALUES ( '程咬金', '17799990007', 'chengyaojin@163.com', '化工', 38, '1', '5', '2001-05-23 00:00:00' );
INSERT INTO tb_user ( NAME, phone, email, profession, age, gender, STATUS, createtime )
VALUES ( '项羽', '17799990008', 'xiaoyu666@qq.com', '金属材料', 43, '1', '0', '2001-09-18 00:00:00' );
INSERT INTO tb_user ( NAME, phone, email, profession, age, gender, STATUS, createtime )
VALUES ( '白起', '17799990009', 'baiqi666@sina.com', '机械工程及其自动 化', 27, '1', '2', '2001-08-16 00:00:00' );
INSERT INTO tb_user ( NAME, phone, email, profession, age, gender, STATUS, createtime )
VALUES ( '韩信', '17799990010', 'hanxin520@163.com', '无机非金属材料工 程', 27, '1', '0', '2001-06-12 00:00:00' );
INSERT INTO tb_user ( NAME, phone, email, profession, age, gender, STATUS, createtime )
VALUES ( '荆轲', '17799990011', 'jingke123@163.com', '会计', 29, '1', '0', '2001-05-11 00:00:00' );
INSERT INTO tb_user ( NAME, phone, email, profession, age, gender, STATUS, createtime )
VALUES ( '兰陵王', '17799990012', 'lanlinwang666@126.com', '工程造价', 44, '1', '1', '2001-04-09 00:00:00' );
INSERT INTO tb_user ( NAME, phone, email, profession, age, gender, STATUS, createtime )
VALUES ( '狂铁', '17799990013', 'kuangtie@sina.com', '应用数学', 43, '1', '2', '2001-04-10 00:00:00' );
INSERT INTO tb_user ( NAME, phone, email, profession, age, gender, STATUS, createtime )
VALUES ( '貂蝉', '17799990014', '84958948374@qq.com', '软件工程', 40, '2', '3', '2001-02-12 00:00:00' );
INSERT INTO tb_user ( NAME, phone, email, profession, age, gender, STATUS, createtime )
VALUES ( '妲己', '17799990015', '2783238293@qq.com', '软件工程', 31, '2', '0', '2001-01-30 00:00:00' );
INSERT INTO tb_user ( NAME, phone, email, profession, age, gender, STATUS, createtime )
VALUES ( '芈月', '17799990016', 'xiaomin2001@sina.com', '工业经济', 35, '2', '0', '2000-05-03 00:00:00' );
INSERT INTO tb_user ( NAME, phone, email, profession, age, gender, STATUS, createtime )
VALUES ( '嬴政', '17799990017', '8839434342@qq.com', '化工', 38, '1', '1', '2001-08-08 00:00:00' );
INSERT INTO tb_user ( NAME, phone, email, profession, age, gender, STATUS, createtime )
VALUES ( '狄仁杰', '17799990018', 'jujiamlm8166@163.com', '国际贸易', 30, '1', '0', '2007-03-12 00:00:00' );
INSERT INTO tb_user ( NAME, phone, email, profession, age, gender, STATUS, createtime )
VALUES ( '安琪拉', '17799990019', 'jdodm1h@126.com', '城市规划', 51, '2', '0', '2001-08-15 00:00:00' );
INSERT INTO tb_user ( NAME, phone, email, profession, age, gender, STATUS, createtime )
VALUES ( '典韦', '17799990020', 'ycaunanjian@163.com', '城市规划', 52, '1', '2', '2000-04-12 00:00:00' );
INSERT INTO tb_user ( NAME, phone, email, profession, age, gender, STATUS, createtime )
VALUES ( '廉颇', '17799990021', 'lianpo321@126.com', '土木工程', 19, '1', '3', '2002-07-18 00:00:00' );
INSERT INTO tb_user ( NAME, phone, email, profession, age, gender, STATUS, createtime )
VALUES ( '后羿', '17799990022', 'altycj2000@139.com', '城市园林', 20, '1', '0', '2002-03-10 00:00:00' );
INSERT INTO tb_user ( NAME, phone, email, profession, age, gender, STATUS, createtime )
VALUES ( '姜子牙', '17799990023', '37483844@qq.com', '工程造价', 29, '1', '4', '2003-05-26 00:00:00' );
执行排序SQL:
explain select id,age,phone from tb_user order by age;
由于 age, phone 都没有索引,所以此时再排序时,出现Using filesort, 排序性能较低。
创建了一个组合索引,然后再进行测试:
CREATE INDEX idx_user_age_phone ON tb_user(age,phone);
explain select id,age,phone from tb_user order by age, phone ;
索引创建的是age、phone然后排序的时候按照phone、age,这时候就出现了Using filesort
索引失效:添加了age和phone的索引,但是查询的列,不止有这两个,导致直接索引失效!
如果就是要查三个列怎么办?要么选择把profession也添加上索引,要么可以采用如下查询方式。其实这样查询也可以保持索引生效,只不过会出现回表查询。
创建索引后,根据age, phone进行降序排序
explain select id,age,phone from tb_user order by age desc , phone desc;
也出现 Using index, 但是此时Extra中出现了 Backward index scan(mysql版本低的话是没有这个的,仍然显示的 Using index),这个代表反向扫描索引,因为在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);
然后再次执行如下SQL:explain select id,age,phone from tb_user order by age asc , phone desc ;
升序/降序联合索引结构图示:
age asc,phone desc:代表的是先按年龄升序,假如年龄相等的到时候,安装phone倒序进行二级排序


由上述的测试,我们得出order by优化原则:
分组操作,我们主要来看看索引对于分组操作的影响。
首先我们先将 tb_user 表的索引全部删除掉,只留了主键索引!
drop index 索引名称 on 表名;

接下来,在没有索引的情况下,执行如下SQL,查询执行计划:
explain select profession , count(*) from tb_user group by profession ;
很明显出现了Using temporary(临时表),效率要远远低于Using index。
然后,我们在针对于 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,查看执行计划:
会发现一个问题:group by也是遵循了索引最左前缀法则,直接使用age进行分组虽然走了索引,但是出现了Using temporary,然后建立索引的时候索引列的顺序是(profession , age , status),而分组的时候使用的是 age,profession,这样也会出现Using temporary。
所以,在分组操作中,我们需要通过以下两点进行优化,以提升性能:
Using temporary。在数据量比较大时,如果进行limit分页查询,在查询时,越往后,分页查询效率越低。
我们一起来看看执行limit分页查询耗时对比:


通过测试我们会看到,越往后,分页查询效率越低,这就是分页查询的问题所在。
因为,当在进行分页查询时,如果执行 limit 2000000,10 ,此时需要MySQL排序前2000010 记录,仅仅返回 2000000 - 2000010 的记录,其他记录丢弃,查询排序的代价非常大 。
优化思路: 一般分页查询时,通过创建 覆盖索引 能够比较好地提高性能,可以通过覆盖索引加子查询形式进行优化。
explain select * from tb_sku t , (select id from tb_sku order by id limit 2000000,10) a where t.id = a.id;`
上面示例当中直接分页,和子查询的方式其实都会走主键索引,两种相比较差距并不是特别大,在1000万数据,然后访问limit9200000,19 ,子查询这种方式速度大概能快个2s左右。
(1)概述
select count(*) from tb_user ;
在之前的测试中,我们发现,如果数据量很大,在执行count操作时,是非常耗时的。
如果说要大幅度提升InnoDB表的count效率,主要的优化思路:自己计数(可以借助于redis这样的数据库进行,但是如果是带条件的count又比较麻烦了)。
(2)count用法
count() 是一个聚合函数,对于返回的结果集,一行行地判断,如果 count 函数的参数不是NULL,累计值就加 1,否则不加,最后返回累计值。

按照效率排序:count(字段) < count(主键 id) < count(1) ≈ count(*)
所以尽量使用 count(*),在阿里巴巴规范当中也有这一点!
我们主要需要注意一下update语句执行时的注意事项。
update course set name = 'javaEE' where id = 1 ;
当我们在执行删除的SQL语句时,会锁定id为1这一行的数据,然后事务提交之后,行锁释放。
但是当我们在执行如下SQL时。
update course set name = 'SpringBoot' where name = 'PHP' ;
当我们开启多个事务,在执行上述的SQL时,我们发现行锁升级为了表锁。 导致该update语句的性能大大降低。
InnoDB的行锁是针对索引加的锁,不是针对记录加的锁 ,并且该索引不能失效,否则会从行锁升级为表锁 。
count(*),效率高,注意:count(*) 会统计值为 NULL 的行,而 count(列名) 不会统计此列为 NULL 值的行。以下部分都来源于阿里巴巴规范当中:
表名、字段名必须使用小写字母或数字。主键索引名为 pk_字段名;唯一索引名为 uk_字段名;普通索引名则为 idx_字段名。小数类型为 decimal,禁止使用 float 和 doublevarchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此值,定义字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索引率。如果存储的字符串长度几乎相等,使用 char 定长字符串类型。(char是定长字符,比vachar效率要高,所谓定长就是声明char(10)初始阶段就是占用10个字节,根本不管他到底存储的数据够不够这些,他都占用这些空间)表必备三字段:id,create_time,update_time。要使用逻辑删除。表的命名最好是遵循“业务名称_表的作用”。及时更新字段注释。单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。以下部分都来源于阿里巴巴规范当中:
业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。超过三个表禁止 join。需要 join 的字段,数据类型保持绝对一致;多表关联查询时,保证被关联的字段需要有索引。在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度。页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。如果有 order by 的场景,请注意利用索引的有序性。order by 最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现 filesort 的情况,影响查询性能。利用覆盖索引来进行查询操作,避免回表。利用延迟关联或者子查询优化超多分页场景。SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,如果可以是 const 最好(指的是执行计划当中的type)。
建组合索引的时候,区分度最高的在最左边。防止因字段类型不同造成的隐式转换,导致索引失效。不要使用 count(列名) 或 count(常量) 来替代 count(*)代码中写分页查询逻辑时,若 count 为 0 应直接返回,避免执行后面的分页语句。不得使用外键与级联,一切外键概念必须在应用层解决。禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。要先 select,避免出现误删除的情况,确认无误才能执行更新语句。在列名前加表的别名(或表名)进行限定。以 t1、t2、t3、...的顺序依次命名。采用 utf8mb4 字符集,字符计数方法需要注意。;反过来,每一个表也必然有一个与之对应。