SQL优化13连问,收藏好!
Posted zhangkaixuan456
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SQL优化13连问,收藏好!相关的知识,希望对你有一定的参考价值。
1.日常工作中,你是怎么优化SQL的?
大家可以从这几个维度回答这个问题:
-
分析慢查询日志
-
使用explain查看执行计划
-
索引优化
-
深分页优化
-
避免全表扫描
-
避免返回不必要的数据(如
select
具体字段而不是select*
) -
使用合适的数据类型(如可以使用
int
类型的话,就不要设计为varchar
) -
优化sql结构(如
join
优化等等) -
适当分批量进行 (如批量更新、删除)
-
定期清理无用的数据
-
适当分库分表
-
读写分离
2. 是否遇到过深分页问题,如何解决
我们可以通过减少回表次数来优化。一般有标签记录法和延迟关联法。
标签记录法
就是标记一下上次查询到哪一条了,下次再来查的时候,从该条开始往下扫描。就好像看书一样,上次看到哪里了,你就折叠一下或者夹个书签,下次来看的时候,直接就翻到啦。
假设上一次记录到100000,则SQL可以修改为:
select id,name,balance FROM account where id > 100000 limit 10;
这样的话,后面无论翻多少页,性能都会不错的,因为命中了id
索引。但是这种方式有局限性:需要一种类似连续自增的字段。
延迟关联法
延迟关联法,就是把条件转移到主键索引树,然后减少回表。假设原生SQL是这样的的,其中id
是主键,create_time
是普通索引
select id,name,balance from account where create_time> '2020-09-19' limit 100000,10;
使用延迟关联法优化,如下:
select acct1.id,acct1.name,acct1.balance FROM account acct1 INNER JOIN
(SELECT a.id FROM account a WHERE a.create_time > '2020-09-19' limit 100000, 10)
AS acct2 on acct1.id= acct2.id;
优化思路就是,先通过idx_create_time
二级索引树查询到满足条件的主键ID
,再与原表通过主键ID
内连接,这样后面直接走了主键索引了,同时也减少了回表。
3. 聊聊explain执行计划
当explain
与SQL
一起使用时,mysql将显示来自优化器的有关语句执行计划的信息。即MySQL
解释了它将如何处理该语句,包括有关如何连接表以及以何种顺序连接表等信息。
一条简单SQL,使用了explain
的效果如下:
一般来说,我们需要重点关注type、rows、filtered、extra、key
。
3.1 type
type表示连接类型,查看索引执行情况的一个重要指标。以下性能从好到坏依次:system > const > eq_ref > ref > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
-
system:这种类型要求数据库表中只有一条数据,是
const
类型的一个特例,一般情况下是不会出现的。 -
const:通过一次索引就能找到数据,一般用于主键或唯一索引作为条件,这类扫描效率极高,,速度非常快。
-
eq_ref:常用于主键或唯一索引扫描,一般指使用主键的关联查询
-
ref : 常用于非主键和唯一索引扫描。
-
ref_or_null:这种连接类型类似于
ref
,区别在于MySQL
会额外搜索包含NULL
值的行 -
index_merge:使用了索引合并优化方法,查询使用了两个以上的索引。
-
unique_subquery:类似于
eq_ref
,条件用了in
子查询 -
index_subquery:区别于
unique_subquery
,用于非唯一索引,可以返回重复值。 -
range:常用于范围查询,比如:between ... and 或 In 等操作
-
index:全索引扫描
-
ALL:全表扫描
3.2 rows
该列表示MySQL估算要找到我们所需的记录,需要读取的行数。对于InnoDB表,此数字是估计值,并非一定是个准确值。
3.3 filtered
该列是一个百分比的值,表里符合条件的记录数的百分比。简单点说,这个字段表示存储引擎返回的数据在经过过滤后,剩下满足条件的记录数量的比例。
3.4 extra
该字段包含有关MySQL如何解析查询的其他信息,它一般会出现这几个值:
-
Using filesort:表示按文件排序,一般是在指定的排序和索引排序不一致的情况才会出现。一般见于order by语句
-
Using index :表示是否用了覆盖索引。
-
Using temporary: 表示是否使用了临时表,性能特别差,需要重点优化。一般多见于group by语句,或者union语句。
-
Using where : 表示使用了where条件过滤.
-
Using index condition:MySQL5.6之后新增的索引下推。在存储引擎层进行数据过滤,而不是在服务层过滤,利用索引现有的数据减少回表的数据。
3.5 key
该列表示实际用到的索引。一般配合possible_keys
列一起看。
注意:有时候,explain
配合show WARNINGS;
(可以查看优化后,最终执行的sql),效果更佳哦。
4.说说大表的优化方案
-
数据库设计优化
合理的数据库设计可以极大地提高查询效率。我们在设计大表时,可以考虑拆分表、使用分区表、添加索引等方式来优化表结构。同时也要避免使用大量冗余字段、避免频繁使用join查询等操作。
-
索引优化
对于大表的查询操作,索引优化是非常重要的一环。可以考虑增加或者修改索引、使用覆盖索引、使用联合索引等方式来提高查询效率。同时也要注意定期清理冗余的索引以及对于经常使用的查询语句建立索引。
-
分区优化
将大表按照某个列分成多个分区表,每个分区表的数据量较小,可以提高查询和更新的性能。分区表还可以帮助在维护表结构的同时,减少锁表时间,提高并发处理能力。
-
数据清理归档
对于一些历史数据或者无用数据,可以进行定期归档,避免数据过多造成SQL查询效率降低。同时也要注意对于大表进行定期的数据备份以及紧急数据恢复的准备工作。
-
缓存优化
对于一些经常被查询的数据,可以使用缓存优化。使用Redis
等缓存中间件来缓存常用的数据,以减少查询数据库的次数,提高查询效率。
-
SQL语句优化
在编写SQL查询语句时,要尽可能地简单明了,避免复杂的查询语句,同时也要避免一些不必要的查询操作。对于复杂的查询语句,可以使用Explain
执行计划来进行优化。同时也要注意避免使用OR
等耗费性能的操作符。
-
分库分表
如果数据量千万级别,需要考虑分库分表哈。分库分表相关知识点,可以看我之前这篇文章哈,我们为什么要分库分表?
5.哪些因素可能导致MySQL慢查询?
慢查询一般有以下这些原因:
大家有兴趣可以看下。我之前写的这篇文章哈:盘点MySQL慢查询的12个原因
6.如何使用索引优化SQL查询?
-
添加合适索引(在
where、group by、order by
等后面的字段添加合适索引) -
选择合适的索引类型 (
B-tree
索引适合范围查询、哈希索引适合等值查询) -
注意不适合加索引的场景(数据量少的表,更新频繁的字段,区分度低的字段)
-
加索引的时候,需要考虑覆盖索引,减少回表,考虑联合索引的最左前缀原则
-
explain
查看SQL
的执行计划,确认是否会命中索引。 -
注意索引并不是越多越好,通常建议在单个表中不要超过5个索引。因为索引会占用磁盘空间,索引更新代价高。
7.聊聊慢SQL的优化思路
-
查看慢查询日志记录,分析慢SQL
-
explain分析SQL的执行计划
-
profile 分析执行耗时
-
Optimizer Trace分析详情
-
确定问题并采用相应的措施
7.1 查看慢查询日志记录,分析慢SQL
如何定位慢SQL呢、我们可以通过slow log来查看慢SQL
。默认的情况下呢,MySQL数据库是不开启慢查询日志(slow query log
)呢。所以我们需要手动把它打开。
查看下慢查询日志配置,我们可以使用show variables like 'slow_query_log%'
命令,如下:
-
slow query log
表示慢查询开启的状态 -
slow_query_log_file
表示慢查询日志存放的位置
我们还可以使用show variables like 'long_query_time'
命令,查看超过多少时间,才记录到慢查询日志,如下:
-
long_query_time
表示查询超过多少秒才记录到慢查询日志。
我们可以通过慢查日志,定位那些执行效率较低的SQL语句,重点关注分析。
7.2 explain查看分析SQL的执行计划
当定位出查询效率低的SQL后,可以使用explain
查看SQL
的执行计划。
当explain
与SQL
一起使用时,MySQL将显示来自优化器的有关语句执行计划的信息。即MySQL
解释了它将如何处理该语句,包括有关如何连接表以及以何种顺序连接表等信息。
一条简单SQL,使用了explain
的效果如下:
一般来说,我们需要重点关注type、rows、filtered、extra、key
。
7.3 profile 分析执行耗时
explain
只是看到SQL
的预估执行计划,如果要了解SQL
真正的执行线程状态及消耗的时间,需要使用profiling
。开启profiling
参数后,后续执行的SQL
语句都会记录其资源开销,包括IO,上下文切换,CPU,内存
等等,我们可以根据这些开销进一步分析当前慢SQL的瓶颈再进一步进行优化。
profiling
默认是关闭,我们可以使用show variables like '%profil%'
查看是否开启,如下:
可以使用set profiling=ON
开启。开启后,可以运行几条SQL,然后使用show profiles
查看一下。
show profiles
会显示最近发给服务器的多条语句,条数由变量profiling_history_size
定义,默认是15。如果我们需要看单独某条SQL的分析,可以show profile
查看最近一条SQL的分析。也可以使用show profile for query id
(其中id就是show profiles中的QUERY_ID)查看具体一条的SQL语句分析。
除了查看profile ,还可以查看cpu和io,如上图。
7.4 Optimizer Trace分析详情
profile只能查看到SQL的执行耗时,但是无法看到SQL真正执行的过程信息,即不知道MySQL优化器是如何选择执行计划。这时候,我们可以使用Optimizer Trace
,它可以跟踪执行语句的解析优化执行的全过程。
我们可以使用set optimizer_trace="enabled=on"
打开开关,接着执行要跟踪的SQL,最后执行select * from information_schema.optimizer_trace
跟踪,如下:
大家可以查看分析其执行树,会包括三个阶段:
-
join_preparation:准备阶段
-
join_optimization:分析阶段
-
join_execution:执行阶段
7.5 确定问题并采用相应的措施
最后确认问题,就采取对应的措施。
-
多数慢SQL都跟索引有关,比如不加索引,索引不生效、不合理等,这时候,我们可以优化索引。
-
我们还可以优化SQL语句,比如一些in元素过多问题(分批),深分页问题(基于上一次数据过滤等),进行时间分段查询
-
SQl没办法很好优化,可以改用ES的方式,或者数仓。
-
如果单表数据量过大导致慢查询,则可以考虑分库分表
-
如果数据库在刷脏页导致慢查询,考虑是否可以优化一些参数,跟DBA讨论优化方案
-
如果存量数据量太大,考虑是否可以让部分数据归档
我之前写了一篇文章,有关于导致慢查询的12个原因,大家看一下哈:盘点MySQL慢查询的12个原因
8.一条sql执行过长的时间,你如何优化,从哪些方面入手?
这道面试题,其实跟慢SQl排查解决有点像,所以大家回答得时候,可以参考上一小节哈。我们可以从这几个方面入手哈:
-
确定瓶颈
-
索引优化
-
优化SQL语句
-
数据库参数优化
-
分析锁的情况
-
数据库硬件升级
确定瓶颈
首先,通过查看MySQL日志,慢查询日志,explain分析SQL的执行计划,profile 分析执行耗时,Optimizer Trace分析详情等操作,确定查询执行的瓶颈在哪里。只有确定了瓶颈,才能有针对性地进行优化。
索引优化
在确定了瓶颈之后,可以考虑通过增加索引来优化查询效率。可以根据查询语句的条件,增加相应的索引,从而加快查询速度。但是索引也会带来一些负面影响,如占用磁盘空间,降低写入效率等,所以需要根据具体情况权衡。
优化SQL语句
有些SQL语句本身可能存在一些问题,如join操作过于频繁,使用了不必要的子查询等,这些都会导致查询效率低下。可以通过优化SQL语句来减少不必要的操作,从而提高查询效率。
数据库参数优化
数据库参数也会影响查询效率,可以通过修改数据库参数来优化查询效率,如修改内存缓存大小、修改连接池大小等。不同的数据库参数优化方式不同,需要根据具体情况进行调整。
分析锁的情况
查询执行时间过长有可能是由于锁的问题导致的,需要分析查询语句中是否存在锁的问题,如果存在锁的问题,可以考虑增加锁的并发度,从而提高查询效率。
数据库硬件升级
如果以上方法都无法解决问题,可以考虑对数据库硬件进行升级,如增加 CPU 数量、加快磁盘读写速度等,从而提高数据库的整体性能。
9. 列举一下,常用的数据库设计优化技巧?
-
字段尽量避免使用NULL
-
合理选择数据类型
-
字段选择合适的长度
-
正确使用索引
-
尽量少定义text类型
-
合理的数据表结构设计
-
适当的冗余设计
-
优化SQL查询语句
-
一张表的字段不宜过多
10.列举日常开发中,列举十个书写高质量SQL的小技巧
-
查询SQL尽量不要使用select *,而是select具体字段。
-
小表驱动大表
-
优化你的like语句
-
尽量避免在索引列上使用mysql的内置函数
-
如果插入数据过多,考虑批量操作。
-
多用limit
-
小表驱动大表
-
exist & in合理利用
-
in元素不要过多
-
尽量用union all替换union
大家可以参考我之前这篇文章哈 :后端程序员必备:书写高质量SQL的30条建议
11.index merge了解过嘛?
index merge
是什么?
在
MySQL
中,当执行一个查询语句需要使用多个索引时,MySQL可以使用索引合并(Index Merge
)来优化查询性能。具体来说,索引合并是将多个单列索引或多个联合索引合并使用,以满足查询语句的需要。
当使用索引合并时,MySQL
会选择最优的索引组合来执行查询,从而避免了全表扫描和排序操作,提高了查询效率。而对于使用多个单列索引的查询语句,MySQL也可以使用索引合并来优化查询性能。
大家可以看一个使用index merge
的例子:
假设有一个名为orders
的表,包含order_id、customer_id、product_id、order_date等字段,其中order_id、customer_id、product_id
三个字段都建有索引。
如果要查询customer_id
为1
,order_date
在2022年1月1日到2022年2月1日之间的订单记录,可以使用以下SQL语句:
SELECT *
FROM orders
WHERE customer_id = 1
AND order_date >= '2022-01-01'
AND order_date < '2022-02-01'
在执行该查询语句时,MySQL可以使用customer_id
索引和order_date
索引来优化查询。如果使用单个索引,则需要扫描整个索引树来匹配查询条件;但如果使用索引合并,则可以先使用customer_id
索引来过滤出符合条件的记录,然后再使用order_date
索引来进一步过滤记录,从而大大减少了扫描的记录数,提高了查询效率。
大家可以使用EXPLAIN关键字可以查看查询计划,确认是否使用了索引合并。例如,执行以下语句:
EXPLAIN SELECT *
FROM orders
WHERE customer_id = 1
AND order_date >= '2022-01-01'
AND order_date < '2022-02-01'
如果查询计划中出现了Using index merge
的信息,则表示该查询使用了索引合并优化。
12. order by查询效率慢,如何优化.
大家是否还记得order by
查询为什么会慢嘛?
order by
排序,分为全字段排序和rowid
排序。它是拿max_length_for_sort_data
和结果行数据长度对比,如果结果行数据长度超过max_length_for_sort_data
这个值,就会走rowid
排序,相反,则走全字段排序。
rowid
排序,一般需要回表去找满足条件的数据,所以效率会慢一点.如果是order by
排序,可能会借助磁盘文件排序的话,效率就更慢一点.
如何优化order by
的文件排序?
-
因为数据是无序的,所以就需要排序。如果数据本身是有序的,那就不会再用到文件排序啦。而索引数据本身是有序的,我们通过建立索引来优化
order by
语句。 -
我们还可以通过调整
max_length_for_sort_data、sort_buffer_size
等参数优化;
大家忘记order by
的话,可以看我之前的这篇文章哈:看一遍就理解:order by详解
13. group by 查询慢的话,如何优化呀.
group by
一般用于分组统计,它表达的逻辑就是根据一定的规则,进行分组。日常开发中,我们使用得比较频繁。如果不注意,很容易产生慢SQL
。
group by
可能会慢在哪里?因为它既用到临时表,又默认用到排序。有时候还可能用到磁盘临时表。
-
如果执行过程中,会发现内存临时表大小到达了上限(控制这个上限的参数就是
tmp_table_size
),会把内存临时表转成磁盘临时表。 -
如果数据量很大,很可能这个查询需要的磁盘临时表,就会占用大量的磁盘空间。
如何优化group by呢?
-
group by 后面的字段加索引
-
order by null 不用排序
-
尽量只使用内存临时表
-
使用SQL_BIG_RESULT
大家可以看下我这篇文章哈:看一遍就理解:group by详解
SQL 优化大全,收藏直接起飞! 转载
转自:https://mp.weixin.qq.com/s/n2yb1Kl4fMbndzG_z1-4tw
大家好,今天分享一篇关于SQL优化的硬核文章,全文有点长,建议收藏后慢慢看。
很多朋友在做数据分析时,分析两分钟,跑数两小时?
在使用SQL过程中不仅要关注数据结果,同样要注意SQL语句的执行效率。
本文涉及三部分,篇幅较长,建议收藏后翻看:
-
SQL介绍
-
SQL优化方法
-
SQL优化实例
1、MySQL的基本架构
1)MySQL的基础架构图
左边的client可以看成是客户端,客户端有很多,像我们经常你使用的CMD黑窗口,像我们经常用于学习的WorkBench,像企业经常使用的Navicat工具,它们都是一个客户端。右边的这一大堆都可以看成是Server(MySQL的服务端),我们将Server在细分为sql层和存储引擎层。
当查询出数据以后,会返回给执行器。执行器一方面将结果写到查询缓存里面,当你下次再次查询的时候,就可以直接从查询缓存中获取到数据了。另一方面,直接将结果响应回客户端。
2)查询数据库的引擎
① show engines;
② show variables like “%storage_engine%”;
3)指定数据库对象的存储引擎
create table tb(
id int(4) auto_increment,
name varchar(5),
dept varchar(5),
primary key(id)
) engine=myISAM auto_increment=1 default charset=utf8;
SQL优化
1)为什么需要进行SQL优化?
在进行多表连接查询、子查询等操作的时候,由于你写出的SQL语句欠佳,导致的服务器执行时间太长,我们等待结果的时间太长。基于此,我们需要学习怎么优化SQL。
2)mysql的编写过程和解析过程
① 编写过程
select dinstinct ..from ..join ..on ..where ..group by ..having ..order by ..limit ..
② 解析过程
from .. on.. join ..where ..group by ..having ..select dinstinct ..order by ..limit ..
提供一个网站,详细说明了mysql解析过程:
https://www.cnblogs.com/annsshadow/p/5037667.html
3)SQL优化—主要就是优化索引
优化SQL,最重要的就是优化SQL索引。
索引相当于字典的目录。利用字典目录查找汉字的过程,就相当于利用SQL索引查找某条记录的过程。有了索引,就可以很方便快捷的定位某条记录。
① 什么是索引?
索引就是帮助MySQL高效获取数据的一种【数据结构】。索引是一种树结构,MySQL中一般用的是【B+树】。
② 索引图示说明(这里用二叉树来帮助我们理解索引)
树形结构的特点是:子元素比父元素小的,放在左侧;子元素比父元素大的,放在右侧。
这个图示只是为了帮我们简单理解索引的,真实的关于【B+树】的说明,我们会在下面进行说明。
索引是怎么查找数据的呢?两个字【指向】,上图中我们给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+树结构。
关于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 表(字段);
建表语句如下:
查询表结构如下:
② 创建索引的第一种方式
Ⅰ 创建单值索引
create index dept_index on tb(dept);
Ⅱ 创建唯一索引:这里我们假定name字段中的值都是唯一的
create unique index name_index on tb(name);
Ⅲ 创建复合索引
create index dept_name_index on tb(dept,name);
③ 创建索引的第二种方式
先删除之前创建的索引以后,再进行这种创建索引方式的测试;
语法:alter table 表名 add 索引类型 索引名(字段)
Ⅰ 创建单值索引
alter table tb add index dept_index(dept);
Ⅱ 创建唯一索引:这里我们假定name字段中的值都是唯一的
alter table tb add unique index name_index(name);
Ⅲ 创建复合索引
alter table tb add index dept_name_index(dept,name);
④ 补充说明
如果某个字段是primary key,那么该字段默认就是主键索引。
主键索引和唯一索引非常相似。相同点:该列中的数据都不能有相同值;不同点:主键索引不能有null值,但是唯一索引可以有null值。
3)索引删除和索引查询
① 索引删除
语法:drop index 索引名 on 表名;
drop index name_index on tb;
② 索引查询
语法:show index from 表名;
show index from tb;
结果如下:
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 :额外的信息
建表语句和插入数据:
# 建表语句
create table course
(
cid int(3),
cname varchar(20),
tid int(3)
);
create table teacher
(
tid int(3),
tname varchar(20),
tcid int(3)
);
create table teacherCard
(
tcid int(3),
tcdesc varchar(200)
);
# 插入数据
insert into course values(1,\'java\',1);
insert into course values(2,\'html\',1);
insert into course values(3,\'sql\',2);
insert into course values(4,\'web\',3);
insert into teacher values(1,\'tz\',1);
insert into teacher values(2,\'tw\',2);
insert into teacher values(3,\'tl\',3);
insert into teacherCard values(1,\'tzdesc\') ;
insert into teacherCard values(2,\'twdesc\') ;
insert into teacherCard values(3,\'tldesc\') ;
explain执行计划常用关键字详解
1)id关键字的使用说明
① 案例:查询课程编号为2 或 教师证编号为3 的老师信息:
# 查看执行计划
explain select t.*
from teacher t,course c,teacherCard tc
where t.tid = c.tid and t.tcid = tc.tcid
and (c.cid = 2 or tc.tcid = 3);
结果如下:
接着,在往teacher表中增加几条数据。
insert into teacher values(4,\'ta\',4);
insert into teacher values(5,\'tb\',5);
insert into teacher values(6,\'tc\',6);
再次查看执行计划。
# 查看执行计划
explain select t.*
from teacher t,course c,teacherCard tc
where t.tid = c.tid and t.tcid = tc.tcid
and (c.cid = 2 or tc.tcid = 3);
结果如下:
表的执行顺序 ,因表数量改变而改变的原因:笛卡尔积。
a b c
2 3 4
最终:2 * 3 * 4 = 6 * 4 = 24
c b a
4 3 2
最终:4 * 3 * 2 = 12 * 2 = 24
分析:最终执行的条数,虽然是一致的。但是中间过程,有一张临时表是6,一张临时表是12,很明显6 < 12,对于内存来说,数据量越小越好,因此优化器肯定会选择第一种执行顺序。
结论:id值相同,从上往下顺序执行。表的执行顺序因表数量的改变而改变。
② 案例:查询教授SQL课程的老师的描述(desc)
# 查看执行计划
explain select tc.tcdesc from teacherCard tc
where tc.tcid =
(
select t.tcid from teacher t
where t.tid =
(select c.tid from course c where c.cname = \'sql\')
);
结果如下:
结论:id值不同,id值越大越优先查询。这是由于在进行嵌套子查询时,先查内层,再查外层。
③ 针对②做一个简单的修改
# 查看执行计划
explain select t.tname ,tc.tcdesc from teacher t,teacherCard tc
where t.tcid= tc.tcid
and t.tid = (select c.tid from course c where cname = \'sql\') ;
结果如下:
结论:id值有相同,又有不同。id值越大越优先;id值相同,从上往下顺序执行。
2)select_type关键字的使用说明:查询类型
① simple:简单查询
不包含子查询,不包含union查询。
explain select * from teacher;
结果如下:
② primary:包含子查询的主查询(最外层)
③ subquery:包含子查询的主查询(非最外层)
④ derived:衍生查询(用到了临时表)
a.在from子查询中,只有一张表;
b.在from子查询中,如果table1 union table2,则table1就是derived表;
explain select cr.cname
from ( select * from course where tid = 1 union select * from course where tid = 2 ) cr ;
结果如下:
⑤ union:union之后的表称之为union表,如上例
⑥ union result:告诉我们,哪些表之间使用了union查询
3)type关键字的使用说明:索引类型
system、const只是理想状况,实际上只能优化到index --> range --> ref这个级别。要对type进行优化的前提是,你得创建索引。
① system
源表只有一条数据(实际中,基本不可能);
衍生表只有一条数据的主查询(偶尔可以达到)。
② const
仅仅能查到一条数据的SQL ,仅针对Primary key或unique索引类型有效。
explain select tid from test01 where tid =1 ;
结果如下:
删除以前的主键索引后,此时我们添加一个其他的普通索引:
create index test01_index on test01(tid) ;
# 再次查看执行计划
explain select tid from test01 where tid =1 ;
结果如下:
③ eq_ref
唯一性索引,对于每个索引键的查询,返回匹配唯一行数据(有且只有1个,不能多 、不能0),并且查询结果和数据条数必须一致。
此种情况常见于唯一索引和主键索引。
delete from teacher where tcid >= 4;
alter table teacherCard add constraint pk_tcid primary key(tcid);
alter table teacher add constraint uk_tcid unique index(tcid) ;
explain select t.tcid from teacher t,teacherCard tc where t.tcid = tc.tcid ;
结果如下:
总结:以上SQL,用到的索引是t.tcid,即teacher表中的tcid字段;如果teacher表的数据个数和连接查询的数据个数一致(都是3条数据),则有可能满足eq_ref级别;否则无法满足。条件很苛刻,很难达到。
④ ref
非唯一性索引,对于每个索引键的查询,返回匹配的所有行(可以0,可以1,可以多)
准备数据:
创建索引,并查看执行计划:
# 添加索引
alter table teacher add index index_name (tname) ;
# 查看执行计划
explain select * from teacher where tname = \'tz\';
结果如下:
⑤ range
检索指定范围的行 ,where后面是一个范围查询(between, >, <, >=, in)
in有时候会失效,从而转为无索引时候的ALL
# 添加索引
alter table teacher add index tid_index (tid) ;
# 查看执行计划:以下写了一种等价SQL写法,查看执行计划
explain select t.* from teacher t where t.tid in (1,2) ;
explain select t.* from teacher t where t.tid <3 ;
结果如下:
⑥ index
查询全部索引中的数据(扫描整个索引)
⑦ ALL
查询全部源表中的数据(暴力扫描全表)
注意:cid是索引字段,因此查询索引字段,只需要扫描索引表即可。但是tid不是索引字段,查询非索引字段,需要暴力扫描整个源表,会消耗更多的资源。
4)possible_keys和key
possible_keys可能用到的索引。是一种预测,不准。了解一下就好。
key指的是实际使用的索引。
# 先给course表的cname字段,添加一个索引
create index cname_index on course(cname);
# 查看执行计划
explain select t.tname ,tc.tcdesc from teacher t,teacherCard tc
where t.tcid= tc.tcid
and t.tid = (select c.tid from course c where cname = \'sql\') ;
结果如下:
有一点需要注意的是:如果possible_key/key是NULL,则说明没用索引。
5)key_len
索引的长度,用于判断复合索引是否被完全使用(a,b,c)。
① 新建一张新表,用于测试
# 创建表
create table test_kl
(
name char(20) not null default \'\'
);
# 添加索引
alter table test_kl add index index_name(name) ;
# 查看执行计划
explain select * from test_kl where name =\'\' ;
结果如下:
结果分析:因为我没有设置服务端的字符集,因此默认的字符集使用的是latin1,对于latin1一个字符代表一个字节,因此这列的key_len的长度是20,表示使用了name这个索引。
② 给test_kl表,新增name1列,该列没有设置“not null”
结果如下:
结果分:如果索引字段可以为null,则mysql底层会使用1个字节用于标识。
③ 删除原来的索引name和name1,新增一个复合索引
# 删除原来的索引name和name1
drop index index_name on test_kl ;
drop index index_name1 on test_kl ;
# 增加一个复合索引
create index name_name1_index on test_kl(name,name1);
# 查看执行计划
explain select * from test_kl where name1 = \'\' ; --121
explain select * from test_kl where name = \'\' ; --60
结果如下:
结果分析:对于下面这个执行计划,可以看到我们只使用了复合索引的第一个索引字段name,因此key_len是20,这个很清楚。再看上面这个执行计划,我们虽然仅仅在where后面使用了复合索引字段中的name1字段,但是你要使用复合索引的第2个索引字段,会默认使用了复合索引的第1个索引字段name,由于name1可以是null,因此key_len = 20 + 20 + 1 = 41呀!
④ 再次怎加一个name2字段,并为该字段创建一个索引。
不同的是:该字段数据类型是varchar
# 新增一个字段name2,name2可以为null
alter table test_kl add column name2 varchar(20) ;
# 给name2字段,设置为索引字段
alter table test_kl add index name2_index(name2) ;
# 查看执行计划
explain select * from test_kl where name2 = \'\' ;
结果如下:
结果分析:key_len = 20 + 1 + 2,这个20 + 1我们知道,这个2又代表什么呢?原来varchar属于可变长度,在mysql底层中,用2个字节标识可变长度。
6)ref
这里的ref的作用,指明当前表所参照的字段。
注意与type中的ref值区分。在type中,ref只是type类型的一种选项值。
# 给course表的tid字段,添加一个索引
create index tid_index on course(tid);
# 查看执行计划
explain select * from course c,teacher t
where c.tid = t.tid
and t.tname = \'tw\';
结果如下:
结果分析:有两个索引,c表的c.tid引用的是t表的tid字段,因此可以看到显示结果为【数据库名.t.tid】,t表的t.name引用的是一个常量"tw",因此可以看到结果显示为const,表示一个常量。
7)rows(这个目前还是有点疑惑)
被索引优化查询的数据个数 (实际通过索引而查询到的数据个数)
explain select *
from course c,teacher t
where c.tid = t.tid
and t.tname = \'tz\' ;
结果如下:
8)extra
表示其他的一些说明,也很有用。
① using filesort:针对单索引的情况
当出现了这个词,表示你当前的SQL性能消耗较大。表示进行了一次“额外”的排序。常见于order by语句中。
Ⅰ 什么是“额外”的排序?
为了讲清楚这个,我们首先要知道什么是排序。我们为了给某一个字段进行排序的时候,首先你得先查询到这个字段,然后在将这个字段进行排序。
紧接着,我们查看如下两个SQL语句的执行计划。
# 新建一张表,建表同时创建索引
create table test02
(
a1 char(3),
a2 char(3),
a3 char(3),
index idx_a1(a1),
index idx_a2(a2),
index idx_a3(a3)
);
# 查看执行计划
explain select * from test02 where a1 =\'\' order by a1 ;
explain select * from test02 where a1 =\'\' order by a2 ;
结果如下:
结果分析:对于第一个执行计划,where后面我们先查询了a1字段,然后再利用a1做了依次排序,这个很轻松。但是对于第二个执行计划,where后面我们查询了a1字段,然而利用的却是a2字段进行排序,此时myql底层会进行一次查询,进行“额外”的排序。
总结:对于单索引,如果排序和查找是同一个字段,则不会出现using filesort;如果排序和查找不是同一个字段,则会出现using filesort;因此where哪些字段,就order by哪些些字段。
② using filesort:针对复合索引的情况
不能跨列(官方术语:最佳左前缀)
# 删除test02的索引
drop index idx_a1 on test02;
drop index idx_a2 on test02;
drop index idx_a3 on test02;
# 创建一个复合索引
alter table test02 add index idx_a1_a2_a3 (a1,a2,a3) ;
# 查看下面SQL语句的执行计划
explain select *from test02 where a1=\'\' order by a3 ; --using filesort
explain select *from test02 where a2=\'\' order by a3 ; --using filesort
explain select *from test02 where a1=\'\' order by a2 ;
结果如下:
结果分析:复合索引的顺序是(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中。
explain select a1 from test02 where a1 in (\'1\',\'2\',\'3\') group by a1 ;
explain select a1 from test02 where a1 in (\'1\',\'2\',\'3\') group by a2 ; --using temporary
结果如下:
结果分析:当你查询哪个字段,就按照那个字段分组,否则就会出现using temporary。
针对using temporary,我们在看一个例子:
using temporary表示需要额外再使用一张表,一般出现在group by语句中。虽然已经有表了,但是不适用,必须再来一张表。
再次来看mysql的编写过程和解析过程。
Ⅰ 编写过程
select dinstinct ..from ..join ..on ..where ..group by ..having ..order by ..limit ..
Ⅱ 解析过程
from .. on.. join ..where ..group by ..having ..select dinstinct ..order by ..limit ..
很显然,where后是group by,然后才是select。基于此,我们再查看如下两个SQL语句的执行计划。
explain select * from test03 where a2=2 and a4=4 group by a2,a4;
explain select * from test03 where a2=2 and a4=4 group by a3;
分析如下:对于第一个执行计划,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
drop index idx_a1_a2_a3 on test02;
# 重新创建一个复合索引idx_a1_a2
create index idx_a1_a2 on test02(a1,a2);
# 查看执行计划
explain select a1,a3 from test02 where a1=\'\' or a3= \'\' ;
explain select a1,a2 from test02 where a1=\'\' and a2= \'\' ;
结果如下:
结果分析:我们创建的是a1和a2的复合索引,对于第一个执行计划,我们却出现了a3,该字段并没有创建索引,因此没有出现using index,而是using where,表示我们需要回表查询。对于第二个执行计划,属于完全的索引覆盖,因此出现了using index。
针对using index,我们在查看一个案例:
explain select a1,a2 from test02 where a1=\'\' or a2= \'\' ;
explain select a1,a2 from test02;
结果如下:
如果用到了索引覆盖(using index时),会对possible_keys和key造成影响:
a.如果没有where,则索引只出现在key中;
b.如果有where,则索引 出现在key和possible_keys中。
⑤ using where
表示需要【回表查询】,表示既在索引中进行了查询,又回到了源表进行了查询。
# 删除test02中的复合索引idx_a1_a2
drop index idx_a1_a2 on test02;
# 将a1字段,新增为一个索引
create index a1_index on test02(a1);
# 查看执行计划
explain select a1,a3 from test02 where a1="" and a3="" ;
结果如下:
结果分析:我们既使用了索引a1,表示我们使用了索引进行查询。但是又对于a3字段,我们并没有使用索引,因此对于a3字段,需要回源表查询,这个时候出现了using where。
⑥ impossible where(了解)
当where子句永远为False的时候,会出现impossible where
# 查看执行计划
explain select a1 from test02 where a1="a" and a1="b" ;
结果如下:
6、优化示例
1)引入案例
# 创建新表
create table test03
(
a1 int(4) not null,
a2 int(4) not null,
a3 int(4) not null,
a4 int(4) not null
);
# 创建一个复合索引
create index a1_a2_a3_test03 on test03(a1,a2,a3);
# 查看执行计划
explain select a3 from test03 where a1=1 and a2=2 and a3=3;
结果如下:
推荐写法:复合索引顺序和使用顺序一致。
下面看看【不推荐写法】:复合索引顺序和使用顺序不一致。
# 查看执行计划
explain select a3 from test03 where a3=1 and a2=2 and a1=3;
结果如下:
结果分析:虽然结果和上述结果一致,但是不推荐这样写。但是这样写怎么又没有问题呢?这是由于SQL优化器的功劳,它帮我们调整了顺序。
最后再补充一点:对于复合索引,不要跨列使用
# 查看执行计划
explain select a3 from test03 where a1=1 and a3=2 group by a3;
结果如下:
结果分析:a1_a2_a3是一个复合索引,我们使用a1索引后,直接跨列使用了a3,直接跳过索引a2,因此索引a3失效了,当使用a3进行分组的时候,就会出现using where。
2)单表优化
# 创建新表
create table book
(
bid int(4) primary key,
name varchar(20) not null,
authorid int(4) not null,
publicid int(4) not null,
typeid int(4) not null
);
# 插入数据
insert into book values(1,\'tjava\',1,1,2) ;
insert into book values(2,\'tc\',2,1,2) ;
insert into book values(3,\'wx\',3,2,1) ;
insert into book values(4,\'math\',4,2,3) ;
结果如下:
案例:查询authorid=1且typeid为2或3的bid,并根据typeid降序排列。
explain
select bid from book
where typeid in(2,3) and authorid=1
order by typeid desc ;
结果如下:
这是没有进行任何优化的SQL,可以看到typ为ALL类型,extra为using filesort,可以想象这个SQL有多恐怖。
优化:添加索引的时候,要根据MySQL解析顺序添加索引,又回到了MySQL的解析顺序,下面我们再来看看MySQL的解析顺序。
from .. on.. join ..where ..group by ..having ..select dinstinct ..order by ..limit ..
① 优化1:基于此,我们进行索引的添加,并再次查看执行计划。
# 添加索引
create index typeid_authorid_bid on book(typeid,authorid,bid);
# 再次查看执行计划
explain
select bid from book
where typeid in(2,3) and authorid=1
order by typeid desc ;
结果如下:
结果分析:结果并不是和我们想象的一样,还是出现了using where,查看索引长度key_len=8,表示我们只使用了2个索引,有一个索引失效了。
② 优化2:使用了in有时候会导致索引失效,基于此有了如下一种优化思路。
将in字段放在最后面。需要注意一点:每次创建新的索引的时候,最好是删除以前的废弃索引,否则有时候会产生干扰(索引之间)。
# 删除以前的索引
drop index typeid_authorid_bid on book;
# 再次创建索引
create index authorid_typeid_bid on book(authorid,typeid,bid);
# 再次查看执行计划
explain
select bid from book
where authorid=1 and typeid in(2,3)
order by typeid desc ;
结果如下:
结果分析:这里虽然没有变化,但是这是一种优化思路。
总结如下:
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:
explain select bid from book
where authorid=1 and typeid =3
order by typeid desc ;
结果如下:
3)两表优化
# 创建teacher2新表
create table teacher2
(
tid int(4) primary key,
cid int(4) not null
);
# 插入数据
insert into teacher2 values(1,2);
insert into teacher2 values(2,1);
insert into teacher2 values(3,3);
# 创建course2新表
create table course2
(
cid int(4) ,
cname varchar(20)
);
# 插入数据
insert into course2 values(1,\'java\');
insert into course2 values(2,\'python\');
insert into course2 values(3,\'kotlin\');
案例:使用一个左连接,查找教java课程的所有信息。
explain
select *
from teacher2 t
left outer join course2 c
on t.cid=c.cid
where c.cname=\'java\';
结果如下:
① 优化
对于两张表,索引往哪里加?答:对于表连接,小表驱动大表。索引建立在经常使用的字段上。
为什么小表驱动大表好一些呢?
小表:10
大表:300
# 小表驱动大表
select ...where 小表.x10=大表.x300 ;
for(int i=0;i<小表.length10;i++)
for(int j=0;j<大表.length300;j++)
...
# 大表驱动小表
select ...where 大表.x300=小表.x10 ;
for(int i=0;i<大表.length300;i++)
for(int j=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字段添加索引。
更深入的说明:一般情况下,左连接给左表加索引。右连接给右表加索引。其他表需不需要加索引,我们逐步尝试。
# 给左表的字段加索引
create index cid_teacher2 on teacher2(cid);
# 查看执行计划
explain
select *
from teacher2 t
left outer join course2 c
on t.cid=c.cid
where c.cname=\'java\';
结果如下:
当然你可以下去接着优化,给cname添加一个索引。索引优化是一个逐步的过程,需要一点点尝试。
# 给cname的字段加索引
create index cname_course2 on course2(cname);
# 查看执行计划
explain
select t.cid,c.cname
from teacher2 t
left outer join course2 c
on t.cid=c.cid
where c.cname=\'java\';
结果如下:
最后补充一个:Using join buffer是extra中的一个选项,表示Mysql引擎使用了“连接缓存”,即MySQL底层动了你的SQL,你写的太差了。
4)三表优化
-
大于等于张表,优化原则一样
-
小表驱动大表
-
索引建立在经常查询的字段上
7、避免索引失效的一些原则
① 复合索引需要注意的点
-
复合索引,不要跨列或无序使用(最佳左前缀)
-
复合索引,尽量使用全索引匹配,也就是说,你建立几个索引,就使用几个索引
② 不要在索引上进行任何操作(计算、函数、类型转换),否则索引失效
explain select * from book where authorid = 1 and typeid = 2;
explain select * from book where authorid*2 = 1 and typeid = 2 ;
结果如下:
③ 索引不能使用不等于(!= <>)或is null (is not null),否则自身以及右侧所有全部失效(针对大多数情况)。复合索引中如果有>,则自身和右侧索引全部失效。
# 针对不是复合索引的情况
explain select * from book where authorid != 1 and typeid =2 ;
explain select * from book where authorid != 1 and typeid !=2 ;
结果如下:
再观看下面这个案例:
# 删除单独的索引
drop index authorid_index on book;
drop index typeid_index on book;
# 创建一个复合索引
alter table book add index idx_book_at (authorid,typeid);
# 查看执行计划
explain select * from book where authorid > 1 and typeid = 2 ;
explain select * from book where authorid = 1 and typeid > 2 ;
结果如下:
21个写好SQL的习惯(建议收藏)