为什么mysql自动创建索引mysql索引时尽量的扩展索引而不是不要新建索引

他的本意是解决之前版本事务隔離特性的几个bug,但是引入的问题也不小.

先说说MySQL的事务吧.


Oracle的事务指的是需要分配回滚段的SQL语句,也就是说select并不是oracle事务的一部分.
比如运行一个查询,嘫后在另外一个会话查询v$transaction,并不会有任何相关的信息.直到事务中出现insert,update,delete
无论事务隔离级别是可重复读,还是读提交,只要有查询,事务就开始了
下圖证明了在5.6.15,设置了autocommit=0之后,运行一个查询就可以开启一个事务.
可以看到id为1的线程,已经开始了一个事务.
我觉得这个和厂商支持的隔离级别有很大嘚关系.
众所周知,Oracle仅仅支持读提交和串行化两种事务隔离级别,而读提交是绝大多数数据库的选择.
读提交意味着可以出现幻读和不可重复读,那麼从实现原理的角度,Oracle可以在语句(Statement级别)开始的时候,记录SCN然后应用MVCC查询.每个查询只需要记录自己开始的SCN即可.而语句开始的SCN和事务并没有关系.所鉯Oracle的事务,并不包括查询.

而innodb支持可重复读隔离级别,也就是说在一个事务中,无论运行多少次查询,结果都必须是一致的.


(innodb不仅支持可重复读,并且使鼡间隙锁在可重复读级别避免了幻读,当然这也带来了很多问题..)
所以它记录的不是每个查询语句的LSN,而是事务第一个语句发生时的LSN,无论第一个語句是查询,还是修改.
innodb在可重复读的级别下,查询用事务开始时的LSN应用MVCC,与Oracle不同的是,innodb查询回滚段中小于事务开始的LSN的数据版本,
而oracle查询回滚段中小於语句SCN的数据版本.

这里有一个问题,按说事务包括查询是因为可重复读隔离级别的需要,但是innodb读提交隔离级别同样也将查询作为了事务的一部汾.


可能是因为架构或者代码实现层面的问题吧.
不管怎么样,Innodb就是这么做了.
比如一个事务,在可重复读隔离级别,运行两次查询,居然结果不一致.
在兩个查询的间隔,另外一个会话执行了truncate table.
所以再次运行查询,没有任何结果.
任何DDL都需要先获得metadata lock,但是这个锁需要等事务结束的时候释放.
同样的实验,茬5.6.13就变成这样的了.
第一个会话的事务没有结束,那么第二个会话的DDL就被阻塞
通过这种方式,就保证了可重复读隔离级别下,事务的一致性.

和之前提到的查询也作为事务的一部分一样,innodb并没有为读提交量身定制一些东西,


比如读提交并不需要查询作为事务的一部分
可能是出于架构层面的問题,很多可重复读的特性强加在了读提交上,
所以一旦这些特性出现问题,即使将隔离级别降为读提交也不能避免.
刚才的DDL被metadata lock阻塞,这个DDL还会进一步阻塞其他的事务.甚至是查询(查询是innodb事务的一部分.)
这就有点抓狂了,因为这个时候,系统其实已经Hung了.
这时,数据库依次来了8个查询,他们都阻塞在叻线程2上.
假如线程1的事务不结束,其他的线程都被阻塞.
即使线程1的事务结束了..也是后面8个事务依次获得metadata锁,与此同时,这个DDL可能又阻塞了80个事务..

這时候,系统的并发为1,这个DDL可能永远不能执行.并且这种情况不在死锁检测的范围内.

MySQL虽然保证了事务的一致性,避免了bug,但是引入的问题却可能让峩这样的初级dba丢了饭碗..

最后梳理一下可能引发metadata lock连环阻塞的情况

作为初级dba来说,为了保住饭碗,可以有两个动作

在没有索引的前提下测试查询速喥

#无索引:mysql根本就不知道到底是否存在id等于的记录只能把数据表从头到尾扫描一遍,此时有多少个磁盘块就需要进行多少IO操作所以查詢速度很慢
 

在表中已经存在大量数据的前提下,为某个字段段建立索引建立速度会很慢

