定义:索引是存储引擎用于快速找到记录的一种数据结构。举例说明:若是查找一本书中的某个特定主题,通常会先看书的目录(相似索引),找到对应页面。在MySQL,存储引擎采用相似的方法使用索引,高效获取查找的数据。html
索引的分类node
1)从存储结构上来划分mysql
2)从应用层次上来划分sql
3)从表记录的排列顺序和索引的排列顺序是否一致来划分数据库
数据库保存的数据是存储在磁盘上,查找数据时须要将磁盘中的数据加载到内存中,在介绍索引的实现以前,先了解下磁盘IO与预读。bash
磁盘读取数据靠的是机械运动,每次读取数据花费的时间能够分为寻道时间、旋转延迟、传输时间三个部分,寻道时间指的是磁臂移动到指定磁道所须要的时间,主流磁盘通常在5ms如下;旋转延迟就是咱们常常据说的磁盘转速,好比一个磁盘7200转,表示每分钟能转7200次,也就是说1秒钟能转120次,旋转延迟就是1/120/2 = 4. 17ms;传输时间指的是从磁盘读出或将数据写入磁盘的时间,通常在零点几毫秒,相对于前两个时间能够忽略不计。那么访问一次磁盘的时间,即一次磁盘IO的时间约等于5+4. 17 = 9ms左右,听起来还挺不错的,但要知道一台500 -MIPS的机器每秒能够执行5亿条指令,由于指令依靠的是电的性质,换句话说执行一次IO的时间能够执行40万条指令,数据库动辄十万百万乃至千万级数据,每次9毫秒的时间,显然是个灾难。数据结构
下图是计算机硬件延迟的对比图,供你们参考:ide
考虑到磁盘IO是很是高昂的操做,计算机操做系统作了一些优化,当一次IO时,不光把当前磁盘地址的数据,而是把相邻的数据也都读取到内存缓冲区内,由于局部预读性原理告诉咱们,当计算机访问一个地址的数据的时候,与其相邻的数据也会很快被访问到。每一次IO读取的数据咱们称之为一页(page)。具体一页有多大数据跟操做系统有关,通常为4k或8k,也就是咱们读取一页内的数据时候,实际上才发生了一次IO,这个理论对于索引的数据结构设计很是有帮助。函数
B-Tree是为磁盘等外存储设备设计的一种平衡查找树。oop
B-Tree结构的数据可让系统高效的找到数据所在的磁盘块。为了描述B-Tree,首先定义一条记录为一个二元组[key, data] ,key为记录的键值,对应表中的主键值,data为一行记录中除主键外的数据。对于不一样的记录,key值互不相同。
一棵m阶的B-Tree有以下特性:
B-Tree中的每一个节点根据实际状况能够包含大量的关键字信息和分支,以下图所示为一个3阶的B-Tree:
每一个节点占用一个盘块的磁盘空间,一个节点上有两个升序排序的关键字和三个指向子树根节点的指针,指针存储的是子节点所在磁盘块的地址。两个关键词划分红的三个范围域对应三个指针指向的子树的数据的范围域。以根节点为例,关键字为17和35,P1指针指向的子树的数据范围为小于17,P2指针指向的子树的数据范围为17~35,P3指针指向的子树的数据范围为大于35。
模拟查找关键字29的过程:
根据根节点找到磁盘块1,读入内存。【磁盘I/O操做第1次】
比较关键字29在区间(17, 35),找到磁盘块1的指针P2。
根据P2指针找到磁盘块3,读入内存。【磁盘I/O操做第2次】
比较关键字29在区间(26, 30),找到磁盘块3的指针P2。
根据P2指针找到磁盘块8,读入内存。【磁盘I/O操做第3次】
在磁盘块8中的关键字列表中找到关键字29。
分析上面过程,发现须要3次磁盘I/O操做,和3次内存查找操做。因为内存中的关键字是一个有序表结构,能够利用二分法查找提升效率。而3次磁盘I/O操做是影响整个B-Tree查找效率的决定因素。B-Tree相对于AVLTree缩减了节点个数,使每次磁盘I/O取到内存的数据都发挥了做用,从而提升了查询效率。
B+Tree是在B-Tree基础上的一种优化,InnoDB存储引擎就是用B+Tree实现其索引结构。
在B+Tree中,全部数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样能够大大加大每一个节点存储的key值数量,下降B+Tree的高度。
因为B+Tree的非叶子节点只存储键值信息,假设每一个磁盘块能存储4个键值及指针信息,则变成B+Tree后其结构以下图所示:
最左前缀匹配原则,很是重要的原则,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)的索引,那么只须要修改原来的索引便可。
explain为mysql提供语句的执行计划信息。能够应用在select、delete、insert、update和place语句上。explain的执行计划,只是做为语句执行过程的一个参考,实际执行的过程不必定和计划彻底一致,可是执行计划中透露出的信息却能够帮助选择更好的索引和写出更优化的查询语句。
explain输出项
Column | JSON Name | Meaning |
---|---|---|
id | select_id | The SELECT identifier |
select_type | None | The SELECT type |
table | table_name | The table for the output row |
partitions | partitions | The matching partitions |
type | access_type | The join type |
possible_keys | possible_keys | The possible indexes to choose |
key | key | The index actually chosen |
key_len | key_length | The length of the chosen key |
ref | ref | The columns compared to the index |
rows | rows | Estimate of rows to be examined |
filtered | filtered | Percentage of rows filtered by table condition |
Extra | None | Additional information |
id列的编号是 select 的序列号,有几个 select 就有几个id,而且id的顺序是按 select 出现的顺序增加的。
MySQL将 select 查询分为简单查询(SIMPLE)和复杂查询(PRIMARY)。复杂查询分为三类:简单子查询、派生表(from语句中的子查询)、union 查询。
id列越大执行优先级越高,id相同则从上往下执行,id为NULL最后执行。
select_type 表示对应行是简单仍是复杂的查询。
这一列表示 explain 的一行正在访问哪一个表。
当 from 子句中有子查询时,table列是 格式,表示当前查询依赖 id=N 的查询,因而先执行 id=N 的查询。
当有 union 时,UNION RESULT 的 table 列的值为<union1, 2>,1和2表示参与 union 的 select 行id。
这一列表示关联类型或访问类型,即MySQL决定如何查找表中的行,查找数据行记录的大概范围。
依次从最优到最差分别为:system > const > eq_ref > ref > range > index > ALL
这一列显示查询可能使用哪些索引来查找。
explain 时可能出现 possible_keys 有列,而 key 显示 NULL 的状况,这种状况是由于表中数据很少,mysql认为索引对此查询帮助不大,选择了全表查询。
若是该列是NULL,则没有相关的索引。在这种状况下,能够经过检查 where 子句看是否能够创造一个适当的索引来提升查询性能,而后用 explain 查看效果。
这一列显示mysql实际采用哪一个索引来优化对该表的访问。
若是没有使用索引,则该列是 NULL。若是想强制mysql使用或忽视possible_keys列中的索引,在查询中使用 force index、ignore index。
这一列显示了mysql在索引里使用的字节数,经过这个值能够算出具体使用了索引中的哪些列。
这一列显示了在key列记录的索引中,表查找值所用到的列或常量,常见的有:const(常量),字段名(例:film. id)
这一列是mysql估计要读取并检测的行数,注意这个不是结果集里的行数。
Using index:查询的列被索引覆盖,而且where筛选条件是索引的前导列(最左侧索引),是性能高的表现。通常是使用了覆盖索引(索引包含了全部查询的字段)。对于innodb来讲,若是是辅助索引性能会有很多提升。
Using where:查询的列未被索引覆盖,where筛选条件非索引的前导列。
Using where Using index:查询的列被索引覆盖,而且where筛选条件是索引列之一但不是索引的前导列,意味着没法直接经过索引查找来查询到符合条件的数据, Using index表明select用到了覆盖索引。
NULL:查询的列未被索引覆盖,而且where筛选条件是索引的前导列,意味着用到了索引,可是部分字段未被索引覆盖,必须经过“回表”来实现,不是纯粹地用到了索引,也不是彻底没用到索引。
Using index condition:与Using where相似,查询的列不彻底被索引覆盖,where条件中是一个前导列的范围;
Using temporary:mysql须要建立一张临时表来处理查询。出现这种状况通常是要进行优化的,首先是想到用索引来优化。
Using filesort:mysql 会对结果使用一个外部索引排序,而不是按索引次序从表里读取行。此时mysql会根据联接类型浏览全部符合条件的记录,并保存排序关键字和行指针,而后排序关键字并按顺序检索行信息。这种状况下通常也是要考虑使用索引来优化的。
不少状况下,咱们写SQL只是为了实现功能,这只是第一步,不一样的语句书写方式对于效率每每有本质的差异,这要求咱们对mysql的执行计划和索引原则有很是清楚的认识,请看下面的语句:
select
distinct cert.emp_id
from
cm_log cl
inner join (
select
emp.id as emp_id,
emp_cert.id as cert_id
from
employee emp
left join emp_certificate emp_cert on emp.id = emp_cert.emp_id
where
emp.is_deleted = 0
) cert on (
cl.ref_table = 'Employee'
and cl.ref_oid = cert.emp_id
)
or (
cl.ref_table = 'EmpCertificate'
and cl.ref_oid = cert.cert_id
)
where
cl.last_upd_date >= '2013-11-07 15:03:00'
and cl.last_upd_date <= '2013-11-08 16:00:00';
复制代码
53 rows in set (1.87 sec)
复制代码
简述一下执行计划,首先mysql根据idx_last_upd_date索引扫描cm_log表得到379条记录;而后查表扫描了63727条记录,分为两部分,derived表示构造表,也就是不存在的表,能够简单理解成是一个语句造成的结果集,后面的数字表示语句的ID。derived2表示的是ID = 2的查询构造了虚拟表,而且返回了63727条记录。咱们再来看看ID = 2的语句究竟作了写什么返回了这么大量的数据,首先全表扫描employee表13317条记录,而后根据索引emp_certificate_empid关联emp_certificate表,rows = 1表示,每一个关联都只锁定了一条记录,效率比较高。得到后,再和cm_log的379条记录根据规则关联。从执行过程上能够看出返回了太多的数据,返回的数据绝大部分cm_log都用不到,由于cm_log只锁定了379条记录。
如何优化呢?能够看到咱们在运行完后仍是要和cm_log作join, 那么咱们能不能以前和cm_log作join呢?仔细分析语句不难发现,其基本思想是若是cm_log的ref_table是EmpCertificate就关联emp_certificate表,若是ref_table是Employee就关联employee表,咱们彻底能够拆成两部分,并用union链接起来,注意这里用union,而不用union all是由于原语句有“distinct”来获得惟一的记录,而union刚好具有了这种功能。若是原语句中没有distinct不须要去重,咱们就能够直接使用union all了,由于使用union须要去重的动做,会影响SQL性能。
优化过的语句以下:
select
emp.id
from
cm_log cl
inner join employee emp on cl.ref_table = 'Employee'
and cl.ref_oid = emp.id
where
cl.last_upd_date >= '2013-11-07 15:03:00'
and cl.last_upd_date <= '2013-11-08 16:00:00'
and emp.is_deleted = 0
union
select
emp.id
from
cm_log cl
inner join emp_certificate ec on cl.ref_table = 'EmpCertificate'
and cl.ref_oid = ec.id
inner join employee emp on emp.id = ec.emp_id
where
cl.last_upd_date >= '2013-11-07 15:03:00'
and cl.last_upd_date <= '2013-11-08 16:00:00'
and emp.is_deleted = 0
复制代码
不须要了解业务场景,只须要改造的语句和改造以前的语句保持结果一致
现有索引能够知足,不须要建索引
用改造后的语句实验一下,只须要10ms 下降了近200倍!
举这个例子的目的在于颠覆咱们对列的区分度的认知,通常上咱们认为区分度越高的列,越容易锁定更少的记录,但在一些特殊的状况下,这种理论是有局限性的。
select
*
from
stage_poi sp
where
sp.accurate_result = 1
and (
sp.sync_status = 0
or sp.sync_status = 2
or sp.sync_status = 4
);
复制代码
951 rows in set (6.22 sec)
复制代码
全部字段都应用查询返回记录数,由于是单表查询 0已经作过了951条。
让explain的rows 尽可能逼近951。
看一下accurate_result = 1的记录数:
select count(*),accurate_result from stage_poi group by accurate_result;
+----------+-----------------+
| count(*) | accurate_result |
+----------+-----------------+
| 1023 | -1 |
| 2114655 | 0 |
| 972815 | 1 |
+----------+-----------------+
复制代码
咱们看到accurate_result这个字段的区分度很是低,整个表只有-1, 0, 1三个值,加上索引也没法锁定特别少许的数据。
再看一下sync_status字段的状况:
select count(*),sync_status from stage_poi group by sync_status;
+----------+-------------+
| count(*) | sync_status |
+----------+-------------+
| 3080 | 0 |
| 3085413 | 3 |
+----------+-------------+
复制代码
一样的区分度也很低,根据理论,也不适合创建索引。
问题分析到这,好像得出了这个表没法优化的结论,两个列的区分度都很低,即使加上索引也只能适应这种状况,很难作广泛性的优化,好比当sync_status 0、3分布的很平均,那么锁定记录也是百万级别的。
找业务方去沟通,看看使用场景。业务方是这么来使用这个SQL语句的,每隔五分钟会扫描符合条件的数据,处理完成后把sync_status这个字段变成1, 五分钟符合条件的记录数并不会太多,1000个左右。了解了业务方的使用场景后,优化这个SQL就变得简单了,由于业务方保证了数据的不平衡,若是加上索引能够过滤掉绝大部分不须要的数据。
alter table stage_poi add index idx_acc_status(accurate_result,sync_status);
复制代码
952 rows in set (0.20 sec)
复制代码
咱们再来回顾一下分析问题的过程,单表查询相对来讲比较好优化,大部分时候只须要把where条件里面的字段依照规则加上索引就好,若是只是这种“无脑”优化的话,显然一些区分度很是低的列,不该该加索引的列也会被加上索引,这样会对插入、更新性能形成严重的影响,同时也有可能影响其它的查询语句。因此咱们第4步调差SQL的使用场景很是关键,咱们只有知道这个业务场景,才能更好地辅助咱们更好的分析和优化查询语句。
select
c.id,
c.name,
c.position,
c.sex,
c.phone,
c.office_phone,
c.feature_info,
c.birthday,
c.creator_id,
c.is_keyperson,
c.giveup_reason,
c.status,
c.data_source,
from_unixtime(c.created_time) as created_time,
from_unixtime(c.last_modified) as last_modified,
c.last_modified_user_id
from
contact c
inner join contact_branch cb on c.id = cb.contact_id
inner join branch_user bu on cb.branch_id = bu.branch_id
and bu.status in (1, 2)
inner join org_emp_info oei on oei.data_id = bu.user_id
and oei.node_left >= 2875
and oei.node_right <= 10802
and oei.org_category = - 1
order by
c.created_time desc
limit
0, 10;
复制代码
仍是几个步骤。
10 rows in set (13.06 sec)
复制代码
explain
从执行计划上看,mysql先查org_emp_info表扫描8849记录,再用索引idx_userid_status关联branch_user表,再用索引idx_branch_id关联contact_branch表,最后主键关联contact表。
rows返回的都很是少,看不到有什么异常状况。咱们在看一下语句,发现后面有order by + limit组合,会不会是排序量太大搞的?因而咱们简化SQL,去掉后面的order by 和 limit,看看到底用了多少记录来排序。
select
count(*)
from
contact c
inner join
contact_branch cb
on c.id = cb.contact_id
inner join
branch_user bu
on cb.branch_id = bu.branch_id
and bu.status in (
1,
2)
inner join
org_emp_info oei
on oei.data_id = bu.user_id
and oei.node_left >= 2875
and oei.node_right <= 10802
and oei.org_category = - 1
+----------+
| count(*) |
+----------+
| 778878 |
+----------+
1 row in set (5.19 sec)
复制代码
发现排序以前竟然锁定了778878条记录,若是针对70万的结果集排序,将是灾难性的,怪不得这么慢,那咱们能不能换个思路,先根据contact的created_time排序,再来join会不会比较快呢?
因而改形成下面的语句,也能够用straight_join来优化:
select
c.id,
c.name,
c.position,
c.sex,
c.phone,
c.office_phone,
c.feature_info,
c.birthday,
c.creator_id,
c.is_keyperson,
c.giveup_reason,
c.status,
c.data_source,
from_unixtime(c.created_time) as created_time,
from_unixtime(c.last_modified) as last_modified,
c.last_modified_user_id
from
contact c
where
exists (
select
1
from
contact_branch cb
inner join branch_user bu on cb.branch_id = bu.branch_id
and bu.status in (1, 2)
inner join org_emp_info oei on oei.data_id = bu.user_id
and oei.node_left >= 2875
and oei.node_right <= 10802
and oei.org_category = - 1
where
c.id = cb.contact_id
)
order by
c.created_time desc
limit
0, 10;
复制代码
验证一下效果 预计在1ms内,提高了13000多倍!
10 rows in set (0.00 sec)
复制代码
本觉得至此大工告成,但咱们在前面的分析中漏了一个细节,先排序再join和先join再排序理论上开销是同样的,为什么提高这么可能是由于有一个limit!大体执行过程是:mysql先按索引排序获得前10条记录,而后再去join过滤,当发现不够10条的时候,再次去10条,再次join,这显然在内层join过滤的数据很是多的时候,将是灾难的,极端状况,内层一条数据都找不到,mysql还傻乎乎的每次取10条,几乎遍历了这个数据表!
用不一样参数的SQL试验下:
select
sql_no_cache c.id,
c.name,
c.position,
c.sex,
c.phone,
c.office_phone,
c.feature_info,
c.birthday,
c.creator_id,
c.is_keyperson,
c.giveup_reason,
c.status,
c.data_source,
from_unixtime(c.created_time) as created_time,
from_unixtime(c.last_modified) as last_modified,
c.last_modified_user_id
from
contact c
where
exists (
select
1
from
contact_branch cb
inner join branch_user bu on cb.branch_id = bu.branch_id
and bu.status in (1, 2)
inner join org_emp_info oei on oei.data_id = bu.user_id
and oei.node_left >= 2875
and oei.node_right <= 2875
and oei.org_category = - 1
where
c.id = cb.contact_id
)
order by
c.created_time desc
limit
0, 10;
Empty set (2 min 18.99 sec)
复制代码
2 min 18. 99 sec!比以前的状况还糟糕不少。因为mysql的nested loop机制,遇到这种状况,基本是没法优化的。这条语句最终也只能交给应用系统去优化本身的逻辑了。
经过这个例子咱们能够看到,并非全部语句都能优化,而每每咱们优化时,因为SQL用例回归时落掉一些极端状况,会形成比原来还严重的后果。因此,第一:不要期望全部语句都能经过SQL优化,第二:不要过于自信,只针对具体case来优化,而忽略了更复杂的状况。
慢查询的案例就分析到这儿,以上只是一些比较典型的案例。咱们在优化过程当中遇到过超过1000行,涉及到16个表join的“垃圾SQL”,也遇到过线上线下数据库差别致使应用直接被慢查询拖死,也遇到过varchar等值比较没有写单引号,还遇到过笛卡尔积查询直接把从库搞死。再多的案例其实也只是一些经验的积累,若是咱们熟悉查询优化器、索引的内部原理,那么分析这些案例就变得特别简单了。