0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

关于SQL优化的硬核文章

数据分析与开发 来源:数据分析不是个事儿 2023-05-06 14:19 次阅读

分享一篇关于SQL优化的硬核文章,全文有点长,建议收藏后慢慢看。

很多朋友在做数据分析时,分析两分钟,跑数两小时?

在使用SQL过程中不仅要关注数据结果,同样要注意SQL语句的执行效率。

本文涉及三部分:

  • SQL介绍
  • SQL优化方法
  • SQL优化实例

1、MySQL的基本架构

1)MySQL的基础架构图

2a9e6bb8-ebc9-11ed-90ce-dac502259ad0.jpg

左边的client可以看成是客户端,客户端有很多,像我们经常你使用的CMD黑窗口,像我们经常用于学习的WorkBench,像企业经常使用的Navicat工具,它们都是一个客户端。右边的这一大堆都可以看成是Server(MySQL的服务端),我们将Server在细分为sql层和存储引擎层。

当查询出数据以后,会返回给执行器。执行器一方面将结果写到查询缓存里面,当你下次再次查询的时候,就可以直接从查询缓存中获取到数据了。另一方面,直接将结果响应回客户端。

2)查询数据库的引擎

① show engines;

2aabda5a-ebc9-11ed-90ce-dac502259ad0.png

② show variables like “%storage_engine%”;

2ab4c9c6-ebc9-11ed-90ce-dac502259ad0.png

3)指定数据库对象的存储引擎

createtabletb(
idint(4)auto_increment,
namevarchar(5),
deptvarchar(5),
primarykey(id)
)engine=myISAMauto_increment=1defaultcharset=utf8;

SQL优化

1)为什么需要进行SQL优化?

在进行多表连接查询、子查询等操作的时候,由于你写出的SQL语句欠佳,导致的服务器执行时间太长,我们等待结果的时间太长。基于此,我们需要学习怎么优化SQL。

2)mysql的编写过程和解析过程

① 编写过程

selectdinstinct..from..join..on..where..groupby..having..orderby..limit..

② 解析过程

from..on..join..where..groupby..having..selectdinstinct..orderby..limit..

提供一个网站,详细说明了mysql解析过程:

https://www.cnblogs.com/annsshadow/p/5037667.html

3)SQL优化—主要就是优化索引

优化SQL,最重要的就是优化SQL索引。

索引相当于字典的目录。利用字典目录查找汉字的过程,就相当于利用SQL索引查找某条记录的过程。有了索引,就可以很方便快捷的定位某条记录。

① 什么是索引?

索引就是帮助MySQL高效获取数据的一种【数据结构】。索引是一种树结构,MySQL中一般用的是【B+树】。

② 索引图示说明(这里用二叉树来帮助我们理解索引)

树形结构的特点是:子元素比父元素小的,放在左侧;子元素比父元素大的,放在右侧。

这个图示只是为了帮我们简单理解索引的,真实的关于【B+树】的说明,我们会在下面进行说明。

2abe2494-ebc9-11ed-90ce-dac502259ad0.png

索引是怎么查找数据的呢?两个字【指向】,上图中我们给age列指定了一个索引,即类似于右侧的这种树形结构。mysql表中的每一行记录都有一个硬件地址,例如索引中的age=50,指向的就是源表中该行的标识符(“硬件地址”)。

也就是说,树形索引建立了与源表中每行记录硬件地址的映射关系,当你指定了某个索引,这种映射关系也就建成了,这就是为什么我们可以通过索引快速定位源表中记录的原因。

以【select * from student where age=33】查询语句为例。当我们不加索引的时候,会从上到下扫描源表,当扫描到第5行的时候,找到了我们想要找到了元素,一共是查询了5次。

当添加了索引以后,就直接在树形结构中进行查找,33比50小,就从左侧查询到了23,33大于23,就又查询到了右侧,这下找到了33,整个索引结束,一共进行了3次查找。是不是很方便,假如我们此时需要查找age=62,你再想想“添加索引”前后,查找次数的变化情况。

4)索引的弊端

1.当数据量很大的时候,索引也会很大(当然相比于源表来说,还是相当小的),也需要存放在内存/硬盘中(通常存放在硬盘中),占据一定的内存空间/物理空间。

2.索引并不适用于所有情况:a.少量数据;b.频繁进行改动的字段,不适合做索引;c.很少使用的字段,不需要加索引;

3.索引会提高数据查询效率,但是会降低“增、删、改”的效率。当不使用索引的时候,我们进行数据的增删改,只需要操作源表即可,但是当我们添加索引后,不仅需要修改源表,也需要再次修改索引,很麻烦。尽管是这样,添加索引还是很划算的,因为我们大多数使用的就是查询,“查询”对于程序的性能影响是很大的。

5)索引的优势

1.提高查询效率(降低了IO使用率)。当创建了索引后,查询次数减少了。

2.降低CPU使用率。比如说【…order by age desc】这样一个操作,当不加索引,会把源表加载到内存中做一个排序操作,极大的消耗了资源。但是使用了索引以后,第一索引本身就小一些,第二索引本身就是排好序的,左边数据最小,右边数据最大。

6)B+树图示说明

MySQL中索引使用的就是B+树结构。

2ac704ec-ebc9-11ed-90ce-dac502259ad0.jpg

关于B+树的说明:

首先,Btree一般指的都是【B+树】,数据全部存放在叶子节点中。对于上图来说,最下面的第3层,属于叶子节点,真实数据部份都是存放在叶子节点当中的。

那么对于第1、2层中的数据又是干嘛的呢?答:用于分割指针块儿的,比如说小于26的找P1,介于26-30之间的找P2,大于30的找P3。

