一、Explain
- EXPLAIN不会告诉你关于触发器、存储过程的信息或函数对查询的影响情况
- EXPLAIN不考虑各种Cache
- EXPLAIN不能显示MySQL在执行查询时所作的优化工作
- 部分统计信息是估算的,并非精确值
- EXPALIN只能解释SELECT操作,其他操作要重写为SELECT后查看执行计划
select_type
查询中每个select子句的类型
-
SIMPLE
(简单SELECT,不使用UNION或子查询等) -
PRIMARY
(查询中若包含任何复杂的子部分,最外层的select被标记为PRIMARY) -
UNION
(UNION中的第二个或后面的SELECT语句) -
DEPENDENT UNION
(UNION中的第二个或后面的SELECT语句,取决于外面的查询) -
UNION RESULT
(UNION的结果) -
SUBQUERY
(子查询中的第一个SELECT) -
DEPENDENT SUBQUERY
(子查询中的第一个SELECT,取决于外面的查询) -
DERIVED
(派生表的SELECT, FROM子句的子查询) -
UNCACHEABLE SUBQUERY
(一个子查询的结果不能被缓存,必须重新评估外链接的第
一行)
Type
表示MySQL在表中找到所需行的方式,又称“访问类型”
。
常用的类型有:ALL
, index
, range
, ref
, eq_ref
, const
, system
, NULL
(从左到右,性能从差到好)
-
all
:Full Table Scan, MySQL将遍历全表以找到匹配的行 -
index
: Full Index Scan,index与ALL区别为index类型只遍历索引树 -
range
:只检索给定范围的行,使用一个索引来选择行 -
ref
:表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值 -
eq_ref
:类似ref,区别就在使用的索引是唯一索引,对于每个索引键值,表中只有一条记录匹配,简单来说,就是多表连接中使用primary key或者 unique key作为关联条件 -
const
、system
:当MySQL对查询某部分进行优化,并转换为一个常量时,使用这些类型访问。如将主键置于where列表中,MySQL就能将该查询转换为一个常量,system
是const
类型的特例,当查询的表只有一行的情况下,使用system
-
null
: MySQL在优化过程中分解语句,执行时甚至不用访问表或索引,例如从一个索引列里选取最小值可以通过单独索引查找完成。
possible_keys
指出MySQL能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用
该列完全独立于EXPLAIN输出所示的表的次序。这意味着在possible_keys中的某些键实际上不能按生成的表次序使用。
如果该列是NULL,则没有相关的索引。在这种情况下,可以通过检查WHERE子句看是否它引用某些列或适合索引的列来提高你的查询性能。如果是这样,创造一个适当的索引并且再次用EXPLAIN检查查询
key
key列显示MySQL实际决定使用的键(索引)
如果没有选择索引,键是NULL。要想强制MySQL使用或忽视possible_keys列中的索引,在查询中使用FORCE INDEX、USE INDEX或者IGNORE INDEX。
key_len
表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度(key_len显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len是根据表定义计算而得,不是通过表内检索出的)
不损失精确性的情况下,长度越短越好
rows
表示MySQL根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数
该数字越小越好
extra
该列包含MySQL解决查询的详细信息,有以下几种情况:
-
Using where
:列数据是从仅仅使用了索引中的信息而没有读取实际的行动的表返回的,这发生在对表的全部的请求列都是同一个索引的部分的时候,表示mysql服务器将在存储引擎检索行后再进行过滤 -
Using temporary
:表示MySQL需要使用临时表来存储结果集,常见于排序和分组查询 -
Using filesort
:MySQL中无法利用索引完成的排序操作称为“文件排序” -
Using join buffer
:改值强调了在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果。如果出现了这个值,那应该注意,根据查询的具体情况可能需要添加索引来改进能。 -
Impossible where
:这个值强调了where语句会导致没有符合条件的行。 -
Select tables optimized away
:这个值意味着仅通过使用索引,优化器可能仅从聚合函数结果中返回一行
二、sql 语句
select *
查询语句最好指明要查询的字段,可以节省不必要的运算
in
in
包含的值不应过多
连续的值用between
不连续的用union all
子查询用exists
,当查询的两个表大小相当,那么in
和exists
差别不大,如果一个表大一个表小,则子查询的表大的用exists
,子查询的表小的用in
排序字段尽量加索引
or
or
两边的字段如果有一个没有索引,则最好用union all
代替。否则会导致查询不走索引,全表扫描
union 和 union all
union
和union all
的差异主要是前者需要将结果集合并后再进行唯一性过滤操作,这就会涉及到排序,增加大量的CPU运算,加大资源消耗及延迟。当然,union all
的前提条件是两个结果集没有重复数据。
null 判断
在sql语句中对字段执行 is null
或 is not null
会导致不走索引,全表扫描
避免在where中对字段运算
where
中避免使用类似 where age*2=8
,建议改成where age=? ,问号中的参数在程序中进行
8/2`后再设置到sql语句中
注意范围查询
对于联合索引来说,如果存在范围查询,比如between
,>
,<
等条件时,会造成后面的索引字段失效。所以查询是将有索引的字段放在范围查询之前比较
Join on
-
inner join
: 自动找出数据少的表作为驱动表 -
left join
:左边的表作为驱动表 -
right join
:右边的表作为驱动表
ON
条件(“A LEFT JOIN B ON 条件表达式”
中的ON
)用来决定如何从 B 表中检索数据行。如果 B 表中没有任何一行数据匹配 ON 的条件,将会额外生成一行所有列为 NULL 的数据,在匹配阶段 WHERE 子句的条件都不会被使用。仅在匹配阶段完成以后,WHERE 子句条件才会被使用。ON将从匹配阶段产生的数据中检索过滤。
在使用Left (right) join
的时候,一定要在先给出尽可能多的匹配满足条件,减少Where
的执行。尽可能满足ON
的条件,而少用Where的条件,从执行性能来看也更加高效。
强制使用索引
select * from a force index(idx_name)
三、索引
索引目的
索引的目的在于提高查询效率,可以类比字典,如果要查“mysql
”这个单词,我们肯定需要定位到m
字母,然后从下往下找到y
字母,再找到剩下的sql。如果没有索引,那么你可能需要把所有单词看一遍才能找到你想要的,如果我想找到m开头的单词呢?或者ze开头的单词呢?是不是觉得如果没有索引,这个事情根本无法完成?
索引原理
除了词典,生活中随处可见索引的例子,如火车站的车次表、图书的目录等。它们的原理都是一样的,通过不断的缩小想要获得数据的范围来筛选出最终想要的结果,同时把随机的事件变成顺序的事件,也就是我们总是通过同一种查找方式来锁定数据。
数据库也是一样,但显然要复杂许多,因为不仅面临着等值查询,还有范围查询(>
、<
、between
、in
)、模糊查询(like)
、并集查询(or)
等等。
数据库应该选择怎么样的方式来应对所有的问题呢?我们回想字典的例子,能不能把数据分成段,然后分段查询呢?最简单的如果1000条数据,1到100分成第一段,101到200分成第二段,201到300分成第三段……这样查第250条数据,只要找第三段就可以了,一下子去除了90%的无效数据。
但如果是1千万的记录呢,分成几段比较好?稍有算法基础的同学会想到搜索树,其平均复杂度是lgN
,具有不错的查询性能。但这里我们忽略了一个关键的问题,复杂度模型是基于每次相同的操作成本来考虑的,数据库实现比较复杂,数据保存在磁盘上,而为了提高性能,每次又可以把部分数据读入内存来计算,因为我们知道访问磁盘的成本大概是访问内存的十万倍左右,所以简单的搜索树难以满足复杂的应用场景。
磁盘IO与预读
前面提到了访问磁盘,那么这里先简单介绍一下磁盘IO和预读,磁盘读取数据靠的是机械运动,每次读取数据花费的时间可以分为寻道时间
、旋转延迟
、传输时间
三个部分。
寻道时间
指的是磁臂移动到指定磁道所需要的时间,主流磁盘一般在5ms
以下;
旋转延迟
就是我们经常听说的磁盘转速,比如一个磁盘7200
转,表示每分钟能转7200次,也就是说1秒钟能转120次,旋转延迟就是1/120/2 = 4.17ms
;
传输时间
指的是从磁盘读出或将数据写入磁盘的时间,一般在零点几毫秒,相对于前两个时间可以忽略不计。
那么访问一次磁盘的时间,即一次磁盘IO
的时间约等于5+4.17 = 9ms
左右,听起来还挺不错的,但要知道一台500 -MIPS的机器每秒可以执行5亿条指令,因为指令依靠的是电的性质,换句话说执行一次IO的时间可以执行40万条指令,数据库动辄十万百万乃至千万级数据,每次9毫秒的时间,显然是个灾难。下图是计算机硬件延迟的对比图,供大家参考:
考虑到磁盘IO是非常高昂的操作,计算机操作系统做了一些优化,当一次IO时,不光把当前磁盘地址的数据,而是把相邻的数据
也都读取到内存缓冲区内,因为局部预读性原理告诉我们,当计算机访问一个地址的数据的时候,与其相邻的数据也会很快被访问到。
每一次IO读取的数据我们称之为一页(page)
。具体一页有多大数据跟操作系统有关,一般为4k
或8k
,也就是我们读取一页内的数据时候,实际上才发生了一次IO,这个理论对于索引的数据结构设计非常有帮助。
索引的数据结构
前面讲了生活中索引的例子,索引的基本原理,数据库的复杂性,又讲了操作系统的相关知识,目的就是让大家了解,任何一种数据结构都不是凭空产生的,一定会有它的背景和使用场景,我们现在总结一下,我们需要这种数据结构能够做些什么
其实很简单,那就是:每次查找数据时把磁盘IO次数控制在一个很小的数量级,最好是常数数量级。那么我们就想到如果一个高度可控的多路搜索树是否能满足需求呢?就这样,b+树应运而生。
详解b+树
如上图,是一颗b+树,这里只说一些重点,浅蓝色的块
我们称之为一个磁盘块
,可以看到每个磁盘块包含几个数据项(深蓝色所示)
和指针(黄色所示)
,如磁盘块1包含数据项17和35,包含指针P1、P2、P3,P1表示小于17的磁盘块,P2表示在17和35之间的磁盘块,P3表示大于35的磁盘块。真实的数据存在于叶子节点即3、5、9、10、13、15、28、29、36、60、75、79、90、99。
非叶子节点只不存储真实的数据,只存储指引搜索方向的数据项,如17、35并不真实存在于数据表中。
b+树的查找过程
如图所示,如果要查找数据项29
,
那么首先会把磁盘块1
由磁盘加载到内存,此时发生一次IO
,在内存中用二分查找确定29
在17
和35
之间,锁定磁盘块1
的P2指针
,内存时间因为非常短(相比磁盘的IO)可以忽略不计
通过磁盘块1
的P2指针
的磁盘地址把磁盘块3
由磁盘加载到内存,发生第二次IO
,29
在26
和30
之间,锁定磁盘块3
的P2指针
通过指针加载磁盘块8
到内存,发生第三次IO
,同时内存中做二分查找找到29
,结束查询,总计三次IO
。
真实的情况是,3层
的b+树
可以表示上百万的数据,如果上百万的数据查找只需要三次IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次IO,那么总共需要百万次的IO,显然成本非常非常高。
b+树性质
通过上面的分析,我们知道IO次数
取决于b+树
的高度h
假设当前数据表的数据为N
,每个磁盘块的数据项的数量是m
,则有h=㏒(m+1)N
,当数据量N
一定的情况下,m
越大,h
越小;
而m = 磁盘块的大小 / 数据项的大小
,磁盘块的大小也就是一个数据页的大小,是固定的,如果数据项占的空间越小,数据项的数量越多,树的高度越低。
这就是为什么每个数据项,即索引字段
要尽量的小
,比如int
占4字节
,要比bigint
8
字节少一半。
这也是为什么b+树要求把真实的数据放到叶子节点而不是内层节点,一旦放到内层节点,磁盘块的数据项会大幅度下降,导致树增高。当数据项等于1时将会退化成线性表。
当b+树
的数据项是复合
的数据结构,比如(name,age,sex
)的时候,b+树是按照从左到右
的顺序来建立搜索树的。
比如当(张三,20,F
)这样的数据来检索的时候,b+树会优先比较name
来确定下一步的所搜方向,如果name
相同再依次比较age
和sex
,最后得到检索的数据;
但当(20,F
)这样的没有name
的数据来的时候,b+树就不知道下一步该查哪个节点,因为建立搜索树的时候name
就是第一个比较因子,必须要先根据name
来搜索才能知道下一步去哪里查询。
比如当(张三,F
)这样的数据来检索时,b+树可以用name
来指定搜索方向,但下一个字段age
的缺失,所以只能把名字等于张三
的数据都找到,然后再匹配性别是F
的数据了, 这个是非常重要的性质,即索引的最左匹配
特性。
建索引的几大原则
-
最左前缀匹配
原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like
)就停止匹配,比如
a = 1 and b = 2 and c > 3 and d = 4
如果建立(a,b,c,d
)顺序的索引,d
是用不到索引的,如果建立(a,b,d,c
)的索引则都可以用到,a,b,d
的顺序可以任意调整。
-
=
和in
可以乱序,比如
a = 1 and b = 2 and c = 3
建立(a,b,c
)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式。
- 尽量选择
区分度
高的列作为索引,区分度的公式是
count(distinct col)/count(*)
表示字段不重复
的比例,比例越大
我们扫描的记录数越少
,唯一键
的区分度是1
,而一些状态、性别字段可能在大数据面前区分度就是0
那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要join
的字段我们都要求是0.1
以上,即平均1条扫描10条记录。
- 索引列不能参与计算,保持列
干净
,比如
from_unixtime(create_time) = ’2014-05-29’
就不能使用到索引,原因很简单,b+树
中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本太大。
所以语句应该写成
create_time = unix_timestamp(’2014-05-29’)
尽量的扩展索引,不要新建索引。
比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。
慢查询优化基本步骤
- 先运行看看是否真的很慢,注意设置
SQL_NO_CACHE
-
where
条件单表查,锁定最小返回记录表。这句话的意思是把查询语句的where
都应用到表中返回的记录数最小的表开始查起,单表每个字段分别查询,看哪个字段的区分度最高 -
explain
查看执行计划,是否与2预期一致(从锁定记录较少的表开始查询) -
order by limit
形式的sql语句让排序的表优先查 - 了解业务方使用场景,根据业务针对性优化
- 加索引时参照建索引的几大原则
四、表结构设计
- 建表时显式指定字符集为
utf8
或utf8mb4
- 如无特殊需求,存储引擎一律为
InnoDB
- 每个表必须设置
主键ID
,主键ID最好是int
或bigint
且为auto_increment
- 所有字段应该都是
not null
并设置合适的default
- 避免使用
text
,blob
等类型,如不可避免则最好单独放一张表,需要时根据主键ID
查询 - 需要经常
join
的字段如user_id
,必须建索引 - 根据业务选取合适的字段类型。数字类型如无负数,则设置无符号
unsigned
;字符类型尽量使用varchar
,根据业务需要设置合适的长度。
五、案例分享
- 批量插入
start transaction;
set session foreign_key_checks=0; // 禁用外键检查
// 批量插入
insert into table_name(name,age) values(?,?),(?,?)...;
// 批量插入或修改
insert into table_name(name,age) values(?,?),(?,?)... on duplicate key update name=values(name),age=values(age);
commit;
set session foreign_key_checks=1; // 启用外键检查