mysql 索引案例与索引策略
目录
索引案例学习
支持多种过滤条件
避免多个范围条件
优化排序
分页查询优化
分而治之总是不错的
索引高性能策略
策略总结
独立的列
前缀索引和索引选择性
多列索引
选择合适的索引列顺序
聚簇索引
lnnoDB和MylSAM的数据分布对比
在lnnoDB表中按主键顺序插入行
覆盖索引
使用索引扫描来做排序
压缩(前缀压缩)索引
冗余和重复索引
未使用的索引
索引和锁
索引案例学习
理解索引最好的办法是结合示例,所以这里准备了一个索引的案例。
假设要设计一个在线约会网站,用户信息表有很多列,包括国家、地区、城市、性别、 眼睛颜色,等等。网站必须支持上面这些特征的各种组合来搜索用户,还必须允许根据 用户的最后在线时间、其他会员对用户的评分等对用户进行排序并对结果进行限制。如 何设计索引满足上面的复杂需求呢?
出人意料的是第一件需要考虑的事情是需要使用索引来排序,还是先检索数据再排序。使用索引排序会严格限制索引和查询的设计。例如,如果希望使用索引做根据其他会员 对用户的评分的排序,则WHERE条件中的age BElWEEN 18 AND 25就无法使用索引。如 果MySQL使用某个索引进行范围查询,也就无法再使用另一个索引(或者是该索引的 后续字段)进行排序了。如果这是很常见的WHERE条件,那么我们当然就会认为很多查询需要做排序操作(例如文件排序filesort)。
支持多种过滤条件
现在需要看看哪些列拥有很多不同的取值,哪些列在WHERE子句中出现得最频繁。在有 更多不同值的列上创建索引的选择性会更好。一般来说这样做都是对的,因为可以让 MySQL更有效地过滤掉不需要的行。
country列的选择性通常不高,但可能很多查询都会用到。sex列的选择性肯定很低,但 也会在很多查询中用到。所以考虑到使用的频率,还是建议在创建不同组合索引的时候 将(sex, country)列作为前缀。
但根据传统的经验不是说不应该在选择性低的列上创建索引的吗?那为什么这里要将两 个选择性都很低的字段作为索引的前缀列?我们的脑子坏了?
我们的脑子当然没坏。这么做有两个理由:第一点,如前所述几乎所有的查询都会用到 sex列。前面曾提到,几乎每一个查询都会用到sex列,甚至会把网站设计成每次都只 能按某一种性别搜索用户。更重要的一点是,索引中加上这一列也没有坏处,即使查询 没有使用sex列也可以通过下面的"诀窍"绕过。
这个"诀窍”就是:如果某个查询不限制性别,那么可以通过在查询条件中新增AND SEX IN ('m','f')来让MySQL选择该索引。这样写并不会过滤任何行,和没有这个条件 时返回的结果相同。但是必须加上这个列的条件,MySQL才能够匹配索引的最左前缀。 这个"诀窍”在这类场景中非常有效,但如果列有太多不同的值,就会让IN()列表太长, 这样做就不行了。
这个案例显示了一个基本原则:考虑表上所有的选项。当设计索引时,不要只为现有的 查询考虑需要哪些索引,还需要考虑对查询进行优化。如果发现某些查询需要创建新索 引,但是这个索引又会降低另一些查询的效率,那么应该想一下是否能优化原来的查询。 应该同时优化查询和索引以找到最佳的平衡,而不是闭门造车去设计最完美的索引。
接下来,需要考虑其他常见WHERE条件的组合,并需要了解哪些组合在没有合适索引的 情况下会很慢。(sex, country, age)上的索引就是一个很明显的选择,另外很有可能还 需要(sex, country, region, age)和(sex, country, region, city, age)这样的组合索引。
这样就会需要大量的索引。如果想尽可能重用索引而不是建立大量的组合索引,可以 使用前面提到的IN()的技巧来避免同时需要(sex, country, age)和(sex, country, region, age)的索引。如果没有指定这个字段搜索,就需要定义一个全部国家列表,或 者国家的全部地区列表,来确保索引前缀有同样的约束(组合所有国家、地区、性别将 会是一个非常大的条件)。
这些索引将满足大部分最常见的搜索查询,但是如何为一些生僻的搜索条件(比如has pictures、eye_color、hair_color和education)来设计索引呢?这些列的选择性高、 使用也不频繁,可以选择忽略它们,让MySQL多扫描一些额外的行即可。另一个可选 的方法是在age列的前面加上这些列,在查询时使用前面提到过的IN()技术来处理搜索 时没有指定这些列的场景。
你可能已经注意到了,我们一直将age列放在索引的最后面。age列有什么特殊的地方 吗?为什么要放在索引的最后?我们总是尽可能让MySQL使用更多的索引列,因为查 询只能使用索引的最左前缀,直到遇到第一个范围条件列。前面提到的列在WHERE子句 中都是等于条件,但是age列则多半是范围查询(例如查找年龄在18 - 25岁之间的人)。
当然,也可以使用IN()来代替范围查询,例如年龄条件改写为IN(l8, 19, 20, 21, 22, 23, 24, 25), 但不是所有的范围查询都可以转换。这里描述的基本原则是,尽可能 将需要做范围查询的列放到索引的后面,以便优化器能使用尽可能多的索引列。
前面提到可以在索引中加人更多的列,并通过IN()的方式覆盖那些不在WHERE子句中的 列。但这种技巧也不能滥用,否则可能会带来麻烦。因为每额外增加一个IN()条件,优 化器需要做的组合都将以指数形式增加,最终可能会极大地降低查询性能。
避免多个范围条件
假设我们有一个last_online列并希望通过下面的查询显示在过去几周上线过的用户:
从EXPLAIN的输出很难区分MySQL是要查询范围值,还是查询列表值。EXPLAIN 使用同样的词"range"来描述这两种情况。例如,从type列来看,MySQL会把 下面这种查询当作是"range"类型:
从EXPLAIN的结果是无法区分这两者的,但可以从值的范围和多个等于条件来得出 不同。在我们看来,笫二个查询就是多个等值条件查询。
我们不是挑剔:这两种访问效率是不同的。对于范围条件查询,MySQL无法再使 用范围列后面的其他索引列了,但是对于“多个等值条件查询”则没有这个限制。
这个查询有一个问题:它有两个范围条件,last_online列和age列,MySQL可以使用 last_online列索引或者age列索引,但无法同时使用它们。
如果条件中只有last_online而没有age, 那么我们可能考虑在索引的后面加上last_ on line列。这里考虑如果我们无法把age字段转换为一个IN ()的列表,并且仍要求对 于同时有last_online和age这两个维度的范围查询的速度很快,那该怎么办?答案是, 很遗憾没有一个直接的办法能够解决这个问题。但是我们能够将其中的一个范围查询转 换为一个简单的等值比较。为了实现这一点,我们需要事先计算好一个active列,这个 字段由定时任务来维护。当用户每次登录时,将对应值设置为1'并且将过去连续七天 未曾登录的用户的值设置为0。
这个方法可以让MySQL使用(active, sex, country, age)索引。active列并不是完全 精确的,但是对于这类查询来说,对精度的要求也没有那么高。如果需要精确数据,可 以把last_online列放到WHERE子句,但不加入到索引中。这和本章前面通过计算URL 哈希值来实现URL的快速查找类似。所以这个查询条件没法使用任何索引,但因为这 个条件的过滤性不高,即使在索引中加入该列也没有太大的帮助。换个角度来说,缺乏 合适的索引对该查询的影响也不明显。
到目前为止,我们可以看到:如果用户希望同时看到活跃和不活跃的用户,可以在查 询中使用IN()列表。我们已经加入了很多这样的列表,但另外一个可选的方案就只能 是为不同的组合列创建单独的索引。至少需要建立如下的索引: (active, sex, country, age), (active, country, age), (sex, country, age)和(country, age)。这些索引对某 个具体的查询来说可能都是更优化的,但是考虑到索引的维护和额外的空间占用的代价, 这个可选方案就不是一个好策略了。
在这个案例中,优化器的特性是影响索引策略的一个很重要的因素。如果未来版本的 MySQL能够实现松散索引扫描,就能在一个索引上使用多个范围条件,那也就不需要 为上面考虑的这类查询使用IN()列表了。
优化排序
在这个学习案例中,最后要介绍的是排序。使用文件排序对小数据集是很快的,但如果 一个查询匹配的结果有上百万行的话会怎样?例如如果WHERE子句只有sex列,如何排 序?
对于那些选择性非常低的列,可以增加一些特殊的索引来做排序。例如,可以创建(sex, rating)索引用于下面的查询:
这个查询同时使用了ORDER BY和LIMIT, 如果没有索引的话会很慢。
即使有索引,如果用户界面上需要翻页,并且翻页翻到比较靠后时查询也可能非常慢。 下面这个查询就通过ORDER BY和LIMIT偏移量的组合翻页到很后面的时候:
无论如何创建索引,这种查询都是个严重的问题。因为随着偏移量的增加,MySQL需 要花费大量的时间来扫描需要丢弃的数据。反范式化、预先计算和缓存可能是解决这类
查询的仅有策略。一个更好的办法是限制用户能够翻页的数量,实际上这对用户体验的影响不大,因为用户很少会真正在乎搜索结果的第10 000页。
优化这类索引的另一个比较好的策略是使用延迟关联,通过使用覆盖索引查询返回需要 的主键,再根据这些主键关联原表获得需要的行。这可以减少MySQL扫描那些需要丢 弃的行数。下面这个查询显示了如何高效地使用(sex, rating)索引进行排序和分页:
分页查询优化
业务要根据时间范围查询交易记录,接口原始的SQL如下:
select * from trade_info where status = 0 and create_time >= '2020-10-01 00:00:00' and create_time <= '2020-10-07 23:59:59' order by id desc limit 102120, 20;
表trade_info上有索引idx_status_create_time(status,create_time),通过上面分析知道,等价于索引(status,create_time,id),对于典型的分页limit m, n来说,越往后翻页越慢,也就是m越大会越慢,因为要定位m位置需要扫描的数据越来越多,导致IO开销比较大,这里可以利用辅助索引的覆盖扫描来进行优化,先获取id,这一步就是索引覆盖扫描,不需要回表,然后通过id跟原表trade_info进行关联,改写后的SQL如下:
select * from trade_info a ,(select id from trade_info where status = 0 and create_time >= '2020-10-01 00:00:00' and create_time <= '2020-10-07 23:59:59' order by id desc limit 102120, 20) as b //这一步走的是索引覆盖扫描,不需要回表where a.id = b.id;
很多同学只知道这样写效率高,但是未必知道为什么要这样改写,理解索引特性对编写高质量的SQL尤为重要。
分而治之总是不错的
营销系统有一批过期的优惠卷要失效,核心SQL如下:
-- 需要更新的数据量500w
update coupons set status = 1 where status =0 and create_time >= '2020-10-01 00:00:00' and create_time <= '2020-10-07 23:59:59';
在Oracle里更新500w数据是很快,因为可以利用多个cpu core去执行,但是MySQL就需要注意了,一个SQL只能使用一个cpu core去处理,如果SQL很复杂或执行很慢,就会阻塞后面的SQL请求,造成活动连接数暴增,MySQL CPU 100%,相应的接口Timeout,同时对于主从复制架构,而且做了业务读写分离,更新500w数据需要5分钟,Master上执行了5分钟,binlog传到了slave也需要执行5分钟,那就是Slave延迟5分钟,在这期间会造成业务脏数据,比如重复下单等。
优化思路:先获取where条件中的最小id和最大id,然后分批次去更新,每个批次1000条,这样既能快速完成更新,又能保证主从复制不会出现延迟。
优化如下:
先获取要更新的数据范围内的最小id和最大id(表没有物理delete,所以id是连续的)
mysql> explain select min(id) min_id, max(id) max_id from coupons where status =0 and create_time >= '2020-10-01 00:00:00' and create_time <= '2020-10-07 23:59:59';
+----+-------------+-------+------------+-------+------------------------+------------------------+---------+---
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+------------------------+------------------------+---------+---
| 1 | SIMPLE | users | NULL | range | idx_status_create_time | idx_status_create_time | 6 | NULL | 180300 | 100.00 | Using where; Using index |
Extra=Using where; Using index使用了索引idx_status_create_time,同时需要的数据都在索引中能找到,所以不需要回表查询数据。
以每次1000条commit一次进行循环update,主要代码如下:
current_id = min_id;
for current_id < max_id do
update coupons set status = 1 where id >=current_id and id <= current_id + 1000; //通过主键id更新1000条很快
commit;
current_id += 1000;
done
这两个案例告诉我们,要充分利用辅助索引包含主键id的特性,先通过索引获取主键id走覆盖索引扫描,不需要回表,然后再通过id去关联操作是高效的,同时根据MySQL的特性使用分而治之的思想既能高效完成操作,又能避免主从复制延迟产生的业务数据混乱。
索引高性能策略
策略总结
表的设计:
表的主键最好是递增的数字,而不是随机的(因为聚簇索引的物理结构)
查询语句中:
查询的列要是单独的
如果有and,不要多个单列索引,可以考虑一个多列索引,注意索引顺序
如果有or,可以考虑用union,或者索引合并
如果查询条件里,有不能完全覆盖的,如like,范围查询,但是where之后返回的结果很少的查询条件,可以使用索引查询到对应的主键,然后使用延迟关联,查询到对应的列。
如果有排序,只有排序字段是索引字段前几个字段,才能使用索引排序(除非索引字段中,排序字段前的字段是常量)
如果很多查询都要where的字段,而且选择性很低,可以建立在索引最左边,如果不要时,使用 in(x,y)
可以把范围查询变成in(a,b,c),但是不能滥用
如果有多个范围查询,可以考虑将其中一个转为in(a,b,c),这样两个字段都能使用索引
如果又有查询条件和排序,而且两者不一致,而且limit的范围还很大,可以限制limit的范围,或者使用延迟关联,通过使用覆盖索引(包含查询条件和排序字段)查询返回需要 的主键,再根据这些主键关联原表获得需要的行。这可以减少MySQL扫描那些需要丢 弃的行数
索引及它的字段:
如果是字符串,可以选择前后缀索引,注意索引选择性。
如果字段有很长的字符串,可以考虑使用自定义哈希索引,节约索引空间。
如果是字符串,如果是MyIsam,可以使用前缀压缩索引。
索引顺序:根据等于和范围查询的字段,和字段的选择性(还有特殊情况),和排序、group by的字段,根据BTREE的最左前缀特性,确定那几个字段加入索引,以及索引的顺序
如果索引包含,查询的字段和where后的字段,可以使用覆盖索引。
尽可能 将需要做范围查询的列放到索引的后面,以便优化器能使用尽可能多的索引列。
通常不要有冗余索引和重复索引,除非第二个字段过长,或者要用到索引对应的主键,(A,ID)变成(A,B,ID),可能可以有冗余索引。
删除未使用的索引。
即使使用了索引,InnoDB也可能锁住一些不需要的数据。如果 不能使用索引查找和锁定行的话问题可能会更糟糕,MySQL会做全表扫描并锁住所有 的行。
聚簇索引
独立的列
我们通常会看到一些查询不当地使用索引,或者使得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;
前缀索引和索引选择性
有时候需要索引很长的字符列,这会让索引变得大且慢。一个策略是前面提到过的模拟 哈希索引。但有时候这样做还不够,还可以做些什么呢?
通常可以索引开始的部分字符,这样可以大大节约索引空间,从而提高索引效率。但 这样也会降低索引的选择性。
索引的选择性是指,不重复的索引值(也称为基数, cardinality)和数据表的记录总数(#T)的比值,范围从1/#T到1之间。索引的选择性 越高则查询效率越高,因为选择性高的索引可以让MySQL在查找时过滤掉更多的行。 唯一索引的选择性是1,这是最好的索引选择性,性能也是最好的。
一般情况下某个列前缀的选择性也是足够高的,足以满足查询性能。对于BLOB、TEXT或 者很长的VARCHAR类型的列,必须使用前缀索引,因为MySQL不允许索引这些列的完 整长度。
诀窍在于要选择足够长的前缀以保证较高的选择性,同时又不能太长(以便节约空间)。 前缀应该足够长,以使得前缀索引的选择性接近于索引整个列。换句话说,前缀的“基 数”应该接近于完整列的“基数”。
为了决定前缀的合适长度,需要找到最常见的值的列表,然后和最常见的前缀列表进行 比较。在示例数据库Sakila中并没有合适的例子,所以我们从表city中生成一个示例表, 这样就有足够的数据进行演示:
现在我们有了示例数据集。数据分布当然不是真实的分布,因为我们使用了RAND(), 所 以你的结果会与此不同,但对这个练习来说这并不重要。首先,我们找到最常见的城 市列表:
注意到,上面每个值都出现了45 - 65次。现在查找到最频繁出现的城市前缀,先从3 个前缀字母开始:
每个前缀都比原来的城市出现的次数更多,因此唯一前缀比唯一城市要少得多。然后我 们增加前缀长度,直到这个前缀的选择性接近完整列的选择性(即显示出来的前10个cnt和一开始的cnt差不多)。经过实验后发现前缀长 度为7时比较合适:
计算合适的前缀长度的另外一个办法就是计算完整列的选择性,并使前缀的选择性接近 于完整列的选择性。下面显示如何计算完整列的选择性:
通常来说(尽管也有例外情况),这个例子中如果前缀的选择性能够接近0.031, 基本上 就可用了。可以在一个查询中针对不同前缀长度进行计算,这对于大表非常有用。下面 给出了如何在同一个查询中计算不同前缀长度的选择性:
查询显示当前缀长度到达7的时候,再增加前缀长度,选择性提升的幅度已经很小了。 只看平均选择性是不够的,也有例外的情况,需要考虑最坏情况下的选择性。平均选择 性会让你认为前缀长度为4或者5的索引已经足够了,但如果数据分布很不均匀,可能 就会有陷阱。如果观察前缀为4的最常出现城市的次数,可以看到明显不均匀:
如果前缀是4个字节,则最常出现的前缀的出现次数比最常出现的城市的出现次数要大 很多。即这些值的选择性比平均选择性要低。如果有比这个随机生成的示例更真实的数 据,就更有可能看到这种现象。例如在真实的城市名上建一个长度为4的前缀索引,对 于以"San"和"New"开头的城市的选择性就会非常糟糕,因为很多城市都以这两个词 开头。
在上面的示例中,已经找到了合适的前缀长度,下面演示一下如何创建前缀索引:
前缀索引是一种能使索引更小、更快的有效办法,但另一方面也有其缺点: MySQL无 法使用前缀索引做ORDER BY和GROUP BY, 也无法使用前缀索引做覆盖扫描。
一个常见的场景是针对很长的十六进制唯一ID使用前缀索引。在前面的章节中已经讨论了很多有效的技术来存储这类ID信息,但如果使用的是打包过的解决方案,因而无 法修改存储结构,那该怎么办?例如使用vBulletin或者其他基于MySQL的应用在存储 网站的会话(SESSION)时,需要在一个很长的十六进制字符串上创建索引。此时如果 采用长度为8的前缀索引通常能显著地提升性能,并且这种方法对上层应用完全透明。
有时候后缀索引(suffi, indes)也有用途(例如,找到某个域名的所有电子邮件地址)。MySQL原生并不支持反向索引,但是可以把字符串反转后存储,并基于此建立前缀索引。可以通过触发器来维护这种索引。
多列索引
很多人对多列索引的理解都不够。 一个常见的错误就是,为每个列创建独立的索引,或 者按照错误的顺序创建多列索引。
先来看第一个问题,为每个列创建独 立的索引,从SHOW CREATE TABLE 中很容易看到这种情况:
这种索引策略,一般是由于人们听到一些专家诸如“把WHERE条件里面的列都建上索引” 这样模糊的建议导致的。实际上这个建议是非常错误的。这样一来最好的情况下也只能 是"一星”索引,其性能比起真正最优的索引可能差几个数最级。有时如果无法设计一 个“三星”索引,那么不如忽略掉WHERE子句,集中精力优化索引列的顺序,或者创建 一个全覆盖索引。
在多个列上建立独立的单列索引大部分情况下并不能提高MySQL的查询性能。
MySQL5.0和更新版本引人了一种叫“索引合并" (index merge)的策略,一定程度上可以使用表上的多个单列索引来定位指定的行。更早版本的MySQL只能使用其中某一个单列索 引,然而这种情况下没有哪一个独立的单列索引是非常有效的。例如,表film_actor在 字段film_id和actor_id上各有一个单列索引。但对于下面这个查询WHERE条件,这两 个单列索引都不是好的选择:
在老的MySQL版本中,MySQL对这个查询会使用全表扫描。除非改写成如下的两个查 询UNION的方式:
但在MySQL 5.0和更新的版本中,查询能够同时使用这两个单列索引进行扫描,并将结 果进行合并。这种算法有三个变种: OR条件的联合(union) ,AND条件的相交(intersection) , 组合前两种情况的联合及相交。下面的查询就是使用了两个索引扫描的联合,通过 EXPLAIN中的Extra列可以看到这点:
MySQL会使用这类技术优化复杂查询,所以在某些语句的Extra列中还可以看到嵌套 操作。
索引合井策略有时候是一种优化的结果,但实际上更多时候说明了表上的索引建得很糟 糕:
当出现服务器对多个索引做相交操作时(通常有多个AND条件),通常意味着需要一 个包含所有相关列的多列索引,而不是多个独立的单列索引。
当服务器需要对多个索引做联合操作时(通常有多个OR条件),通常需要耗费大量 CPU和内存资源在算法的缓存、排序和合并操作上。特别是当其中有些索引的选择 性不高,需要合并扫描返回的大量数据的时候。
更重要的是,优化器不会把这些计算到“查询成本" (cost)中,优化器只关心随机 页面读取。这会使得查询的成本被“低估",导致该执行计划还不如直接走全表扫描。 这样做不但会消耗更多的CPU和内存资源,还可能会影响查询的并发性,但如果是 单独运行这样的查询则往往会忽略对并发性的影响。通常来说,还不如像在MySQL 4.1或者更早的时代一样,将查询改写成UNION的方式往往更好。
如果在EXPLAIN中看到有索引合并,应该好好检查一下查询和表的结构,看是不是已 经是最优的。
也可以通过参数optimizer_switch来关闭索引合并功能。也可以使用 lGNORE lNDEX提示让优化器忽略掉某些索引。
选择合适的索引列顺序
我们遇到的最容易引起困惑的问题就是索引列的顺序。正确的顺序依赖于使用该索引的 查询,并且同时需要考虑如何更好地满足排序和分组的需要(顺便说明,本节内容适用 于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条件的分支对应的数据基数有多大:
根据前面的经验法则,应该将索引列customer _id放到前面,因为对应条件值的 customer_id数量更小。我们再来看看对于这个customer_id的条件值,对应的staff_ id列的选择性如何:
这样做有一个地方需要注意,查询的结果非常依赖于选定的具体值。如果按上述办法优 化,可能对其他一些条件值的查询不公平,服务器的整体性能可能变得更糟,或者其他 某些查询的运行变得不如预期。
如果是从诸如pt-query-digest这样的工具的报告中提取“最差“查询,那么再按上述办 法选定的索引顺序往往是非常高效的。
如果没有类似的具体查询来运行,那么最好还是 按经验法则来做,因为经验法则考虑的是全局基数和选择性,而不是某个具体查询:
当使用前缀索引的时候,在某些条件值的基数比正常值高的时候,问题就来了。例如, 在某些应用程序中,对于没有登录的用户,都将其用户名记录为"guset", 在记录用户 行为的会话(session)表和其他记录用户活动的表中"guest"就成为了一个特殊用户。一且查询涉及这个用户,那么和对于正常用户的查询就大不同了,因为通常有很多 会话都是没有登录的。系统账号也会导致类似的问题。一个应用通常都有一个特殊的管 理员账号,和普通账号不同,它并不是一个具体的用户,系统中所有的其他用户都是这 个用户的好友,所以系统往往通过它向网站的所有用户发送状态通知和其他消息。这个 账号的巨大的好友列表很容易导致网站出现服务器性能问题。
这实际上是一个非常典型的问题。任何的异常用户,不仅仅是那些用于管理应用的设计 糟糕的账号会有同样的问题,那些拥有大量好友、图片、状态、收藏的用户,也会有前面提到的系统账号同样的问题。
下面是一个我们遇到过的真实案例,在一个用户分享购买商品和购买经验的论坛上,这 个特殊表上的查询运行得非常慢:
这个查询看似没有建立合适的索引,所以客户咨询我们是否可以优化。EXPLAIN的结果 如下:
MySQL为这个查询选择了索引(groupid, userid), 如果不考虑列的基数,这看起来是 一个非常合理的选择。但如果考虑一下user ID和group ID条件匹配的行数,可能就会 有不同的想法了:
从上面的结果来看符合组(groupld)条件几乎满足表中的所有行,符合用户(userld) 条件的有130万条记录——也就是说索引基本上没什么用。因为这些数据是从其他应用 中迁移过来的,迁移的时候把所有的消息都赋予了管理员组的用户。这个案例的解决办 法是修改应用程序代码,区分这类特殊用户和组,禁止针对这类用户和组执行这个查询。
从这个小案例可以看到经验法则和推论在多数情况是有用的,但要注意不要假设平均情况下的性能也能代表特殊情况下的性能,特殊情况可能会摧毁整个应用的性能。
最后,尽管关于选择性和基数的经验法则值得去研究和分析,但一定要记住别忘了 WHERE子句中的排序、分组和范围条件等其他因素,这些因素可能对查询的性能造成非 常大的影响。
聚簇索引
聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。
具体的细节依赖于其 实现方式,但InnoDB的聚簇索引实际上在同一个结构中保存了B-Tree索引和数据行。
当表有聚簇索引时,它的数据行实际上存放在索引的叶子页(leaf page)中。术语“聚簇" 表示数据行和相邻的键值紧凑地存储在一起。因为无法同时把数据行存放在两个不同 的地方,所以一个表只能有一个聚簇索引(不过,覆盖索引可以模拟多个聚簇索引的情况)。
因为是存储引擎负责实现索引,因此不是所有的存储引擎都支持聚簇索引。本节我们主 要关注InnoDB, 但是这里讨论的原理对于任何支持聚簇索引的存储引擎都是适用的。
图展示了聚簇索引中的记录是如何存放的。注意到,叶子页包含了行的全部数据, 但是节点页只包含了索引列。在这个案例中,索引列包含的是整数值。
一些数据库服务器允许选择哪个索引作为聚簇索引,但直到本书写作之际,还没有任何一个MySQL内建的存储引擎支持这一点。InnoDB将通过主键聚集数据,这也就是说图 中的”被索引的列”就是主键列。
如果没有定义主键,InnoDB会选择一个唯一的非空索引代替。如果没有这样的索引, InnoDB会隐式定义一个主键来作为聚簇索引。
InnoDB只聚集在同一个页面中的记录。 包含相邻键值的页面可能会相距甚远。
聚簇主键可能对性能有帮助,但也可能导致严重的性能问题。所以需要仔细地考虑聚簇 索引,尤其是将表的存储引擎从InnoDB改成其他引擎的时候(反过来也一样)。
聚集的数据有一些重要的优点:
可以把相关数据保存在一起。例如实现电子邮箱时,可以根据用户ID来聚集数据, 这样只需要从磁盘读取少数的数据页就能获取某个用户的全部邮件。如果没有使用 聚簇索引,则每封邮件都可能导致一次磁盘I/0。
数据访问更快。聚簇索引将索引和数据保存在同一个B-Tree中,因此从聚簇索引中 获取数据通常比在非聚簇索引中查找要快。
使用覆盖索引扫描的查询可以直接使用页节点中的主键值。
如果在设计表和查询时能充分利用上面的优点,那就能极大地提升性能。
同时,聚簇索 引也有一些缺点:
聚簇数据最大限度地提高了I/O密集型应用的性能,但如果数据全部都放在内存中, 则访问的顺序就没那么重要了,聚簇索引也就没什么优势了。
插入速度严重依赖于插入顺序。按照主键的顺序插人是加载数据到InnoDB表中速 度最快的方式。但如果不是按照主键顺序加载数据,那么在加载完成后最好使用 OPTIMIZE TABLE命令重新组织一下表。
更新聚簇索引列的代价很高,因为会强制InnoDB将每个被更新的行移动到新的位 置。
基于聚簇索引的表在插入新行,或者主键被更新导致需要移动行的时候,可能面临页分裂(page split)的问题。当行的主键值要求必须将这一行插入到某个已满的 页中时,存储引擎会将该页分裂成两个页面来容纳该行,这就是一次页分裂操作。 页分裂会导致表占用更多的磁盘空间。
聚簇索引可能导致全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致数据存 储不连续的时候。
二级索引(非聚簇索引)可能比想象的要更大,因为在二级索引的叶子节点包含了 引用行的主键列。
二级索引访问需要两次索引查找,而不是一次。
最后一点可能让人有些疑惑,为什么二级索引需要两次索引查找?答案在于二级索引中 保存的“行指针”的实质。要记住,二级索引叶子节点保存的不是指向行的物理位置的 指针,而是行的主键值。
这意味着通过二级索引查找行,存储引擎需要找到二级索引的叶子节点获得对应的主键 值,然后根据这个值去聚簇索引中查找到对应的行。这里做了重复的工作:两次B-Tree 查找而不是一次。对于lnnoDB, 自适应哈希索引能够减少这样的重复工作。
lnnoDB和MylSAM的数据分布对比
聚簇索引和非聚簇索引的数据分布有区别,以及对应的主键索引和二级索引的数据分布 也有区别,通常会让人感到困扰和意外。来看看InnoDB和MyISAM是如何存储下面这 个表的:
假设该表的主键取值为1 ~ 10 000, 按照随机顺序插入并使用OPTIMIZE TABLE命令做 了优化。换句话说,数据在磁盘上的存储方式已经最优,但行的顺序是随机的。列col2 的值是从1 ~ 100之间随机赋值,所以有很多重复的值。
MylSAM的数据分布。
MyISAM的数据分布非常简单,所以先介绍它。MyISAM按照数 据插入的顺序存储在磁盘上,如图所示。
在行的旁边显示了行号,从0开始递增。因为行是定长的,所以MyISAM可以从表的开 头跳过所需的字节找到需要的行(MyISAM并不总是使用图中的“行号”,而是根 据定长还是变长的行使用不同策略)。
这种分布方式很容易创建索引。下面显示的一系列图,隐藏了页的物理细节,只显示索 引中的”节点”,索引中的每个叶子节点包含“行号”。图显示了表的主键。
这里忽略了一些细节,例如前一个B-Tree节点有多少个内部节点,不过这并不影响对非 聚簇存储引擎的基本数据分布的理解。
那col2列上的索引又会如何呢?有什么特殊的吗?回答是否定的:它和其他索引没有什 么区别。图显示了col2列上的索引。
事实上,MyISAM中主键索引和其他索引在结构上没有什么不同。主键索引就是一个名为PRIMARY的唯一非空索引。
lnnoDB的数据分布。
因为InnoDB支持聚簇索引,所以使用非常不同的方式存储同样的 数据。InnoDB以如图所示的方式存储数据。
第一眼看上去,感觉该图和前面的图没有什么不同,但再仔细看细节,会注意到该 图显示了整个表,而不是只有索引。因为在InnoDB中,聚簇索引”就是”表,所以不 像MyISAM那样需要独立的行存储。
聚簇索引的每一个叶子节点都包含了主键值、事务ID、用于事务和MVCC的回滚指 针以及所有的剩余列(在这个例子中是col2)。如果主键是一个列前缀索引,InnoDB也 会包含完整的主键列和剩下的其他列。
还有一点和MyISAM的不同是,InnoDB的二级索引和聚簇索引很不相同。InnoDB二 级索引的叶子节点中存储的不是“行指针”,而是主键值,并以此作为指向行的”指针”。
这样的策略减少了当出现行移动或者数据页分裂时二级索引的维护工作。使用主键值当 作指针会让二级索引占用更多的空间,换来的好处是,InnoDB在移动行时无须更新二 级索引中的这个”指针”。
图显示了示例表的col2索引。每一个叶子节点都包含了索引列(这里是col2), 紧 接着是主键值(col1)。
图展示了B-Tree的叶子节点结构,但我们故意省略了非叶子节点这样的细节。 InnoDB的非叶子节点包含了索引列和一个指向下级节点的指针(下一级节点可以是非 叶子节点,也可以是叶子节点)。这对聚簇索引和二级索引都适用。
在lnnoDB表中按主键顺序插入行
如果正在使用InnoDB表并且没有什么数据需要聚集,那么可以定义一个代理键 (surrogate key)作为主键,这种主键的数据应该和应用无关,最简单的方法是使用 AUTO_INCREMENT自增列。这样可以保证数据行是按顺序写入,对于根据主键做关联操作 的性能也会更好。
最好避免随机的(不连续且值的分布范围非常大)聚簇索引,特别是对于I/O密集型的 应用。例如,从性能的角度考虑,使用UUID来作为聚簇索引则会很糟糕:它使得聚簇 索引的插人变得完全随机,这是最坏的情况,使得数据没有任何聚集特性。
注意到向UUID主键插入行不仅花费的时间更长,而且索引占用的空间也更大。这一方 面是由于主键字段更长,另一方面毫无疑问是由于页分裂和碎片导致的。
为了明白为什么会这样,来看看往第一个表中插入数据时,索引发生了什么变化。 图显示了插满一个页面后继续插入相邻的下一个页面的场景。
如图所示,因为主键的值是顺序的,所以InnoDB把每一条记录都存储在上一条 记录的后面。当达到页的最大填充因子时(InnoDB默认的最大填充因子是页大小的 15/16, 留出部分空间用于以后修改),下一条记录就会写入新的页中。一且数据按照这 种顺序的方式加载,主键页就会近似于被顺序的记录填满,这也正是所期望的结果(然而, 二级索引页可能是不一样的)。
对比一下向第二个使用了UUID聚簇索引的表插入数据,看看有什么不同,图显示 了结果。
因为新行的主键值不一定比之前插入的大,所以InnoDB无法简单地总是把新行插人到 索引的最后,而是需要为新的行寻找合适的位置—一通常是已有数据的中间位置一一并 且分配空间。这会增加很多的额外工作,并导致数据分布不够优化。
下面是总结的一些 缺点:
写入的目标页可能已经刷到磁盘上井从缓存中移除,或者是还没有被加载到缓存中, InnoDB在插入之前不得不先找到并从磁盘读取目标页到内存中。这将导致大量的随 机I/O。
因为写入是乱序的,lnnoDB不得不频繁地做页分裂操作,以便为新的行分配空间。
页分裂会导致移动大量数据,一次插入最少需要修改三个页而不是一个页。
由于频繁的页分裂,页会变得稀疏并被不规则地填充,所以最终数据会有碎片。
在把这些随机值载人到聚簇索引以后,也许需要做一次OPTIMIZE TABLE来重建表并优化 页的填充。
从这个案例可以看出,使用InnoDB时应该尽可能地按主键顺序插入数据,并且尽可能 地使用单调增加的聚簇键的值来插人新行。
对于高并发工作负载,在InnoDB中按主键顺序插入可能会造成明显的争用。主键 的上界会成为“热点”。因为所有的插入都发生在这里,所以并发插入可能导致间 隙锁竞争。另一个热点可能是AUTO_INCREMENT锁机制;如果遇到这个问题,则可 能需要考虑重新设计表或者应用,或者更改innodb_autoinc_lock_mode配置。如 果你的服务器版本还不支持innodb_autoinc_lock_mode参数,可以升级到新版本 的InnoDB, 可能对这种场景会工作得更好。
覆盖索引
通常大家都会根据查询的WHERE条件来创建合适的索引,不过这只是索引优化的一个方 面。设计优秀的索引应该考虑到整个查询,而不单单是WHERE条件部分。索引确实是一 种查找数据的高效方式,但是MySQL也可以使用索引来直接获取列的数据,这样就不 再需要读取数据行。如果索引的叶子节点中已经包含要查询的数据,那么还有什么必要再回表查询呢?如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为"覆盖索引”。
覆盖索引是非常有用的工具,能够极大地提高性能。考虑一下如果查询只需要扫描索引 而无须回表,会带来多少好处:
索引条目通常远小于数据行大小,所以如果只需要读取索引,那MySQL就会极大 地减少数据访问量。这对缓存的负载非常重要,因为这种情况下响应时间大部分花 费在数据拷贝上。覆盖索引对于I/O密集型的应用也有帮助,因为索引比数据更小, 更容易全部放入内存中(这对于MylSAM尤其正确,因为MyISAM能压缩索引以 变得更小)。
因为索引是按照列值顺序存储的(至少在单个页内是如此),所以对于I/O密集型的 范围查询会比随机从磁盘读取每一行数据的I/O要少得多。对于某些存储引擎,例 如MyISAM和Percona XtraDB, 甚至可以通过OPTIMIZE命令使得索引完全顺序排 列,这让简单的范围查询能使用完全顺序的索引访问。
一些存储引擎如MyISAM在内存中只缓存索引,数据则依赖于操作系统来缓存,因 此要访问数据需要一次系统调用。这可能会导致严重的性能问题,尤其是那些系统 调用占了数据访问中的最大开销的场景。
由于InnoDB的聚簇索引,覆盖索引对lnnoDB表特别有用。 InnoDB的二级索引在 叶子节点中保存了行的主键值,所以如果二级主键能够覆盖查询,则可以避免对主 键索引的二次查询。
在所有这些场景中,在索引中满足查询的成本一般比查询行要小得多。
不是所有类型的索引都可以成为覆盖索引。覆盖索引必须要存储索引列的值,而哈希索 引、空间索引和全文索引等都不存储索引列的值,所以MySQL只能使用B-Tree索引做 覆盖索引。另外,不同的存储引擎实现覆盖索引的方式也不同,而且不是所有的引擎都 支持覆盖索引(在写作本书时,Memory存储引擎就不支持覆盖索引)。
当发起一个被索引覆盖的查询(也叫做索引覆盖查询)时,在EXPLAIN的Extra列可以 看到"Using index"的信息。
很容易把Extra列的"Using index"和type列的"index"搞混淆。其实这两者完全不同,type列和覆盖索引毫无关系;它只是表示这个查询访问数据的方式,或者说是MySQL查找行的方式。 MySQL手册中称之为连接方式(join type)。
例如,表saki la.inventory有一个多列索引(store_id, film_id)。MySQL如果只需访问这两列,就可以使用这个索引做覆盖索引,如下所示:
索引覆盖查询还有很多陷阱可能会导致无法实现优化。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。
使用上面的三种数据集来测试两种不同的查询,得到的结果如表所示。
下面是对结果的分析:
注意:原来的查询是,仅仅使用了actor索引,找到了actor对应的所有的主键,然后在主键索引找到所有的列,然后对标题进行过滤。
优化后的查询是,使用了(actor,title,produce_id)索引,找到了actor和title对应的所有的主键,然后进行关联表,然后在关联后的结果中找到对应的列。
在示例l中,查询返回了一个很大的结果集,因此看不到优化的效果。大部分时间 都花在读取和发送数据上了。
在示例2中,经过索引过滤,尤其是第二个条件过滤后只返回了很少的结果集,优 化的效果非常明显:在这个数据集上性能提高了5倍,优化后的查询的效率主要得 益于只需要读取40行完整数据行,而不是原查询中需要的30 000行。
在示例3中,显示了子查询效率反而下降的情况。因为索引过滤时符合第一个条件的结果集已经很小,所以子查询带来的成本反而比从表中直接提取完整行更高。
在大多数存储引擎中,覆盖索引只能覆盖那些只访问索引中部分列的查询。不过,可以 更进一步优化InnoDB。回想一下,InnoDB的二级索引的叶子节点都包含了主键的值, 这意味着InnoDB的二级索引可以有效地利用这些“额外”的主键列来覆盖查询。
例如,saki la.actor使用lnnoDB存储引擎,并在last_name字段有二级索引,虽然该 索引的列不包括主键actor_id, 但也能够用于对actor_id做覆盖查询:
使用索引扫描来做排序
MySQL有两种方式可以生成有序的结果:通过排序操作或者按索引顺序扫描,如果 EXPLAIN出来的type列的值为"index", 则说明MySQL使用了索引扫描来做排序(不 要和Extra列的"Using index"搞混淆了)。
扫描索引本身是很快的,因为只需要从一条索引记录移动到紧接着的下一条记录。但如 果索引不能覆盖查询所需的全部列,那就不得不每扫描一条索引记录就都回表查询一次 对应的行。这基本上都是随机I/O, 因此按索引顺序读取数据的速度通常要比顺序地全 表扫描慢,尤其是在I/O密集型的工作负载时。
MySQL可以使用同一个索引既满足排序,又用于查找行。因此,如果可能,设计索引 时应该尽可能地同时满足这两种任务,这样是最好的。
只有当索引的列顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向(倒序或正序)都一样时,MySQL才能够使用索引来对结果做排序。
如果查询需要关联多 张表,则只有当ORDER BY子句引用的字段全部为第一个表时,才能使用索引做排序。
ORDER BY子句和查找型查询的限制是一样的:需要满足索引的最左前缀的要求。否则, MySQL都需要执行排序操作,而无法利用索引排序。
有一种情况下ORDER BY子句可以不满足索引的最左前缀的要求,就是前导列为常量的时 候。如果WHERE子句或者JOIN子句中对这些列指定了常量,就可以“弥补"索引的不足。
例如,Sakila示例数据库的表rental在列(rental_ date, inventory_ id, customer_ id) 上有名为rental_date的索引。
MySQL可以使用rental_date索引为下面的查询做排序,从EXPLAIN中可以看到没有 出现文件排序(filesort)操作
即使ORDER BY子句不满足索引的最左前缀的要求,也可以用于查询排序,这是因为索引 的第一列被指定为一个常数。
还有更多可以使用索引做排序的查询示例。下面这个查询可以利用索引排序,是因为查 询为索引的第一列提供了常量条件,而使用第二列进行排序,将两列组合在一起,就形 成了索引的最左前缀:
下面这个例子理论上是可以使用索引进行关联排序的,但由于优化器在优化时将film actor表当作关联的第二张表,所以实际上无法使用索引:
使用索引做排序的一个最重要的用法是当查询同时有ORDER BY和LIMIT子句的时候。后 面我们会具体介绍这些内容。
压缩(前缀压缩)索引
MyISAM使用前缀压缩来减少索引的大小,从而让更多的索引可以放入内存中,这在某 些情况下能极大地提高性能。默认只压缩字符串,但通过参数设置也可以对整数做压缩。
MyISAM压缩每个索引块的方法是,先完全保存索引块中的第一个值,然后将其他值和 第一个值进行比较得到相同前缀的字节数和剩余的不同后缀部分,把这部分存储起来即 可。例如,索引块中的第一个值是"perform", 第二个值是"performance", 那么第二 个值的前缀压缩后存储的是类似"7,ance"这样的形式。MyISAM对行指针也采用类似 的前缀压缩方式。
压缩块使用更少的空间,代价是某些操作可能更慢。因为每个值的压缩前缀都依赖前面 的值,所以MyISAM查找时无法在索引块使用二分查找而只能从头开始扫描。正序的扫 描速度还不错,但是如果是倒序扫描一一例如ORDER BY DESC一一就不是很好了。所有 在块中查找某一行的操作平均都需要扫描半个索引块。
测试表明,对于CPU密集型应用,因为扫描需要随机查找,压缩索引使得MyISAM在 索引查找上要慢好几倍。压缩索引的倒序扫描就更慢了。压缩索引需要在CPU内存资源 与磁盘之间做权衡。压缩索引可能只需要十分之一大小的磁盘空间,如果是I/O密集型 应用,对某些查询带来的好处会比成本多很多。
可以在CREATE TABLE语句中指定PACK_KEYS参数来控制索引压缩的方式。
冗余和重复索引
MySQL允许在相同列上创建多个索引,无论是有意的还是无意的。MySQL需要单独维 护重复的索引,并且优化器在优化查询的时候也需要逐个地进行考虑,这会影响性能。
重复索引是指在相同的列上按照相同的顺序创建的相同类型的索引。应该避免这样创建 重复索引,发现以后也应该立即移除。
有时会在不经意间创建了重复索引,例如下面的代码:
一个经验不足的用户可能是想创建一个主键,先加上唯一限制,然后再加上索引以供查 询使用。事实上,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的前缀压缩)的时候。
考虑一下前面”在InnoDB中按主键顺序插入行“一节提到的userinfo表。这个表有l 000 000行,对每个state_id值大概有20 000条记录。在state_id列有一个索引对下 面的查询有用,假设查询名为Ql:
mysql> SELECT count(*) FROM userinfo WHERE state_id=5;
一个简单的测试表明该查询的执行速度大概是每秒115次(QPS)。还有一个相关查询需 要检索几个列的值,而不是只统计行数,假设名为Q2:
mysql> SELECT state_id, city, address FROM userinfo WHERE state_id=5;
对于这个查询,测试结果QPS小于10。提升该查询性能的最简单办法就是扩展索引为 (state_id, city, address), 让索引能覆盖查询:
mysql> ALTER TABLE userinfo DROP KEY state_id,ADD KEY state_id_2 (state_id, city, address);
索引扩展后,Q2运行得更快了,但是QI却变慢了。如果我们想让两个查询都变得更快, 就需要两个索引,尽管这样一来原来的单列索引是冗余的了。表显示这两个查询在 不同的索引策略下的详细结果,分别使用MyISAM和InnoDB存储引擎。注意到只有state_id_2索引时,InnoDB引擎上的查询Q1的性能下降井不明显,这是因为InnoDB 没有使用索引压缩。
有两个索引的缺点是索引成本更高。表显示了向表中插入100万行数据所需要的时间
可以看到,表中的索引越多插入速度会越慢。一般来说,增加新索引将会导致INSERT、 UPDATE、DELETE等操作的速度变慢,特别是当新增索引后导致达到了内存瓶颈的时候。
解决冗余索引和重复索引的方法很简单,删除这些索引就可以。
但首先要做的是找出 这样的索引。可以通过写一些复杂的访问INFORMATION_SCHEMA表的查询来找,不过还 有两个更简单的方法。可使用Shlomi Noach的common_schema中的一些视图来定位, common _schema是一系列可以安装到服务器上的常用的存储和视图(http://code.google. com/p/common-schemal)。这比自己编写查询要快而且简单。另外也可以使用Percona Toolkit中的pt-duplicate-key-checker, 该工具通过分析表结构来找出冗余和重复的索引。 对于大型服务器来说,使用外部的工具可能更合适些,如果服务器上有大量的数据或者 大批的表,查询INFORMATION_SCHEMA表可能会导致性能问题。
在决定哪些索引可以被删除的时候要非常小心。回忆一下,在前面的InnoDB的示例表 中,因为二级索引的叶子节点包含了主键值,所以在列(A)上的索引就相当于在(A, ID) 上的索引。如果有像WHERE A = 5 ORDER BY ID这样的查询,这个索引会很有作用。但 如果将索引扩展为(A, B)'则实际上就变成了(A, B, ID), 那么上面查询的ORDER BY子句 就无法使用该索引做排序,而只能用文件排序了。
未使用的索引
除了冗余索引和重复索引,可能还会有一些服务器永远不用的索引。这样的索引完全是 累赘,建议考虑删除。有两个工具可以帮助定位未使用的索引。最简单有效的办法是 在Percona Server或者MariaDB中先打开userstates服务器变量(默认是关闭的),然 后让服务器正常运行一段时间,再通过查询INFOMATION _SCHEMA. INDEX_ STATISTICS就 能查到每个索引的使用频率。
另外,还可以使用Percona Toolkit中的pt-index-usage, 该工具可以读取查询日志,并对日志中的每条查询进行EXPLAIN操作,然后打印出关于索引和查询的报告。这个工具不仅可以找出哪些索引是未使用的,还可以了解查询的执行计划——例如在某些情况
有些类似的查询的执行方式不一样,这可以帮助你定位到那些偶尔服务质量差的查询,优化它们以得到一致的性能表现。该工具也可以将结果写入到MySQL的表中,方便查询结果。
索引和锁
索引可以让查询锁定更少的行。如果你的查询从不访问那些不需要的行,那么就会锁定 更少的行,从两个方面来看这对性能都有好处。首先,虽然InnoDB的行锁效率很高, 内存使用也很少,但是锁定行的时候仍然会带来额外开销;其次,锁定超过需要的行会 增加锁争用并减少并发性。
InnoDB只有在访问行的时候才会对其加锁,而索引能够减少InnoDB访问的行数,从 而减少锁的数量。但这只有当InnoDB在存储引擎层能够过滤掉所有不需要的行时才有 效。如果索引无法过滤掉无效的行,那么在InnoDB检索到数据并返回给服务器层以后, MySQL服务器才能应用WHERE子句。这时已经无法避免锁定行了: InnoDB已经锁住 了这些行,到适当的时候才释放。
在MySQL 5.1和更新的版本中,InnoDB可以在服务 器端过滤掉行后就释放锁,但是在早期的MySQL版本中,InnoDB只有在事务提交后才 能释放锁。
通过下面的例子再次使用数据库Sakila很好地解释了这些情况:
这条查询仅仅会返回2-4之间的行,但是实际上获取了1 - 4之间的行的排他锁。 InnoDB会锁住第1行,这是因为MySQL为该查询选择的执行计划是索引范围扫描:
换句话说,底层存储引擎的操作是“从索引的开头开始获取满足条件actor_id < 5的 记录”,服务器并没有告诉InnoDB可以过滤第1行的WHERE条件。注意到EXPLAIN的 Extra列出现了"Using where", 这表示MySQL服务器将存储引擎返回行以后再应用 WHERE过滤条件。
下面的第二个查询就能证明第1行确实已经被锁定,尽管第一个查询的结果中并没有这 个第1行。保持第一个连接打开,然后开启第二个连接并执行如下查询:
这个查询将会挂起,直到第一个事务释放第1行的锁。这个行为对于基于语句的复制的正常运行来说是必要的。
就像这个例子显示的,即使使用了索引,InnoDB也可能锁住一些不需要的数据。如果 不能使用索引查找和锁定行的话问题可能会更糟糕,MySQL会做全表扫描并锁住所有 的行,而不管是不是需要。
关于InnoDB、索引和锁有一些很少有人知道的细节: InnoDB在二级索引上使用共享 (读)锁,但访问主键索引需要排他(写)锁。这消除了使用覆盖索引的可能性,并且 使得SELECT FOR UPDATE比LOCK IN SHARE MODE或非锁定查询要慢很多。
mysql 索引案例与索引策略相关推荐
- mysql索引实例_mysql索引之十:Mysql 索引案例学习
理解索引最好的办法是结合示例,所以这里准备了一个索引的案例. 假设要设计一个在线约会网站,用户信息表有很多列,包裹国家,地区,城市,性别,眼睛颜色,等等.完整必须支持上面这些特征的各种组合来搜索用户, ...
- Mysql 索引案例学习
理解索引最好的办法是结合示例,所以这里准备了一个索引的案例. 假设要设计一个在线约会网站,用户信息表有很多列,包裹国家,地区,城市,性别,眼睛颜色,等等.完整必须支持上面这些特征的各种组合来搜索用户, ...
- MySQL之高性能的索引策略(索引优化)
高性能的索引策略 高效地选择和使用索引有很多种方式,其中有些是针对特殊案例的优化方法,有些则是针对特定行为的优化. 独立的列 如果查询中的列不是独立的,则MySQL就不会使用索引."独立的列 ...
- mysql btree检索策略_MySQL之Btree索引和HASH索引的区别以及索引优化策略
索引是帮助mysql获取数据的数据结构.最常见的索引是Btree索引和Hash索引. 不同的引擎对于索引有不同的支持:Innodb和MyISAM默认的索引是Btree索引:而Mermory默认的索引是 ...
- mysql优化之前缀索引--带案例分析
为什么要有前缀索引 有时候需要索引很长的字符串,这会让索引变的大且慢,通常情况下可以使用某个列开始的部分字符串,这样大大的节约索引空间,从而提高索引效率,但这会降低索引的选择性,索引的选择性是指不重复 ...
- 《高性能MySQL》阅读-高性能索引策略
[四非凡人]:才德兼备是圣人,无才有德是贤人, 有才无德是小人,才德俱失是庸人. 高性能的索引策略 独立的列 ① 是指索引列不能是表达式的一部分,也不能是函数的参数(MySQL8.0版本之后可以有函数 ...
- mysql联合索引案例_mysql多个联合索引的案例分析
mysql多个联合索引的案例分析 发布时间:2020-11-23 14:54:29 来源:亿速云 阅读:61 作者:小新 小编给大家分享一下mysql多个联合索引的案例分析,相信大部分人都还不怎么了解 ...
- mysql数据库索引案例_MySQL 数据库案例优化分享-爱可生
原标题:MySQL 数据库案例优化分享-爱可生 前言 在数据库表结构变更发布之前,我们会和开发沟通索引设计是否合理,发现部分开发同学对于索引设计还是有一些知识盲区.本文把常见的案例记录下来,做个分析, ...
- MySQL数据库索引案例
MySQL索引可以用来快速地寻找某些具有特定值的记录,所有 MySQL.索引都以B-树形式保存.如果 MySQL没有索引,执行 select时会从第一个记录开始扫描整个表的所有记录,直至找到符合要求的 ...
最新文章
- kafka实现异步发送_Kafka 异步消息也会阻塞?记一次 Dubbo 频繁超时排查过程
- 一步一步使用Ext JS MVC与Asp.Net MVC 3开发简单的CMS后台管理系统之用户管理(1)...
- HDU1081:To The Max(最大子矩阵,线性DP)
- 剑指offer--剪绳子
- 使用SignalR从服务端主动推送警报日志到各种终端(桌面、移动、网页)
- 用php当作cat使用
- postmain请求中午乱码_完美解决Get和Post请求中文乱码的问题
- OpenStack管理界面开源啦!
- LWIP之TCP协议
- python入门程序异常_Python 入门 之 异常处理
- 几行VB代码拿下注册表
- 手机窃取PC信息,APT基础。
- 关于SpringMVC中model的attribute无法指定别名的解决方案
- 网站建设解决了传统的销售模式
- pojo和vo的含义
- python 手机摄像头文字识别软件_手机上有哪些好用的文字识别工具?
- css折角的简单实现 真实项目实践
- 瑞云Rayvision渲染的原创动画《吃饭睡觉打豆豆》震撼来袭 ——创造产业历史,日点击量过200万次...
- 《鬼谷子》飞箝第五(译文)
- 1,515美元的价格可让您驾驭野兽式机械套装