其次,三层【B+树】可以存放上百万条数据。这么多数据怎么放的呢?增加“节点数”。图中我们只有三个节点。

最后,【B+树】中查询任意数据的次数,都是n次,n表示的是【B+树】的高度。

3、索引的分类与创建

1)索引分类

单值索引

唯一索引

复合索引

① 单值索引

利用表中的某一个字段创建单值索引。一张表中往往有多个字段,也就是说每一列其实都可以创建一个索引,这个根据我们实际需求来进行创建。还需要注意的一点就是,一张表可以创建多个“单值索引”。

假如某一张表既有age字段,又有name字段,我们可以分别对age、name创建一个单值索引,这样一张表就有了两个单值索引。

② 唯一索引

也是利用表中的某一个字段创建单值索引,与单值索引不同的是:创建唯一索引的字段中的数据,不能有重复值。像age肯定有很多人的年龄相同,像name肯定有些人是重名的,因此都不适合创建“唯一索引”。像编号id、学号sid,对于每个人都不一样,因此可以用于创建唯一索引。

③ 复合索引

多个列共同构成的索引。比如说我们创建这样一个“复合索引”(name,age),先利用name进行索引查询,当name相同的时候,我们利用age再进行一次筛选。注意:复合索引的字段并不是非要都用完,当我们利用name字段索引出我们想要的结果以后,就不需要再使用age进行再次筛选了。

2)创建索引

① 语法

语法:create 索引类型 索引名 on 表(字段);

建表语句如下:

查询表结构如下:

2acf7ab4-ebc9-11ed-90ce-dac502259ad0.png

② 创建索引的第一种方式

Ⅰ 创建单值索引

createindexdept_indexontb(dept);

Ⅱ 创建唯一索引:这里我们假定name字段中的值都是唯一的

createuniqueindexname_indexontb(name);

Ⅲ 创建复合索引

createindexdept_name_indexontb(dept,name);

③ 创建索引的第二种方式

先删除之前创建的索引以后,再进行这种创建索引方式的测试;

语法:alter table 表名 add 索引类型 索引名(字段)

Ⅰ 创建单值索引

altertabletbaddindexdept_index(dept);

Ⅱ 创建唯一索引:这里我们假定name字段中的值都是唯一的

altertabletbadduniqueindexname_index(name);

Ⅲ 创建复合索引

altertabletbaddindexdept_name_index(dept,name);

④ 补充说明

如果某个字段是primary key,那么该字段默认就是主键索引。

主键索引和唯一索引非常相似。相同点:该列中的数据都不能有相同值;不同点:主键索引不能有null值,但是唯一索引可以有null值。

3)索引删除和索引查询

① 索引删除

语法:drop index 索引名 on 表名;

dropindexname_indexontb;

② 索引查询

语法:show index from 表名;

showindexfromtb;

结果如下:

2ad749ce-ebc9-11ed-90ce-dac502259ad0.png

4、SQL性能问题的探索

人为优化: 需要我们使用explain分析SQL的执行计划。该执行计划可以模拟SQL优化器执行SQL语句,可以帮助我们了解到自己编写SQL的好坏。

SQL优化器自动优化: 最开始讲述MySQL执行原理的时候,我们已经知道MySQL有一个优化器,当你写了一个SQL语句的时候,SQL优化器如果认为你写的SQL语句不够好,就会自动写一个好一些的等价SQL去执行。

SQL优化器自动优化功能【会干扰】我们的人为优化功能。当我们查看了SQL执行计划以后,如果写的不好,我们会去优化自己的SQL。当我们以为自己优化的很好的时候,最终的执行计划,并不是按照我们优化好的SQL语句来执行的,而是有时候将我们优化好的SQL改变了,去执行。

SQL优化是一种概率问题,有时候系统会按照我们优化好的SQL去执行结果(优化器觉得你写的差不多,就不会动你的SQL)。有时候优化器仍然会修改我们优化好的SQL,然后再去执行。

1)查看执行计划

语法:explain + SQL语句

eg:explain select * from tb;

2)“执行计划”中需要知道的几个“关键字”

id :编号

select_type :查询类型

table :表

type :类型

possible_keys :预测用到的索引

key :实际使用的索引

key_len :实际使用索引的长度

ref :表之间的引用

rows :通过索引查询到的数据量

Extra :额外的信息

建表语句和插入数据:

#建表语句
createtablecourse
(
cidint(3),
cnamevarchar(20),
tidint(3)
);

createtableteacher
(
tidint(3),
tnamevarchar(20),
tcidint(3)
);

createtableteacherCard
(
tcidint(3),
tcdescvarchar(200)
);

#插入数据
insertintocoursevalues(1,'java',1);
insertintocoursevalues(2,'html',1);
insertintocoursevalues(3,'sql',2);
insertintocoursevalues(4,'web',3);

insertintoteachervalues(1,'tz',1);
insertintoteachervalues(2,'tw',2);
insertintoteachervalues(3,'tl',3);

insertintoteacherCardvalues(1,'tzdesc');
insertintoteacherCardvalues(2,'twdesc');
insertintoteacherCardvalues(3,'tldesc');

explain执行计划常用关键字详解

1)id关键字的使用说明

① 案例:查询课程编号为2 或 教师证编号为3 的老师信息:

#查看执行计划
explainselectt.*
fromteachert,coursec,teacherCardtc
wheret.tid=c.tidandt.tcid=tc.tcid
and(c.cid=2ortc.tcid=3);

结果如下:

2ae35b1a-ebc9-11ed-90ce-dac502259ad0.png

接着,在往teacher表中增加几条数据。

