索引(在 MySQL 中也叫做“键(key)”)是存储引擎用于快速找到记录的一种数据结构。
索引对于良好的性能非常关键。尤其是当表中的数据量越来越大时,索引对性能的影响愈发重要。在数据量较小且负载较低时,不恰当的索引对性能的影响可能还不明显,但当数据量逐渐增大时,性能则会急剧下降
索引优化应该是对查询性能优化最有效的手段了。索引能够轻易将查询性能提高几个数量级,“最优”的索引有时比一个“好的”索引性能要好两个数量级。创建一个真正“最优”的索引经常需要重写查询。
1、索引基础
要理解 MySQL中索引是如何工作的,最简单的方法就是去看看一本书的“索引”部分:如果想在一本书中找到某个特定主题,一般会先看书的“索引”,找到对应的页码。
在 MySQL中,存储引擎用类似的方法使用索引,其先在索引中找到对应值,然后根据匹配的索引记录找到对应的数据行。假如要运行下面的查询:
SELECT first_name FROM sakila.actor WHERE actor_id=5;
如果在 actor _id列上建有索引,则 MSQL 将使用该索引找到 actor _id为5的行,也就是说,MySQL先在索引上按值进行查找,然后返回所有包含该值的数据行。
索引可以包含一个或多个列的值。如果索引包含多个列,那么列的顺序也十分重要,因为 MySQL 只能高效地使用索引的最左前缀列。创建一个包含两个列的索引,和创建两个只包含一列的索引是大不相同的,下面将详细介绍。
1.1、索引的类型
索引有很多种类型,可以为不同的场景提供更好的性能。在 MySQL中,索引是在存储引擎层而不是服务器层实现的。所以,并没有统一的索引标准:不同存储引擎的索引的工作方式并不一样,也不是所有的存储引擎都支持所有类型的索引。即使多个存储引擎支持同一种类型的索引,其底层的实现也可能不同。
下面我们先来看看 MySQL 支持的索引类型,以及它们的优点和缺点。
B-Tree 索引
当人们谈论索引的时候,如果没有特别指明类型,那多半说的是 B-Tree索引,它使用B-Tree数据结构来存储数据。大多数 MySQL引擎都支持这种索引。Archive 引擎是一个例外:5.1 之前 Archive不支持任何索引,直到 5.1才开始支持单个自增列(AUTO_INCREMENT)的索引。
我们使用术语“B-Tree”,是因为MySQL 在 CREATE TABLE 和其他语句中也使用该关键字。不过,底层的存储引擎也可能使用不同的存储结构,例如,NDB 集群存储引擎内部实际上使用了 T-Tree 结构存储这种索引,即使其名字是 BTREE;InnoDB 则使用的是 B+Tree。各种数据结构和算法的变种不在本书的讨论范围之内。
存储引擎以不同的方式使用 B-Tree 索引,性能也各有不同,各有优劣。例如,MyISAM使用前缀压缩技术使得索引更小,但InnoDB则按照原数据格式进行存储。再如MyISAM 索引通过数据的物理位置引用被索引的行,而 InnoDB 则根据主键引用被索引的行。
B-Tree 通常意味着所有的值都是按顺序存储的,并且每一个叶子页到根的距离相同。下图展示了 B-Tree索引的抽象表示,大致反映了InnoDB索引是如何工作的。MyISAM使用的结构有所不同,但基本思想是类似的。
B-Tree 索引能够加快访问数据的速度,因为存储引擎不再需要进行全表扫描来获取需要的数据,取而代之的是从索引的根节点(图示并未画出)开始进行搜索。根节点的槽中存放了指向子节点的指针,存储引擎根据这些指针向下层查找。通过比较节点页的值和要查找的值可以找到合适的指针进入下层子节点,这些指针实际上定义了子节点页中值的上限和下限。最终存储引擎要么是找到对应的值,要么该记录不存在。
叶子节点比较特别,它们的指针指向的是被索引的数据,而不是其他的节点页(不同引擎的“指针”类型不同)。上图中仅绘制了一个节点和其对应的叶子节点,其实在根节点和叶子节点之间可能有很多层节点页。树的深度和表的大小直接相关。
B-Tree 对索引列是顺序组织存储的,所以很适合查找范围数据。例如,在一个基于文本域的索引树上,按字母顺序传递连续的值进行查找是非常合适的,所以像“找出所有以I到K开头的名字”这样的查找效率会非常高。
下面是一些关于 B-Tree 索引的限制:
如果不是按照索引的最左列开始查找,则无法使用索引。
不能跳过索引中的列。
如果查询中有某个列的范围查询,则其右边所有列都无法使用索引优化查找。
这些限制都和索引列的顺序有关。在优化性能的时候,可能需要使用相同的列但顺序不同的索引来满足不同类型的查询需求。也有些限制并不是 B-Tree 本身导致的,而是 MySQL 优化器和存储引擎使用索引的方式导致的,这部分限制在未来的版本中可能就不再是限制了。
哈希索引
哈希索引(hash index)基于哈希表实现,只有精确匹配索引所有列的査询才有效。对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码(hash code),哈希码是一个较小的值,并且不同键值的行计算出来的哈希码也不一样。哈希索引将所有的哈希码存储在索引中,同时在哈希表中保存指向每个数据行的指针。
在 MySQL 中,只有 Memory 引擎显式支持哈希索引。这也是 Memory 引擎表的默认索引类型,Memory 引擎同时也支持 B-Tree 索引。值得一提的是,Memory 引擎是支持非唯一哈希索引的,这在数据库世界里面是比较与众不同的。如果多个列的哈希值相同索引会以链表的方式存放多个记录指针到同一个哈希条目中。
因为索引自身只需存储对应的哈希值,所以索引的结构十分紧凑,这也让哈希索引查找的速度非常快。然而,哈希索引也有它的限制:
a.哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能的影响并不明显。
B.哈希索引数据并不是按照索引值顺序存储的,所以也就无法用于排序。哈希索引也不支持部分索引列匹配查找,因为哈希索引始终是使用索引列的全部内容来计算哈希值的。例如,在数据列(A,B)上建立哈希索引,如果查询只有数据列 A,则无法使用该索引。
c.哈希索引只支持等值比较查询,包括 =、IN()、<>(注意<>和<=> 是不同的操作)也不支持任何范围查询,例如 WHERE price> 100。
d.访问哈希索引的数据非常快,除非有很多哈希冲突(不同的索引列值却有相同的哈希值)。当出现哈希冲突的时候,存储引擎必须遍历链表中所有的行指针,逐行进行比较,直到找到所有符合条件的行。
e.如果哈希冲突很多的话,一些索引维护操作的代价也会很高。例如,如果在某个选择性很低(哈希冲突很多)的列上建立哈希索引,那么当从表中删除一行时,存储引擎需要遍历对应哈希值的链表中的每一行,找到并删除对应行的引用,冲突越多,代价越大。
因为这些限制,哈希索引只适用于某些特定的场合。而一旦适合哈希索引,则它带来的性能提升将非常显著。举个例子,在数据仓库应用中有一种经典的“星型”schema,需要关联很多查找表,哈希索引就非常适合查找表的需求。
除了 Memory 引擎外,NDB集群引擎也支持唯一哈希索引,且在NDB 集群引擎中作用非常特殊。
InnoDB引擎有一个特殊的功能叫做“自适应哈希索引(adaptive hash index)”。当InnoDB 注意到某些索引值被使用得非常频繁时,它会在内存中基于 B-Tree 索引之上再创建一个哈希索引,这样就让 B-Trec 索引也具有哈希索引的一些优点,比如快速的哈希查找。这是一个完全自动的、内部的行为,用户无法控制或者配置,不过如果有必要完全可以关闭该功能。
创建自定义哈希索引。如果存储引擎不支持哈希索引,则可以模拟像InnoDB 一样创建哈希索引,这可以享受一些哈希索引的便利,例如只需要很小的索引就可以为超长的键创建索引。
思路很简单:在 B-Tree基础上创建一个伪哈希索引。这和真正的哈希索引不是一回事,因为还是使用 B-Tree进行查找,但是它使用哈希值而不是键本身进行索引查找。你需要做的就是在查询的 WHERE 子句中手动指定使用哈希函数。
下面是一个实例,例如需要存储大量的URL,并需要根据 URL进行搜索查找。如果使用 B-Tree 来存储 URL,存储的内容就会很大,因为 URL本身都很长。正常情况下会有如下查询:
mysql> SELECT id FROM urI WHERE url="http://www.mysql.com";
若删除原来 URL列上的索引,而新增一个被索引的 url_crc列,使用 CRC32做哈希,就可以使用下面的方式查询:
mysql> SELECT id FROM url WHERE url="http://www.mysql.com"AND url_crc=CRC32("http://www.mysql.com");
这样做的性能会非常高,因为 MySQL优化器会使用这个选择性很高而体积很小的基于url crc列的索引来完成查找(在上面的案例中,索引值为 1560514994)。即使有多个记录有相同的索引值,查找仍然很快,只需要根据哈希值做快速的整数比较就能找到索引条目,然后一一比较返回对应的行。另外一种方式就是对完整的 URL字符串做索引,那样会非常慢。
空间数据索引(R-Tree)
MyISAM 表支持空间索引,可以用作地理数据存储。和B-Tree索引不同,这类索引无须前缀查询。空间索引会从所有维度来索引数据。查询时,可以有效地使用任意维度来组合查询。必须使用 MySQL的 GIS 相关函数如MBRCONTAINS()等来维护数据。MySQI的 GIS 支持并不完善,所以大部分人都不会使用这个特性。开源关系数据库系统中对GIS 的解决方案做得比较好的是 PostgreSQL的 PostGIS。
全文索引
全文索引是一种特殊类型的索引,它查找的是文本中的关键词,而不是直接比较索引中的值。全文搜索和其他几类索引的匹配方式完全不一样。它有许多需要注意的细节,如停用词、词干和复数、布尔搜索等。全文索引更类似于搜索引擎做的事情,而不是简单的 WHERE 条件匹配。
在相同的列上同时创建全文索引和基于值的 B-Tree索引不会有冲突,全文索引适用于MATCH AGAINST操作,而不是普通的 WHERE 条件操作。
其他索引类别
还有很多第三方的存储引擎使用不同类型的数据结构来存储索引。例如 TokuDB 使用分形树索引(fractal tree index),这是一类较新开发的数据结构,既有 B-Tree 的很多优点,也避免了 B-Tree的一些缺点。
2、索引的优点
索引可以让服务器快速地定位到表的指定位置。但是这并不是索引的唯一作用,到目前为止可以看到,根据创建索引的数据结构不同,索引也有一些其他的附加作用。
最常见的 B-Tree 索引,按照顺序存储数据,所以 MySQL 可以用来做 ORDER BY和 GROUP BY操作。因为数据是有序的,所以 B-Tree也就会将相关的列值都存储在一起。最后因为索引中存储了实际的列值,所以某些查询只使用索引就能够完成全部查询。据此特性,总结下来索引有如下三个优点:
1.索引大大减少了服务器需要扫描的数据量
2.索引可以帮助服务器避免排序和临时表。
3. 索引可以将随机 I/O 变为顺序 I/O。
3、高性能的索引策略
正确地创建和使用索引是实现高性能查询的基础。前面已经介绍了各种类型的索引及其对应的优缺点。现在我们一起来看看如何真正地发挥这些索引的优势。
高效地选择和使用索引有很多种方式,其中有些是针对特殊案例的优化方法,有些则是针对特定行为的优化。使用哪个索引,以及如何评估选择不同索引的性能影响的技巧则需要持续不断地学习。接下来的几个小节将帮助读者理解如何高效地使用索引。
3.1 独立的列
我们通常会看到一些查询不当地使用索引,或者使得 MySQL无法使用已有的索引。如果查询中的列不是独立的,则MySQL就不会使用索引。“独立的列”是指索引列不能是表达式的一部分,也不能是函数的参数。
例如,下面这个查询无法使用 actor_id列的索引:
mysql> SELECT actor_id FROM sakila.actor WHERE actor_id+1=5;
凭肉眼很容易看出WHERE 中的表达式其实等价于 actor_id = 4,但是 MySQL 无法自动解析这个方程式。这完全是用户行为。我们应该养成简化WHERE条件的习惯,始终将索引列单独放在比较符号的一侧。
下面是另一个常见的错误:
mysql> SELECT ... WHERE TO DAYS(CURRENT DATE) - TODAYS(date col) <= 10;
3.2 前缀索引和索引选择性
有时候需要索引很长的字符列,这会让索引变得大且慢。一个策略是前面提到过的模拟哈希索引。但有时候这样做还不够,还可以做些什么呢?
通常可以索引开始的部分字符,这样可以大大节约索引空间,从而提高索引效率。但这样也会降低索引的选择性。索引的选择性是指,不重复的索引值(也称为基数,cardinality)和数据表的记录总数(#T)的比值,范围从 I/#T到1之间。索引的选择性越高则查询效率越高,因为选择性高的索引可以让 MySQL在查找时过滤掉更多的行唯一索引的选择性是1,这是最好的索引选择性,性能也是最好的。
一般情况下某个列前缀的选择性也是足够高的,足以满足查询性能。对于 BLOB、TEXT或者很长的 VARCHAR类型的列,必须使用前缀索引,因为 MySQL不允许索引这些列的完整长度。
诀窍在于要选择足够长的前缀以保证较高的选择性,同时又不能太长(以便节约空间)。前缀应该足够长,以使得前缀索引的选择性接近于索引整个列。换句话说,前缀的“基数”应该接近于完整列的“基数”。
前缀索引是一种能使索引更小、更快的有效办法,但另一方面也有其缺点:MySQL 无法使用前缀索引做 0RDER BY和 GROUP BY,也无法使用前缀索引做覆盖扫描。
3.3 多列索引
一个常见的错误就是,为每个列创建独立的索引,或很多人对多列索引的理解都不够。
或者按照错误的顺序创建多列索引。
我们会在 5.3.4 节中单独讨论索引列的顺序问题。先来看第一个问题,为每个列创建独立的索引,从 SHOW CREATE TABLE 中很容易看到这种情况:
在多个列上建立独立的单列索引大部分情况下并不能提高 MySQL的查询性能。MySQL5.0和更新版本引入了一种叫“索引合并”(index merge)的策略,一定程度上可以使用表上的多个单列索引来定位指定的行。更早版本的 MySQL 只能使用其中某一个单列索引,然而这种情况下没有哪一个独立的单列索引是非常有效的。例如,表 film actor在字段 film _id 和 actor id上名有一个单列索引。但对于下面这个查询 WHERE条件,这两个单列索引都不是好的选择:
mysql> SELECT film_id, actor_id FROM sakila.film_actor WHERE actor_id=1 OR film_id= 1;
在老的 MySQL版本中,MySQL对这个查询会使用全表扫描。除非改写成如下的两个查询 UNION 的方式:
mysql> SELECT film_id, actor_id FROM sakila.film_actor WHERE actor_id = 1UNION ALL
SELECT film_id, actor_id FROM sakila.film_actor WHERE film_id = 1 AND actor_id <> 1;
但在 MySOL 5.0和更新的版本中,查询能够同时使用这两个单列索引进行扫描,并将结果进行合并。这种算法有三个变种:OR条件的联合(union),AND条件的相交(intersection)组合前两种情况的联合及相交。下面的查询就是使用了两个索引扫描的联合,通过EXPLAIN 中的 Extra 列可以看到这点:
mysql> EXPLAIN SELECT film_id, actor_id FROM sakila.film_actor->WHERE actor_id =1 OR film_id=1\G
id:1
select type: SIMPLE
table: film_actor
type: index_merge
possible keys: PRIMARY,idx_fk_film_id
key: PRIMARY,idx_fk_film_id
key len: 2,2
ref: NULL
rows: 29
Extra: Using union(PRIMARy,idx_fk_film_id); Using where
MySQL会使用这类技术优化复杂查询,所以在某些语句的 Extra列中还可以看到嵌套操作。
3.4 选择合适的索引列顺序
我们遇到的最容易引起困惑的问题就是索引列的顺序。正确的顺序依赖于使用该索引的查询,并且同时需要考虑如何更好地满足排序和分组的需要(顺便说明,本节内容适用于 B-Tree 索引,哈希或者其他类型的索引并不会像 B-Tree 索引一样按顺序存储数据)。
在一个多列 B-Tree 索引中,索引列的顺序意味着索引首先按照最左列进行排序,其次是第二列,等等。所以索引可以按照升序或者降序进行扫描,以满足精确符合列顺序的ORDER BY、GROUP BY 和 DISTINCT等子句的査询需求。
所以多列索引的列顺序至关重要。在 Lahdenmaki和Leach 的“三星索引”系统中,列顺序也决定了一个索引是否能够成为一个真正的“三星索引”。
对于如何选择索引的列顺序有一个经验法则:将选择性最高的列放到索引最前列。这个建议有用吗?在某些场景可能有帮助,但通常不如避免随机IO 和排序那么重要,考虑问题需要更全面(场景不同则选择不同,没有一个放之四海皆准的法则。这里只是说明,这个经验法则可能没有你想象的重要)。
当不需要考虑排序和分组时,将选择性最高的列放在前面通常是很好的。这时候索引的作用只是用于优化 WHERE条件的査找。在这种情况下,这样设计的索引确实能够最快地过滤出需要的行,对于在 WHERE 子句中只使用了索引部分前缀列的查询来说选择性也更高。然而,性能不只是依赖于所有索引列的选择性(整体基数),也和查询条件的具体值有关,也就是和值的分布有关。这和前面介绍的选择前缀的长度需要考虑的地方一样可能需要根据那些运行频率最高的查询来调整索引列的顺序,让这种情况下索引的选择性最高。
3.5 聚簇索引
聚簇索引”?并不是一种单独的索引类型,而是一种数据存储方式。具体的细节依赖于其实现方式,但 InnoDB 的聚簇索引实际上在同一个结构中保存了 B-Tree 索引和数据行。
当表有聚簇索引时,它的数据行实际上存放在索引的叶子页(leafpage)中。术语“聚簇”表示数据行和相邻的键值紧凑地存储在一起。因为无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。
因为是存储引擎负责实现索引,因此不是所有的存储引擎都支持聚簇索引。本节我们主要关注 InnoDB,但是这里讨论的原理对于任何支持聚簇索引的存储引擎都是适用的。
下图展示了聚簇索引中的记录是如何存放的。注意到,叶子页包含了行的全部数据但是节点页只包含了索引列。在这个案例中,索引列包含的是整数值。
InnoDB 将通过主键聚集数据,这也就是说上图 中的“被索引的列”就是主键列。
如果没有定义主键,InnoDB会选择一个唯一的非空索引代替。如果没有这样的索引InnoDB 会隐式定义一个主键来作为聚簇索引。InnoDB只聚集在同一个页面中的记录包含相邻键值的页面可能会相距甚远、
聚簇主键可能对性能有帮助,但也可能导致严重的性能问题。所以需要仔细地考虑聚簇索引,尤其是将表的存储引擎从 InnoDB 改成其他引擎的时候(反过来也一样)。
聚集的数据有一些重要的优点:
a.可以把相关数据保存在一起。例如实现电子邮箱时,可以根据用户ID 来聚集数据这样只需要从磁盘读取少数的数据页就能获取某个用户的全部邮件。如果没有使用聚簇索引,则每封邮件都可能导致一次磁盘 I/O。
b.数据访问更快。聚簇索引将索引和数据保存在同一个 B-Tree 中,因此从聚簇索引中获取数据通常比在非聚簇索引中査找要快。
c.使用覆盖索引扫描的查询可以直接使用页节点中的主键值。
如果在设计表和查询时能充分利用上面的优点,那就能极大地提升性能。同时,聚簇索引也有一些缺点:
a.聚簇数据最大限度地提高了 I/0 密集型应用的性能,但如果数据全部都放在内存中则访问的顺序就没那么重要了,聚簇索引也就没什么优势了。
b.插入速度严重依赖于插入顺序。按照主键的顺序插入是加载数据到InnoDB 表中速度最快的方式。但如果不是按照主键顺序加载数据,那么在加载完成后最好使用OPTIMIZE TABLE 命令重新组织一下表。
c.更新聚簇索引列的代价很高,因为会强制InnoDB 将每个被更新的行移动到新的位置。
d.基于聚簇索引的表在插入新行,或者主键被更新导致需要移动行的时候,可能面临“页分裂(page split)”的问题。当行的主键值要求必须将这一行插入到某个已满的页中时,存储引擎会将该页分裂成两个页面来容纳该行,这就是一次页分裂操作。页分裂会导致表占用更多的磁盘空间。
e.聚簇索引可能导致全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致数据存储不连续的时候。
f.二级索引(非聚簇索引)可能比想象的要更大,因为在二级索引的叶子节点包含了引用行的主键列。
g.二级索引访问需要两次索引查找,而不是一次。
最后一点可能让人有些疑惑,为什么二级索引需要两次索引查找?答案在于二级索引中保存的“行指针”的实质。要记住,二级索引叶子节点保存的不是指向行的物理位置的指针,而是行的主键值。
这意味着通过二级索引查找行,存储引警需要找到二级索引的叶子节点获得对应的主键值,然后根据这个值去聚簇索引中査找到对应的行。这里做了重复的工作:两次 B-Tree査找而不是一次。对于 InnoDB,自适应哈希索引能够减少这样的重复工作。
InnoDB 和 MyISAM 的数据分布对比
聚簇索引和非聚簇索引的数据分布有区别,以及对应的主键索引和二级索引的数据分布也有区别,通常会让人感到困扰和意外。来看看 InnoDB 和 MyISAM 是如何存储下面这个表的:
CREATE TABLE layout test(
col1 int NOT NULL,
col2 int NOT NULL,
PRIMARY KEY(col1),
KEY(col2)
);
假设该表的主键取值为1~ 10000,按照随机顺序插入并使用 0PTIMIZE TABLE 命令做了优化。换句话说,数据在磁盘上的存储方式已经最优,但行的顺序是随机的。列col2的值是从1~ 100 之间随机赋值,所以有很多重复的值。
MyISAM 的数据分布。MyISAM 的数据分布非常简单,所以先介绍它。MyISAM 按照数据插入的顺序存储在磁盘上,如下图所示。
在行的旁边显示了行号,从0开始递增。因为行是定长的,所以 MyISAM 可以从表的开头跳过所需的字节找到需要的行(MyISAM 并不总是使用上图的“行号”,而是根据定长还是变长的行使用不同策略)。
这种分布方式很容易创建索引。下面显示的一系列图,隐藏了页的物理细节,只显示索引中的“节点”,索引中的每个叶子节点包含“行号”。下图显示了表的主键。
这里忽略了一些细节,例如前一个 B-Tree 节点有多少个内部节点,不过这并不影响对非聚簇存储引擎的基本数据分布的理解。
那 col2列上的索引又会如何呢?有什么特殊的吗?回答是否定的:它和其他索引没有什么区别。下图显示了 co12列上的索引。
事实上,MyISAM 中主键索引和其他索引在结构上没有什么不同。主键索引就是一个名为 PRIMARY 的唯一非空索引。
InnoDB 的数据分布。因为 InnoDB 支持聚簇索引,所以使用非常不同的方式存储同样的数据。InnoDB 以下图所示的方式存储数据。
第一眼看上去,感觉该图和前面的MyISAM没有什么不同,但再仔细看细节,会注意到该图显示了整个表,而不是只有索引。因为在InnoDB 中,聚簇索引“就是”表,所以不像 MyISAM 那样需要独立的行存储。
聚簇索引的每一个叶子节点都包含了主键值、事务ID、用于事务和 MVCC的回滚指针以及所有的剩余列(在这个例子中是 col2)。如果主键是一个列前缀索引,InnoDB 也会包含完整的主键列和剩下的其他列。
还有一点和 MyISAM 的不同是,InnoDB的二级索引和聚簇索引很不相同。InnoDB 二级索引的叶子节点中存储的不是“行指针",而是主键值,并以此作为指向行的“指针"这样的策略减少了当出现行移动或者数据页分裂时二级索引的维护工作。使用主键值当作指针会让二级索引占用更多的空间,换来的好处是,InnoDB 在移动行时无须更新二级索引中的这个“指针”
下图显示了示例表的 col2索引。每一个叶子节点都包含了索引列(这里是 col2),紧接着是主键值(col1)。
上图展示了 B-Tree的叶子节点结构,但我们故意省略了非叶子节点这样的细节InnoDB 的非叶子节点包含了索引列和一个指向下级节点的指针
下图是描述 InnoDB 和 MyISAM 如何存放表的抽象图。从图 中可以很容易看出InnoDB 和 MyISAM 保存数据和索引的区别。
在 InnoDB 表中按主键顺序插入行
如果正在使用InnoDB表并且没有什么数据需要聚集,那么可以定义一个代理键(surrogate key)作为主键,这种主键的数据应该和应用无关,最简单的方法是使用AUTO INCREMENT 自增列。这样可以保证数据行是按顺序写入,对于根据主键做关联操作的性能也会更好。
最好避免随机的(不连续且值的分布范围非常大)聚簇索引,特别是对于 I/0 密集型的应用。例如,从性能的角度考虑,使用UUID 来作为聚簇索引则会很糟糕:它使得聚簇索引的插入变得完全随机,这是最坏的情况,使得数据没有任何聚集特性。
3.6 覆盖索引
通常大家都会根据查询的WHERE条件来创建合适的索引,不过这只是索引优化的一个方面。设计优秀的索引应该考虑到整个查询,而不单单是 WHERE条件部分。索引确实是一种查找数据的高效方式,但是MySQL也可以使用索引来直接获取列的数据,这样就不再需要读取数据行。如果索引的叶子节点中已经包含要查询的数据,那么还有什么必要再回表查询呢?如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为“覆盖索引”。
覆盖索引是非常有用的工具,能够极大地提高性能。考虑一下如果查询只需要扫描索引而无须回表,会带来多少好处:
a.索引条目通常远小于数据行大小,所以如果只需要读取索引,那 MySQL就会极大地减少数据访问量。这对缓存的负载非常重要,因为这种情况下响应时间大部分花费在数据拷贝上。覆盖索引对于 I/0 密集型的应用也有帮助,因为索引比数据更小,更容易全部放入内存中(这对于 MyISAM 尤其正确,因为 MyISAM 能压缩索引以变得更小)。
b.因为索引是按照列值顺序存储的(至少在单个页内是如此),所以对于I/O 密集型的范围查询会比随机从磁盘读取每一行数据的 I/0 要少得多。对于某些存储引擎,例如 MyISAM 和 Percona XtraDB,甚至可以通过 0PTIMIZE 命令使得索引完全顺序排列,这让简单的范围查询能使用完全顺序的索引访问。
c.一些存储引擎如 MyISAM 在内存中只缓存索引,数据则依赖于操作系统来缓存,因此要访问数据需要一次系统调用。这可能会导致严重的性能问题,尤其是那些系统调用占了数据访问中的最大开销的场景。
d.由于 InnoDB 的聚簇索引,覆盖索引对 InnoDB 表特别有用。 InnoDB 的二级索引在叶子节点中保存了行的主键值,所以如果二级主键能够覆盖查询,则可以避免对主键索引的二次查询。
在所有这些场景中,在索引中满足查询的成本一般比查询行要小得多。
不是所有类型的索引都可以成为覆盖索引。覆盖索引必须要存储索引列的值,而哈希索引、空间索引和全文索引等都不存储索引列的值,所以 MySQL 只能使用 B-Tree 索引做覆盖索引。另外,不同的存储引擎实现覆盖索引的方式也不同,而且不是所有的引擎都支持覆盖索引。
当发起一个被索引覆盖的査询(也叫做索引覆盖査询)时,在 EXPLAIN的 Extra列可以看到“Using index”的信息。例如,表 sakila.inventory有一个多列索引(store_id,flim_id)。MySQL如果只需访问这两列,就可以使用这个索引做覆盖索引,如下所示:
索引覆盖查询还有很多陷阱可能会导致无法实现优化。MySQL查询优化器会在执行查询前判断是否有一个索引能进行覆盖。假设索引覆盖了 WHERE条件中的字段,但不是整个查询涉及的字段。如果条件为假(false),MySQL5.5 和更早的版本也总是会回表获取数据行,尽管并不需要这一行且最终会被过滤掉。
来看看为什么会发生这样的情况,以及如何重写查询以解决该问题。从下面的查询开始:
这里索引无法覆盖该查询,有两个原因:
a.没有任何索引能够覆盖这个查询。因为查询从表中选择了所有的列,而没有任何索引覆盖了所有的列。不过,理论上 MySQL还有一个捷径可以利用:WHERE 条件中的列是有索引可以覆盖的,因此 MySQL可以使用该索引找到对应的 actor 并检査 titile是否匹配,过滤之后再读取需要的数据行。
b.MySQL不能在索引中执行 LIKE操作。这是底层存储引擎 API的限制,MySQL 5.5和更早的版本中只允许在索引中做简单比较操作(例如等于、不等于以及大于)。MySQL能在索引中做最左前缀匹配的LIKE比较,因为该操作可以转换为简单的比较操作,但是如果是通配符开头的LIKE查询,存储引擎就无法做比较匹配。这种情况下,MySQL服务器只能提取数据行的值而不是索引值来做比较。
3.7 使用索引扫描来做排序
MySQL有两种方式可以生成有序的结果:通过排序操作,或者按索引顺序扫描,如果EXPLAIN 出来的 type列的值为“index",则说明 MySQL 使用了索引扫描来做排序(不要和 Extra 列的“Using index”搞混淆了)。
扫描索引本身是很快的,因为只需要从一条索引记录移动到紧接着的下一条记录。但如果索引不能覆盖查询所需的全部列,那就不得不每扫描一条索引记录就都回表查询一次对应的行。这基本上都是随机I/O,因此按索引顺序读取数据的速度通常要比顺序地全表扫描慢,尤其是在 I/0 密集型的工作负载时。
MySQL 可以使用同一个索引既满足排序,又用于查找行。因此,如果可能,设计索引时应该尽可能地同时满足这两种任务,这样是最好的。
只有当索引的列顺序和 ORDER BY子句的顺序完全一致,并且所有列的排序方向(倒序或正序)都一样时,MySQL才能够使用索引来对结果做排序"。如果查询需要关联多张表,则只有当ORDER BY子句引用的字段全部为第一个表时,才能使用索引做排序ORDER BY子句和查找型查询的限制是一样的:需要满足索引的最左前缀的要求,否则MySQL都需要执行排序操作,而无法利用索引排序。
有一种情况下 ORDER BY子句可以不满足索引的最左前缀的要求,就是前导列为常量的时候。如果 WHERE 子句或者 J0IN 子句中对这些列指定了常量,就可以“弥补”索引的不足