3.建立高性能的索引
索引是存储引擎用于快速找到记录的一种数据结构。
对良好的性能很是关键。
对查询性能优化最有效的手段。轻易将查询性能提升几个数量级。
3.1 索引基础
存储引擎使用索引,先在索引中找到对应的值,而后根据匹配的索引记录找到对应的数据行。
能够包含一个或多个列的值。若是包含多个列,那么列的顺序也很重要,由于MySQL只能高效地使用索引的最左前缀列。
3.1.1 索引的类型
索引是在存储引擎层而不是服务器层实现的,因此没有统一的索引标准:不一样存储引擎的索引的工做方式不同,也不是蓑鲉的存储引擎都支持全部类型的索引。即便多个存储引擎支持同一种类型的索引,其底层的实现也可能不一样。mysql
B-Tree索引:算法
实际上不少存储引擎使用的是B+Tree,即每个叶子节点都包含指向下一个叶子节点的指针,从而方便叶子节点的范围遍历。sql
对索引列是顺序组织存储的,很适合查找范围数据。数据库
索引对多个值进行排序的依据是CREATE TABLE语句中定义索引时列的顺序。缓存
MyISAM使用前缀压缩尽速使得索引更小,并经过数据的物理位置引用被索引的列;InnoDB则按照原数据格式进行存储,并根据主键引用被索引的行。性能优化
优化性能的时候,可能须要使用相同的列但顺序不一样的索引来知足不一样类型的查询需求。服务器
B+Tree结构示例:数据结构
B+Tree结构
意味着全部的值都是按顺序存储的,而且每一个叶子页到跟的距离相同。其中叶子节点的指针指向的是被索引的数据,而不是其它的节点页。以下为一个节点和其对应的叶子节点示例图,其实在根结点和叶子结点之间可能有不少层节点页,树的深度和表的大小直接相关。并发
从索引的根结点(并未画出)开始搜索,于是存储引擎再也不须要进行全表扫描,加快访问数据的速度。
根结点的槽中存放了指向子节点的指针,存储引擎根据这些指针向下查找
经过比较节点页的值和要查找的值能够找到合适的指针进入下层子节点,这些指针实际上定义了子节点页中值的上限和下限。
最终找到对应的值,要么记录不存在。
创建在B-Tree结构(从技术上来讲是B+Tree)上的索引
B-Tree索引的查询类型(适用于全键值、键值范围或键前缀查找,其中键前缀查找只适用于根据最左前缀的查找):函数
全值匹配:对索引中全部的列进行匹配
匹配最左前缀:匹配最左索引的列
匹配列前缀:匹配某一列的值的开头部分,需包含最左列
匹配范围值:匹配某一列值的某一特定范围,需包含最左列
精确匹配某一列并范围匹配另一列,需包含最左列
只访问索引的查询:查询只须要访问索引,而无须访问数据行。
除了按值查找外,还能够用于查询中的ORDER BY进行排序操做(按顺序查找)。若是ORDER BY知足以前的查询类型,也能够知足对应的排序需求。
限制:
若是不是按照索引的最左列开始查找,则没法使用索引。
不能跳过索引的列
若是查询中有某个列的范围查询,如LIKE,则其右边全部列都没法使用索引优化查找。
哈希索引:
基于哈希表实现,只有精确匹配索引全部列的查询才有效。对于每一行数据,存储引擎都会对全部的索引列计算一个哈希码(hash code),哈希码是一个较小的值,而且不一样键值的行计算出来的哈希码也不同。哈希索引将全部的哈希码存储在索引中,同时在哈希表中保存指向每一个数据行的指针。
MySQL中,只有Memory引擎显示支持哈希索引,也是该引擎表的默认索引类型,也支持B-Tree索引,另外还支持非惟一哈希索引(若是多个列的哈希值相同,索引会以链表的方式存放多个记录指针到同一个哈希条目中)。
索引自身只需存储对应的哈希值,因此索引的结构十分紧凑,也让哈希索引查找的速度很是快。
限制:
只包含哈希值和行指针,而不存储字段值,因此不能使用索引中的值来避免读取行。但因为在内存中,对性能的影响并不明显。
并非按照索引值顺序存储的,因此没法用于排序。
不支持部分索引列匹配查找,由于哈希索引始终是使用索引列的所有内容来计算哈希值。
只支持等值比较查询,包括=、IN()、<=>。也不支持任何范围查询。
访问哈希索引的数据很是快,除非有不少哈希冲突(不一样索引列值却有相同的hash值)。当出现哈希冲突时,存储引擎必须遍历全部的行指针,逐行比较,直到找到对应的行。
若是哈希冲突不少的话,一些索引维护操做的代价也很是高。例如删除一行时,需遍历对应哈希值的每一行。
应用场景:
数据仓库应用中有一种经典的“星型”schema,须要管理不少查找表
InnoDB的自适应哈希索引(adaptive hash index):
当InnoDB注意到某些值被使用的很是频繁时,会在内存中基于B-Tree索引之上在建立一个hash索引,从而拥有hash索引的优势,如快速的hash查找。这是一个彻底自动的,内部的行为,用户没法控制或者配置,但能够关闭。
建立自定义哈希索引:
若是存储引擎不支持hash索引,能够模仿像InnoDB同样建立hash索引。
思路:在B-Tree基础上建立一个伪哈希索引,即将要索引的列删除索引,对其建立一个被索引哈希列,里面存放原索引列每一行数据的哈希值。
缺陷:须要维护哈希值,能够手动维护,也可使用触发器实现。
使用时不要使用SHA1()和MD5()做为哈希函数,由于这两个函数计算出来的哈希值很是长,浪费大量空间并且比较时也会更慢。可使用CRS32()。
若是数据表很是大,CRS32()会出现大量的数据冲突,能够自行实现一个简单的64位哈希函数,这个函数要返回整数,而不是字符串。也可使用MD5()函数返回值的一部分来做为哈希函数。
SELECT CONV(RIGHT(MD5("str"), 16), 16, 10) AS HASH64
使用哈希索引进行查询时,必须在WHERE子句包含对应列值,由于可能会有哈希冲突从而选出多个不一样的数据。
空间数据索引(R-Tree):
与B-Tree不一样,这类索引无需前缀查询,会从全部维度来索引数据。
查询时可使用任意维度来组合查询。
必须使用MySQL的GIS相关函数如MBRCONTAINS()等来维护数据。(MySQL的GIS支持不完善,开源关系数据库中较好的解决方案是PostgreSQL的PostGIS)
MyISAM表支持空间索引,能够用做地理数据存储
全文索引:
查找的是文中关键词,而不是直接比较索引中的值。
与其余索引彻底不同,需注意如停用词、词干和复数、布尔搜索等细节。
更相似于搜索引擎作的事,而不是简单的WHERE条件匹配。
在相同列上同时建立全文索引和基于值的B-Tree索引不会有冲突,全文索引适用于MATCH AGAINST操做。
3.2 索引的优势
优势:
大大减小服务器须要扫描的数据量
帮助服务器避免排序和临时表(B-Tree会将相关的列值存储在一块儿,便于ORDER BY 和GROUP BY进行排序)
能够将随机IO变为顺序IO
索引适合某个查询的“三星系统”:
将相关记录放到一块儿则得到一星
索引中的数据顺序和查找中的排列顺序一致得到二星。
索引中的列包含了查询中须要的所有列得到三星。
索引并非最好的解决方案:
很是小的表,大部分状况下全表扫描更高效。
中到大型的表,索引很是有效。
特大型的表,创建和使用索引的代价随之增加,须要区分出查询须要的一组数据,如分区技术。
表的数量特别多,能够创建一个元数据信息表,用来查询须要用到的某些特性。例如执行那些须要聚合多个应用分布在多个表的数据的查询,则须要记录“哪一个用户的信息存储在哪一个表中”的元数据,这样在查询时就能够直接忽略掉那些不包含指定用户信息的表。对大型系统是一个经常使用的技巧
对于TB级别的数据,定位单条记录的意义不大,因此常用块级元数据技术来代替索引
3.3 高性能的索引策略
3.3.1 独立的列
3.3.2 前缀索引和索引选择性
索引很长的字符列,会让索引变得大且慢。一个策略是模拟的哈希索引,另外一个策略是前缀索引。
索引选择性:
指不重复的索引值(也成基数,cardinality)和数据表的记录总数(#T)的比值
选择性越高则查询效率越高,惟一索引的选择性是1,性能是最好的。
前缀索引:
索引开始的部分字符串,便可节约索引空间,从而提升索引效率,但会下降索引的选择性。
通常状况下某个列前缀的选择性也是足够高的,足以知足查询性能。
针对BLOB,TEXT或很长的VARCHAR类型的列,必须使用前缀索引,由于MySQL不容许索引这些列的完整长度。
选择前缀长度的诀窍:保持较高的选择性(接近于索引完整列),同时又不能太长。换句话说,前缀的基数应接近于完整列的基数。
方法:
先计算出完整列的选择性:SELECT COUNT(DISTINCT col)/COUNT(*) FROM mytable
, 再与平均选择性和最差选择性比较。
平均选择性:在一个查询中针对不一样前缀长度进行计算
SELECT COUNT(DISTINCT LEFT(col, 3))/COUNT(*) AS sel3, COUNT(DISTINCT LEFT(col, 4))/COUNT(*) AS sel4, ... FROM mytable;复制代码
最差选择性:针对平均选择性选出的多个前缀长度,考虑其数据分布很不均匀下的选择性。
缺点:
MySQL没法使用前缀索引作ORDER BY和GROUP BY
没法作覆盖扫描
应用场景:
针对很长的十六进制惟一ID,如保存网站的会话(SESSION),可采用长度为8的前缀索引,并且对上层应用彻底透明。
有时使用后缀索引也有用途,如找到某个域名的全部电子邮件地址。但MySQL原生不支持反向索引,能够经过触发器把字符串反转后存储,并基于此创建前缀索引。
3.3.3 多列索引
在多个列上创建独立的单列索引大部分状况下并不能提升MySQL的查性能。
MySQL5.0及之后版本引入“索引合并(index merge)"的策略,必定程度上可使用表上的多个单列索引来定位指定的行。对示例查询同时使用两个单列索引进行扫描,并将结果进行合并,能够经过EXPLAIN的Extra看到过程。这算法有三个变种:
OR条件的联合(union)
AND条件的相交(intersection)
组合前两种状况的联合及相交
更早版本的MySQL只能使用其中的某一个单列索引,而这种状况下没有哪个独立的单列索引是很是有效的。对示例查询使用全表扫描,除非改为UNION的方式。
-- 两个单列索引的查询 mysql> SELECT film_id, actor_id FROM sakila.film_actor -> WHERE actor_id = 1 OR film_id = 1;复制代码
索引合并策略有时候是一种优化的结果,但实际上更多时候说明了表上的索引建得很糟糕:
当出现服务器对多个索引作相交操做时(一般有多个AND条件),一般意味着须要一个包含全部相关列的多列索引,而不是多个独立的单列索引
当服务器对多个索引作联合操做时(一般有多个OR条件),一般须要耗费大量的CPU和内存在算法的缓存、排序和合并操做上。特别是当其中有些索引的选择性不高,须要合并扫描返回的大量数据的时候。
优化器不会把这些计算到“查询成本(cost)”中,而优化器只关心随机页面读取。这会使得查询的成本被低估,致使执行该计划还不如直接走全表扫描。这样作不但会消耗更多的CPU和内存资源,还可能会影响查询的并发性,但若是是单独运行这样的查询每每会忽略对并发性的影响。一般来讲,将查询改成UNION的方式每每更好。
若是在EXPLAIN中看到有索引合并,应检查下查询和表的结构以达到最优。也能够经过参数optimizer_switch来关闭索引合并功能,或使用INGORE INDEX提示让优化器忽略掉某些索引。
3.3.4 选择合适的索引列顺序(B-Tree场景)
正确的顺序依赖于使用该索引的查询,而且同时须要考虑如何更好地知足排序和分组的须要。
选择索引列顺序的经验法则:
当不须要考虑排序和分组时,将选择性最高的列放到索引最前列
性能不仅是依赖于全部索引列的选择性(总体基数),也和查询条件的具体值有关,也就是和值的分布有关。
若是某些索引值的选择性很是小,即匹配的范围很是大,说明该索引基本没什么用。该特殊状况可能会摧毁整个应用的性能。
3.3.5 聚簇索引(主要关注InnoDB)
不是一种单独的索引类型,而是一种数据存储的方式。具体的细节依赖于其实现方式,但InnoDB的聚簇索引实际上在同一个结构中保存了B-Tree索引和数据行。
当表有聚簇索引时,它的数据行实际上存放在索引中的叶子页(leaf page)中,但节点也只包含了索引列。术语的“聚簇”表示数据行和相邻的键值紧凑地存放在一块儿(Oracle中为索引组织表)。如图,被索引的列是主键列
聚簇索引的数据分布
一个表只能有一个聚簇索引,由于没法同时将数据行存放在两个不一样的地方
MySQL内建的存储引擎不支持选择索引做为聚簇索引,InnoDB将经过主键汇集数据,其默认使用聚簇索引:
若是没有定义主键,InnoDB会选择一个惟一的非空索引。
若是没有惟一的非空索引,InnoDB会隐式定义一个逐渐。
InnoDB只汇集在同一个页面的记录,包含相邻键值的页面可能会相距甚远。
优缺点:
优势(设计表和查询时充分利用可极大地提示性能):
能够把相关数据保存在一块儿。如实现电子邮箱时,根据用户ID来汇集数据,这样只需从磁盘读取少数的数据页就能获取某个用户的所有邮件。若是没有使用聚簇索引,则每封电子邮件均可能致使一次磁盘IO。
数据访问更快。由于索引和数据都保存在同一个B-Tree中。
使用覆盖索引扫描的查询能够直接使用页节点中的主键值。
缺点:
最大限度地提升了I/O密集型应用的性能,但若是数据所有存放在内存中,则访问的顺序就没那么重要了,聚簇索引也就没什么优点。
插入速度严重依赖于插入顺序。按照主键的顺序插入是加载数据到InnoDB表中速度最快的方式。若是不是按照主键顺序加载数据,加载完成后最好使用OPTIMIZE TABLE命令从新组织一下表。
更新聚簇索引列的代价很高,由于会强制InnoDB将每一个被更新的行移动到新的位置。
基于聚簇索引的表在插入新行,或者主键被更新致使须要移动行的时候,可能面临“页分裂(page split)”的问题。当行的主键值要求必须将这一行插入到某个已满的页中时,存储引擎会将该页分裂成两个页面来容纳该行,这会致使表占用更多的磁盘空间。
可能致使全表扫描变慢,尤为是行比较稀疏,或者因为页分裂致使数据存储不连续的时候。
二级索引(非聚簇索引)可能比想象的要更大,由于在二级索引的叶子节点包含了引用行的主键列。
二级索引访问须要两次索引查找,而不是一次。由于二级索引叶子节点保存的不是指向行的物理位置的指针,而是行的主键值。(InnoDB的自适应哈希索引可以减小这样的重复工做)
InnoDB和MyISAM的数据分布对比
聚簇和非聚簇表对比图
InnoDB:
因为采用了聚簇索引,其保存了整个表
聚簇索引每一个叶子节点都包含了主键值、事务ID、用于事务和MVVC的回滚指针以及全部的剩余列。
二级索引的叶子节点存储的不是"行指针",而是主键值,并以此做为指向行的“指针”。即叶子节点包含被索引的列和主键列。这样的策略会让二级索引占用更多的空间,但减小了当出现行移动或者数据页分裂时二级索引的维护工做,由于无须更新二级索引中的指针。
MyISAM
采用了独立的行存储,按照数据插入的顺序存储在磁盘上
主键索引和其余索引在结构上同样,主键索引是一个名为PRIMARY的惟一非空索引。
在InnoDB表中按主键顺序插入行
若是没有数据须要汇集,建议定义一个代理键做为主键,而且主键的数据应该和应用无关。最简单是使用AUTO_INCREMENT自增列,这样能够保证数据行是按顺序写入的,对于根据主键作关联操做的性能更好。
最好避免随机的(不连续且值的分布范围很是大)聚簇索引,特别是对于I/O密集型的应用,好比使用UUID做为聚簇索引可能会带来糟糕的性能,它使得聚簇索引的插入彻底随机,使得插入行的时间更长,并且索引占用的空间更大。由于主键的字段更长,还因为页分裂和碎片致使。
根据顺序id插入数据:
每条记录都存储在上一条记录的后面,当达到页的最大填充因子时(InnoDB默认为页大小的15/16,留出部分空间用于之后修改),下一条记录会插入新的页中。一旦数据按照这种顺序的方式加载,主键页就会被近似于被顺序的记录填满(二级索引页多是不同的)
形成更坏结果的场景:
对于高并发工做负载,可能会形成明显的争用。由于全部的插入都发生在这里,可能致使间隙锁竞争。
AUTO_INCREMENT锁机制也可能会被争用,需考虑从新设计表或者应用,或者更改innodb_autoinc_lock_mode配置。
使用随机id插入数据:
新行的主键值不必定比以前插入的大,所以须要为新行找到合适的位置——一般是已有数据的中间位置——并分配空间。这会增长不少额外的工做,并致使数据分布不够优化。
缺点:
写入的目标页可能已经刷新到磁盘并从缓存中移除,或是尚未被加载到缓存中,InnoDB在插入以前需先从磁盘读取目标页到内存中,这将致使大量的随机IO。
由于写入是乱序的,须要频繁地作页分裂操做,以便为新行分配空间。由于页分裂会致使移动大量数据,一次插入最少须要修改三个页而不是一个页。
因为频繁的页分裂,页会变得稀疏并被不规则地填充,因此最终数据会有碎片。
3.3.6 覆盖索引
设计优秀的索引应该考虑到整个查询,而不仅仅是WHERE条件部分
覆盖索引:一个索引包含(覆盖)全部须要查询的字段的值
查询只须要扫描索引而无须回表读取数据行的好处:
索引条目一般小于数据行大小,若是只须要读取索引会极大地减小数据访问量。这对缓存的负载很是重要,由于这种状况下响应时间大部分花在数据拷贝上。覆盖索引对IO密集型的应用也有帮助,由于索引被数据更小,更容易所有放入内存中(尤为是MyISAM能压缩索引)
索引是按照列值顺序存储的(至少在单个页内是如此 ),因此对于IO密集型的范围查询会比随机从磁盘读取每一行数据的IO要少得多。
一些存储引擎如MyISAM在内存中只缓存索引,数据则依赖于操做系统来缓存,所以访问数据须要一次系统调用。这可能会致使严重的性能问题,尤为是那些系统调用占了数据访问中的最大开销。
覆盖索引对使用了聚簇索引的InnoDB的表很是有用。InnoDB的二级索引在叶子节点保存了行的主键值,因此若是二级节点可以覆盖查询,则能够避免对主键索引的二次查询。
覆盖索引必需要存储索引列的值,而哈希索引、空间索引和全文索引都不存储,MySQL只能使用B-Tree索引作覆盖索引。
没法使用覆盖索引的缘由:
没有任何索引可以覆盖这个查询。
不能再索引执行LIKE操做。
可使用延迟关联使用覆盖索引,由于延迟了对列的访问。先在查询第一阶段使用覆盖索引,再在外层查询所要获取的列值。
InnoDB的二级索引的叶子节点包含了主键的值,这意味着二级索引能够有效地利用这些主键列来覆盖查询。
-- last_name字段有二级索引,虽然该索引的列不包括逐渐actor_id,但也能用于对actor_id作覆盖查询 mysql>EXPLAIN SELECT actor_id, last_name -> FROM sakila.actor WHERE last_name = "HOPPER"\G复制代码
使用InnoDB的表经过主键查询全部列,并非覆盖查询,虽然聚簇索引的叶子节点包含了全部列的数据,但它只是一种数据存储方式,并不算索引。
3.3.7 使用索引扫描来作排序
MySQL生成有序结果的方式:
经过排序操做
按索引顺序扫描。EXPLAIN出来的type列的值为“index”
索引若是不能覆盖查询所须要的所有列,那每一条记录都须要回表查询。这基本上是随机IO,比顺序地全表扫描更慢,尤为是在IO密集型的工做负载时。
设计索引尽量知足排序和查找行。
索引扫描排序的要求(如不知足都要执行排序操做):
只有索引的列顺序和ORDER BY子句顺序彻底一致,而且全部列的排序方向(倒序或正序)都同样,MySQL才可以使用索引来对结果进行排序。
若是查询须要关联多个表,只有当ORDER BY子句引用的字段所有为第一个表时
ORDER BY子句和查找型查询的限制是同样的:需知足索引的最左前缀要求
ORDER BY子句能够在前导列为常量\常数的时候忽略该限制,若是WHERE或者JOIN子句对这些列定义了常量。... WHERE col1="xxx" ORDER BY col2 DESC;
,其中col1和col2为联合索引。
不能使用索引作排序的查询:
ORDER BY使用了两种不一样的排序方向
ORDER BY引用了不在索引中的列
WHERE 和ORDER BY 中的列没法组合索引的最左前缀
WHERE在第一列是范围查询,MySQL没法索引其他列
在某列上有多个等于条件,对排序来讲也是范围查询。
3.3.8 压缩(前缀压缩)索引
MyISAM使用前缀压缩来减小索引大小,从而让更多索引能够放入内存中,在某些状况下能极大地提升性能。
默认只压缩字符串,经过设置也能压缩整数。
压缩每一个索引块的方法:先彻底保存索引块的第一个值,而后将其余值和第一个值比较获得相同的前缀字节数和剩余的不一样后缀部分,再把这部分存储起来。MyISAM对指针也采用相似的压缩方式。
压缩块使用更少的空间,代价是某些操做可能更慢。由于每一个值都依赖前面的值,没法使用二分查找只能从头开始扫描,而对倒序的扫描性能更差。
对CPU密集型应用,由于扫描常常要随机查找,不推荐使用该索引。
在CREATE TABLE语句中制定PACK_KEYS参数来控制索引压缩的方式。
3.3.9 冗余和重复索引
冗余索引:在相同列上建立多个索引。MySQL须要单独维护重复的索引,而且优化器在查询时也须要逐个考虑,可能会影响性能。
(A)是(A,B)的冗余索引,(B,A)和(B)则不是,只针对B-Tree索引来讲
(A,ID)也是冗余索引,由于对InnoDB主键列已经包含在二级索引中
其余类型的如哈希索引也不会是B-Tree的冗余索引
增长新索引会致使INSERT,UPDATE等操做的速度变得更慢,特别是新增索引达到了内存瓶颈的时候。
重复索引:在相同列上按照相同顺序建立的相同类型的索引。应该避免这种操做,常见错误作法是对一个主键添加惟一限制和查询索引,这属于三个重复的索引。(若是索引的类型不一样,并不算重复索引)
大多数状况下都不须要冗余索引,应该尽可能扩展已有的索引而不是建立新的索引。除非扩展已有的索引会致使其变得太大,从而影响其余使用该索引查询的性能。
假如在整数列上有一个查询,如今须要额外增长很长的VARCHAR列来扩展该索引,可能会致使性能急剧降低。特别是有查询把这个索引看成覆盖查询,或者是MyISAM表而且有不少范围查询。
-- Q1查询: SELECT count(*) FROM userinfo WHERE state_id=5; -- Q2查询: SELECT state_id, city, address FROM userinfo WHERE state_id=5; -- Q2的查询速度会比Q1慢,最简单的办法是扩展索引变成覆盖查询: ALTER TABLE userinfo DROP key state_id, ADD KEY state_id_2 (state_id, city, address); -- 索引扩展后,Q2运行更快,但Q1变慢了。若是想要两个查询都变得更快,就须要两个索引,尽管这是冗余的。复制代码
解决冗余和重复索引的办法只须要删除它们。找出这些索引的办法:
写一些复杂的访问INFORMATION_SCHEMA表的查询(服务器若是有大量的数据或表,可能会致使性能问题)
第三方工具。
因为二级索引包含了主键值,所以(A)至关于(A,ID),对WHERE A=5 ORDER BY ID这样的查询颇有用。但若是(A)扩展为(A,B)至关于(A,B,ID),前面的查询就没法使用该索引排序,而只能用文件排序。
3.3.10 未使用的索引
找出它们,删掉!不过有些索引的功能至关于惟一约束,虽然一直没被查询使用,可是是用于避免产生重复数据的。
3.3.11 索引和锁
索引能够锁定更少的行。若是查询从不访问那些不须要的行,那么就会锁定更少的行:
虽然InnoDB的行锁效率很高,内存使用也不多,可是锁定行的时候仍然会带来额外的开销。
锁定超过须要的行会增长锁争用并减小并发性。
InnoDB只有在访问行的时候才会对其加锁,而索引可以减小InnoDB访问的次数,从而减小锁的数量。但只有当InnoDB在存储引擎层可以过滤掉全部不须要的行时才有效。
若是索引没法过滤掉无效的行,那么在InnoDB检索到数据并返回给服务器层后,MySQL服务器才能应用WHERE子句。而这时候InnoDB已经锁住了这些行(包含有没被索引的行数据,这些是要在服务器层被过滤掉的,由于索引只在存储引擎层工做),到适当的时候才释放。
MySQL5.0及新版本,InnoDB能够在服务器端过滤掉行就释放锁;但在早期版本,只有在事务提交后才能释放锁。
若是不使用索引查找和锁定行的话,MySQL可能会作全表扫描并锁住全部的行,而无论是否须要。
InnoDB在二级索引上使用共享(读)锁,但访问主键索引须要排他(写)锁,这消除了使用覆盖索引的可能性(不理解?????),而且使得SELECT FOR UPDATE比LOCK IN SHARE MODE或非锁定查询要慢得多。
3.4 索引案例学习
设计一个在线约会网站,用户信息表包括国家、地区、城市、性别、眼睛颜色等等。网站必须支持上面这些特征的各类组合来搜索用户,还必须容许根据用户的最后在线时间、其余会员对用户的评分等对用户进行排序并对结果进行限制。
使用索引排序,仍是先检索数据再排序?使用索引排序会严格限制索引和查询的设计。
3.4.1 支持多种过滤条件
3.4.2 避免多个范围条件
假设有一个last_online列并但愿经过下面的查询显示在过去几周上线过的用户:
WHERE eye_color IN('brown', 'blue', 'hazel') AND hair_color IN('black', 'red', 'blonde', 'brown') AND sex IN("M", "F") AND last_online > DATE_SUB(NOW(), INTERVAL 7 DAY) AND age BETWEEN 18 AND 25; -- MySQL会将age>18和age IN(18,19)都认为是范围查询(经过EXPLAIN查看),但两种访问效率是不一样的,由于第二个查询是多个等值条件查询。对MySQL来讲,没法在使用范围查询后面的其余索引列,但对多个等值范围查询没有这个限制。复制代码
这个查询有两个范围条件,MySQL没法同时使用它们。
若是没法将age字段转换为一个IN()的列表,而且要求对这两个维度的范围查询的速度很快,很遗憾没有一个直接的办法解决该问题,但能够将其中的一个范围查询转换成一个简单的等值比较:
事先计算好一个active列,这个字段由定时任务来维护。当用户每次登录时,将对应值设置为1,而且将过去连续7天未登录的用户的值设置为0
这个方法可使用(active, sex, country, age)索引。active并非彻底精确的,由于对这类查询的精度要求并不高。若是须要精确次数,能够把last_online列放到WHERE子句,但不加入到索引中。因此这个查询条件无法使用任何索引,但由于这个条件的过滤性不高,即便在索引中加入该列也没有太大的帮助,或者说缺少合适的索引对该查询的影响也不明显。
若是用户系统同时看到活跃和不活跃用户,能够在查询中使用IN()列表。另外一个可选方案是为不一样的组合建立单独的索引,至少要包含(active, sex, country, age),(active, country, age),(sex, country, age)和(country, age),这些索引对某个具体的查询来讲多是更优化的,可是考虑到索引的维护额额外的空间占有代价,并非一个好策略。
3.4.3 优化排序
对选择性很是低的列,能够增长一些特殊的索引来作排序。例如,能够建立(sex, rating),这个查询同时使用了ORDER BY和LIMIT,若是没有索引会很慢
SELECT <cols> FROM profiles WHERE sex="M" ORDER BY rating LIMIT 10
即便有索引,若是用户界面上要翻页,而且翻页翻到比较靠后时查询也可能很是慢:
SELECT <cols> FROM profiles WHERE sex="M" ORDER BY rating LIMIT 10000, 10;
不管如何建立索引,这种查询都是严重的问题。由于随着偏移量的增长,MySQL须要花费大量的时间来扫描须要丢弃的数据。反范式化、预先计算和缓存多是解决这类查询的仅有策略。一个更好的办法是限制用户可以翻页的数量,而实际上这对用户体验的影响并不大,由于用户不多真正在意搜索结果的第10000页。
优化这类索引另外一个较好的办法是使用延迟关联,经过使用覆盖索引查询返回须要的主键,再根据这些主键关联原表得到须要的行。这能够减小MySQL扫描那些须要丢弃的行数。
SELECT <cols> FROM profiles INNER JOIN ( SELECT <primary key cols> FROM profiles WHERE sex="M" ORDER BY rating LIMIT 10000, 10 ) AS x USING(<primary key cols>);复制代码
3.5 维护和索引表
维护表的三个目的:找到并修复损坏的表,维护准确的索引统计信息,减小碎片
3.5.1 找到并修复顺坏的表
损坏的索引会致使查询返回错误的结果或莫须有的主键冲突问题,严重时还会致使数据库崩溃。
尝试运行CHECK TABLE来检查是否发生了表损坏(注意有些引擎不支持该命令),一般可以找出大多数的表和索引的错误。
修复表错误的办法:
可使用REPAIR TABLE来修复损坏的表(注意有些引擎不支持该命令)。
若是存储引擎不支持REPAIR TABLE,也可经过一个不作任何操做的ALTER来重建表,如修改表的存储引擎为当前引擎:ALTER TABLE innodb_dbl ENGINE=INNODB;
将数据导出一份,而后再从新导入。
使用第三方工具
若是损坏的是系统区域,或者是表的"行数据"区域,而不是索引,那么以前的办法就没有用了。只能从备份中恢复表,或者尝试从损坏的数据文件中尽量恢复数据。
若是InnoDB引擎的表出现了损坏,那么必定是发现了严重的错误,须要马上调查下缘由。由于InnoDB的设计通常不会出现损坏。若是发生损坏,多是数据库的硬件问题,或者在MySQL外部操做了数据文件,亦或是InnoDB的缺陷(不太可能)。不存在任何查询让InnoDB损坏。
若是出现了数据损坏,最重要的是找出缘由,而不是简单的修复,不然颇有可能会不断的损坏。能够经过设置innodb_force_recovery参数进入InnoDB的强制恢复数据模式来修复数据。
3.5.2 更新索引统计信息
MySQL的查询优化器经过两个API来了解存储引擎的索引值分布信息:
records_in_range(),经过传入两个边界值获取在这个范围大概有多少条记录。对某些存储引擎如MyISAM返回精确值,对InnoDB返回一个估算值。
info(),返回各类类型的数据,包括索引的基数(每一个键值有多少条记录)
若是存储引擎向优化器提供的扫描行数信息是不许确的数据,或者执行计划自己太复杂而没法精确地获取各个阶段匹配的行数,那么优化器会使用索引统计信息来估算扫描行数。
MySQL优化器使用的是基于成本的模型,而衡量成本的主要指标就是一个查询须要扫描多少行。若是表没有统计信息,或者统计信息不许确,优化器极可能作出错误的决定。经过运行ANALYZE TABLE来从新生成统计信息解决这个问题。而每种存储引擎实现的统计信息的方式不一样,须要进行ANALYZE TABLE的频率和每次运行的成本也不一样:
Memory引擎根本不存储索引统计信息
MyISAM将索引统计信息存储在磁盘中,ANALYZE TABLE须要进行一次全索引扫描来计算索引基数,在整个过程当中须要锁表。
直到MySQL5.5版本,InnoDB也不在磁盘存储索引统计信息,而是经过随机的索引访问进行评估并将其存储在内存中。
使用SHOW INDEX FROM table;
命令来查看索引的基数(cadinality)。基数显示了存储引擎估算索引列有多少个不一样的取值。在MySQL5.0及之后的版本,能够经过INFORMATION_SCHEMA.STATISTICS表很方便地查询到这些信息,不过若是服务器的库表很是多,从这里获取元数据的速度会很是慢,并且会给MySQL带来额外的压力。
InnoDB的统计信息:
该引擎经过抽样的方式来计算统计信息,首先随机地读取少许的索引页面,而后以此为样本计算索引的统计信息。老版本中样本页数是8,新版本能够设置innodb_stats_sample_pages来设置样本页的数量。理论上越大的值能够帮助生成更准确的索引信息,特别是对某些超大数据表来讲。
会在表首次打开,或者执行ANALYZE TABLE,抑或表的大小发生很是大的变化(该变化超过十六分之一或者新插入20亿行)的时候计算索引的统计信息。
会在打开某些INFORMATION_SCHEMA表,或者使用SHOW TABLE STATUS和SHOW INDEX,抑或MySQL客户端开启自动补全功能的时候都会触发索引统计信息的更新。
若是服务器上有大量的数据,可能会致使严重的问题,尤为是IO比较慢的时候,客户端或者监控程序触发索引信息采样更新时可能会致使大量的锁,并给服务器带来额外的压力。能够关闭innodb_stats_on_metadata参数来避免上面提到的问题。
3.5.3 减小索引和数据的碎片
索引碎片化:
B-Tree索引可能会碎片化,这会下降查询的效率。碎片化的索引可能会以不好或者无序的方式存储在磁盘上。
根据设计,B-Tree须要随机磁盘访问才能定位到叶子页,因此随机访问是不可避免的。然而,若是叶子页在物理分布上是顺序且紧密的,那么查询的性能就会更好。不然对于范围查询、索引覆盖扫描等操做来讲,速度可能会下降不少倍;对于索引覆盖扫描这一点更明显
表的数据存储碎片化(比索引碎片化更加复杂):
类型:
行碎片(Row fragmentation):数据行被存储为多个地方的多个片断中。及时查询只从索引中访问一行记录,也会致使性能降低。
行间碎片(Intra-row fragmentation):指逻辑上顺序的页,或者行在磁盘上不是顺序存储的。行间碎片对诸如全表扫描和聚簇索引扫描之类的操做有很大的影响,由于这些操做本来可以从磁盘上的顺序存储的数据收益。
剩余空间碎片(Free space fragmentation):数据页中有大量的空余空间。这会致使服务器读取大量不须要的数据,形成浪费。
对MyISAM表,这三类碎片化均可能发生;但InnoDB不会出现短小的行碎片,它会移动短小的行并重写到一个片断中。
从新整理数据方式:
OPTIMIZE TABLE
导出再导入
排序算法重建索引(针对MyISAM)
“在线”添加和删除索引的功能,能够经过先删除,而后在从新建立索引来消除索引碎片(针对最新版本InnoDB)
经过一个不作任何操做的ALTER TABLE <table> ENGINE = <engine>;
来重建表(针对不支持OPTIMIZE TABLE的引擎)
应该经过一些实际测量而不是随意假设来肯定是否须要消除索引和表的碎片化,还要考虑数据是否已达到稳定状态(若是进行碎片整理将数据压缩到一块儿,可能会致使后续的更新操做触发一系列的页分裂和重组,对性能形成不良的影响直到数据达到新的稳定状态)
3.6总结
MySQL和存储引擎访问数据的方式,加上索引的特性,使得索引成为一个影响数据访问的有利而灵活的工做(不管数据实在磁盘仍是在内存中)
大多数状况下都会使用B-Tree索引,其余类型的索引大多只适用于特殊目的。
选择索引和编写利用这些索引的查询时,有以下三个原则始终须要记住:
单行访问是很慢的。若是服务器从存储中读取一个数据块只是为了获取其中的一行,那么就浪费了不少工做。最好的读取的块中能包含尽量多须要的行。使用索引能够建立位置引用提升效率。
按顺序访问范围数据是很快的。有两个缘由:顺序IO不须要屡次磁盘寻道而比随机IO快不少;若是服务器可以按须要的顺序读取数据,就再也不须要额外的排序操做,而且GROUP BY查询也无须再作排序和将行按组进行聚合计算。
索引覆盖查询是很快的。若是一个索引包含了查询须要的全部列,那么存储引擎就不须要再回表查找行,避免了大量的单行访问。
编写查询语句应该尽量选择合适的索引以免单行查找,尽量地使用数据原生顺序而避免额外的数据排序操做,并尽量使用索引覆盖查询。
对某些查询不可能建立一个“三星”索引,必需要有所取舍,或者寻求替代策略(例如反范式话、或者提早计算汇总表)
理解索引的工做原理来建立最适合的索引
判断为一个系统建立的索引的合理性:按响应时间对查询进行分析。
找出那些消耗时间最长的或给服务器带来最大压力的查询
检查这些查询的schema,SQL和索引结构
判断是否有查询扫描了太多的行,是否作了不少额外的排序或者使用了临时表,是否使用随机IO访问数据,或者是有太多回表查询那些不在索引中的列的操做。
若是一个查询没法从全部可能的索引中获益,则应该看看是否能够建立一个更合适的索引来提高性能。若是不行,也要尝试是否能够重写该查询,将其转化成一个可以高效利用现有索引或者新建立索引的查询。