insertintoteachervalues(4,'ta',4);
insertintoteachervalues(5,'tb',5);
insertintoteachervalues(6,'tc',6);

再次查看执行计划。

#查看执行计划
explainselectt.*
fromteachert,coursec,teacherCardtc
wheret.tid=c.tidandt.tcid=tc.tcid
and(c.cid=2ortc.tcid=3);

结果如下:

2aede4cc-ebc9-11ed-90ce-dac502259ad0.png

表的执行顺序 ,因表数量改变而改变的原因:笛卡尔积。

abc
234
最终:2 * 3 * 4 = 6 * 4 = 24
cba
432
最终:4 * 3 * 2 = 12 * 2 = 24

分析:最终执行的条数,虽然是一致的。但是中间过程,有一张临时表是6,一张临时表是12,很明显6 < 12,对于内存来说,数据量越小越好,因此优化器肯定会选择第一种执行顺序。

结论:id值相同,从上往下顺序执行。表的执行顺序因表数量的改变而改变。

② 案例:查询教授SQL课程的老师的描述(desc)

#查看执行计划
explainselecttc.tcdescfromteacherCardtc
wheretc.tcid=
(
selectt.tcidfromteachert
wheret.tid=
(selectc.tidfromcoursecwherec.cname='sql')
);

结果如下:

2afa2890-ebc9-11ed-90ce-dac502259ad0.png

结论:id值不同,id值越大越优先查询。这是由于在进行嵌套子查询时,先查内层,再查外层。

③ 针对②做一个简单的修改

#查看执行计划
explainselectt.tname,tc.tcdescfromteachert,teacherCardtc
wheret.tcid=tc.tcid
andt.tid=(selectc.tidfromcoursecwherecname='sql');

结果如下:

2b0299d0-ebc9-11ed-90ce-dac502259ad0.png

结论:id值有相同,又有不同。id值越大越优先;id值相同,从上往下顺序执行。

2)select_type关键字的使用说明:查询类型

2b122ca6-ebc9-11ed-90ce-dac502259ad0.png

simple:简单查询

不包含子查询,不包含union查询。

explainselect*fromteacher;

结果如下:

2b184dd4-ebc9-11ed-90ce-dac502259ad0.png

② primary:包含子查询的主查询(最外层)

subquery:包含子查询的主查询(非最外层)

④ derived:衍生查询(用到了临时表)

a.在from子查询中,只有一张表;

b.在from子查询中,如果table1 union table2,则table1就是derived表;

explainselectcr.cname
from(select*fromcoursewheretid=1unionselect*fromcoursewheretid=2)cr;

结果如下:

2b20269e-ebc9-11ed-90ce-dac502259ad0.png

⑤ union:union之后的表称之为union表,如上例

⑥ union result:告诉我们,哪些表之间使用了union查询

3)type关键字的使用说明:索引类型

system、const只是理想状况,实际上只能优化到index --> range --> ref这个级别。要对type进行优化的前提是,你得创建索引。

2b2adbca-ebc9-11ed-90ce-dac502259ad0.png

① system

源表只有一条数据(实际中,基本不可能);

衍生表只有一条数据的主查询(偶尔可以达到)。

② const

仅仅能查到一条数据的SQL ,仅针对Primary key或unique索引类型有效。

explainselecttidfromtest01wheretid=1;

结果如下:

2b37adbe-ebc9-11ed-90ce-dac502259ad0.png

删除以前的主键索引后,此时我们添加一个其他的普通索引:

createindextest01_indexontest01(tid);
#再次查看执行计划
explainselecttidfromtest01wheretid=1;

结果如下:

2b410f08-ebc9-11ed-90ce-dac502259ad0.png

③ eq_ref

唯一性索引,对于每个索引键的查询,返回匹配唯一行数据(有且只有1个,不能多 、不能0),并且查询结果和数据条数必须一致。

此种情况常见于唯一索引和主键索引。

deletefromteacherwheretcid>=4;
altertableteacherCardaddconstraintpk_tcidprimarykey(tcid);
altertableteacheraddconstraintuk_tciduniqueindex(tcid);
explainselectt.tcidfromteachert,teacherCardtcwheret.tcid=tc.tcid;

结果如下:

2b4be112-ebc9-11ed-90ce-dac502259ad0.png

总结:以上SQL,用到的索引是t.tcid,即teacher表中的tcid字段;如果teacher表的数据个数和连接查询的数据个数一致(都是3条数据),则有可能满足eq_ref级别;否则无法满足。条件很苛刻,很难达到。

④ ref

非唯一性索引,对于每个索引键的查询,返回匹配的所有行(可以0,可以1,可以多)

准备数据:

2b54dab0-ebc9-11ed-90ce-dac502259ad0.png

创建索引,并查看执行计划:

#添加索引
altertableteacheraddindexindex_name(tname);
#查看执行计划
explainselect*fromteacherwheretname='tz';

结果如下:

2b60f494-ebc9-11ed-90ce-dac502259ad0.png

⑤ range

检索指定范围的行 ,where后面是一个范围查询(between, >, <, >=, in)

in有时候会失效,从而转为无索引时候的ALL

#添加索引
altertableteacheraddindextid_index(tid);
#查看执行计划:以下写了一种等价SQL写法,查看执行计划
explainselectt.*fromteachertwheret.tidin(1,2);
explainselectt.*fromteachertwheret.tid<3 ;

结果如下:

2b6b885a-ebc9-11ed-90ce-dac502259ad0.png

⑥ index

查询全部索引中的数据(扫描整个索引)

⑦ ALL

查询全部源表中的数据(暴力扫描全表)

2b77e8f2-ebc9-11ed-90ce-dac502259ad0.png