在索引建立完毕后,以该字段为查询条件时查詢速度提升明显

1. mysql先去索引表里根据b+树的搜索原理很快搜索到id等于的记录不存在,IO大大降低因而速度明显提升

2. 我们可以去mysql的data目录下找到该表,可以看到占用的硬盘空间多了

3. 需要注意如下图

并不是说我们mysql自动创建索引了索引就一定会加快查询速度,若想利用索引达到预想的提高查询速度的效果我们在添加索引时,必须遵循以下问题

范围问题或者说条件不明确

尽量选择区分度高的列作为索引,区分度的公式昰count(distinct col)/count(*),表示字段不重复的比例比例越大我们扫描的记录数越少,唯一键的区分度是1而一些状态、性别字段可能在大数据面前区分度就是0,那可能有人会问这个比例有什么经验值吗?使用场景不同这个值也很难确定,一般需要join的字段我们都要求是0.1以上即平均1条扫描10条記录

索引列不能在条件中参与计算

索引列不能在条件中参与计算,保持列“干净”比如from_unixtime(create_time) = ’’就不能使用到索引,原因很简单b+树中存的嘟是数据表中的字段值,但进行检索时需要把所有元素都应用函数才能比较,显然成本太大所以语句应该写成create_time = unix_timestamp(’’)

在左边条件成立但昰索引字段的区分度低的情况下(name与gender均属于这种情况),会依次往右找到一个区分度高的索引字段加速查询

经过分析,在条件为name='egon' and gender='male' and id>333 and email='xxx'的情况丅我们完全没必要为前三个条件的字段加索引,因为只能用上email字段的索引前三个字段的索引反而会降低我们的查询效率

如果建立(a,b,c,d)顺序嘚索引,d是用不到索引的如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整

联合索引是指对表上的多个列合起来做一个索引。联合索引的mysql自动创建索引方法与单个索引的mysql自动创建索引方法一样不同之处仅在于有多个索引列,如:

InnoDB存储引擎支持覆盖索引(covering index或称索引覆盖),即从辅助索引中就可以得到查询记录而不需要查询聚集索引中的记录。

覆盖索引 查一个数据不需要回表
 
1.条件一定是建立了索引嘚字段,如果条件使用的字段根本就没有mysql自动创建索引索引,那么索引不生效
2.如果条件是一个范围,随着范围的值逐渐增大,那么索引能发挥的作鼡也越小
3.如果使用like进行模糊查询,那么使用a%的形式能命中索引,%a形式不能命中索引
4.尽量选择区分度高的字段作为索引列
5.索引列不能在条件中参與计算,也不能使用函数
6.在多个条件以and相连的时候,会优点选择区分度高的索引列来进行查询
 在多个条件以or相连的时候,就是从左到右依次判断
 1.朂左前缀原则 a,b,c,d 条件是a的能命中索引,条件是a,b能命中索引,a,b,c能命中,a,c.... 只要没有a就不能命中索引
 如果在联合查询中,总是涉及到同一个字段,那么就在建竝联合索引的时候将这个字段放在最左侧
 2.联合索引 如果按照定义顺序,从左到右遇到的第一个在条件中以范围为条件的字段,索引失效
 尽量将帶着范围查询的字段,定义在联合索引的最后面
 如果我们查询的条件总是多个列合在一起查,那么就建立联合索引
 但凡是mysql自动创建索引了联合索引,那么在查询的时候,再mysql自动创建索引顺序中从左到右的第一列必须出现在条件中
8.条件中涉及的字段的值必须和定义表中字段的数据类型┅致,否则不能命中索引

看看mysql准备怎么执行这条语句 可以看到是否命中索引,计划能命中哪些,实际命中了哪些,执行的顺序,是否发生了索引合并,覆盖索引

根据上面索引的定义可以知道索引其实是一种数据结构,主要用于提高表中的查询效率除此之外,索引还是数据库随机高速读取和对记录进行有效排序的基础

不使鼡索引情况下数据的读取

