文章目录
- 五、创建高性能的索引
- 5.1 索引基础
- 5.1.1 索引的类型
- B-Tree索引
- 哈希索引
- 空间数据索引(R-Tree)
- 全文索引
- 其他索引类别
- 5.2 索引的优点
- 索引是最好的解决方案吗?
- 5.3 高性能的索引策略
- 5.3.1 独立的列
- 5.3.2 前缀索引和索引选择性
- 5.3.3 多列索引
- 5.3.4 选择合适的索引列顺序
- 5.3.5 聚簇索引
- 5.3.6 InnoDB和MyISAM的数据分布对比
- MyISAM的数据分布
- InnoDB的数据分布
- 在InnoDB表中按主键顺序插入行
- 顺序的主键什么时候会造成更坏的结果?
- 5.3.6 覆盖索引
- 5.3.7 使用索引扫描来做排序
- 5.3.8 压缩(前缀压缩)索引
- 5.3.9 冗余和重复索引
- 5.3.11 索引和锁
- 附录
五、创建高性能的索引
索引(在MySQL中也叫做“键(key)”) 是存储引擎用于快速找到记录的一种数据结构。
索引对于良好的性能非常关键。尤其是当表中的数据量越来越大时,索引对性能的影响愈发重要。
在数据量较小且负载较低时,不恰当的索引对性能的影响可能还不明显,但当数据量逐渐增大时,性能则会急剧下降。
索引优化应该是对查询性能优化最有效的手段了。索引能够轻易将查询性能提高几个数量级,“最优”的索引有时比一个“好的”索引性能要好两个数量级。
创建一个真正“最优”的索引经常需要重写查询。
5.1 索引基础
要理解MySQL中索引是如何工作的,最简单的方法就是去看看一本书的“索引”部分:
如果想在一本书中找到某个特定主题,一 般会先看书的‘索引”, 找到对应的页码。
在MySQL中,存储引擎用类似的方法使用索引,其先在索引中找到对应值,然后根据匹配的索引记录找到对应的数据行。
假如要运行下面的查询:
mysql> SELECT first_name FROM sakila.actor WHERE actor_id = 5;
如果在actor_id列上建有索引,则MySQL将使用该索引找到actor_id为5的行,也就是说,MySQL先在索引上按值进行查找,然后返回所有包含该值的数据行。
索引可以包含一个或多个列的值。如果索引包含多个列,那么列的顺序也十分重要,因为MySQL只能高效地使用索引的最左前缀列。
最左原则
创建一个包含两个列的索引,和创建两个只包含一列的索引是大不相同的,下面将详细介绍。
如果使用的是ORM,是否还需要关心索引?
是的,仍然需要理解索引,即使是使用对象关系映射(ORM)工具。
ORM工具能够生产符合逻辑的、合法的查询(多数时候),除非只是生成非常基本的查询(例如仅是根据主键查询),否则它很难生成适合索引的查询。
无论是多么复杂的ORM工具,在精妙和复杂的索引面前都是“浮云”。
5.1.1 索引的类型
索引有很多种类型,可以为不同的场景提供更好的性能。在MySQL中,索引是在存储引擎层而不是服务器层实现的。所以,并没有统一的索引标准:不同存储引擎的索引的工作方式并不一样,也不是所有的存储引擎都支持所有类型的索引。即使多个存储引擎支持同一种类型的索引,其底层的实现也可能不同。
B-Tree索引
当人们谈论索引的时候,如果没有特别指明类型,那多半说的是B-Tree索引,它使用B-Tree数据结构来存储数据。
大多数MySQL引擎都支持这种索引。
我们使用术语“B-Tree”,是因为MySQL在CREATE TABLE和其他语句中也使用该关键字。
不过,底层的存储引擎也可能使用不同的存储结构,例如,NDB集群存储引擎内部实际上使用了T-Tree结构存储这种索引,即使其名字是BTREE;
InnoDB则使用的是B+Tree,各种数据结构和算法的变种不在书的讨论范围之内。
存储引擎以不同的方式使用B-Tree索引,性能也各有不同,各有优劣。
例如,MyISAM使用前缀压缩技术使得索引更小,但InnoDB则按照原数据格式进行存储。再如MyISAM索引通过数据的物理位置引用被索引的行,而InnoDB则根据主键引用被索引的行。
B-Tree通常意味着所有的值都是按顺序存储的,并且每一个叶子页到根的距离相同。
图5-1展示了B-Tree索引的抽象表示,大致反映了InnoDB索引是如何工作的。MyISAM使用的结构有所不同,但基本思想是类似的。
B-Tree索引能够加快访问数据的速度,因为存储引擎不再需要进行全表扫描来获取需要的数据,取而代之的是从索引的根节点(图示并未画出)开始进行搜索。根节点的槽中存放了指向子节点的指针,存储引擎根据这些指针向下层查找。通过比较节点页的值和要查找的值可以找到合适的指针进入下层子节点,这些指针实际上定义了子节点页中值的上限和下限。最终存储引擎要么是找到对应的值,要么该记录不存在。
叶子节点比较特别,它们的指针指向的是被索引的数据,而不是其他的节点页(不同引擎的“指针”类型不同)。图5-1中仅绘制了一个节点和其对应的叶子节点,其实在根节点和叶子节点之间可能有很多层节点页。树的深度和表的大小直接相关。
B-Tree对索引列是顺序组织存储的,所以很适合查找范围数据。
例如,在一个基于文本域的索引树上,按字母顺序传递连续的值进行查找是非常合适的,所以像“找出所有以I到K开头的名字”这样的查找效率会非常高。
假设有如下数据表:
CREATE TABLE People(
last_name varchar(50) not null,
first_name varchar(50) not null,
dob date not null,
gender enum('m','f')not null,
key(last_name, first_name, dob)
);
对于表中的每一行数据,索引中包含了last_name、 first_name 和dob列的值,图5-2显示了该索引是如何组织数据的存储的。
请注意,索引对多个值进行排序的依据是CREATE TABLE语句中定义索引时列的顺序。看一下最后两个条目,两个人的姓和名都-样,则根据他们的出生日期来排列顺序。
可以使用B-Tree索引的查询类型。B-Tree 索引适用于全键值、键值范围或键前缀查找。
其中键前缀查找只适用于根据最左前缀的查找生。前面所述的索引对如下类型的查询有效。
- 全值匹配
全值匹配指的是和索引中的所有列进行匹配,例如前面提到的索引可用于查找姓名为Cuba Allen、出生于1960-01-01 的人。
- 匹配最左前缀
前面提到的索引可用于查找所有姓为Allen的人,即只使用索引的第一列。
- 匹配列前缀
也可以只匹配某–列的值的开头部分。例如前面提到的索引可用于查找所有以J开头的姓的人。这里也只使用了索引的第一列。
- 匹配范围值
例如前面提到的索引可用于查找姓在Allen和Barrymore之间的人。这里也只使用了索引的第一列。
- 精确匹配某一列并范围匹配另外一列
前面提到的索引也可用于查找所有姓为Allen,并且名字是字母K开头的人。即第一列last_ name 全匹配,第二列first_name 范围匹配。
- 只访问索引的查询
B-Tree通常可以支持“只访问索引的查询”,即查询只需要访问索引,而无须访问数据行。也叫做 覆盖索引
这种访问不需要回表,因此特别快
下面是一些关于B-Tree索引的限制:
- 如果不是按照索引的最左列开始查找,则无法使用索引。
例如上面例子中的索引无法用于查找名字为Bill的人,也无法查找某个特定生日的人,因为这两列都不是最左数据列。类似地,也无法查找姓氏以某个字母结尾的人。
- 不能跳过索引中的列。
也就是说,前面所述的索引无法用于查找姓为Smith并且在某个特定日期出生的人。如果不指定名(first_name),则MySQL只能使用索引的第一列。
- 如果查询中有某个列的范围查询,则其右边所有列都无法使用索引优化查找。
例如有查询
WHERE last_name=' Smith' AND first_name LIKE 'J%' AND dob = '1976-12-23'
这个查询只能使用索引的前两列,因为这里LIKE是一个范围条件(但是服务器可以把其余列用于其他目的)。如果范围查询列值的数量有限,那么可以通过使用多个等于条件来代替范围条件。
索引下推(Index Condition Pushdown,简称ICP),是MySQL5.6版本的新特性,它能减少回表查询次数,提高查询效率。
用于联合索引,在有列需要范围索引时,让其后的列依然可以使用索引。也就是不会有这个问题了。
Mysql:好好的索引,为什么要下推?
索引列的顺序十分重要:这些限制都和索引列的顺序有关。在优化性能的时候,可能需要使用相同的列但顺序不同的索引来满足不同类型的查询需求。
也有些限制并不是B-Tree本身导致的,而是MySQL优化器和存储引擎使用索引的方式导致的,这部分限制在未来的版本中可能就不再是限制了。
哈希索引
哈希索引(hashindex)基于哈希表实现,只有精确匹配索引所有列的查询才有效。
对于每一行数据,存储引擎都会对所有的索引列计算-一个哈希码(hash code),哈希码是一个较小的值,并且不同键值的行计算出来的哈希码也不一样。哈希索引将所有的哈希码存储在索引中,同时在哈希表中保存指向每个数据行的指针。
在MySQL中,只有Memory引擎显式支持哈希索引。这也是Memory引擎表的默认索引类型,Memory引擎同时也支持B-Tree索引。值得- -提的是,Memory引擎是支持非.唯一哈希索引的,这在数据库世界里面是比较与众不同的。如果多个列的哈希值相同,索引会以链表的方式存放多个记录指针到同-一个哈希条目中。
下面来看一个例子。假设有如下表:
CREATE TABLE testhash (
fname VARCHAR(50) NOT NULL,
lname VARCHAR(50) NOT NULL,
KEY USING HASH(fname)
) ENGINE=MEMORY;
表中有如下数据
假设索引使用假想的哈希函数f(),它返回下面的值(都是示例数据,非真实数据) :
索引结构如下:
注意每个槽的编号是顺序的,但是数据行不是。现在,来看如下查询:
mysql> SELECT lname FROM testhash WHERE fname='Peter';
MySQL先计算’Peter’ 的哈希值,并使用该值寻找对应的记录指针。因为f(‘Peter’)=8784,所以MySQL在索引中查找8784,可以找到指向第3行的指针,最后一步是比较第三行的值是否为’Peter’, 以确保就是要查找的行。
因为索引自身只需存储对应的哈希值,所以索引的结构十分紧凑,这也让哈希索引查找的速度非常快。
然而,哈希索引也有它的限制:
-
哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能的影响并不明显。
-
哈希索引数据并不是按照索引值顺序存储的,所以也就无法用于排序。
-
哈希索引也不支持部分索引列匹配查找,因为哈希索引始终是使用索引列的 全部内容 来计算哈希值的。例如,在数据列(A,B) 上建立哈希索引,如果查询只有数据列A,则无法使用该索引。
-
哈希索引只支持等值比较查询,包括=、IN()、 < => (注意<>和<=>是不同的操作)。也不支持任何范围查询,例如 WHERE price > 100。
-
访问哈希索引的数据非常快,除非有很多哈希冲突(不同的索引列值却有相同的哈希值)。当出现哈希冲突的时候,存储引擎必须遍历链表中所有的行指针,逐行进行比较,直到找到所有符合条件的行。
-
如果哈希冲突很多的话,一些索引维护操作的代价也会很高。例如,如果在某个选择性很低(哈希冲突很多)的列上建立哈希索引,那么当从表中删除一行时,存储引擎需要遍历对应哈希值的链表中的每一行,找到并删除对应行的引用,冲突越多,代价越大。
什么是Hash冲突?如何解决Hash冲突?
因为这些限制,哈希索引只适用于某些特定的场合。而一旦适合哈希索引,则它带来的性能提升将非常显著。举个例子,在数据仓库应用中有一种经典的“星型”schema,需要关联很多查找表,哈希索引就非常适合查找表的需求。
星型模型与雪花模型(Star Schema and Snowflake Schema)
InnoDB引擎有一个特殊的功能叫做 “自适应哈希索引(adaptive hash index)” 。
当InnoDB注意到某些索引值被使用得非常频繁时,它会在内存中基于B-Tree索引之上再创建一个哈希索引,这样就让B-Tree索引也具有哈希索引的一些优点,比如快速的哈希查找。
这是一个完全自动的、内部的行为,用户无法控制或者配置,不过如果有必要,完全可以关闭该功能。
创建自定义哈希索引。如果存储引擎不支持哈希索引,则可以模拟像InnoDB一样创建哈希索引,这可以享受一些哈希索引的便利,例如只需要很小的索引就可以为超长的键创建索引。
思路很简单:
- 在B-Tree基础上创建一个伪哈希索引。
这和真正的哈希索引不是一回事,因为还是使用B-Tree进行查找,但是它使用哈希值而不是键本身进行索引查找。你需要做的就是在查询的WHERE子句中手动指定使用哈希函数。
下面是一个实例,例如需要存储大量的URL,并需要根据URL进行搜索查找。如果使用B-Tree来存储URL,存储的内容就会很大,因为URL本身都很长。正常情况下会有如下查询:
mysql> SELECT id FROM url WHERE url="http://ww.mysql.com";
若删除原来URL列上的索引,而新增一个被索引的url_ crc列,使用CRC32做哈希,就可以使用下面的方式查询:
mysql> SELECT id FROM url WHERE url="http://www.mysql.com"
->AND url_ crc=CRC32("http://www.mysq1.com");
这样做的性能会非常高,因为MySQL优化器会使用这个选择性很高而体积很小的基于url_ crc列的索引来完成查找(在上面的案例中,索引值为1560514994)。即使有多个记录有相同的索引值,查找仍然很快,只需要根据哈希值做快速的整数比较就能找到索引条目,然后一一比较返回对应的行。
另外一种方式就是对完整的URL字符串做索引, 那样会非常慢。
这样实现的缺陷是需要维护哈希值。可以手动维护,也可以使用触发器实现。下面的案例演示了触发器如何在插入和更新时维护url_crc列。首先创建如下表:
CREATE TABLE pseudohash (
id int unsigned NOT NULL auto_ increment,
url varchar(255) NOT NULL,
url_ _CrC int unsigned NOT NULL DEFAULT 0,
PRIMARY KEY(id)
然后创建触发器。先临时修改一下语 句分隔符,这样就可以在触发器定义中使用分号:
DELIMITER
CREATE TRIGGER pseudohash crc_ins BEFORE
INSERT ON pseudohash FOR EACH ROW BEGIN
SET NEW.url_crC=crC32(NEW.ur1);
END;
CREATE TRIGGER pseudohash CrC_upd BEFORE UPDATE ON pseudohash FOR EACH ROW BEGIN
SET NEW.url _crC=crC32(NEW.ur1);
END;
DELIMITER ;
如果采用这种方式,记住不要使用SHA1()和MD5()作为哈希函数。因为这两个函数计算出来的哈希值是非常长的字符串,会浪费大量空间,比较时也会更慢。
SHA1() 和MD5()是强加密函数,设计目标是最大限度消除冲突,但这里并不需要这样高的要求。简单哈希函数的冲突在一个可以接受的范围,同时又能够提供更好的性能。
如果数据表非常大,CRC32()会出现大量的哈希冲突,则可以考虑自己实现一个简单的64位哈希函数。这个自定义函数要返回整数,而不是字符串。一个简单的办法可以使用MD5()函数返回值的一部分来作为自定义哈希函数。
空间数据索引(R-Tree)
MyISAM表支持空间索引,可以用作地理数据存储。和B-Tree索引不同,这类索引无须前缀查询。空间索引会从所有维度来索引数据。
查询时,可以有效地使用任意维度来组合查询。必须使用MySQL的GIS相关函数如MBRCONTAINS()等来维护数据。MySQL的GIS支持并不完善,所以大部分人都不会使用这个特性。开源关系数据库系统中对GIS的解决方案做得比较好的是PostgreSQL的PostGIS
全文索引
全文索引是一种特殊类型的索引,它查找的是文本中的关键词,而不是直接比较索引中的值。
全文搜索和其他几类索引的匹配方式完全不一样。它有许多需要注意的细节,如停用词、词干和复数、布尔搜索等。
全文索引更类似于搜索引擎做的事情,而不是简单的WHERE条件匹配。
在相同的列上同时创建全文索引和基于值的B-Tree索引不会有冲突,全文索引适用于MATCH AGAINST 操作,而不是普通的WHERE条件操作。
其他索引类别
还有很多第三方的存储引擎使用不同类型的数据结构来存储索引。
例如TokuDB使用 分形树索引(fractal tree index) ,这是一类较新开发的数据结构,既有B-Tree的很多优点,也避免了B-Tree的一些缺点。多数情况下,针对InnoDB的讨论也都适用于TokuDB。
ScaleDB使用Patricia tries (这个词不是拼写错误),其他一些存储引擎技术如InfiniDB和Infobright则使用了一些特殊的数据结构来优化某些特殊的查询。
还有Elastic Search 使用的倒排索引
5.2 索引的优点
索引可以让服务器快速地定位到表的指定位置。但是这并不是索引的唯一作用, 到目前为止可以看到,根据创建索引的数据结构不同,索引也有一些其他的附加作用。
最常见的B-Tree索引,按照顺序存储数据,所以MySQL可以用来做ORDER BY 和GROUPBY操作。因为数据是有序的,所以B-Tree也就会将相关的列值都存储在一起。最后,因为索引中存储了实际的列值,所以某些查询只使用索引就能够完成全部查询。据此特性,总结下来索引有如下三个优点:
- 索引大大减少了服务器需要扫描的数据量。
- 索引可以帮助服务器避免排序和临时表。
- 索引可以将随机I/O变为顺序I/O。
Lahdenmaki和Leach介绍了如何评价一个索引是否适合某个查询的 “三星系统”(three-star system) :
- 索引将相关的记录放到一起则获得一星;
- 如果索引中的数据顺序和查找中的排列顺序一致则获得二星;
- 如果索引中的列包含了查询中需要的全部列则获得“三星”。
索引是最好的解决方案吗?
索引并不总是最好的工具。
总的来说,只有当索引帮助存储引擎快速查找到记录带来的好处大于其带来的额外工作时,索引才是有效的。
-
对于非常小的表,大部分情况下简单的全表扫描更高效。
-
对于中到大型的表,索引就非常有效。
-
但对于特大型的表,建立和使用索引的代价将随之增长。这种情况下,则需要一种技术可以直接区分出查询需要的一组数据,而不是一条记录一条记录地匹配。例如可以使用分区技术。
-
如果表的数量特别多,可以建立一个元数据信息表,用来查询需要用到的某些特性。例如执行那些需要聚合多个应用分布在多个表的数据的查询,则需要记录。哪个用户的信息存储在哪个表中”的元数据,这样在查询时就可以直接忽略那些不包含指定用户信息的表。对于大型系统,这是一个常用的技巧。
5.3 高性能的索引策略
正确地创建和使用索引是实现高性能查询的基础。
高效地选择和使用索引有很多种方式,其中有些是针对特殊案例的优化方法,有些则是针对特定行为的优化。
5.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) - TO_ DAYS(date_col) <= 10;
5.3.2 前缀索引和索引选择性
有时候需要索引很长的字符列,这会让索引变得大且慢。
一个策略是前面提到过的模拟哈希索引。通常可以索引开始的部分字符,这样可以大大节约索引空间,从而提高索引效率。
但这样也会降低索引的选择性。索引的选择性是指,不重复的索引值(也称为基数,cardinality)和数据表的记录总数(#T) 的比值,范围从1/#T到1之间。
索引的选择性越高则查询效率越高,因为选择性高的索引可以让MySQL在查找时过滤掉更多的行。
唯一索引的选择性,这是最好的索引选择性,性能也是最好的。
一般情况下某个列前缀的选择性也是足够高的,足以满足查询性能。对于BLOB、TEXT 或者很长的VARCHAR类型的列,必须使用前缀索引,因为MySQL不允许索引这些列的完整长度。
诀窍在于要选择足够长的前缀以保证较高的选择性,同时又不能太长(以便节约空间)。
前缀应该足够长,以使得前缀索引的选择性接近于索引整个列。换句话说,前缀的“基数”应该接近于完整列的“基数”。
为了决定前缀的合适长度,需要找到最常见的值的列表,然后和最常见的前缀列表进行比较。在示例数据库Sakila中并没有合适的例子,所以我们从表city中生成一个示例表,这样就有足够的数据进行演示:
CREATE TABLE sakila.city_demo(city VARCHAR(50) NOT NULL);
INSERT INTO sakila.city_demo(city) SELECT city FROM sakila.city;
-- Repeat the next statement five times:
INSERT INTO sakila.city_demo(city) SELECT city FROM sakila.city_demo;
-- Now randomize the distribution (inefficiently but conveniently):
UPDATE sakila.city_demo
SET city = (SELECT city FROM sakila.city ORDER BY RAND() LIMIT 1);
现在我们有了示例数据集。
首先,我们找到最常见的城市列表:
mysql> SELECT COUNT(*) AS cnt,city
-> FROM sakila.city_demo GROUP BY city ORDER BY cnt DESC LIMIT 10;
mysql> SELECT COUNT(*) AS cnt,city
-> FROM sakila.city_demo GROUP BY city ORDER BY cnt DESC LIMIT 10;
注意到,上面每个值都出现了45 ~ 65次。现在查找到最频繁出现的城市前缀,先从3个前缀字母开始:
mysql> SELECT COUNT(*) AS cnt, LEFT(city, 3) AS pref
-> FROM sakila.city_demo GROUP BY pref ORDER BY cnt DESC LIMIT 10;
每个前缀都比原来的城市出现的次数更多,因此唯一前缀比唯一城市要少得多。
然后我们增加前缀长度,直到这个前缀的选择性接近完整列的选择性。经过实验后发现前缀长度为7时比较合适:
mysql> SELECT COUNT(*) AS cnt, LEFT(city, 7) AS pref
-> FROM sakila.city_demo GROUP BY pref ORDER BY cnt DESC LIMIT 10;
计算合适的前缀长度的另外一个办法就是计算完整列的选择性,并使前缀的选择性接近于完整列的选择性。
下面显示如何计算完整列的选择性:
mysql> SELECT COUNT(DISTINCT city)/COUNT(*) FROM sakila.city_demo;
通常来说(尽管也有例外情况),这个例子中如果前缀的选择性能够接近0.031,基本上就可用了。可以在一个查询中针对不同前缀长度进行计算,这对于大表非常有用。
下面给出了如何在同一个查询中计算不同前缀长度的选择性:
mysql> SELECT COUNT(DISTINCT LEFT(city, 3))/COUNT(*) AS sel3,
->COUNT(DISTINCT LEFT(city, 4))/COUNT(*) AS sel4,
->COUNT(DISTINCT LEFT(city, 5))/COUNT(*) AS sel5,
->COUNT(DISTINCT LEFT(city, 6))/COUNT(*) AS se16,
->COUNT(DISTINCT LEFT(city, 7))/COUNT(*) AS sel7,
-> FROM sakila.city demo;
只看平均选择性是不够的,也有例外的情况,需要考虑最坏情况下的选择性。平均选择性会让你认为前缀长度为4或者5的索引已经足够了,但如果数据分布很不均匀,可能就会有陷阱。
在上面的示例中,已经找到了合适的前缀长度,下 面演示一下如何创建前缀索引:
mysql> ALTER TABLE sakila.city_demo ADD KEY (city(7));
前缀索引是一种能使索引更小、更快的有效办法,但另一方面也有其缺点: MySQL无法使用前缀索引做ORDER BY和GROUP BY,也无法使用前缀索引做覆盖扫描。
一个常见的场景是针对很长的十六进制唯一ID使用前缀索引。
在前面的章节中已经讨论了很多有效的技术来存储这类ID信息,但如果使用的是打包过的解决方案,因而无法修改存储结构,那该怎么办?
- 例如使用vBulletin或者其他基于MySQL的应用在存储网站的会话(SESSION)时,需要在一个很长的十六进制字符串上创建索引。
- 此时如果采用长度为8的前缀索引通常能显著地提升性能,并且这种方法对上层应用完全透明。
有时候后缀索引(suffix index)也有用途(例如,找到某个域名的所有电子邮件地址)。MySQL原生并不支持反向索引,但是可以把字符串反转后存储,并基于此建立前缀索引。可以通过触发器来维护这种索引。
5.3.3 多列索引
很多人对多列索引的理解都不够。一个常见的错误就是,为每个列创建独立的索引,或者按照错误的顺序创建多列索引。
为每个列创建独立的索引,从SHOW CREATE TABLE中很容易看到这种情况:
CREATE TABLE t (
C1 INT,
C2 INT,
C3 INT,
KEY(c1),
KEY(c2),
KEY(c3)
);
这种索引策略,一般是由于人们听到一些专家诸如“把WHERE条件里面的列都建上索引”,这样模糊的建议导致的。
实际上这个建议是非常错误的。这样一来最好的情况下也只能是“一星”索引,其性能比起真正最优的索引可能差几个数量级。
有时如果无法设计一个“三星”索引,那么不如忽略掉WHERE子句,集中精力优化索引列的顺序,或者创建一个全覆盖索引。
在多个列上建立独立的单列索引大部分情况下并不能提高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 = 10 or film_id = 1;
在老的MySQL版本中, MySQL对这个查询会使用全表扫描。除非改写成如下的两个查询UNION的方式:
mysql> SELECT film_id, actor_id FROM sakila.film_actor WHERE actor_id = 1
-> UNION ALL
-> SELECT film_id, actor_id FROM sakila.fi1m_actor WHERE film_id = 1
->AND actor_id < 1;
但在MySQL5.0和更新的版本中,查询能够同时使用这两个单列索引进行扫描,并将结果进行合并。
这种算法有三个变种:
- OR条件的联合(union)
- AND条件的相交(intersection)
- 组合前两种情况的联合及相交
下面的查询就是使用了两个索引扫描的联合,通过EXPLAIN中的Extra列可以看到这点:
MySQL会使用这类技术优化复杂查询,所以在某些语句的Ext ra列中还可以看到嵌套操作。
索引合并策略有时候是一种优化的结果,但实际上更多时候说明了表上的索引建得很糟糕:
- 当出现服务器对多个索引做相交操作时(通常有多个AND条件),通常意味着需要一个包含所有相关列的多列索引,而不是多个独立的单列索引。
- 当服务器需要对多个索引做联合操作时(通常有多个OR条件),通常需要耗费大量CPU和内存资源在算法的缓存、排序和合并操作.上。特别是当其中有些索引的选择性不高,需要合并扫描返回的大量数据的时候。
- 更重要的是,优化器不会把这些计算到“查询成本”(cost) 中,优化器只关心随机页面读取。这会使得查询的成本被“低估”,导致该执行计划还不如直接走全表扫描。
这样做不但会消耗更多的CPU和内存资源,还可能会影响查询的并发性,但如果是单独运行这样的查询则往往会忽略对并发性的影响。通常来说,还不如像在MySQL4.1或者更早的时代一样,将查询改写成UNION的方式往往更好。
如果在EXPLAIN中看到有索引合并,应该好好检查一下查询和表的结构,看是不是已经是最优的。也可以通过参数optimizer_switch来关闭索引合并功能。也可以使用IGNORE INDEX 提示让优化器忽略掉某些索引。
5.3.4 选择合适的索引列顺序
我们遇到的最容易引起困惑的问题就是索引列的顺序。
正确的顺序依赖于使用该索引的查询,并且同时需要考虑如何更好地满足排序和分组的需要
书中的内容适用于B-Tree索引
哈希或者其他类型的索引并不会像B-Tree索引一样按顺序存储数据
在一个多列B-Tree索引中,索引列的顺序意味着索引首先按照最左列进行排序,其次是第二列,等等。所以,索引可以按照升序或者降序进行扫描,以满足精确符合列顺序的ORDER BY、GROUP BY和DISTINCT等子句的查询需求。所以多列索引的列顺序至关重要。
在Lahdenmaki和Leach的“三星索引”系统中,列顺序也决定了一个索引是否能够成为一个真正的“三星索引”。
对于如何选择索引的列顺序有一个经验法则:
- 将选择性最高的列放到索引最前列。
虽然该法则有用,但通常不如 避免随机IO和排序 那么重要,考虑问题需要更全面
场景不同则选择不同,没有一个放之四海皆准的法则。这里只是说明,这个经验法则可能没有你想象的重要。
当不需要考虑排序和分组时,将选择性最高的列放在前面通常是很好的。
这时候索引的作用只是用于优化WHERE条件的查找。在这种情况下,这样设计的索引确实能够最快地过滤出需要的行,对于在WHERE子句中只使用了索引部分前缀列的查询来说选择性也更高。
然而,性能不只是依赖于所有索引列的选择性(整体基数),也和查询条件的具体值有关,也就是和值的分布有关。这和前面介绍的选择前缀的长度需要考虑的地方一样。
可能需要根据那些运行频率最高的查询来调整索引列的顺序,让这种情况下索引的选择性最高。以下面的查询为例:
SELECT * FROM payment WHERE staff.id = 2 AND customer._id = 584;
是应该创建一个(staff_id, customer_ id) 索引还是应该颠倒一下顺序?
可以跑一些查询来确定在这个表中值的分布情况,并确定哪个列的选择性更高。
先用下面的查询预测一下,看看各个WHERE条件的分支对应的数据基数有多大:
MySQl中的\g和\G
根据前面的经验法则,应该将索引列customer_id放到前面,因为对应条件值的customer_ id数量更小。
我们再来看看对于这个customer_ id 的条件值,对应的staff_id列的选择性如何:
这样做有一个地方需要注意,查询的结果非常依赖于选定的具体值。如果按上述办法优化,可能对其他一些条件值的查询不公平,服务器的整体性能可能变得更糟,或者其他某些查询的运行变得不如预期。
如果是从诸如pt-query-digest这样的工具的报告中提取“最差”查询,那么再按上述办法选定的索引顺序往往是非常高效的。
如果没有类似的具体查询来运行,那么最好还是按经验法则来做,因为经验法则考虑的是全局基数和选择性,而不是某个具体查询:
customer_id的选择性更高,所以答案是将其作为索引列的第一列:
当使用前缀索引的时候,在某些条件值的基数比正常值高的时候,问题就来了。
mysql> ALTER TABLE payment ADD KEY(customer_id, staff_id);
例如,在某些应用程序中,对于没有登录的用户,都将其用户名记录为“guset”, 在记录用户行为的 会话(session) 表和其他记录用户活动的表中“guest" 就成为了一个特殊用户ID。一旦查询涉及这个用户,那么和对于正常用户的查询就大不同了,因为通常有很多会话都是没有登录的。
系统账号也会导致类似的问题。一个应用通常都有一个特殊的管理员账号,和普通账号不同,它并不是一个具体的用户,系统中所有的其他用户都是这个用户的好友,所以系统往往通过它向网站的所有用户发送状态通知和其他消息。这个账号的巨大的好友列表很容易导致网站出现服务器性能问题。
这实际上是一个非常典型的问题。任何的异常用户,不仅仅是那些用于管理应用的设计糟糕的账号会有同样的问题。
那些拥有大量好友、图片、状态、收藏的用户,也会有前下面是一个真实案例,在一个用户分享购买商品和购买经验的论坛上,这个特殊表上的查询运行得非常慢:
mysq1> SELECT COUNT(DISTINCT thread_Id) AS COUNT_VALUE
-> FROM Message
-> WHERE (groupId = 10137) AND (userId = 1288826) AND (anonymous = 0)
-> ORDER BY priority DESC, modifiedDate DESC
这个查询看似没有建立合适的索引,所以客户咨询是否可以优化。EXPLAIN的结果如下:
MySQL为这个查询选择了索引(groupId, userId),如果不考虑列的基数,这看起来是一个非常合理的选择。
但如果考虑一下 user ID和group ID条件匹配的行数,可能就会有不同的想法了:
从上面的结果来看符合组(groupId) 条件几乎满足表中的所有行,符合用户(userId)条件的有130万条记录一也就是说索引基本上没什么用。
因为这些数据是从其他应用中迁移过来的,迁移的时候把所有的消息都赋予了管理员组的用户。这个案例的解决办法是修改应用程序代码,区分这类特殊用户和组,禁止针对这类用户和组执行这个查询。
从这个小案例可以看到经验法则和推论在多数情况是有用的,但要注意不要假设平均情况下的性能也能代表特殊情况下的性能,特殊情况可能会摧毁整个应用的性能。
最后,尽管关于选择性和基数的经验法则值得去研究和分析,但一定要记住别忘了WHERE子句中的排序、分组和范围条件等其他因素,这些因素可能对查询的性能造成非常大的影响。
5.3.5 聚簇索引
聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。
具体的细节依赖于其实现方式,但InnoDB的聚簇索引实际上在同一个结构中保存了B-Tree 索引和数据行。
当表有聚簇索引时,它的数据行实际上存放在索引的 叶子页(leaf page) 中。
术语**“聚簇”** 表示数据行和相邻的键值紧凑地存储在一起生8。因为无法同时把数据行存放在两个不同的地方,所以一个表 只能有一个聚簇索引
覆盖索引可以模拟多个聚簇索引的情况。
因为是存储引擎负责实现索引,因此不是所有的存储引擎都支持聚簇索引。本节我们主要关注InnoDB,但是这里讨论的原理对于任何支持聚簇索引的存储引擎都是适用的。
图5-3展示了聚簇索引中的记录是如何存放的。注意到,叶子页包含了行的全部数据,但是节点页只包含了索引列。在这个案例中,索引列包含的是整数值。
一些数据库服务器允许选择哪个索引作为聚簇索引。
InnoDB 将通过主键聚集数据,这也就是说图5-3中的“被索引的列”就是主键列。
如果没有定义主键,InnoDB 会选择一个唯一的非空索引代替。如果没有这样的索引,InnoDB会隐式定义一个主键来作为聚簇索引。InnoDB只聚集在同一个页面中的记录。包含相邻键值的页面可能会相距甚远。
聚簇主键可能对性能有帮助,但也可能导致严重的性能问题。所以需要仔细地考虑聚簇索引,尤其是将表的存储引擎从InnoDB改成其他引擎的时候( 反过来也一样)。
聚集的数据有一些重要的优点:
- 可以把相关数据保存在一起。例如实现电子邮箱时,可以根据用户ID来聚集数据,这样只需要从磁盘读取少数的数据页就能获取某个用户的全部邮件。如果没有使用聚簇索引,则每封邮件都可能导致一次 磁盘I/O。
- 数据访问更快。聚簇索引将索弓|和数据保存在同一个B-Tree中,因此从聚簇索引中获取数据通常比在非聚簇索引中查找要快。
- 使用覆盖索引扫描的查询可以直接使用页节点中的主键值。
聚簇索引也有一些缺点:
- 聚簇数据最大限度地提高了I/O密集型应用的性能,但如果数据全部都放在内存中,则访问的顺序就没那么重要了,聚簇索引也就没什么优势了。
- 插入速度严重依赖于插入顺序。按照主键的顺序插入是加载数据到InnoDB表中速度最快的方式。但如果不是按照主键顺序加载数据,那么在加载完成后最好使用OPTIMIZE TABLE命令重新组织一下表。
OPTIMIZE TABLE
- 更新聚簇索引列的代价很高,因为会强制InnoDB将每个被更新的行移动到新的位置。
- 基于聚簇索引的表在插入新行,或者主键被更新导致需要移动行的时候,可能面临“页分裂(page split)” 的问题。当行的主键值要求必须将这一行插人到某个已满的页中时,存储引擎会将该页分裂成两个页面来容纳该行,这就是一次页分裂操作。页分裂会导致表占用更多的磁盘空间。
- 聚簇索引可能导致全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致数据存储不连续的时候。
- 二级索引 (非聚簇索引)可能比想象的要更大,因为在二级索引的叶子节点包含了引用行的主键列。
- 二级索引访问需要两次索引查找,而不是一次。(回表)
二级索引需要两次索引查找
保存的“行指针”的实质。要记住,二级索引叶子节点保存的不是指向行的物理位置的指针,而是行的主键值。
这意味着通过二级索引查找行,存储引擎需要找到二级索引的叶子节点获得对应的主键值,然后根据这个值去聚簇索引中查找到对应的行。
这里做了重复的工作:两次B-Tree查找而不是一次。
对于InnoDB,自适应哈希索引能够减少这样的重复工作。
5.3.6 InnoDB和MyISAM的数据分布对比
聚簇索引和非聚簇索引的数据分布有区别,以及对应的主键索引和二级索引的数据分布也有区别,通常会让人感到困扰和意外。来看看InnoDB和MyISAM是如何存储下面这个表的:
CREATE TABLE layout_ test (
col1 int NOT NULL,
col2 int NOT NULL,
PRIMARY KEY(col1),
KEY(co12)
);
假设该表的主键取值为1 ~ 10 000,按照随机顺序插人并使用0PTIMIZE TABLE 命令做了优化。换句话说,数据在磁盘上的存储方式已经最优,但行的顺序是随机的。列col2的值是从1~100之间随机赋值,所以有很多重复的值。
MyISAM的数据分布
MyISAM按照数据插入的顺序存储在磁盘上,如图5-4所示。
在行的旁边显示了行号,从0开始递增。因为行是定长的,所以MyISAM可以从表的开头跳过所需的字节找到需要的行(MyISAM并不总是使用图5-4中的“行号”,而是根据定长还是变长的行使用不同策略)。
这种分布方式很容易创建索引。下面显示的一系列图,隐藏了页的物理细节,只显示索引中的“节点”,索引中的每个叶子节点包含“行号”。图5-5显示了表的主键。
这里忽略了一些细节,例如前一个B-Tree节点有多少个内部节点,不过这并不影响对非聚簇存储引擎的基本数据分布的理解。
图5-6显示了col2 列上的索引。
事实上,MyISAM中主键索引和其他索引在结构上没有什么不同。主键索引就是一个名为PRIMARY的唯一非空索引。
InnoDB的数据分布
因为InnoDB支持聚簇索引,所以使用非常不同的方式存储同样的数据。InnoDB 以如图5-7所示的方式存储数据。
InnoDB的聚簇索引包含了了整个表,而不是只有索引。
因为在InnoDB中,聚簇索引“就是”表,所以不像MyISAM那样需要独立的行存储。
-
聚簇索引的每一个叶子节点都包含了主键值、事务ID、用于事务和MVCC的回滚指针以及所有的剩余列(在这个例子中是col2)。如果主键是一个列前缀索引,InnoDB 也会包含完整的主键列和剩下的其他列。
-
InnoDB的二级索弓|和聚簇索引很不相同。InnoDB 二级索引的叶子节点中存储的不是“行指针”,而是主键值,并以此作为指向行的“指针”。
- 这样的策略减少了当出现行移动或者数据页分裂时二级索引的维护工作。
- 使用主键值当作指针会让二级索引占用更多的空间,换来的好处是,InnoDB在移动行时无须更新二级索引中的这个“指针”。
图5-8显示了示例表的col2索引。每一个叶子节点都包含了索引列(这里是col2),紧接着是主键值(col1)。
图5-8展示了B-Tree的叶子节点结构,但我们故意省略了非叶子节点这样的细节。
InnoDB的非叶子节点包含了索引列和一个指向下级节点的指针(下一级节点可以是非叶子节点,也可以是叶子节点)。这对聚簇索引和二级索引都适用。
图5-9是描述InnoDB和MyISAM如何存放表的抽象图。从图5-9中可以很容易看出InnoDB和MyISAM保存数据和索引的区别。
在InnoDB表中按主键顺序插入行
如果正在使用InnoDB表并且没有什么数据需要聚集,那么可以定义一个 代理键(surrogate key) 作为主键,这种主键的数据应该和应用无关,最简单的方法是使用AUTO_ INCREMENT 自增列。
这样可以保证数据行是按顺序写入,对于根据主键做关联操作的性能也会更好。
最好避免随机的(不连续且值的分布范围非常大)聚簇索引,特别是对于I/O密集型的应用。
例如,从性能的角度考虑,使用UUID来作为聚簇索引则会很糟糕:它使得聚簇索引的插入变得完全随机,这是最坏的情况,使得数据没有任何聚集特性。
顺序的主键什么时候会造成更坏的结果?
对于高并发工作负载,在InnoDB中按主键顺序插入可能会造成明显的争用。
主键的上界会成为 “热点” 。
-
因为所有的插入都发生在这里,所以并发插入可能导致间隙锁竞争。
-
另一个热点可能是AUTO_ INCREMENT 锁机制
如果遇到这个问题,则可能需要考虑重新设计表或者应用,或者更改innodb_autoinc_lock_mode 配置。
自增主键不连续?了解下Mysql的innodb_autoinc_lock_mode
如果你的服务器版本还不支持innodb_autoinc_lock_mode 参数,可以升级到新版本的InnoDB,可能对这种场景会工作得更好。
5.3.6 覆盖索引
如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为“ 覆盖索引"。
覆盖索引是非常有用的工具,能够极大地提高性能。考虑一下如果查询只需要扫描索引而无须回表,会带来多少好处:
- 索引条目通常远小于数据行大小,所以如果只需要读取索引,那MySQL就会极大地减少数据访问量。这对缓存的负载非常重要,因为这种情况下响应时间大部分花费在数据拷贝上。覆盖索引对于I/O密集型的应用也有帮助,因为索引比数据更小,更容易全部放入内存中(这对于MyISAM尤其正确,因为MyISAM能压缩索引以变得更小)。
- 因为索引是按照列值顺序存储的(至少在单个页内是如此),所以对于I/O密集型的范围查询会比随机从磁盘读取每一行数据的I/O要少得多。对于某些存储引擎,例如MyISAM和Percona XtraDB,甚至可以通过OPTIMIZE命令使得索引完全顺序排列,这让简单的范围查询能使用完全顺序的索引访问。
- 一些存储引擎如MyISAM在内存中只缓存索引,数据则依赖于操作系统来缓存,因此要访问数据需要一次系统调用。这可能会导致严重的性能问题,尤其是那些系统调用占了数据访问中的最大开销的场景。
- 由于InnoDB的聚簇索引,覆盖索引对InnoDB表特别有用。InnoDB 的二级索引在叶子节点中保存了行的主键值,所以如果二级主键能够覆盖查询,则可以避免对主键索引的二次查询。
不是所有类型的索引都可以成为覆盖索引。
- 覆盖索引必须要存储索引列的值,而哈希索引、空间索引和全文索引等都不存储索引列的值,所以MySQL只能使用B-Tree索引做覆盖索引。
不同的存储引擎实现覆盖索引的方式也不同,而且不是所有的引擎都支持覆盖索引(在写作本书时,Memory 存储引擎就不支持覆盖索引)。
当发起一个被索引覆盖的查询(也叫做索引覆盖查询)时,在EXPLAIN的Extra列可以看到“Using index”的信息。
索引覆盖查询还有很多陷阱可能会导致无法实现优化
MySQL查询优化器会在执行查询前判断是否有一个索引能进行覆盖。
假设索引覆盖了WHERE条件中的字段,但不是整个查询涉及的字段。
- 如果条件为假(false), MySQL 5.5和更早的版本也总是会回表获取数据行,尽管并不需要这一行且最终会被过滤掉。
请查看如下例子:
-
没有任何索引能够覆盖这个查询。因为查询从表中选择了所有的列,而没有任何索引覆盖了所有的列。
- 不过,理论上MySQL还有一个捷径可以利用:WHERE条件中的列是有索引可以覆盖的,因此MySQL可以使用该索引找到对应的actor并检查title是否匹配,过滤之后再读取需要的数据行。
-
MySQL不能在索引中执行LIKE操作。这是底层存储引擎API的限制,MySQL 5.5和更早的版本中只允许在索引中做简单比较操作(例如等于、不等于以及大于)。MySQL能在索引中做最左前缀匹配的LIKE比较,因为该操作可以转换为简单的比较操作,但是如果是通配符开头的LIKE查询,存储引擎就无法做比较匹配。这种情况下,MySQL服务器只能提取数据行的值而不是索引值来做比较。
也有办法可以解决上面说的两个问题,需要重写查询并巧妙地设计索引。先将索引扩展至覆盖三个数据列(artist, title, prod_ id),然后按如下方式重写查询:
我们把这种方式叫做延迟关联(deferred join),因为延迟了对列的访问。在查询的第一阶段MySQL可以使用覆盖索引,在FROM子句的子查询中找到匹配的prod_ id,然后根据这些prod_id值在外层查询匹配获取需要的所有列值。虽然无法使用索引覆盖整个查询,但总算比完全无法利用索引覆盖的好。
这样优化的效果取决于WHERE条件匹配返回的行数。假设这个products表有100万行,我们来看一下上面两个查询在三个不同的数据集.上的表现,每个数据集都包含100 万行:
- 第一个数据集,Sean Carrey出演了30 000部作品,其中有20 000部的标题中包含了Apollo。
- 第二个数据集,Sean Carrey出演了30 000部作品,其中40部的标题中包含了Apollo。
- 第三个数据集,Sean Carrey出演了50部作品,其中10部的标题中包含了Apollo。
使用上面的三种数据集来测试两种不同的查询,得到的结果如表5-2所示。
-
在示例1中,查询返回了一个很大的结果集,因此看不到优化的效果。大部分时间都花在读取和发送数据上了。
-
在示例2中,经过索引过滤,尤其是第二个条件过滤后只返回了很少的结果集,优化的效果非常明显:在这个数据集上性能提高了5倍,优化后的查询的效率主要得益于只需要读取40行完整数据行,而不是原查询中需要的30000行。
-
在示例3中,显示了子查询效率反而下降的情况。因为索引过滤时符合第一个条件的结果集已经很小,所以子查询带来的成本反而比从表中直接提取完整行更高。
在大多数存储引擎中,覆盖索引只能覆盖那些只访问索引中部分列的查询。通过InnoDB的二级索引的叶子节点都包含了主键的值,这意味着InnoDB的二级索引可以有效地利用这些“额外”的主键列来覆盖查询。
5.3.7 使用索引扫描来做排序
MySQL有两种方式可以生成有序的结果:
- 通过排序操作
- 按索引顺序扫描
如果EXPLAIN出来的type列的值为“index”, 则说明MySQL使用了索引扫描来做排序(不要和Extra列的“Using index”搞混淆了)。
type:表示查询访问数据的方式,或者说MYSQL查找行的方式
extra: 其中的Using index 代表使用了覆盖索引
扫描索引本身是很快的,因为只需要从一条索引记录移动到紧接着的下一条记录。但如果索引不能覆盖查询所需的全部列,那就不得不每扫描一条索引记录就都回表查询一次对应的行。这基本上都是随机I/O,因此按索引顺序读取数据的速度通常要比顺序地全表扫描慢,尤其是在I/O密集型的工作负载时。
MySQL可以使用同一个索引既满足排序,又用于查找行。因此,如果可能,设计索引时应该尽可能地同时满足这两种任务,这样是最好的。
只有当索引的列顺序和ORDER BY 子句的顺序完全一致,并且所有列的排序方向(倒序或正序)都一样时,MySQL才能够使用索引来对结果做排序。如果查询需要关联多张表,则只有当ORDER BY 子句引用的字段全部为第一个表时,才能使用索引做排序。
-
ORDER BY 子句和查找型查询的限制是一样的:需要满足索弓|的最左前缀的要求;否则,MySQL都需要执行排序操作,而无法利用索引排序。
-
有一种情况下ORDERBY子句可以不满足索引的最左前缀的要求,就是前导列为常量的时候。如果WHERE子句或者JOIN子句中对这些列指定了常量,就可以“弥补”索引的不足。
所谓前导列,就是在创建复合索引语句的第一列或者连续的多列
例如,Sakila 示例数据库的表rental在列(rental_date, inventory_id, customer_id)上有名为rental_ date的索引。
(rental_date, inventory_id, customer_id):
CREATE TABLE rental(
...
PRIMARY KEY(rental_id),
UNIQUE KEY rental_date(rental_date , inventory_id, customer_id),
KEY idx_fk_inventory_id(inventory_id),
KEY idx_fk_customer_id (customer_id),
KEY idx_fk_staff_id(staff_id),
...
);
即使ORDER BY子句不满足索引的最左前缀的要求,也可以用于查询排序,这是因为索引的第一列被指定为一个常数。
5.3.8 压缩(前缀压缩)索引
MyISAM使用前缀压缩来减少索引的大小,从而让更多的索引可以放入内存中,这在某些情况下能极大地提高性能。默认只压缩字符串,但通过参数设置也可以对整数做压缩。
MyISAM压缩每个索引块的方法是,先完全保存索引块中的第一个值, 然后将其他值和第一个值进行比较得到相同前缀的字节数和剩余的不同后缀部分,把这部分存储起来即可。
例如,索引块中的第一个值是“perform”, 第二个值是"performance", 那么第二个值的前缀压缩后存储的是类似“7,ance" 这样的形式。MyISAM对行指针也采用类似的前缀压缩方式。
压缩块使用更少的空间,代价是某些操作可能更慢。
因为每个值的压缩前缀都依赖前面的值,所以MyISAM查找时无法在索引块使用二分查找而只能从头开始扫描。正序的扫描速度还不错,但是如果是倒序扫描一例如 ORDER BY DESC 就不是很好了。所有在块中查找某一行 的操作平均都需要扫描半个索引块。
测试表明,对于CPU密集型应用,因为扫描需要随机查找,压缩索引使得MyISAM在索引查找上要慢好几倍。压缩索引的倒序扫描就更慢了。压缩索引需要在CPU内存资源与磁盘之间做权衡。压缩索引可能只需要十分之一大小的磁盘空间,如果是I/O密集型应用,对某些查询带来的好处会比成本多很多。
可以在CREATE TABLE语句中指定PACK_ KEYS 参数来控制索引压缩的方式。
5.3.9 冗余和重复索引
MySQL允许在相同列上创建多个索引,无论是有意的还是无意的。
MySQL需要单独维护重复的索引,并且优化器在优化查询的时候也需要逐个地进行考虑,这会影响性能。
重复索引是指在相同的列上按照相同的顺序创建的相同类型的索引。应该避免这样创建重复索引,发现以后也应该立即移除。
有时会在不经意间创建了重复索引,例如下面的代码:
CREATE TABLE test (
ID INT NOT NULL PRIMARY KEY,
A INT NOT NULL,
B INT NOT NULL,
UNIQUE(ID),
INDEX(ID)
) ENGINE=InnoDB;
MySQL的唯一限制和主键限制都是通过索引实现的,上面的写法实际上在相同的列上创建了三个重复的索引。
通常并没有理由这样做,除非是在同一列上创建不同类型的索引来满足不同的查询需求。
冗余索引和重复索引有一些不同
-
如果创建了索引(A, B),再创建索引(A)就是冗余索引,因为这只是前一个索引的前缀索引。
-
因此索引(A, B)也可以当作索引(A)来使用(这种冗余只是对B-Tree索引来说的)。
-
但是如果再创建索引(B, A),则不是冗余索引,索引(B)也不是,因为B不是索引(A, B)的最左前缀列。
-
另外,其他不同类型的索引(例如哈希索引或者全文索引)也不会是B-Tree索引的冗余索引,而无论覆盖的索引列是什么。
冗余索引通常发生在为表添加新索引的时候。
例如,有人可能会增加一个新的索引(A, B)而不是扩展已有的索引(A)。还有一种情况是将-一个索引扩展为(A, ID),其中ID是主键,对于InnoDB来说主键列已经包含在二级索引中了,所以这也是冗余的。
大多数情况下都不需要冗余索引,应该尽量扩展已有的索引而不是创建新索引。但也有时候出于性能方面的考虑需要冗余索引,因为扩展已有的索引会导致其变得太大,从而影响其他使用该索引的查询的性能。
例如,如果在整数列上有一个索引,现在需要额外增加一个很长的VARCHAR列来扩展该索引,那性能可能会急剧下降。特别是有查询把这个索引当作覆盖索引,或者这是MyISAM表并且有很多范围查询(由于MyISAM的前缀压缩)的时候。
5.3.11 索引和锁
索引可以让查询锁定更少的行。如果你的查询从不访问那些不需要的行,那么就会锁定更少的行,从两个方面来看这对性能都有好处。
- 虽然InnoDB的行锁效率很高,内存使用也很少,但是锁定行的时候仍然会带来额外开销
- 锁定超过需要的行会增加锁争用并减少并发性
InnoDB只有在访问行的时候才会对其加锁,而索引能够减少InnoDB访问的行数,从而减少锁的数量。但这只有当InnoDB在存储引擎层能够过滤掉所有不需要的行时才有效。
如果索引无法过滤掉无效的行,那么在InnoDB检索到数据并返回给服务器层以后,MySQL服务器才能应用WHERE子句。这时已经无法避免锁定行了:InnoDB已经锁住了这些行,到适当的时候才释放。
在MySQL 5.1和更新的版本中,InnoDB 可以在服务器端过滤掉行后就释放锁,但是在早期的MySQL版本中,InnoDB 只有在事务提交后才能释放锁。
(索引案例与维护修复略)
附录
《高性能MySQL》
Baron Schwartz, Peter Zaitsev, Vadim Tkachenko 著
宁海元 周振兴 彭立勋 翟卫祥 刘辉 译