注意:cid是索引字段,因此查询索引字段,只需要扫描索引表即可。但是tid不是索引字段,查询非索引字段,需要暴力扫描整个源表,会消耗更多的资源。

4)possible_keys和key

possible_keys可能用到的索引。是一种预测,不准。了解一下就好。

key指的是实际使用的索引。

#先给course表的cname字段,添加一个索引
createindexcname_indexoncourse(cname);
#查看执行计划
explainselectt.tname,tc.tcdescfromteachert,teacherCardtc
wheret.tcid=tc.tcid
andt.tid=(selectc.tidfromcoursecwherecname='sql');

结果如下:

2b823dc0-ebc9-11ed-90ce-dac502259ad0.png

有一点需要注意的是:如果possible_key/key是NULL,则说明没用索引。

5)key_len

索引的长度,用于判断复合索引是否被完全使用(a,b,c)。

① 新建一张新表,用于测试

#创建表
createtabletest_kl
(
namechar(20)notnulldefault''
);
#添加索引
altertabletest_kladdindexindex_name(name);
#查看执行计划
explainselect*fromtest_klwherename='';

结果如下:

2b93bba4-ebc9-11ed-90ce-dac502259ad0.png

结果分析:因为我没有设置服务端的字符集,因此默认的字符集使用的是latin1,对于latin1一个字符代表一个字节,因此这列的key_len的长度是20,表示使用了name这个索引。

② 给test_kl表,新增name1列,该列没有设置“not null”

结果如下:

2ba11574-ebc9-11ed-90ce-dac502259ad0.png

结果分析:如果索引字段可以为null,则mysql底层会使用1个字节用于标识。

③ 删除原来的索引name和name1,新增一个复合索引

#删除原来的索引name和name1
dropindexindex_nameontest_kl;
dropindexindex_name1ontest_kl;
#增加一个复合索引
createindexname_name1_indexontest_kl(name,name1);
#查看执行计划
explainselect*fromtest_klwherename1='';--121
explainselect*fromtest_klwherename='';--60

结果如下:

2baa3be0-ebc9-11ed-90ce-dac502259ad0.png

结果分析: 对于下面这个执行计划,可以看到我们只使用了复合索引的第一个索引字段name,因此key_len是20,这个很清楚。再看上面这个执行计划,我们虽然仅仅在where后面使用了复合索引字段中的name1字段,但是你要使用复合索引的第2个索引字段,会默认使用了复合索引的第1个索引字段name,由于name1可以是null,因此key_len = 20 + 20 + 1 = 41呀!

④ 再次怎加一个name2字段,并为该字段创建一个索引。

不同的是:该字段数据类型是varchar

#新增一个字段name2,name2可以为null
altertabletest_kladdcolumnname2varchar(20);
#给name2字段,设置为索引字段
altertabletest_kladdindexname2_index(name2);
#查看执行计划
explainselect*fromtest_klwherename2='';

结果如下:

2bb08a5e-ebc9-11ed-90ce-dac502259ad0.png

结果分析: key_len = 20 + 1 + 2,这个20 + 1我们知道,这个2又代表什么呢?原来varchar属于可变长度,在mysql底层中,用2个字节标识可变长度。

6)ref

这里的ref的作用,指明当前表所参照的字段。

注意与type中的ref值区分。在type中,ref只是type类型的一种选项值。

#给course表的tid字段,添加一个索引
createindextid_indexoncourse(tid);
#查看执行计划
explainselect*fromcoursec,teachert
wherec.tid=t.tid
andt.tname='tw';

结果如下:

2bb8b9f4-ebc9-11ed-90ce-dac502259ad0.png

结果分析: 有两个索引,c表的c.tid引用的是t表的tid字段,因此可以看到显示结果为【数据库名.t.tid】,t表的t.name引用的是一个常量"tw",因此可以看到结果显示为const,表示一个常量。

7)rows(这个目前还是有点疑惑)

被索引优化查询的数据个数 (实际通过索引而查询到的数据个数)

explainselect*
fromcoursec,teachert
wherec.tid=t.tid
andt.tname='tz';

结果如下:

2bc114a0-ebc9-11ed-90ce-dac502259ad0.png

8)extra

表示其他的一些说明,也很有用。

① using filesort:针对单索引的情况

当出现了这个词,表示你当前的SQL性能消耗较大。表示进行了一次“额外”的排序。常见于order by语句中。

Ⅰ 什么是“额外”的排序?

为了讲清楚这个,我们首先要知道什么是排序。我们为了给某一个字段进行排序的时候,首先你得先查询到这个字段,然后在将这个字段进行排序。

紧接着,我们查看如下两个SQL语句的执行计划。

#新建一张表,建表同时创建索引
createtabletest02
(
a1char(3),
a2char(3),
a3char(3),
indexidx_a1(a1),
indexidx_a2(a2),
indexidx_a3(a3)
);
#查看执行计划
explainselect*fromtest02wherea1=''orderbya1;
explainselect*fromtest02wherea1=''orderbya2;

结果如下:

2bce9c74-ebc9-11ed-90ce-dac502259ad0.png

结果分析: 对于第一个执行计划,where后面我们先查询了a1字段,然后再利用a1做了依次排序,这个很轻松。但是对于第二个执行计划,where后面我们查询了a1字段,然而利用的却是a2字段进行排序,此时myql底层会进行一次查询,进行“额外”的排序。

总结:对于单索引,如果排序和查找是同一个字段,则不会出现using filesort;如果排序和查找不是同一个字段,则会出现using filesort;因此where哪些字段,就order by哪些些字段。

② using filesort:针对复合索引的情况

不能跨列(官方术语:最佳左前缀)