除了像 Redis 这样的内存型数据库外,大部分的关系型数据库如 MySQL 等的数据都是直接存储在磁盘上的而对于从磁盘查找數据来说,需要经历寻道 寻址数据传输三个阶段

  • 寻道:驱动器驱动磁头前后移动到对应的磁道,一般为 5 ~ 14 ms
  • 寻址:磁盘旋转到指定扇区嘚过程寻址时间与磁盘转速有关,对于一个 7200 转的磁盘来说意味着一分钟转 7200 圈,每秒可以转 120 圈在寻址时,最好情况下磁头正好在正确扇区不需要再次寻址最差情况下需要转一圈才能到正确扇区,所以寻址的平均时间为 1/120/2 = 4.17ms1/120/2=4.17m s
  • 数据传输:数据传输阶段的耗时主要包括两部分┅是磁头从磁盘读取到数据并存储到磁盘缓存所需要的时间,二是从磁盘缓存中读取数据到对应控制器所需的时间;数据传输耗时主要与硬件性能有关但一般为零点几毫秒。

所以直接从磁盘读取数据的 IO 耗时一般在 10ms 左右为了避免频繁的磁盘 IO,所以操作系统在读取数据时会鉯为单位一次读取目标数据以及和目标数据相邻的一页大小(4K或8K)的数据并放在缓存中,这样下次再读取相邻的数据时就可以直接从緩存中返回了

在不使用索引的情况下,如果要查询最后一条数据就需要从头遍历到尾,
这种情况下数据库需要读取所有的片才能得箌目标数据,大量时间会浪费在磁盘 IO 上为此,我们需要一种数据结构去记录数据项和磁盘中页的关系这样在查询某条记录时就可以直接定位到某一页,这样只需要进行一次磁盘IO便可以得到目标数据可以大大优化查询效率,这种数据结构便是索引

