一、基础概念
1.什么是索引?
以下mysql官方的解释:
索引是一个将索引列按照一定顺序排序,并维护到一种存储结构中如B+树或者hash列表中。
在这个列表中存储着索引的值和包含这个值的数据所在行的物理地址。
在数据十分庞大的时候,索引可以大大加快查询的速度,这是因为使用索引后可以不用扫描全表来定位某行的数据,而是先通过索引表找到该行数据对应的物理地址,然后访问所在行相应的数据。
自我总结:索引其实就是一种维持一种特殊顺序的数据结构。
2.索引的优缺点
优势:
1.可以快速检索,减少I/O次数,加快检索速度,即避免了全表扫描;
2.根据索引字段分组和排序时,可以加快分组和排序性能;
劣势:
- 索引本身也是表,因此会占用存储空间,一般来说,索引表占用的空间是数据表的1.5倍;
- 索引表的维护和创建需要时间成本,这个成本随着数据量增大而增大;
- 构建索引会降低数据表的修改操作(删除,添加,修改)的效率,因为在修改数据表的同时还需要修改索引表;
3.索引类型
索引目前使用主要两类索引:
hash类型的索引:查询单条快,范围查询慢。
btree类型的索引:b+树,层数越多,数据量指数级增长(我们就用它,因为innodb默认支持它)
4 索引分类
常见的索引类型有:主键索引、唯一索引、普通索引、全文索引、组合索引
1、主键索引:即主索引,根据主键pk_clolum(length)建立索引,不允许重复,不允许空值;
ALTER TABLE 'table_name' ADD PRIMARY KEY('col');
2、唯一索引:用来建立索引的列的值必须是唯一的,允许空值
sql
create unique index goods_no_unique on t_goods_info (goods_no);
ALTER TABLE 'table_name' ADD UNIQUE('col');
3、普通索引:用表中的普通列构建的索引,没有任何限制
ALTER TABLE 'table_name' ADD INDEX index_name('col');
4、全文索引:用大文本对象的列构建的索引(使用比较少)
ALTER TABLE 'table_name' ADD FULLTEXT('col');
5、组合索引:用多个列组合构建的索引,这多个列中的值不允许有空值
ALTER TABLE 'table_name' ADD INDEX index_name('col1','col2','col3');
创建联合唯一索引
sql
alter table t_aa add unique index(aa,bb);
5.索引的使用场景
1.什么时候要使用索引?
1.主键自动建立唯一索引;
2.经常作为查询条件在WHERE或者ORDER BY 语句中出现的列要建立索引;
3.作为排序的列要建立索引;
4.查询中与其他表关联的字段,外键关系建立索引
5.高并发条件下倾向组合索引(为什么????)
2.什么时候不要使用索引?
1.经常增删改的列不要建立索引;
2.有大量重复的列不建立索引;
3.表记录太少不要建立索引;
6. 使用索引的注意事项
1.LIKE操作中,使用通配符时要注意。
'%aaa%'不会使用索引,也就是索引会失效,但是'aaa%'可以使用到索引。
同时在查询条件中使用正则表达式时,只有在搜索模板的第一个字符不是通配符的情况下才能使用索引。
2.在索引的列上使用表达式或者函数会使索引失效。
例如:select * from users where YEAR(adddate)<2007,将在每个行上进行运算,这将导致索引失效而进行全表扫描,因此我们可以改成:select * from users where adddate<'2007-01-01′。
3.在查询条件中使用<>会导致索引失效。
4.在查询条件中使用IS NULL会导致索引失效。
5.如果限制条件中其他字段没有索引,尽量少用or。
or两边的字段中,如果有一个不是索引字段,而其他条件也不是索引字段,会造成该查询不走索引的情况。很多时候使用 union all 或者是union(必要的时候)的方式来代替"or"会得到更好的效果
6.尽量不要包括多列排序,如果一定要,最好为这多列构建组合索引;
7.索引字段不能太长,如果索引的值很长,那么查询的速度会受到影响。
例如,对一个CHAR(100)类型的字段进行全文检索需要的时间肯定要比对CHAR(10)类型的字段需要的时间要多。
长度为 20 的索引,区分度会高达 90%以上可以使用count(distinct left(列名, 索引长度))/count(*)区分度来确定
8.注意范围查询,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a =1 and b=2 c=> 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
9.关联查询时,尽量使用小表驱动大表
二、索引数据结构
InnoDB存储引擎支持以下几种常见的索引:
- B+树索引
- 全文索引
- 哈希索引
索引的查找算法:二叉查找法
1. 哈希索引
哈希表是一种以键 - 值(key-value)存储数据的结构,我们只要输入待查找的值即 key,就可以找到其对应的值即 Value。哈希的思路很简单,把值放在数组里,用一个哈希函数把 key 换算成一个确定的位置,然后把 value 放在数组的这个位置。
不可避免地,多个 key 值经过哈希函数的换算,会出现同一个值的情况。处理这种情况的一种方法是,拉出一个链表。
假设,你现在维护着一个身份证信息和姓名的表,需要根据身份证号查找对应的名字,这时对应的哈希索引的示意图如下所示:
图中,User2 和 User4 根据身份证号算出来的值都是 N,但没关系,后面还跟了一个链表。假设,这时候你要查 ID_card_n2 对应的名字是什么,处理步骤就是:首先,将 ID_card_n2 通过哈希函数算出 N;然后,按顺序遍历,找到 User2。
需要注意的是,图中四个 ID_card_n 的值并不是递增的,这样做的好处是增加新的 User 时速度会很快,只需要往后追加。但缺点是,因为不是有序的,所以哈希索引做区间查询的速度是很慢的。
你可以设想下,如果你现在要找身份证号在 [ID_card_X, ID_card_Y] 这个区间的所有用户,就必须全部扫描一遍了。
哈希索引也没办法利用索引完成排序,以及like 'xxx%' 这样的部分模糊查询(这种部分模糊查询,其实本质上也是范围查询);
哈希索引也不支持多列联合索引的最左匹配规则;
所以,哈希表这种结构适用于只有等值查询的场景,比如 Memcached 及其他一些 NoSQL 引擎。
2.有序数组
而有序数组在等值查询和范围查询场景中的性能就都非常优秀。还是上面这个根据身份证号查名字的例子,如果我们使用有序数组来实现的话,示意图如下所示:
这里我们假设身份证号没有重复,这个数组就是按照身份证号递增的顺序保存的。这时候如果你要查 ID_card_n2 对应的名字,用二分法就可以快速得到,这个时间复杂度是 O(log(N))。
同时很显然,这个索引结构支持范围查询。你要查身份证号在 [ID_card_X, ID_card_Y] 区间的 User,可以先用二分法找到 ID_card_X(如果不存在 ID_card_X,就找到大于 ID_card_X 的第一个 User),然后向右遍历,直到查到第一个大于 ID_card_Y 的身份证号,退出循环。
如果仅仅看查询效率,有序数组就是最好的数据结构了。但是,在需要更新数据的时候就麻烦了,你往中间插入一个记录就必须得挪动后面所有的记录,成本太高。
所以,有序数组索引只适用于静态存储引擎,比如你要保存的是 2017 年某个城市的所有人口信息,这类不会再修改的数据。
3.二叉查找树
1.定义
二叉查找树这个时间复杂度是 O(log(N))。当然为了维持 O(log(N)) 的查询复杂度,你就需要保持这棵树是平衡二叉树。为了做这个保证,更新的时间复杂度也是 O(log(N))。
特点:
2.线型结构
比如插入顺序:2,3,5,7,6,8这样的顺序,就会造成下图的线下结构。
如果数据结构变成了线性结构,那么二叉查找树的查询效率就会非常低
3.红黑树
相比于二叉查找树,他通过自平衡降低了树的高度,但由于每个节点只能存一个数据,当数据量比较大时,其高度也是比较高
4.多叉树
树可以有二叉,也可以有多叉。多叉树就是每个节点有多个儿子,儿子之间的大小保证从左到右递增。二叉树是搜索效率最高的,但是实际上大多数的数据库存储却并不使用二叉树。其原因是,索引不止存在内存中,还要写到磁盘上。
你可以想象一下一棵 100 万节点的平衡二叉树,树高 20。一次查询可能需要访问 20 个数据块。在机械硬盘时代,从磁盘随机读一个数据块需要 10 ms 左右的寻址时间。也就是说,对于一个 100 万行的表,如果使用二叉树来存储,单独访问一个行可能需要 20 个 10 ms 的时间,这个查询可真够慢的。
为了让一个查询尽量少地读磁盘,就必须让查询过程访问尽量少的数据块。那么,我们就不应该使用二叉树,而是要使用"N 叉"树。这里,"N 叉"树中的"N"取决于数据块的大小。
以 InnoDB 的一个整数字段索引为例,这个 N 差不多是 1200。这棵树高是 4 的时候,就可以存 1200 的 3 次方个值,这已经 17 亿了。考虑到树根的数据块总是在内存中的,一个 10 亿行的表上一个整数字段的索引,查找一个值最多只需要访问 3 次磁盘。其实,树的第二层也有很大概率在内存中,那么访问磁盘的平均次数就更少了。
N 叉树由于在读写上的性能优点,以及适配磁盘的访问模式,已经被广泛应用在数据库引擎中了。
1.B-树(B树)
b-树索引数据结构:主要应用于文件系统和部分数据库索引,如非关系型数据库mogodb。
相比于二叉查找树,B-树的最大特点是通过左旋和右旋,维护了树的高度
相比于红黑树,节点可以存不止一个数据,当数据量比较大时,维护了树的高度
结构如下:
下面来具体介绍一下B-树(Balance Tree),一个m阶的B树具有如下几个特征:
1.根结点至少有两个子女。
2.每个中间节点都包含k-1个元素和k个孩子,其中 m/2 <= k <= m
3.每一个叶子节点都包含k-1个元素,其中 m/2 <= k <= m
4.所有的叶子结点都位于同一层。
5.每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。
检索流程:
查找为5的节点:
第1次磁盘IO:在内存中定位(和9比较):
第2次磁盘IO:在内存中定位(和2,6比较)
第3次磁盘IO:在内存中定位(和3,5比较)
2.B+树
B+树的演变:先由二叉查找树--> b树--> B+树。
B+树的特征:
1.有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。
2.所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺连接,且连接数据项的链表为双向链表。
3.所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。
B+树检索流程:
B+树自顶向下查找,直到搜索到叶子节点,然后检索到对应的数据
第一次磁盘IO:
第二次磁盘IO:
第三次磁盘IO:
范围检索流程:
查找(3,11)范围的数据
自顶向下,查找到范围的下限(3):
通过链表指针,遍历到元素6, 8:
通过链表指针,遍历到元素9, 11,遍历结束
B+树索引的关键字检索效率比较平均,不像B树那样波动幅度大,在有大量重复键值情况下,哈希索引的效率也是极低的,因为存在哈希碰撞问题
思考
1.B+树的优势
1.单一节点存储更多的索引字段,使得查询的IO次数更少。
2.所有查询都要查找到叶子节点,查询性能稳定。
3.所有叶子节点形成有序链表,便于范围查询。
2.B+树对比B树的优势
1.B 树能够在非叶节子点中存储数据, 但是这也导致在查询连续数据时可能会带来更多的随机 I/O, 而 B+树的所有叶节点可以通过指针相互连接, 能够减少顺序遍历时产生的额外随机 I/O,即B树不利于范围查询
2.B 树一个节点里存的是数据, 而 B+树存储的是索引( 地址) , 所以 B 树里一个节点存不了很多个数据。 但是 B+树一个节点能存很多索引,所以上面1,2层可以存储更多的索引字段,降低树的高度
3.HASH 与B-Tree 的区别
1、Hash索引仅仅能够满足"=","IN"和"<=>"查询,不能使用范围查询。
由于Hash索引比较的是进行Hash运算之后的Hash值,所以它只能用于等值的过滤,不能用于基于范围的过滤,因为进过相应的Hash算法处理之后的Hash值的大小关系,并不能保证和Hash运算前完全一样。
2、Hash索引无法被用来避免数据的排序操作
由于Hash索引中存放的是经过Hash计算之后的Hash值,而且Hash值的大小管理并不一定和Hash运算前的键值完全一样,所以数据库无法利用索引的数据来避免任何排序运算。
3、Hash索引不能利用组合索引
对于组合索引,Hash索引在计算Hash值的时候是组合索引键合并后再一起计算Hashs值,而不是单独计算Hash值,所以通过组合索引的前面一个或者几个索引键进行查询的时候,Hash索引也无法被利用。
4、Hash索引在任何时候都不能避免表扫描
前面已经知道,Hash索引是将索引键通过Hash运算之后,将Hash运算结果的Hash值和对应的行指针信息存放于一个Hash表中,由于不同索引键存在相同Hash值,所以即使取满足某个Hash键值的数据的记录条数,也无法从Hash索引中年直接完成查询,还要通过访问链表中实际数据进行相应的比较,并得到相应的结果。
5、Hash索引遇到大量Hash值相等的情况后性能不一定会比B-Tree索引高
对于选择性比较低的索引键,如果创建Hash索引,那么将会存在大量记录指针信息存于同一个Hash值相关联。这样要定位某一条记录时就会非常麻烦,会浪费多次表数据的访问,而造成整体性能低下。
参考资料
索引原理-btree索引与hash索引的区别 https://www.cnblogs.com/zhouguowei/p/9753828.html
4.为什么建议ID做主键,为什么主键要自增?
不设置主键,数据库会自动设置逐渐增加了mysql的开销。
如果不自增,插入一个随机的值,会导致树的重新平衡影响插入性能
5. 索引数据结构对比
-
二叉查找树
缺点:可能造成线性结构,树的高度过高,查找效率低下
-
红黑树
特点:能够自动维持平衡,不至于树的高度过高。但是如果对于大数据 的情况下,因为数只有两个节点,故大数据下树的高度还是会很高。
-
HASH
特点:精确查询速度比较快,索引的检索可以一次定位,不像B-Tree索引需要从根节点到枝节点,最后才能访问到页节点这样多次的IO访问。但是对于范围查询,效率就会很低
-
B-Tree
特点:查询性能比二叉查找树高,但插入和删除等复杂,因为会涉及到左旋和右旋来维护二叉树的高度。
三、B+树索引
在 InnoDB 中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表
B+树索引在mysql中的性能。即树的高度一般2-4层
1.聚集索引与非聚集索引
两种索引数据检索过程如下:
注意对于myISAm:主键或者其他索引字段叶子节点存的是索引的物理地址,查询真实数据时,还需要一次磁盘IO找到物理地址对应的数据data
1.聚集(clustered)索引,也叫聚簇索引。
聚集(clustered)索引,也叫聚簇索引,主索引。
定义:数据存储与索引放到了一块、并且是按照一定的顺序组织的,数据的物理存放顺序与索引顺序是一致的,找到索引也就找到了数据,一个表中只能拥有一个聚集索引。
并且mysql中的行数据就是按照聚集索引的方式进行编排的。
2.非聚集(unclustered)索引,也叫辅助索引
定义:叶子节点不存储数据、存储的是辅助索引和数据行地址(主键ID)的映射。
叶子节点并不包含行记录的全部数据。叶子节点除了包含键值以外,每个叶子节点中的索引行中还包含了一个书签(bookmark)。该书签用来告诉 InnoDB存储引擎哪里可以找到与索引相对应的行数据。由于 InnoDB存储引擎表是索引组织表,因此 InnoDB存储引擎的辅助索引的书签就是相应行数据的聚集索引键。
其实按照定义,除了聚集索引以外的索引都是非聚集索引,只是人们想细分一下非聚集索引,分成普通索引,唯一索引,全文索引。
辅助索引检索流程:
当通过辅助索引来寻找数据时, InnoDB存储引擎会遍历辅助索引并通过叶级别的指针获得指向主键索引的主键,然后再通过主键索引来找到个完整的行记录。
举例说,如果在一棵高度为3的辅助索引树中查找数据,那需要对这棵辅助索引树遍历3次找到指定主键,如果聚集索引树的高度同样为3,那么还需要对聚集索引树进行3次查找,最终找到一个完整的行数据所在的页,因此一共需要6次逻辑IO访问以得到最终的一个数据页。
3.索引维护
B+ 树为了维护索引有序性,在插入新值的时候需要做必要的维护。以上面这个图为例,如果插入新的行 ID 值为 700,则只需要在 R5 的记录后面插入一个新记录。如果新插入的 ID 值为 400,就相对麻烦了,需要逻辑上挪动后面的数据,空出位置。
而更糟的情况是,如果 R5 所在的数据页已经满了,根据 B+ 树的算法,这时候需要申请一个新的数据页,然后挪动部分数据过去。这个过程称为页分裂。在这种情况下,性能自然会受影响。
除了性能外,页分裂操作还影响数据页的利用率。原本放在一个页的数据,现在分到两个页中,整体空间利用率降低大约 50%。
当然有分裂就有合并。当相邻两个页由于删除了数据,利用率很低之后,会将数据页做合并。合并的过程,可以认为是分裂过程的逆过程。
基于上面的索引维护过程说明,我们来讨论一个案例:
你可能在一些建表规范里面见到过类似的描述,要求建表语句里一定要有自增主键。当然事无绝对,我们来分析一下哪些场景下应该使用自增主键,而哪些场景下不应该。
自增主键是指自增列上定义的主键,在建表语句中一般是这么定义的: NOT NULL PRIMARY KEY AUTO_INCREMENT。
插入新记录的时候可以不指定 ID 的值,系统会获取当前 ID 最大值加 1 作为下一条记录的 ID 值。
也就是说,自增主键的插入数据模式,正符合了我们前面提到的递增插入的场景。每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。
而有业务逻辑的字段做主键,则往往不容易保证有序插入,这样写数据成本相对较高。
除了考虑性能外,我们还可以从存储空间的角度来看。假设你的表中确实有一个唯一字段,比如字符串类型的身份证号,那应该用身份证号做主键,还是用自增字段做主键呢?
由于每个非主键索引的叶子节点上都是主键的值。如果用身份证号做主键,那么每个二级索引的叶子节点占用约 20 个字节,而如果用整型做主键,则只要 4 个字节,如果是长整型(bigint)则是 8 个字节。
显然,主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。
所以,从性能和存储空间方面考量,自增主键往往是更合理的选择。
有没有什么场景适合用业务字段直接做主键的呢?还是有的。
比如,有些业务的场景需求是这样的:
只有一个索引;该索引必须是唯一索引。
你一定看出来了,这就是典型的 KV 场景。
由于没有其他索引,所以也就不用考虑其他索引的叶子节点大小的问题。
这时候我们就要优先考虑上一段提到的"尽量使用主键查询"原则,直接将这个索引设置为主键,可以避免每次查询需要搜索两棵树。
4.两种索引对比
基于主键索引和普通索引的查询有什么区别?
- 如果语句是 select * from T where ID=500,即主键查询方式,则只需要搜索 ID 这棵 B+ 树;
- 如果语句是 select * from T where k=5,即普通索引查询方式,则需要先搜索 k 索引树,得到 ID 的值为500,再到 ID 索引树搜索一次。这个过程称为回表。
也就是说,基于非主键索引的查询需要多扫描一棵索引树。因此,我们在应用中应该尽量使用主键查询。
优势:
1、查询通过聚簇索引可以直接获取数据,相比非聚簇索引需要第二次查询(非覆盖索引的情况下)效率 要高
2、聚簇索引对于范围查询的效率很高,因为其数据是按照大小排列的
3、聚簇索引适合用在排序的场合,非聚簇索引不适合
劣势:
1、维护索引很昂贵,特别是插入新行或者主键被更新导至要分页(page split)的时候。
2、表因为使用UUId(随机ID)作为主键,使数据存储稀疏,这就会出现聚簇索引有可能有比全表扫面 更慢,所以建议使用int的auto_increment作为主键
3、如果主键比较大的话,那辅助索引将会变的更大,因为辅助索引的叶子存储的是主键值;过长的主键 值,会导致非叶子节点占用占用更多的物理空间
总结:
1.使用聚集索引的查询效率要比非聚集索引的效率要高,但是如果需要频繁去改变聚集索引的值,写入性能并不高,因为需要移动对应数据的物理位置。
2.非聚集索引在查询的时候可以的话就避免二次查询,这样性能会大幅提升,比如使用覆盖索引。
2.组合索引
1.最左前缀规则
为了直观地说明这个概念,我们用(name,age)这个联合索引来分析。
可以看到,索引项是按照索引定义里面出现的字段顺序排序的。
当你的逻辑需求是查到所有名字是"张三"的人时,可以快速定位到 ID4,然后向后遍历得到所有需要的结果。
如果你要查的是所有名字第一个字是"张"的人,你的 SQL 语句的条件是"where name like '张 %'"。这时,你也能够用上这个索引,查找到第一个符合条件的记录是 ID3,然后向后遍历,直到不满足条件为止。
可以看到,不只是索引的全部定义,只要满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符。
索引的最左前缀和和B+Tree中的"最左前缀原理"有关。
举例来说就是如果设置了组合索引《col1,col2,col3》那么以下3中情况可以使用索引:col1,《col1,col2》,《col1,col2,col3》,其它的列,比如《col2,col3》,《col1,col3》,《col2,col3》等等都是不能使用到索引的。
根据最左前缀原则,我们一般把排序分组频率最高的列放在最左边,以此类推。
那么,如果既有联合查询,又有基于 a、b 各自的查询呢?查询条件里面只有 b 的语句,是无法使用 (a,b) 这个联合索引的,这时候你不得不维护另外一个索引,也就是说你需要同时维护 (a,b)、(b) 这两个索引。
这时候,我们要考虑的原则就是空间了。比如上面这个市民表的情况,name 字段是比 age 字段大的 ,那我就建议你创建一个(name,age) 的联合索引和一个 (age) 的单字段索引。
2.最左前缀原理
mysql创建联合索引的规则是首先会对联合索引的最左边的,也就是第一个字段col1的数据进行排序,在第一个字段的排序基础上,然后再对后面第二个字段col2进行排序。
其实就相当于实现了类似 order by col1 col2这样一种排序规则。
3.索引下推
上一段我们说到满足最左前缀原则的时候,最左前缀可以用于在索引中定位记录。这时,你可能要问,那些不符合最左前缀的部分,会怎么样呢?
我们还是以市民表的联合索引(name, age)为例。如果现在有一个需求:检索出表中"名字第一个字是张,而且年龄是 10 岁的所有男孩"。那么,SQL 语句是这么写的:
mysql> select * from tuser where name like '张 %' and age=10 and ismale=1;
你已经知道了前缀索引规则,所以这个语句在搜索索引树的时候,只能用 "张",找到第一个满足条件的记录 ID3。当然,这还不错,总比全表扫描要好。
然后呢?
当然是判断其他条件是否满足。
在 MySQL 5.6 之前,只能从 ID3 开始一个个回表。到主键索引上找出数据行,再对比字段值。
而 MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
图 3 和图 4,是这两个过程的执行流程图。
图 3 无索引下推执行流程
图 4 索引下推执行流程
在图 3 和 4 这两个图里面,每一个虚线箭头表示回表一次。
图 3 中,在 (name,age) 索引里面我特意去掉了 age 的值,这个过程 InnoDB 并不会去看 age 的值,只是按顺序把"name 第一个字是'张'"的记录一条条取出来回表。因此,需要回表 4 次。
图 4 跟图 3 的区别是,InnoDB 在 (name,age) 索引内部就判断了 age 是否等于 10,对于不等于 10 的记录,直接判断并跳过。在我们的这个例子中,只需要对 ID4、ID5 这两条记录回表取数据判断,就只需要回表 2 次。
4.组合索引的优点
1.效率高
使用组合索引,只需查找索引一次,而如果多个列分别使用索引,那么命中第一个索引后,还得再去命中第二个索引,这样性能就会有所下降。
如:有1000W条数据的表,有如下sql:select from table where col1=1 and col2=2 and col3=3,假设假设每个条件可以筛选出10%的数据,如果只有单值索引,那么通过该索引能筛选出1000W10%=100w条数据,然后再回表从100w条数据中找到符合col2=2 and col3= 3的数据,然后再排序,再分页;如果是联合索引,通过索引筛选出1000w10% 10% *10%=1w,效率提升可想而知!
2.减少开销,节约磁盘空间
建一个联合索引(col1,col2,col3),实际相当于建了(col1),(col1,col2),(col1,col2,col3)三个索引。每多一个索引,都会增加写操作的开销和磁盘空间的开销。对于大量数据的表,使用联合索引会大大的减少开销!
3.覆盖索引
对联合索引(col1,col2,col3),如果有如下的sql: select col1,col2,col3 from test where col1=1 and col2=2。
那么MySQL可以直接通过遍历索引取得数据,而无需回表,这减少了很多的随机io操作。
减少io操作,特别的随机io其实是dba主要的优化策略。所以,在真正的实际应用中,覆盖索引是主要的提升统计性能的优化手段之一。
3.覆盖索引
在下面这个表 T 中,如果我执行 select * from T where k between 3 and 5,需要执行几次树的搜索操作,会扫描多少行?
现在,我们一起来看看这条 SQL 查询语句的执行流程:
1.在 k 索引树上找到 k=3 的记录,取得 ID = 300;
2.再到 ID 索引树查到 ID=300 对应的 R3;
3.在 k 索引树取下一个值 k=5,取得 ID=500;
4.再回到 ID 索引树查到 ID=500 对应的 R4;
5.在 k 索引树取下一个值 k=6,不满足条件,循环结束。
在这个过程中,回到主键索引树搜索的过程,我们称为回表。
1.定义
InnoDB存储引擎支持覆盖索引(covering index,或称索引覆盖),即从辅助索引中就可以得到查询的记录,而不需要回查聚集索引中的记录。
使用覆盖索引的一个好处是辅助索引不包含整行记录的所有信息,故其大小要远小于聚集索引,因此可以减少大量的IO操作。
注意覆盖索引技术最早是在 InnoDB Plugin中完成并实现。这意味着对于InnoDB版本小于1.0的,或者 MySQL数据库版本为5.0或以下的, InnoDB存储引擎不支持覆盖索引特性。
自己理解:即SQL只需要通过索引就可以返回查询所需要的数据,而不必通过二级索引回表查到主键之后再去查询数据。
2.应用场景
1.覆盖索引覆盖到查询字段
覆盖索引指的是在⼀次查询中,如果⼀个索引包含或者说覆盖所有需要查询的字段的值,我们就称之为覆盖索引,⽽不再需要回表查询
⽽要确定⼀个查询是否是覆盖索引,我们只需要explain sql语句看Extra的结果是否是"Using index"即可。
覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。
以上⾯的user表来举例,我们再增加⼀个name字段,然后做⼀些查询试试。
explain select * from user where age=1; //查询的name⽆法从索引数据获取
explain select id,age from user where age=1; //可以直接从索引获取
对于 InnoDB存储引擎的辅助索引而言,由于其包含了主键信息,因此其叶子节点存放的数据为(primary key1, primary key2,...key1,key2,...)。
查询的列必须只有主键且where条件的字段都是索引字段
例如,下列语句都可仅使用一次辅助联合索引来完成查询:
SELECT key2 FROM table Where key1=xxx:
SELECT primary key2, key 2 FROM table Where key1=xxx:
SELECT primary key1, key 2 FROM table Where key1=xxx:
SELECT primary key1,primary key2, key2 FROM table Where key1=xxx:
注意:判断的依据是查询的字段,是否有覆盖到where条件上对应索引字段。
2.统计选择辅助索引
覆盖索引的另一个好处是对某些统计问题而言的,它可以自动判断使用辅助索引来统计行数。
辅助索引空间远小于聚集索引,选择辅助索引可以减少IO操作
还是对于上一小节创建的表buy_log要进行如下查询:
select count(*) from buy_log;
InnoDB存储引擎并不会选择通过查询聚集索引来进行统计。由于 buy_log表上还有辅助索引,故优化器的选择如下:
mysql> explain select count(*) from buy_log;
+----+-------------+---------+------------+-------+---------------+--------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------+------------+-------+---------------+--------+---------+------+------+----------+-------------+
| 1 | SIMPLE | buy_log | NULL | index | NULL | userid | 4 | NULL | 7 | 100.00 | Using index |
+----+-------------+---------+------------+-------+---------------+--------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
可以看到, possible_keys列为NULL,但是实际执行时优化器却选择了userid索引,而列 Extra列的 Using index就是代表了优化器进行了覆盖索引操作。
3.联合索引中应用覆盖索引
在一个市民信息表上,是否有必要将身份证号和名字建立联合索引?
我们知道,身份证号是市民的唯一标识。也就是说,如果有根据身份证号查询市民信息的需求,我们只要在身份证号字段上建立索引就够了。而再建立一个(身份证号、姓名)的联合索引,是不是浪费空间?
如果现在有一个高频请求,要根据市民的身份证号查询他的姓名,这个联合索引就有意义了。
它可以在这个高频请求上用到覆盖索引,不再需要回表查整行记录,减少语句的执行时间。
当然,索引字段的维护总是有代价的。因此,在建立冗余索引来支持覆盖索引时就需要权衡考虑了。这正是业务 DBA,或者称为业务数据架构师的工作。
4.统计覆盖到联合索引中的字段
此外,在通常情况下,诸如(a,b)的联合索引,一般是不可以选择列b中所谓的查询条件。但是如果是统计操作,并且是覆盖索引的,则优化器会进行选择,如下述语句:
mysql> explain SELECT COUNT(*) FROM buy_log Where buy_date>='2011-01-01' and buy_date< '2011-02-01';
+----+-------------+---------+------------+-------+---------------+----------+---------+------+------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------+------------+-------+---------------+----------+---------+------+------+----------+--------------------------+
| 1 | SIMPLE | buy_log | NULL | index | NULL | userid_2 | 8 | NULL | 7 | 14.29 | Using where; Using index |
+----+-------------+---------+------------+-------+---------------+----------+---------+------+------+----------+--------------------------+
1 row in set, 1 warning (0.01 sec)
表 buy_log有(userid, buy_date)的联合索引,这里只根据列buy_date进行条件查询,一般情况下是不能进行该联合索引的,但是这句SQL查询是统计操作,并且可以利用到覆盖索引的信息,因此优化器会选择该联合索引。
四. MySQL Explain性能调优
explain这个命令可以来查看这些SQL语句的执行计划,查看该SQL语句有没有使用上了索引,有没有做全表扫描,这都可以通过explain命令来查看。
expain出来的信息有10列,分别是id、select_type、table、type、possible_keys、key、key_len、ref、rows、Extra,下面对这些字段出现的可能进行解释:
核心的主要是:
1.type:用的是ALL还是index
2.Key:key列显示MySQL实际决定使用的键(索引),如果没有选择索引,键是NULL。
3.rows:表示MySQL根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数
4.extra:查看sql查询的详细信息,可以判断是否用覆盖索引等
1.Explain列分析
1、 id
SQL执行的顺序的标识,SQL从大到小的执行
1)id相同时,执行顺序由上至下
2)如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行
3)id如果相同,可以认为是一组,从上往下顺序执行;在所有组中,id值越大,优先级越高,越先执行
2、select_type
示查询中每个select子句的类型
(1) SIMPLE(简单SELECT,不使用UNION或子查询等)
(2) PRIMARY(查询中若包含任何复杂的子部分,最外层的select被标记为PRIMARY)
(3) UNION(UNION中的第二个或后面的SELECT语句)
(4) DEPENDENT UNION(UNION中的第二个或后面的SELECT语句,取决于外面的查询)
(5) UNION RESULT(UNION的结果)
(6) SUBQUERY(子查询中的第一个SELECT)
(7) DEPENDENT SUBQUERY(子查询中的第一个SELECT,取决于外面的查询)
(8) DERIVED(派生表的SELECT, FROM子句的子查询)
(9) UNCACHEABLE SUBQUERY(一个子查询的结果不能被缓存,必须重新评估外链接的第一行)
3、table
显示这一行的数据是关于哪张表的,有时不是真实的表名字,看到的是derivedx(x是个数字,我的理解是第几步执行的结果)
mysql> explain select * from (select * from ( select * from t1 where id=2602) a) b;
+----+-------------+------------+--------+-------------------+---------+---------+------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+--------+-------------------+---------+---------+------+------+-------+
| 1 | PRIMARY | <derived2> | system | NULL | NULL | NULL | NULL | 1 | |
| 2 | DERIVED | <derived3> | system | NULL | NULL | NULL | NULL | 1 | |
| 3 | DERIVED | t1 | const | PRIMARY,idx_t1_id | PRIMARY | 4 | | 1 | |
+----+-------------+------------+--------+-------------------+---------+---------+------+------+-------+
4、type
表示MySQL在表中找到所需行的方式,又称"访问类型"。
常用的类型有: ALL, index, range, ref, eq_ref, const, system, NULL(从左到右,性能从差到好)
ALL
Full Table Scan, MySQL将遍历全表以找到匹配的行
index
Full Index Scan,index与ALL区别为index类型只遍历索引树,这通常比ALL快,因为索引文件通常比数据文件小。
range
只检索给定范围的行,使用一个索引来选择行。
当使用=、<>、>、>=、<、<=、IS NULL、<=>、BETWEEN或者IN操作符,用常量比较关键字列时,可以使用range:
ref: 表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值
eq_ref: 类似ref,区别就在使用的索引是唯一索引,对于每个索引键值,表中只有一条记录匹配,简单来说,就是多表连接中使用primary key或者 unique key作为关联条件
const:表示索引一次就能找到结果(索引可以是主键或惟一索引)。因为只有一行,这个值实际就是常数,因为MYSQL先读这个值然后把它当做常数来对待
system: 表只有一行:system表。这是const连接类型的特殊情况
NULL: MySQL在优化过程中分解语句,执行时甚至不用访问表或索引,例如从一个索引列里选取最小值可以通过单独索引查找完成。
5、possible_keys
指出MySQL能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用
6、Key
key列显示MySQL实际决定使用的键(索引),如果没有选择索引,键是NULL。
要想强制MySQL使用或忽视possible_keys列中的索引,在查询中使用FORCE INDEX、USE INDEX或者IGNORE INDEX。
7、key_len
表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度(key_len显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len是根据表定义计算而得,不是通过表内检索出的)
不损失精确性的情况下,长度越短越好
8、ref
表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值
9、rows
表示MySQL根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数(可以快速判断是否有命中索引)
10、Extra
该列包含MySQL解决查询的详细信息,有以下几种情况:
Distinct
MySQL发现第1个匹配行后,停止为当前的行组合搜索更多的行
Using index
官方定义:从只使用索引树中的信息而不需要进一步搜索读取实际的行来检索表中的列信息。当查询只使用作为单一索引一部分的列时,可以使用该策略。
简单理解:通过二级普通索引查找,实现了覆盖索引,不用进行回表查询
using index condition
using index condition通过二级普通索引查找,在通过索引查到的结果后还有where条件过滤,而且这个过滤筛选是只需要用二级普通索引就可以实现,不用在内存中进行判断筛选。但是需要回表查询需要的字段值。
Using where:
官方定义:列数据是从仅仅使用了索引中的信息而没有读取实际的行动的表返回的,这发生在对表的全部的请求列都是同一个索引的部分的时候,表示mysql服务器将在存储引擎检索行后再进行过滤
简单理解:不管有没有通过索引查找,只要加载了数据到内存进行where条件筛选,都是
using index & using where
查找使用了索引,但是需要的数据都在索引列中能找到,所以不需要回表查询数据
Using temporary:表示MySQL需要使用临时表来存储结果集,常见于排序和分组查询
Using filesort:MySQL中无法利用索引完成的排序操作称为"文件排序"
Using join buffer:改值强调了在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果。如果出现了这个值,那应该注意,根据查询的具体情况可能需要添加索引来改进能。
Impossible where:这个值强调了where语句会导致没有符合条件的行。
Select tables optimized away:这个值意味着仅通过使用索引,优化器可能仅从聚合函数结果中返回一行
思考
1.Extra中using index,using index condition,using where,using index & using where的关系
举一个实际例子。假如有一个表xxx,给modify_date上索引
1.using index通过二级普通索引查找,实现了覆盖索引,不用进行回表查询
explain
SELECT modify_date
from xxx
2.using index condition通过二级普通索引查找,需要回表
explain
SELECT *
from xxx where modify_date > "2022-06-27 10:27:07" and modify_date < "2022-06-28 0:0:0"
3.using where不走索引查找
explain
SELECT *
from xxx
where create_date > "2022-06-27 10:27:07" and create_date < "2022-06-28 0:0:0"
4.using index & using where:查找使用了索引,但是需要的数据都在索引列中能找到,所以不需要回表查询数据
explain
SELECT modify_date
from xxx where modify_date > "2022-06-27 10:27:07" and modify_date < "2022-06-28 0:0:0"
返回结果
五、索引应用实战
1.sql调优和索引失效
1.mysql深分页问题
mysql分页原理:MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回 N行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL 改写。
1.参数语句调整
select id,name from product limit 866613, 20
使用上述sql语句做分页的时候,可能有人会发现,随着表数据量的增加,直接使用limit分页查询会越来越慢。
优化的方法如下:可以取前一页的最大行数的id,然后根据这个最大的id来限制下一页的起点。比如此列中,上一页最大的id是866612。sql可以采用如下的写法:
select id,name from product where id> 866612 limit 20
2.参数语句调整
采用延迟关联的方式进行处理,减少 SQL 回表,但是要记得索引需要完全覆盖才有效果
select * from _t where a = 1 and b = 2 order by c desc limit 10000, 10;
SQL改动如下:
select t1.* from _t t1, (select id from _t where a = 1 and b = 2 order by c desc limit 10000, 10) t2 where t1.id = t2.id;
2.隐式转换
where 子句中出现 column 字段的类型和传入的参数类型不一致的时候发生的类型转换,建议先确定where中的参数类型或者在索引的列上使用表达式或者函数会使索引失效。
例如:
1.select * from users where YEAR(adddate)<2007,将在每个行上进行运算,这将导致索引失效而进行全表扫描,因此我们可以改成:select * from users where adddate<'2007-01-01′。
2.隐式转换相当于在索引上做运算,会让索引失效。mobile 是字符类型,使用了数字,应该使用字符串匹配,否则 MySQL 会用到隐式替换,导致索引失效。
KEY
idx_mobile
(mobile
)select * from _user where mobile=12345678901
3.组合索引范围查询阻断
注意范围查询,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a =1 and b=2 c=> 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
4.不等于、不包含条件
在索引上,避免使用 NOT、!=、<>、!<、!>、NOT EXISTS、NOT IN、NOT LIKE等。
1.在查询条件中使用<>会导致索引失效。
2.在查询条件中使用IS NULL会导致索引失效,所以在建表时,默认值最好不要为null。因为索引是不索引空值得。
5.or查询
or 语句前后没有同时使用索引字段则索引失效。
当 or 语句查询条件的前后列均为索引时, 索引才会生效。
所以如果限制条件中其他字段没有索引,尽量少用or。
很多时候使用 union all 或者是union(必要的时候)的方式来代替"or"会得到更好的效果
6.少用大于或者小于匹配
比如使用小于匹配,会查找小于某一个数的所有索引数据,当返回的查询结果的数量超过1/5,那么走全表扫描,索引失效。业务上尽量用区间匹配,减少遍历索引的数量。
6.关联查询时,尽量使用小表驱动大表
小表有10000条数据,大表有100000条数据
假设关联的表的执行计划返回的rows为1,小表的关联大表则是100001,大表关联小表则会是1000001
其他
1.LIKE操作中,使用通配符时要注意。
'%aaa%'不会使用索引,也就是索引会失效,但是'aaa%'可以使用到索引。
同时在查询条件中使用正则表达式时,只有在搜索模板的第一个字符不是通配符的情况下才能使用索引。
6.尽量不要包括多列排序,如果一定要,最好为这多列构建组合索引;
7.索引字段不能太长,如果索引的值很长,那么查询的速度会受到影响。
例如,对一个CHAR(100)类型的字段进行全文检索需要的时间肯定要比对CHAR(10)类型的字段需要的时间要多。
长度为 20 的索引,区分度会高达 90%以上可以使用count(distinct left(列名, 索引长度))/count(*)区分度来确定
1.SQL语句中IN包含的值不应过多
MySQL对于IN做了相应的优化,即将IN中的常量全部存储在一个数组里面,而且这个数组是排好序的。但是如果数值较多,产生的消耗也是比较大的。再例如:select id from t where num in(1,2,3) 对于连续的数值,能用 between 就不要用 in 了;再或者使用连接来替换
摘自阿里巴巴开发手册:in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控制在 1000 个之内。
2.SELECT语句务必指明字段名称
SELECT *增加很多不必要的消耗(cpu、io、内存、网络带宽),主要是解析得消耗;所以要求直接在select后面接上字段名。
3.尽量用union all代替union
union和union all的差异主要是前者需要将结果集合并后再进行唯一性过滤操作,这就会涉及到排序,增加大量的CPU运算,加大资源消耗及延迟。当然,union all的前提条件是两个结果集没有重复数据。
参考
1.面试官:如何优化慢SQL?
2.唯一索引和普通索引选型
1.读机制差异
假设,执行查询的语句是 select id from T where k=5。这个查询语句在索引树上查找的过程,先是通过 B+ 树从树根开始,按层搜索到叶子节点,也就是图中右下角的这个数据页,然后可以认为数据页内部通过二分法来定位记录。
对于普通索引来说,查找到满足条件的第一个记录 (5,500) 后,需要查找下一个记录,直到碰到第一个不满足 k=5 条件的记录。
对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。
那么,这个不同带来的性能差距会有多少呢?答案是,微乎其微。
因为引擎是按页读写的,所以说,当找到 k=5 的记录的时候,它所在的数据页就都在内存里了。那么,对于普通索引来说,要多做的那一次"查找和判断下一条记录"的操作,就只需要一次指针寻找和一次计算。
当然,如果 k=5 这个记录刚好是这个数据页的最后一个记录,那么要取下一个记录,必须读取下一个数据页,这个操作会稍微复杂一些。
但是,我们之前计算过,对于整型字段,一个数据页可以放近千个 key,因此出现这种情况的概率会很低。所以,我们计算平均性能差异时,仍可以认为这个操作成本对于现在的 CPU 来说可以忽略不计。
2.写机制差异
那么我们再一起来看看如果要在这张表中插入一个新记录 (4,400) 的话,InnoDB 的处理流程是怎样的。
第一种情况是,这个记录要更新的目标页在内存中。这时,InnoDB 的处理流程如下:
对于唯一索引来说,找到 3 和 5 之间的位置,判断到没有冲突,插入这个值,语句执行结束;
对于普通索引来说,找到 3 和 5 之间的位置,插入这个值,语句执行结束。
这样看来,普通索引和唯一索引对更新语句性能影响的差别,只是一个判断,只会耗费微小的 CPU 时间。
但,这不是我们关注的重点。
第二种情况是,这个记录要更新的目标页不在内存中。这时,InnoDB 的处理流程如下:
对于唯一索引来说,需要将数据页读入内存,判断到没有冲突,插入这个值,语句执行结束;
对于普通索引来说,则是将更新记录在 change buffer,语句执行就结束了。
将数据从磁盘读入内存涉及随机 IO 的访问,是数据库里面成本最高的操作之一。
change buffer 因为减少了随机磁盘访问,所以对更新性能的提升是会很明显的。
之前我就碰到过一件事儿,有个 DBA 的同学跟我反馈说,他负责的某个业务的库内存命中率突然从 99% 降低到了 75%,整个系统处于阻塞状态,更新语句全部堵住。而探究其原因后,我发现这个业务有大量插入数据的操作,而他在前一天把其中的某个普通索引改成了唯一索引。
3.综合角度
从性能角度,普通索引要比唯一索引写入性能高
从业务角度,如果业务确定字段是唯一的,那么最好用唯一索引,他能保证数据的最终唯一性
3.优化器索引选型
优化器选择索引的目的,是找到一个最优的执行方案,并用最小的代价去执行语句。
在数据库里面,扫描行数是影响执行代价的因素之一。扫描的行数越少,意味着访问磁盘数据的次数越少,消耗的 CPU 资源越少。
当然,扫描行数并不是唯一的判断标准,优化器还会结合是否使用临时表、是否排序等因素进行综合判断。
而至于优化器为什么会选错索引,更多的是底层判断的局限性而产生的内部bug,所以对于非DBA人员就不用去钻牛角尖了。。。
1.优化器选错索引之-范围查询回表
在某些情况下,当执行 EXPLAIN命令进行SQL语句的分析时,会发现优化器并没有选择索引去查找数据,而是通过扫描聚集索引,也就是直接进行全表的扫描来得到数据。这种情况多发生于范围查找、JOIN链接操作等情况下。
例如:
SELECT * FROM orderdetails Where orderid>10000 and orderid<102000;
在于用户要选取的数据是整行信息,而 OrderID索引不能覆盖到我们要査询的信息,因此在对 OrderID索引查询到指定数据后,还需要一次书签访问来查找整行数据的信息。
虽然 OrderID索引中数据是顺序存放的,但是再一次进行书签查找的数据则是无序的,因此变为了磁盘上的离散读操作。
如果要求访问的数据量很小,则优化器还是会选择辅助索引,但是当访问的数据占整个表中数据的蛮大一部分时(一般是20%左右),优化器会选择通过聚集索引来查找数据。
解决办法
1.强制索引
若用户使用的磁盘是固态硬盘,随机读操作非常快,同时有足够的自信来确认使用辅助索引可以带来更好的性能,那么可以使用关键字 FORCE INDEX来强制使用某个索引,如:
SELEC t FROM orderdetails FORCE INDEX(OrderID) Where orderid>10000 and orderid<102000;
2.analyze table t 命令
既然是统计信息不对,那就修正。analyze table t 命令,可以用来重新统计索引信息。
2.优化器选错索引之-排序
依然是基于这个表 t,我们看看另外一个语句:
mysql> select * from t where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 1;
从条件上看,这个查询没有符合条件的记录,因此会返回空集合。
在开始执行这条语句之前,你可以先设想一下,如果你来选择索引,会选择哪一个呢?
为了便于分析,我们先来看一下 a、b 这两个索引的结构图。
图 7 a、b 索引的结构图
如果使用索引 a 进行查询,那么就是扫描索引 a 的前 1000 个值,然后取到对应的 id,再到主键索引上去查出每一行,然后根据字段 b 来过滤。显然这样需要扫描 1000 行。
如果使用索引 b 进行查询,那么就是扫描索引 b 的最后 50001 个值,与上面的执行过程相同,也是需要回到主键索引上取值再判断,所以需要扫描 50001 行。
所以你一定会想,如果使用索引 a 的话,执行速度明显会快很多。那么,下面我们就来看看到底是不是这么一回事儿。
mysql> explain select * from t where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 1;
图 8 是执行 explain 的结果。
可以看到,返回结果中 key 字段显示,这次优化器选择了索引 b,而 rows 字段显示需要扫描的行数是 50198。
解决办法
1.强制索引
像我们第一个例子一样,采用 force index 强行选择一个索引
既然优化器放弃了使用索引 a,说明 a 还不够合适,所以第二种方法就是,我们可以考虑修改语句,引导 MySQL 使用我们期望的索引。比如,在这个例子里,显然把"order by b limit 1" 改成 "order by b,a limit 1" ,语义的逻辑是相同的。
我们来看看改之后的效果:
图 10 order by b,a limit 1 执行结果
之前优化器选择使用索引 b,是因为它认为使用索引 b 可以避免排序(b 本身是索引,已经是有序的了,如果选择索引 b 的话,不需要再做排序,只需要遍历),所以即使扫描行数多,也判定为代价更小。
现在 order by b,a 这种写法,要求按照 b,a 排序,就意味着使用这两个索引都需要排序。因此,扫描行数成了影响决策的主要条件,于是此时优化器选了只需要扫描 1000 行的索引 a。
当然,这种修改并不是通用的优化手段,只是刚好在这个语句里面有 limit 1,因此如果有满足条件的记录, order by b limit 1 和 order by b,a limit 1 都会返回 b 是最小的那一行,逻辑上一致,才可以这么做。
如果你觉得修改语义这件事儿不太好,这里还有一种改法,图 11 是执行效果。
mysql> select * from (select * from t where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 100)alias limit 1;
图 11 改写 SQL 的 explain
在这个例子里,我们用 limit 100 让优化器意识到,使用 b 索引代价是很高的。其实是我们根据数据特征诱导了一下优化器,也不具备通用性。
3.优化器选错索引解决方案
1.强制索引
优化器没有选择正确的索引,force index 起到了"矫正"的作用。
不过很多程序员不喜欢使用 force index,一来这么写不优美,二来如果索引改了名字,这个语句也得改,显得很麻烦。而且如果以后迁移到别的数据库的话,这个语法还可能会不兼容。
但其实使用 force index 最主要的问题还是变更的及时性。因为选错索引的情况还是比较少出现的,所以开发的时候通常不会先写上 force index。而是等到线上出现问题的时候,你才会再去修改 SQL 语句、加上 force index。但是修改之后还要测试和发布,对于生产系统来说,这个过程不够敏捷。
所以,数据库的问题最好还是在数据库内部来解决。那么,在数据库里面该怎样解决呢?
2.删掉误用的索引
在有些场景下,我们可以新建一个更合适的索引,来提供给优化器做选择,或删掉误用的索引。
实际上我碰到过两次这样的例子,最终是 DBA 跟业务开发沟通后,发现这个优化器错误选择的索引其实根本没有必要存在,于是就删掉了这个索引,优化器也就重新选择到了正确的索引。
3.修改语句引导优化器
如使用临时表和limit等语句等
4.前缀索引
MySQL 是支持前缀索引的,也就是说,你可以定义字符串的一部分作为索引。
默认地,如果你创建索引的语句不指定前缀长度,那么索引就会包含整个字符串。
1.总结
- 尽量少使用前缀索引,前缀索引只能带来节省磁盘空间的好处,但引入了其他的性能消耗如增加查询扫描次数,并且不能使用覆盖索引
- 如果对于区分度不高的前缀索引索引,可以使用倒序存储和hash 字段索引,但都无法支持范围查询,具体方式和原理可参考++极客时间《MySQL实战45讲》++
比如,这两个在 email 字段上创建索引的语句:
|---|--------------------------------------------------------|
| | mysql> alter table SUser add index index1(email);
|
| | 或
|
| | mysql> alter table SUser add index index2(email(6));
|
第一个语句创建的 index1 索引里面,包含了每个记录的整个字符串;而第二个语句创建的 index2 索引里面,对于每个记录都是只取前 6 个字节。
那么,这两种不同的定义在数据结构和存储上有什么区别呢?如图 2 和 3 所示,就是这两个索引的示意图。
图 1 email 索引结构
图 2 email(6) 索引结构
从图中你可以看到,由于 email(6) 这个索引结构中每个邮箱字段都只取前 6 个字节(即:zhangs),所以占用的空间会更小,这就是使用前缀索引的优势。
但,这同时带来的损失是,可能会增加额外的记录扫描次数。
接下来,我们再看看下面这个语句,在这两个索引定义下分别是怎么执行的。
|---|---------------------------------------------------------------------|
| | select id,name,email from SUser where email='zhangssxyz@xxx.com';
|
如果使用的是 index1(即 email 整个字符串的索引结构),执行顺序是这样的:
-
从 index1 索引树找到满足索引值是'zhangssxyz@xxx.com'的这条记录,取得 ID2 的值;
-
到主键上查到主键值是 ID2 的行,判断 email 的值是正确的,将这行记录加入结果集;
-
取 index1 索引树上刚刚查到的位置的下一条记录,发现已经不满足 email='zhangssxyz@xxx.com'的条件了,循环结束。
这个过程中,只需要回主键索引取一次数据,所以系统认为只扫描了一行。
如果使用的是 index2(即 email(6) 索引结构),执行顺序是这样的:
-
从 index2 索引树找到满足索引值是'zhangs'的记录,找到的第一个是 ID1;
-
到主键上查到主键值是 ID1 的行,判断出 email 的值不是'zhangssxyz@xxx.com',这行记录丢弃;
-
取 index2 上刚刚查到的位置的下一条记录,发现仍然是'zhangs',取出 ID2,再到 ID 索引上取整行然后判断,这次值对了,将这行记录加入结果集;
-
重复上一步,直到在 idxe2 上取到的值不是'zhangs'时,循环结束。
在这个过程中,要回主键索引取 4 次数据,也就是扫描了 4 行。
通过这个对比,你很容易就可以发现,使用前缀索引后,可能会导致查询语句读数据的次数变多。
但是,对于这个查询语句来说,如果你定义的 index2 不是 email(6) 而是 email(7),也就是说取 email 字段的前 7 个字节来构建索引的话,即满足前缀'zhangss'的记录只有一个,也能够直接查到 ID2,只扫描一行就结束了。
也就是说使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。
2.前缀索引引起覆盖索引失效
前面我们说了使用前缀索引可能会增加扫描行数,这会影响到性能。其实,前缀索引的影响不止如此,我们再看一下另外一个场景。
你先来看看这个 SQL 语句:
|---|----------------------------------------------------------------|
| | select id,email from SUser where email='zhangssxyz@xxx.com';
|
与前面例子中的 SQL 语句
|---|---------------------------------------------------------------------|
| | select id,name,email from SUser where email='zhangssxyz@xxx.com';
|
相比,这个语句只要求返回 id 和 email 字段。
所以,如果使用 index1(即 email 整个字符串的索引结构)的话,可以利用覆盖索引,从 index1 查到结果后直接就返回了,不需要回到 ID 索引再去查一次。而如果使用 index2(即 email(6) 索引结构)的话,就不得不回到 ID 索引再去判断 email 字段的值。
即使你将 index2 的定义修改为 email(18) 的前缀索引,这时候虽然 index2 已经包含了所有的信息,但 InnoDB 还是要回到 id 索引再查一下,因为系统并不确定前缀索引的定义是否截断了完整信息。
也就是说,使用前缀索引就用不上覆盖索引对查询性能的优化了,这也是你在选择是否使用前缀索引时需要考虑的一个因素。
参考资料
1.mysql 官网 https://www.mysqlzh.com/
2.漫画算法:什么是 B 树?http://blog.jobbole.com/111757/
3.深入理解MySQL索引原理和实现------为什么索引可以加速查询?https://blog.csdn.net/tongdanping/article/details/79878302
4.Mysql联合索引最左匹配原则 https://segmentfault.com/a/1190000015416513
5.聚集索引与非聚集索引的总结 https://www.cnblogs.com/s-b-b/p/8334593.html
6.极客时间《MySQL实战45讲》