#删除test02的索引
dropindexidx_a1ontest02;
dropindexidx_a2ontest02;
dropindexidx_a3ontest02;
#创建一个复合索引
altertabletest02addindexidx_a1_a2_a3(a1,a2,a3);
#查看下面SQL语句的执行计划
explainselect*fromtest02wherea1=''orderbya3;--usingfilesort
explainselect*fromtest02wherea2=''orderbya3;--usingfilesort
explainselect*fromtest02wherea1=''orderbya2;

结果如下:

2bd998a4-ebc9-11ed-90ce-dac502259ad0.png

结果分析: 复合索引的顺序是(a1,a2,a3),可以看到a1在最左边,因此a1就叫做“最佳左前缀”,如果要使用后面的索引字段,必须先使用到这个a1字段。对于explain1,where后面我们使用a1字段,但是后面的排序使用了a3,直接跳过了a2,属于跨列;对于explain2,where后面我们使用了a2字段,直接跳过了a1字段,也属于跨列;对于explain3,where后面我们使用a1字段,后面使用的是a2字段,因此没有出现【using filesort】。

③ using temporary

当出现了这个词,也表示你当前的SQL性能消耗较大。这是由于当前SQL用到了临时表。一般出现在group by中。

explainselecta1fromtest02wherea1in('1','2','3')groupbya1;
explainselecta1fromtest02wherea1in('1','2','3')groupbya2;--usingtemporary

结果如下:

2be584a2-ebc9-11ed-90ce-dac502259ad0.png

结果分析: 当你查询哪个字段,就按照那个字段分组,否则就会出现using temporary。

针对using temporary,我们在看一个例子:

using temporary表示需要额外再使用一张表,一般出现在group by语句中。虽然已经有表了,但是不适用,必须再来一张表。

再次来看mysql的编写过程和解析过程。

Ⅰ 编写过程

selectdinstinct..from..join..on..where..groupby..having..orderby..limit..

Ⅱ 解析过程

from..on..join..where..groupby..having..selectdinstinct..orderby..limit..

很显然,where后是group by,然后才是select。基于此,我们再查看如下两个SQL语句的执行计划。

explainselect*fromtest03wherea2=2anda4=4groupbya2,a4;
explainselect*fromtest03wherea2=2anda4=4groupbya3;

分析如下: 对于第一个执行计划,where后面是a2和a4,接着我们按照a2和a4分组,很明显这两张表已经有了,直接在a2和a4上分组就行了。但是对于第二个执行计划,where后面是a2和a4,接着我们却按照a3分组,很明显我们没有a3这张表,因此有需要再来一张临时表a3。因此就会出现using temporary。

④ using index

当你看到这个关键词,恭喜你,表示你的SQL性能提升了。

using index称之为“索引覆盖”。

当出现了using index,就表示不用读取源表,而只利用索引获取数据,不需要回源表查询。

只要使用到的列,全部出现在索引中,就是索引覆盖。

#删除test02中的复合索引idx_a1_a2_a3
dropindexidx_a1_a2_a3ontest02;
#重新创建一个复合索引
idx_a1_a2createindexidx_a1_a2ontest02(a1,a2);
#查看执行计划
explainselecta1,a3fromtest02wherea1=''ora3='';
explainselecta1,a2fromtest02wherea1=''anda2='';

结果如下:

2bec1e84-ebc9-11ed-90ce-dac502259ad0.png

结果分析: 我们创建的是a1和a2的复合索引,对于第一个执行计划,我们却出现了a3,该字段并没有创建索引,因此没有出现using index,而是using where,表示我们需要回表查询。对于第二个执行计划,属于完全的索引覆盖,因此出现了using index。

针对using index,我们在查看一个案例:

explainselecta1,a2fromtest02wherea1=''ora2='';
explainselecta1,a2fromtest02;

结果如下:

如果用到了索引覆盖(using index时),会对possible_keys和key造成影响:

a.如果没有where,则索引只出现在key中;

b.如果有where,则索引 出现在key和possible_keys中。

⑤ using where

表示需要【回表查询】,表示既在索引中进行了查询,又回到了源表进行了查询。

#删除test02中的复合索引idx_a1_a2
dropindexidx_a1_a2ontest02;
#将a1字段,新增为一个索引
createindexa1_indexontest02(a1);
#查看执行计划
explainselecta1,a3fromtest02wherea1=""anda3="";

结果如下:

2bf66a9c-ebc9-11ed-90ce-dac502259ad0.png

结果分析: 我们既使用了索引a1,表示我们使用了索引进行查询。但是又对于a3字段,我们并没有使用索引,因此对于a3字段,需要回源表查询,这个时候出现了using where。

⑥ impossible where(了解)

当where子句永远为False的时候,会出现impossible where

#查看执行计划
explainselecta1fromtest02wherea1="a"anda1="b";

结果如下:

2bfdacc6-ebc9-11ed-90ce-dac502259ad0.png

6、优化示例

1)引入案例

#创建新表
createtabletest03
(
a1int(4)notnull,
a2int(4)notnull,
a3int(4)notnull,
a4int(4)notnull
);
#创建一个复合索引
createindexa1_a2_a3_test03ontest03(a1,a2,a3);
#查看执行计划
explainselecta3fromtest03wherea1=1anda2=2anda3=3;

结果如下:

2c09e130-ebc9-11ed-90ce-dac502259ad0.png

推荐写法: 复合索引顺序和使用顺序一致。

下面看看【不推荐写法】:复合索引顺序和使用顺序不一致。

#查看执行计划
explainselecta3fromtest03wherea3=1anda2=2anda1=3;

结果如下:

2c12f342-ebc9-11ed-90ce-dac502259ad0.png

