先和大家说一下,这个系列讨论的重点会围绕InnoDB下的B-Tree索引展开,如果有想了解全文索引、空间索引甚至其他类型索引的同学们可以自行去学习哦。
好!我们接着巴拉。
先简单说下B-Tree索引。
一般情况下,如果没有特别指明,那么大家口中的索引就是指B-Tree索引,它使用B-Tree数据结构来存储数据(InnoDB实际上是B+树)。大多数Mysql引擎都支持这种索引。存储引擎以不同的方式使用B-Tree,性能也各有不同,各有优劣吧可以说。例如,MyISAM使用前缀压缩技术(后续会展开讨论)使得索引更小,但InnoDB则按照原数据格式进行存储。还有就是MyISAM索引通过数据的物理位置引用被索引的行,而InnoDB则根据主键引用被索引的行。
ok,接下来,高性能的索引策略!come on!
正确的创建索引和使用索引是实现高兴查询的的基础。我们来看看如何才能发挥出索引的优势。
一、独立的列
我们通常会看到一些查询不当地使用索引,或者使用Mysql无法使用已有的索引,如果查询中的列不是独立的,则Mysql就不会使用索引。“独立的列”是指索引列不能是表达式的一部分,也不能是函数的参数。
来看个示例:
很容易看出where条件中的表达式其实等价于age = 14,但是mysql无法自动解析这个方程式。这完全就是用户行为。我们应该养成简化where条件的习惯,始终将索引列单独放在比较符号的一侧。
二、前缀索引和索引的选择性
当遇到需要索引很长的字符列,这会让索引列变得大且慢。模拟hash索引是一种解决方式,但还有其他范式。
通常可以索引开始的部分索引,这样大大节约索引空间,从而提高索引效率,但是这样降低了索引的选择性(等同于我们通常说道的区分度,指不重复的索引值/表记录总数(#T)的比值,范围从1/#T到1之间),索引的选择性越高则查询效率越高因为选择性高的索引可以让mysql在查找时过滤掉更多的行。唯一索引的选择性是1,这是最好的索引选择性,性能最赞。
一般情况下某个前缀列的选择性也是足够高的,足以满足查询性能。对于BLOG、TEXT或者很长的VARCHAR类型的列,必须使用前缀索引,因为mysql也不会允许索引这些列的完整长度。这个解决方案的诀窍还是要选择足够长的前缀以保证较高的选择性,同时又不能太长(以便节约空间)
计算合适的前缀长度的算法(来自:高性能mysql第三版):
首先数据有这么多:
最适合的前缀选择性:
计算出个长度前缀的选择性:
如上所示最适合的前缀长度是10,其实这些知识随机出来的数据,如果当数据分布很不均匀的时候,还得同学们根据数据实际进行观察分析。
前缀索引是一种额能使索引更小,更快的有效办法,但是另一方面也有缺点:mysql无法使用前缀索引做order by和group by操作,也无法使用前缀索引做覆盖操作,如果不理解没有关系,接着看。
奇巧淫技:
既然有前缀索引,那么会不会有后缀索引这玩意儿呢。大家想想如果我们在某种情况下需要找到某个域名结尾下的所有点子邮件地址,这时候后缀索引就很屌了。mysql原生并不支持后缀索引,但是可以吧字符串翻转后再进行存储,再建立前缀索引。如果能再加上hash索引来维护这个后缀的hash值,那查询速度就飞快了。
三、多列索引
不跟你多BB,先上代码:
使用两个单列索引查询的结果:
可以看到这个查询使用到了两个索引扫描的联合,mysql会使用这类技术优化复杂查询,索引合并策略有时候是一种优化的结果,但实际上更多时候说明表上的索引建的很糟糕,是要被批评的。
当出现服务器对多个索引做相交操作时(通常多个AND条件),通常意味着需要一个包含所有相关列的多列索引,而不是多个独立的单列索引
当服务器需要对多个索引做联合操作时(通常有多个OR条件),通常需要耗费大量cpu和内存资源在算法的缓存,排序和合并操作上,特别是当其中有些索引的选择性不高,需要合并扫描返回大量数据的时候
ps:可通过OPTIMIZER_SWITCH来关闭索引合并功能。也可以使用IGNORE INDEX提示优化器忽略掉某些索引
四、选择合适索引列顺序
在一个多列的B-Tree索引中,索引列的顺序意味着首先按照最左列进行排序,其次是第二列类推。所以索引可以按照升序或者降序进行扫描,以满足精确复合列顺序的order by、group by和distinct等子句的查询需求。
选择索引列顺序的经验法则:将选择性最高的列放到索引最前列。当然通常最重要的还是避免随机IO和排序
五、聚簇索引
其实说到聚簇索引,大家可能通常会说,就是主键索引,就是在B-Tree的根节点上存储了数据之类等等,笔者在面试的时候经常被问到什么是聚簇索引,我会很开心,因为这个问题随便糊弄糊弄就会过去(别说面试官是阿里P7,哈哈哈哈),其实他们也许也不能很准确的说出来。
聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。InnoDB的聚簇索引实际上在同一个结构中保存了B-Tree索引和数据行。它的数据行实际上存放在索引的叶子页(leaf page)中。术语“聚簇”标识数据行和相邻的键值紧凑地存储在一起。因为无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。不过覆盖索引可以模拟多个聚簇索引的情况(稍后我们就扒覆盖索引)。
InnoDB将通过主键聚集数据。如果没有定义主键,InnoDB会选择一个唯一的非空索引代替。如果没有这样的索引,InnoDB会隐定义一个主键来作为聚簇索引。InnoDB只聚集在同一个页面中的记录。包含相邻键值的页面可能会相距甚远。
优点:
可以把相关数据保存在一起
数据访问更快
使用覆盖索引扫描的查询可以直接 使用页节点的主键值。
缺点(没想到就连聚簇索引都有缺点吧):
聚簇数据最大限度的提高了IO密集型应用的性能,但如果数据全部都存放在内存中,则访问的顺序就不重要了,聚簇索引就没有啥优势了。
插入速度严重依赖于插入顺序。按照主键的顺序插入是加载数据到InnoDB表中最快的方式。如果不是按照主键顺序加载数据,那么在加载完成后最好使用OPTIMIZE TABLE命令重新组织一下表。
更新聚簇索引列的代价很高,因为会强制InnoDB将每个被更新的行移动到新的位置。
基于聚簇索引的表在插入新行,或者主键被更新导致需要移动的时候,可能面临“页分裂”的问题,当行的主键值要求必须将这一行插入到某个已满页面中时,存储引擎会将该页分裂成两个页面容纳该行。页分裂会导致表占用更多的磁盘空间。
聚簇索引可能导致全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致的数据存储不连续的时候。
二级索引(非聚簇索引)可能比想象的要更大,因为在二级索引的叶子节点包含了引用行的主键列。
二级索引访问需要两次查找,二级索引叶子节点保存的不是指向行的物理位置指针,而是主键值。这样就将查找变成了两 次。InnoDB中自适应哈希索引能减少这样的重复工作。乍一看是不是很lowb的设计,不过我们接着看这是为什么呢:这样的策略减少了当出现行移动或数据页分裂时二级索引的维护工作。使用主键值当做指针会让二级索引占用更多的空间,换来的好处是,InnoDB在移动行时无需更新二级索引中的“指针”。
聚簇和费局促表对比:
在InnoDB表中按主键顺序插入行,最简单的方法就是使用auto_increment自增列作为主键,这可以保证数据时按顺序写入,对于根据主键做关联操作的性能也会更好。
最好避免随机的(不连续且值的分布范围非常大)聚簇索引,特别是对于IO密集型的应用。例如,从性能角度考虑,使用UUID来作为聚簇索引则会很糟糕:它使得聚簇索引的插入变得完全随机,这是最坏的情况,使得数据没有任何聚集特性。
我们来看个示例:
两个示例表,userinfo使用自增列作为主键、userinfo_uuid使用uuid列作为主键
我们先插入一百万行数据,接着继续插入三百万行,对比图如下:
向uuid主键插入行不仅花费时间更长,而且索引占用的空间也更大。一方面由于主键字段更长,另一方面毫无疑问是由于页分裂和碎片导致的。如图示:
向聚簇索引插入顺序的索引值:
因为主键的值是顺序的,所以InnoDB把每一条记录都存储在上一条记录的后面。当达到也的最大填充因子时(InnoDB默认的最大填充因子是页大小的15/16,留出部分空间用于以后修改,可以规避一些主键被修改之后需要做页分裂操作)下一条记录就会写入新的页中。一旦数据按照这种顺序的方式加载,主键页就会近似于被顺序的记录填满,这也正式我们所期望的(二级索引页可能是不一样的)
向聚簇索引插入非顺序的索引值:
因为新行的主键值不一定比之前插入的大,所以InnoDB无法简单地总是把新行插入到索引的最后,而是需要为新的行寻找合适的位置——通常是已有数据的中间位置——并且分配空间,这增加了很多额外工作,并导致数据分布不够优化。
写入的目标页可能已经刷到磁盘上并从缓存中移除,或者是还没有被加载到内存中,InnoDB在插入之前不得不先找到并从磁盘读取目标页到内存中。这将导致大量的随机IO
因为写入是乱序的,InnoDB不得不频繁地做页分裂操作,以便为新的行分配空间。页分裂会导致移动大量数据。
由于频繁的页分裂,页会变的洗漱并被不规则的填充,最终数据会有碎片。
在把这些随机值载入到聚簇索引以后,也许需要做一次OPTIMIZE TABLE来重建表并优化页的填充。
总结:不用我总结了吧。。哈哈哈
But
顺序主键就是完美的吗?不一定:
对于高并发工作负载,在InnoDB中安主键顺序插入可能会造成明显的征用。主键上界会成为“热点”。因为所有的插入都发生在这里,所以并发插入可能导致间隙锁竞争。另一个热点可能是auto_increment锁机制。
先到这里吧,可耻的饿了嘻嘻