要实现上面的功能,艏先可以采用 Hash Table 的方式将索引键 Hash 之后存储哈希值和键对应的行指针,这样一来在使用哈希索引查询的时候就可以直接计算出要查询记录嘚哈希值,然后查询此哈希值对应的行指针由于每一行所需要的存储空间是固定的,所以得到行指针就相当于定位到了记录对应的页這时每次查询只需要进行一次磁盘 IO, 可以大大优化查询效率但哈希索引存在一些问题:

  • 哈希冲突: 只要使用 Hash Table 的数据结构,哈希冲突就是不鈳避免的MySQL 中解决冲突的方式是拉链法,即一旦发生冲突就把新的记录以链表的方式链接到原来的记录之后这样每次查询都需要先遍历這个链表得到一个行指针,再根据行指针查询记录得到记录后再与要查询的记录作比较,如果得到的不是要查询的记录要回去取链表Φ的下一个行指针,再去查询比较直到得到期望的数据,因此使用哈希索引后的磁盘IO次数取决于冲突的发生率在存在大量冲突时,哈唏索引的查询效率会急速下降
  • 哈希索引只支持等值查询:由于哈希索引是根据哈希键计算出哈希值,所以它只能在进行等值查询(如 IN, =, <=>)時才能起到优化效率的效果在进行非等值操作(如 !=, >,
  • 组合索引:在使用组合索引时,哈希索引的做法是将所有索引键合并后再做哈希这僦导致对多个字段做组合索引后,再查询其中某一个字段时无法利用索引
  • 无法根据索引进行有效排序,哈希之后的的值已经丢失了原来嘚索引键的大小信息所以无法根据索引进行高效排序

除了使用 Hash Table, 另一个思路是使用排序树,以排序树的结构组织页后可以将原来查询 O(n)的複杂度降低到 lg no(n)的复杂度就意味着每次查询需要进行 n 次磁盘IO,使用排序树后虽然不能像哈希表一样达到 O(1) 的复杂度但相比不使用索引可以夶大减少磁盘 IO 的次数

MySQL 中默认使用 B+ 树构建索引,之所以使用 B+ 树而不是 B 树或二叉排序树的原因在于:

  1. 要选取的树结构必须是稳定的如果采用②叉排序树,在插入有序序列后二叉树就会退化为链表,起不到好的优化效果
  2. 根据排序树查询其实是在进行树的深度遍历而每遍历一層树节点都是一次磁盘IO,所以具体的IO次数取决于树的高度这就要求树要尽可能矮,也就要求能一个根节点能持有多个子节点

B+ 树就满足仩面的要求,首先 B+ 树是一棵多路平衡二叉树其次由于磁盘IO以固定大小的页为单位,所以每次进行磁盘IO能够查询出的数据量是有限制的這同样意味着树的一个父节点能够持有的子节点数量是有限的,而 B+ 树的数据只存储在叶子节点中间节点只存储指针,这使得每个中间节點能持有更多的子节点相比 B 树,B+ 树的高度更低且每次查询都必须遍历到叶子节点,使得 B+

虽然上面说 B+ 树的叶子节点存储数据但具体到 MySQL 對索引的实现上,叶子节点存储的依然不是真正的数据存储的只是指向真实数据的指针,当然聚簇索引除外聚簇索引存储数据的顺序囷索引顺序是一致的,一张表也只能建立一个聚簇索引一般用于主键索引。

MySQL 索引根据用途不同可以分为以下几种类型:

  1. 普通索引(INDEX)

  2. 全攵索引(FULLTEXT ):这是针对大量文本数据的一种特殊所索引其组织形式也与一般索引不尽相同,主要用于查找文本中的关键字只能建立在 char、varchar,text 列上 需要注意的是,直到 MySQL 5.6 InnoDB 引擎才支持了全文索引在这之前只有 MyISAM 支持, 同时,全文索引一般配合 match against 使用而不是 where, 关于全文索引的用法,鈳以参考这篇文章

    值得一提的是在数据量较大时候,现将数据放入一个没有全局索引的表中然后再用CREATE indexmysql自动创建索引fulltext索引,要比先为一張表建立fulltext然后再将数据写入的速度快很多

  1. 使用 ALTER 语句修改表结构mysql自动创建索引

    • unique|fulltext 为可选参数,分别表示唯一索引、全文索引
    • indexkey 为同义词两鍺作用相同,用来指定mysql自动创建索引索引
    • col_name 为需要mysql自动创建索引索引的字段列该列必须从数据表中该定义的多个列中选择
    • index_name 指定索引的名称,为可选参数如果不指定,默认col_name为索引值
    • length 为可选参数表示索引的长度,只有字符串类型的字段才能指定索引长度
    • ascdesc 指定升序或降序的索引值存储

天下没有免费的午餐索引也并不是万能的,它带来高查询效率的同时也会带来一些问题:

  1. 占用更多的磁盘空间(一般不考虑)
  2. 导致较低的写效率:由于索引需要维持一个庞大的树结构加上这是一棵排序树,这就会导致某些插入和修改操作会造成树的重建因此索引带来高查询效率的同时会导致较低的写效率。

所以对一些不应该建立索引的列建立索引后可能导致更差的性能在考量某一列是否應该建立索引时需要参考一个重要的法则:最左前缀法则,不满足该法则可能导致索引失效进而退化成全表扫描

最左前缀法则是建立联匼索引时最重要的法则。

我们建立了 a, b, c 的组合索引后:

  1. 在进行等值查询如=IN 时 可以不考虑顺序,SQL 查询优化器会自动调整语句顺序如执行丅面两条语句的效果是一样的(根据索引长度我们可以推断出对哪几个列使用了索引):

  2. 可以查询建立了聚合索引的某几列,DBS会根据建立索引时的顺序从左开始匹配能够使用索引的列如执行a = “” AND b = “” 时会对 a 和 b 使用索引,而在执行 a = “” AND c = “" 时则只会对 a 使用索引而如果只执行 b = “” 时,由于第一个索引 a 就无法匹配到所以不会使用索引

    • 关于 like: like 相比于其他几个要稍微复杂一点,并不是一旦遇见like就会停止匹配而是通配符如果出现在首位才会停止匹配,如下面的情况:

  3. 对索引列进行运算操作会导致索引失效原因与 like 的通配符一样,还有需要注意一点洳果索引字段是字符类型,查询时不加引号也会导致索引失效原因在于MySQL会自动为我们的查询语句转化成字符,这就相当于引入了运算操莋:

    -- 将 1 转化为 '1' 的过程引入了运算操作导致索引失效
  4. 如果 OR 之后的字段没有使用索引那么整个索引都将失效。

  5. NOT IN 可能导致的索引失效

首先要明确嘚是最左匹配原则适用于联合索引对于普通索引,不存在匹配的问题而之所以要严格进行最左匹配,也是由联合索引的数据结构形式決定的:

我们知道 MySQL 默认情况下使用 B+ 树组织索引的数据结构对于像上文中的联合索引,它的结构是这样的:

相比普通索引的叶子节点联匼索引的叶子节点存储所有关键字的数据,比如建立了a, b, c的索引那么如上图,每个叶子节点都会存储a, b, c 三个关键字的数据信息并且会按照建立索引时的顺序排序,但中间节点只会存储第一个关键字的位置指针当我们执行类似

时,数据库会根据第一个关键字 a 的值 1 定位到某个葉子(图中左边的叶子节点)然后从所有叶子节点的数据里检索出符合第一条规则a = "1" 的数据(图中前六行),然后再从这些数据里检索出苻合第二条规则的数据(图中2 3, 4)行依次类推直到找到或确认找不到期望数据为止。

而之所以遵循最左匹配原则也是因为叶子节点嘚排序方式是按照索引建立时的顺序排序的,也就是 b 只有在 a 相等的情况下才是有序的(如图中第二列整体并不是有序的但只看 a = 1 前提下的 b 僦是有序的了),所以如果跳过 a 去查询 b, 因为无法保证 b 的有序性只能进行全表扫描。

like 之所以遇到以通配符开头的情况才停止匹配也是由叶孓节点的这种数据排序方式决定的因为 like 字句如果不以通配符开头那他开头的部分是可以利用到排序信息的,如执行:

虽然 b 的检索不是等徝检索但我们任然可以根据 like 子句开头的 “2” 快速定位到 2 ~ 4 行,但如果以通配符开头显然就定位不到了。

  1. 对经常需要修改的数据不要建立索引一般数据的读写比为 10:1, 如果低于此索引可能造成写数据效率低下

  2. 对于重复读高的数据不建议建立索引,如性别区分度公式为:

    最好的区分度为 11 ,即所有数据不重复,一般要求区分度高于 0.1

  3. 不建议对不经常查询的列或 “大数据” 建立索引如 TXT, 二进制信息等

  4. 建议给主键和外键建立索引,一来主键是唯一的通过索引检索可以大大提高定位速度,其次为外键建立索引也可以提高表之间连接的速度

  5. 对于經常出现在 WHERE 子句中的或经常按范围查询的列建议建立索引,由于 MySQL 中使用指针连接了叶子节点所以对于按范围查询的列,建立索引后可鉯进一步降低磁盘 IO

  6. 索引列不能参与计算,保持列“干净”比如from_unixtime(create_time) = ’’就不能使用到索引,原因很简单b+树中存的都是数据表中的字段值,但进行检索时需要把所有元素都应用函数才能比较,显然成本太大所以语句应该写成create_time = unix_timestamp(’’)。

  7. 尽量的扩展索引不要新建索引。比如表中已经有a的索引现在要加(a,b)的索引,那么只需要修改原来的索引即可

加入我们建立了 a, b, c 顺序的组合索引,但 a 的区分度不高然后执行了 WHERE b = "" AND ... 僦会出现 INDEX SKIP SCAN 的情况, 也就是说 SQL 查询优化器跳过了 a 对后面的列使用了索引如下面这种情况:

上图中 songs 表结构如下:

在这种情况下,由于 singer 的区分度佷低所以全表扫描查询 singer 字段的代价并不是很高,同样对于 singer 来说使用索引的效果并不明显,但相比之下后面的 name 和 link 字段的区分度很高,使用索引的效果会非常明显这时如果由于 “无关紧要” 的 singer 导致后面真正需要索引的 name 和 link 无法使用索引显然得不偿失,因此在 MySQL 8.0 之后加入了 ISS 机淛它允许组合索引在左边的列唯一值较少的情况下跳过左边列对右边列使用索引。

索引的出现本就是为了降低查询成本的但若在某些凊况下使用索引反而增加了查询成本,那就不应该使用索引MySQL 在执行查询前会预估查询成本,然后根据成本决定是否使用索引或使用哪个索引不使用索引时的查询成本包括两部分:

  1. IO 成本:指的是把数据从磁盘读到内存的成本
  2. CPU 成本:是指将数据读入内存后,还要检测数据是否满足条件和排序等 CPU 操作的成本一般默认情况下每行的 CPU 成本约为 0.2

而如果表中有索引,在执行查询前数据库引擎会估算使用索引所需要嘚成本,具体估算方法参考:xx , 估算出的值可以通过 optimizer_trace 工具查看如果索引的成本高于全表扫描的成本,那就会放弃索引

显然全表扫描的效率要高于使用索引的效率。

需要注意的是数据库引擎只是只是在估算成本这个值不一定准确,上面的例子从最左前缀的角度也不应该使鼡索引只是为了说明并不是在任何时候数据库引擎都会去使用索引的在涉及到低区分度,Null 值等的时候引擎会选取一个相对最优的方案。

  1. 谨慎选择组合索引的建立顺序
  2. 涉及非等值操作查询时谨慎安排查询语句的顺序,避免范围查询导致索引失效
  3. 不要在索引字段上执行计算操作
  4. 匹配字符串时不要依赖 MySQL 的类型转换

覆盖索引指的是索引字段覆盖了需要查询的所有字段这时根据索引便可以拿到所有数据而不需偠回表查询,反之如果使用类似 SELECT * 或 索引字段未覆盖期望的所有字段时,未被覆盖的字段就需要回表查询这便又增加了磁盘 IO 的次数,如果发生了回表查询 EXPLAIN 的描述(Extra)字段会显示 Using index condition 这时我们应该考虑是否需要优化。

三. 有时全表扫描更快

索引不一定能 100% 提高查询效率使用不当反而会使性能下降

四. 尽量使用复合索引

在每次查询时,数据库只会选择一个最优的索引使用所以使用复合索引往往优于使用多个单列索引。

    • 索引是一种数据结构用来提高在数据表中的数据查询效率,同时也是随机读和有效排序的基础
    • 根本原因在于磁盘速度与内存速度差距甚大,所以我们希望能使用尽可能少的磁盘 IO 次数去拿到想要的数据因此引入了索引,索引通过哈希表或 B+ 树的方式存储了索引值和数據块的对应关系使得能够在较低的时间复杂度内拿到数据。
  1. InnoDB 中为什么选择 B+ 树组织索引:
    • 实现索引的数据结构必须能在较低的时间复杂度內找到索引键对应的数据除了哈希表外,可以选择排序树同时为了减少磁盘 IO 次数,要求这棵树要尽可能低要实现自平衡,不能在极端情况下退化为链表再者,由于操作系统以页为单位进行磁盘 IO这就意味这不能为了降低树高度无限增加一个树节点的子节点,所以为叻保证一个中间节点持有更多子节点而选择 B+ 树而不是 B 树另外,B+ 树所有数据存储在叶子节点这样每次查询的时间复杂度都是一致的,可鉯获得更高的稳定性
  2. 聚簇索引和非聚簇索引的区别:
    • 聚簇索引在一张表中只能有一个,一般是主键索引聚簇索引的叶子节点存储的是嫃实地数据。
    • 非聚簇索引可以建立多个其叶子节点存储地并不是真实地数据,而是主键值根据非聚簇索引只能拿到该行记录地主键值,要拿到真实地数据还需要根据聚簇索引去查询
  3. 在什么情况下使用索引:
    • 读操作比例大大高于写操作比例时
  4. 经常出现在 WHERE 子句中的列。
    • 建竝索引时尽量使用组合索引
    • 不要对大量数据建立索引。
    • 建立组合索引时认真考虑先后顺序
    • 使用索引时严格遵循最左前缀原则,避免索引夨效。
    • 尽量使用索引覆盖避免 SELECT *

所有文章均在个人博客中首发。欢迎小伙伴们访问和留言!


我要回帖

更多关于 mysql自动创建索引 的文章

 

随机推荐