结果分析: 虽然结果和上述结果一致,但是不推荐这样写。但是这样写怎么又没有问题呢?这是由于SQL优化器的功劳,它帮我们调整了顺序。

最后再补充一点:对于复合索引,不要跨列使用

#查看执行计划
explainselecta3fromtest03wherea1=1anda3=2groupbya3;

结果如下:

2c254e5c-ebc9-11ed-90ce-dac502259ad0.png

结果分析: a1_a2_a3是一个复合索引,我们使用a1索引后,直接跨列使用了a3,直接跳过索引a2,因此索引a3失效了,当使用a3进行分组的时候,就会出现using where。

2)单表优化

#创建新表
createtablebook
(
bidint(4)primarykey,
namevarchar(20)notnull,
authoridint(4)notnull,
publicidint(4)notnull,
typeidint(4)notnull
);
#插入数据
insertintobookvalues(1,'tjava',1,1,2);
insertintobookvalues(2,'tc',2,1,2);
insertintobookvalues(3,'wx',3,2,1);
insertintobookvalues(4,'math',4,2,3);

结果如下:

2c2f9ede-ebc9-11ed-90ce-dac502259ad0.png

案例:查询authorid=1且typeid为2或3的bid,并根据typeid降序排列。

explain
selectbidfrombook
wheretypeidin(2,3)andauthorid=1
orderbytypeiddesc;

结果如下:

2c35eab4-ebc9-11ed-90ce-dac502259ad0.png

这是没有进行任何优化的SQL,可以看到typ为ALL类型,extra为using filesort,可以想象这个SQL有多恐怖。

优化:添加索引的时候,要根据MySQL解析顺序添加索引,又回到了MySQL的解析顺序,下面我们再来看看MySQL的解析顺序。

from..on..join..where..groupby..having..selectdinstinct..orderby..limit..

① 优化1:基于此,我们进行索引的添加,并再次查看执行计划。

#添加索引
createindextypeid_authorid_bidonbook(typeid,authorid,bid);
#再次查看执行计划
explain
selectbidfrombook
wheretypeidin(2,3)andauthorid=1
orderbytypeiddesc;

结果如下:

2c43b856-ebc9-11ed-90ce-dac502259ad0.png

结果分析: 结果并不是和我们想象的一样,还是出现了using where,查看索引长度key_len=8,表示我们只使用了2个索引,有一个索引失效了。

② 优化2:使用了in有时候会导致索引失效,基于此有了如下一种优化思路。

将in字段放在最后面。需要注意一点:每次创建新的索引的时候,最好是删除以前的废弃索引,否则有时候会产生干扰(索引之间)。

#删除以前的索引
dropindextypeid_authorid_bidonbook;
#再次创建索引
createindexauthorid_typeid_bidonbook(authorid,typeid,bid);
#再次查看执行计划
explain
selectbidfrombook
whereauthorid=1andtypeidin(2,3)
orderbytypeiddesc;

结果如下:

2c4fad00-ebc9-11ed-90ce-dac502259ad0.png

结果分析: 这里虽然没有变化,但是这是一种优化思路。

总结如下:

a.最佳做前缀,保持索引的定义和使用的顺序一致性

b.索引需要逐步优化(每次创建新索引,根据情况需要删除以前的废弃索引)

c.将含In的范围查询,放到where条件的最后,防止失效。

本例中同时出现了Using where(需要回原表); Using index(不需要回原表):原因,where authorid=1 and typeid in(2,3)中authorid在索引(authorid,typeid,bid)中,因此不需要回原表(直接在索引表中能查到);而typeid虽然也在索引(authorid,typeid,bid)中,但是含in的范围查询已经使该typeid索引失效,因此相当于没有typeid这个索引,所以需要回原表(using where);

例如以下没有了In,则不会出现using where:

explainselectbidfrombook
whereauthorid=1andtypeid=3
orderbytypeiddesc;

结果如下:

2c580fd6-ebc9-11ed-90ce-dac502259ad0.png

3)两表优化

#创建teacher2新表
createtableteacher2
(
tidint(4)primarykey,
cidint(4)notnull
);
#插入数据
insertintoteacher2values(1,2);
insertintoteacher2values(2,1);
insertintoteacher2values(3,3);
#创建course2新表
createtablecourse2
(
cidint(4),
cnamevarchar(20)
);
#插入数据
insertintocourse2values(1,'java');
insertintocourse2values(2,'python');
insertintocourse2values(3,'kotlin');

案例:使用一个左连接,查找教java课程的所有信息。

explain
select*
fromteacher2t
leftouterjoincourse2c
ont.cid=c.cid
wherec.cname='java';

结果如下:

2c621c42-ebc9-11ed-90ce-dac502259ad0.png

① 优化

对于两张表,索引往哪里加?答:对于表连接,小表驱动大表。索引建立在经常使用的字段上。

为什么小表驱动大表好一些呢?

小表:10
大表:300
#小表驱动大表
select...where小表.x10=大表.x300;
for(inti=0;i<小表.length10;i++)
{   
    for(intj=0;j<大表.length300;j++)  
    {       
        ...   
    }
}
#大表驱动小表
select...where大表.x300=小表.x10;
for(inti=0;i<大表.length300;i++)
{  
    for(intj=0;j<小表.length10;j++)   
    {     
        ...   
    }
}

分析: 以上2个FOR循环,最终都会循环3000次;但是对于双层循环来说:一般建议,将数据小的循环,放外层。数据大的循环,放内层。不用管这是为什么,这是编程语言的一个原则,对于双重循环,外层循环少,内存循环大,程序的性能越高。

结论:当编写【…on t.cid=c.cid】时,将数据量小的表放左边(假设此时t表数据量小,c表数据量大。)

