理解索引最好的办法就是结合案例。
案例
假设要设计一个在线约会网站,用户信息表有很多列,包括国家、地区、城市、性别、眼睛颜色等等。网站必须支持上面这些特征的各种组合来搜索用户,还必须允许根据用户的最后在线时间、其他会员对用户的评分等对用户进行排序并对结果进行限制。如何设计索引满足上面复杂的需求呢?
思考
第一件要考虑的事情是需要使用索引来排序,还是先检索数据再排序。使用索引排序会严格限制索引和查询的设计。例如,如果希望使用索引做根据其他会员对用户的评分的排序,则WHERE条件中的age between 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,county,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(18,19,20,21,22,23,24,25),但不是所有的范围查询都可以转换。这里描述的基本原则是,尽可能将需要做范围查询的列放到索引的后面,以便优化器能使用尽可能多的索引项。
前面提到可以在索引中加入更多的列。并通过IN()的方式覆盖那些不在WHERE子句中的列。但这种技巧也不鞥呢滥用,否则可能带来麻烦。因为每额外增加一个IN()条件,优化器需要做的组合都将以指数形式增加,最终可能会极大地降低查询性能。考虑下面的WHERE子句:
where eye_color in('brown','blue','hazel')
and hair_color in('black','red','blonde','brown')
and sex in('M','F')
优化器则会转化成4X3X2=24种组合,执行计划需要检查WHERE子句中所有的24种组合。对于MySQL来说,24种组合不是很夸张,但如果组合数达到上千个则需要特别小心。组合数超过一定数量后就不再进行执行计划评估了,这可能会导致MySQL不能很好地利用索引。