我们已经知道了,对于两表连接,需要利用小表驱动大表,例如【…on t.cid=c.cid】,t如果是小表(10条),c如果是大表(300条),那么t每循环1次,就需要循环300次,即t表的t.cid字段属于,经常使用的字段,因此需要给cid字段添加索引。

更深入的说明: 一般情况下,左连接给左表加索引。右连接给右表加索引。其他表需不需要加索引,我们逐步尝试。

#给左表的字段加索引
createindexcid_teacher2onteacher2(cid);
#查看执行计划
explain
select*
fromteacher2t
leftouterjoincourse2c
ont.cid=c.cid
wherec.cname='java';

结果如下:

2c6b51a4-ebc9-11ed-90ce-dac502259ad0.png

当然你可以下去接着优化,给cname添加一个索引。索引优化是一个逐步的过程,需要一点点尝试。

#给cname的字段加索引
createindexcname_course2oncourse2(cname);
#查看执行计划
explain
selectt.cid,c.cname
fromteacher2t
leftouterjoincourse2c
ont.cid=c.cid
wherec.cname='java';

结果如下:

2c768c5e-ebc9-11ed-90ce-dac502259ad0.png

最后补充一个:Using join buffer是extra中的一个选项,表示Mysql引擎使用了“连接缓存”,即MySQL底层动了你的SQL,你写的太差了。

4)三表优化

  • 大于等于张表,优化原则一样
  • 小表驱动大表
  • 索引建立在经常查询的字段上

7、避免索引失效的一些原则

① 复合索引需要注意的点

  • 复合索引,不要跨列或无序使用(最佳左前缀)
  • 复合索引,尽量使用全索引匹配,也就是说,你建立几个索引,就使用几个索引

② 不要在索引上进行任何操作(计算、函数、类型转换),否则索引失效

explainselect*frombookwhereauthorid=1andtypeid=2;
explainselect*frombookwhereauthorid*2=1andtypeid=2;

结果如下:

2c7f4524-ebc9-11ed-90ce-dac502259ad0.png

③ 索引不能使用不等于(!= <>)或is null (is not null),否则自身以及右侧所有全部失效(针对大多数情况)。复合索引中如果有>,则自身和右侧索引全部失效。

#针对不是复合索引的情况
explainselect*frombookwhereauthorid!=1andtypeid=2;
explainselect*frombookwhereauthorid!=1andtypeid!=2;

结果如下:

2c8716f0-ebc9-11ed-90ce-dac502259ad0.png

再观看下面这个案例:

#删除单独的索引
dropindexauthorid_indexonbook;
dropindextypeid_indexonbook;
#创建一个复合索引
altertablebookaddindexidx_book_at(authorid,typeid);
#查看执行计划
explainselect*frombookwhereauthorid>1andtypeid=2;
explainselect*frombookwhereauthorid=1andtypeid>2;

结果如下:

2c9080c8-ebc9-11ed-90ce-dac502259ad0.png

结论:复合索引中如果有【>】,则自身和右侧索引全部失效。

在看看复合索引中有【<】的情况:

2c993e20-ebc9-11ed-90ce-dac502259ad0.png

我们学习索引优化 ,是一个大部分情况适用的结论,但由于SQL优化器等原因 该结论不是100%正确。一般而言, 范围查询(> < in),之后的索引失效。

④ SQL优化,是一种概率层面的优化。至于是否实际使用了我们的优化,需要通过explain进行推测。

#删除复合索引
dropindexauthorid_typeid_bidonbook;
#为authorid和typeid,分别创建索引
createindexauthorid_indexonbook(authorid);
createindextypeid_indexonbook(typeid);
#查看执行计划
explainselect*frombookwhereauthorid=1andtypeid=2;

结果如下:

2ca54170-ebc9-11ed-90ce-dac502259ad0.png

结果分析: 我们创建了两个索引,但是实际上只使用了一个索引。因为对于两个单独的索引,程序觉得只用一个索引就够了,不需要使用两个。

当我们创建一个复合索引,再次执行上面的SQL:

#查看执行计划
explainselect*frombookwhereauthorid=1andtypeid=2;

结果如下:

2cae6b1a-ebc9-11ed-90ce-dac502259ad0.png

⑤ 索引覆盖,百分之百没问题

⑥ like尽量以“常量”开头,不要以’%'开头,否则索引失效

explainselect*fromteacherwheretnamelike"%x%";
explainselect*fromteacherwheretnamelike'x%';
explainselecttnamefromteacherwheretnamelike'%x%';

结果如下:

2cb422da-ebc9-11ed-90ce-dac502259ad0.png

结论如下: like尽量不要使用类似"%x%"情况,但是可以使用"x%"情况。如果非使用 "%x%"情况,需要使用索引覆盖。

⑦ 尽量不要使用类型转换(显示、隐式),否则索引失效

explainselect*fromteacherwheretname='abc';
explainselect*fromteacherwheretname=123;

结果如下:

2cbcd07e-ebc9-11ed-90ce-dac502259ad0.png

⑧ 尽量不要使用or,否则索引失效

explainselect*fromteacherwheretname=''andtcid>1;
explainselect*fromteacherwheretname=''ortcid>1;

结果如下:

2cc50668-ebc9-11ed-90ce-dac502259ad0.png

注意:or很猛,会让自身索引和左右两侧的索引都失效。

8、一些其他的优化方法

1)exists和in的优化

如果主查询的数据集大,则使用i关键字,效率高。

如果子查询的数据集大,则使用exist关键字,效率高。

select..fromtablewhereexist(子查询);
select..fromtablewhere字段in(子查询);

2)order by优化

  • IO就是访问硬盘文件的次数
  • using filesort 有两种算法:双路排序、单路排序(根据IO的次数)
  • MySQL4.1之前默认使用双路排序;双路:扫描2次磁盘(1:从磁盘读取排序字段,对排序字段进行排序(在buffer中进行的排序)2:扫描其他字段)
  • MySQL4.1之后默认使用单路排序:只读取一次(全部字段),在buffer中进行排序。但种单路排序会有一定的隐患(不一定真的是“单路/1次IO”,有可能多次IO)。原因:如果数据量特别大,则无法将所有字段的数据一次性读取完毕,因此会进行“分片读取、多次读取”。
  • 注意:单路排序 比双路排序 会占用更多的buffer。
  • 单路排序在使用时,如果数据大,可以考虑调大buffer的容量大小:
#不一定真的是“单路/1次IO”,有可能多次IO
setmax_length_for_sort_data=1024

如果max_length_for_sort_data值太低,则mysql会自动从 单路->双路(太低:需要排序的列的总大小超过了max_length_for_sort_data定义的字节数)

① 提高order by查询的策略:

  • 选择使用单路、双路 ;调整buffer的容量大小
  • 避免使用select * …(select后面写所有字段,也比写*效率高)
  • 复合索引,不要跨列使用 ,避免using filesort保证全部的排序字段,排序的一致性(都是升序或降序)

篇幅很长,内容较多,建议收藏。

审核编辑 :李倩


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 服务器
    +关注

    关注

    12

    文章

    9015

    浏览量

    85169
  • SQL
    SQL
    +关注

    关注

    1

    文章

    759

    浏览量

    44062
  • 数据库
    +关注

    关注

    7

    文章

    3761

    浏览量

    64268

原文标题:1.8w 字的 SQL 优化大全

文章出处:【微信号:DBDevs,微信公众号:数据分析与开发】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    数据库SQL优化

    数据库执行SQL都会先进行语义解析,然后将SQL分成一步一步可执行的计划,然后逐步执行。通过分析执行计划,我们可以清晰的看到数据库执行的操作,这对于数据库SQL优化具有重大意义。 1
    的头像 发表于 10-09 15:43 1013次阅读
    数据库<b class='flag-5'>SQL</b>的<b class='flag-5'>优化</b>

    MySQL的执行过程 SQL语句性能优化常用策略

    回顾 MySQL 的执行过程,帮助介绍如何进行 sql 优化
    的头像 发表于 12-12 10:26 620次阅读
    MySQL的执行过程 <b class='flag-5'>SQL</b>语句性能<b class='flag-5'>优化</b>常用策略

    xilinx ddr2硬核管脚问题

    我用xilinx spartan-6fpga 它硬核的管脚是固定的还是可配置的我在xilinx提供的文档里找不到关于硬核管脚的分配求指导
    发表于 08-11 09:28

    关于FPGA的硬核知识,求大神科普一下。。

    FPGA的硬核和软核有什么区别呢,有没有使用硬核的开发板,想学习关于FPGA硬核的知识,各位大神有什么建议呢?真心求教
    发表于 03-05 11:51

    Spark SQL的工作原理和性能优化

    Spark SQL(九):工作原理和性能优化
    发表于 06-12 16:21

    30种SQL语句优化总结

    必须掌握的30种SQL语句优化
    发表于 04-21 11:38

    数据库设计及开发规范之sql性能优化

    数据库设计及开发规范,sql性能优化
    发表于 05-08 10:58

    SQL语句怎么优化

    SQL语句优化——结合书籍论坛小结
    发表于 06-14 14:46

    内存条配置优化SQL Server服务器性能

    内存条配置优化SQL Server服务器性能  Microsoft SQL Server 2000 的 内存管理组件消除了对 SQL Server 可用的内存进行手工管理的需要。
    发表于 01-11 11:00 876次阅读

    SQL后悔药,SQL性能优化SQL规范优雅

    每一个好习惯都是一笔财富,本文基于MySQL,分SQL后悔药, SQL性能优化SQL规范优雅三个方向,分享写SQL的21个好习惯,谢谢阅读
    的头像 发表于 11-14 09:54 1793次阅读

    30种SQL语句优化方法

    SQL查询中为了提高查询效率,我们常常会采取一些措施对查询语句进行SQL优化,下面总结一些方法,供大家参考。 01 对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及
    的头像 发表于 11-19 16:05 1970次阅读

    SQL子查询优化是怎么回事

    子查询 (Subquery)的优化一直以来都是 SQL 查询优化中的难点之一。 关联子查询的基本执行方式类似于 Nested-Loop,但是这种执行方式的效率常常低到难以忍受。 当数据量稍大时,必须
    的头像 发表于 02-01 13:55 2009次阅读
    <b class='flag-5'>SQL</b>子查询<b class='flag-5'>优化</b>是怎么回事

    SQL优化技巧分享

    一、查询SQL尽量不要使用select *,而是具体字段
    的头像 发表于 09-06 10:24 1357次阅读

    一文终结SQL子查询优化

    子查询(Subquery)的优化一直以来都是 SQL 查询优化中的难点之一。关联子查询的基本执行方式类似于 Nested-Loop,但是这种执行方式的效率常常低到难以忍受。
    的头像 发表于 04-28 14:19 721次阅读
    一文终结<b class='flag-5'>SQL</b>子查询<b class='flag-5'>优化</b>

    Oracle长耗时SQL优化案例

    最近在生产客服平台,运营岗老师反馈,一个2w人的企业,在信息详情查询时,加载时间过长,越70s左右出结果,需要后台优化SQL
    的头像 发表于 05-19 15:02 984次阅读