用例子来理解索引的概念是再简单不过的方法了。因此我们研究一个索引示例。
假使我们要做一个在线约会的网站。用户的资料就会有很多列,比如country, state/region, city, sex, age, eye color等等。这个站点必须支持这些属性的组合来查询用户资料。也必须支持让用户通过用户最近的在线时间,其他会员的打分等等来排序和限制结果。对于这么个复杂的需求我们怎样来设计索引?
奇怪的是,第一个要决定的就是我们是否必须使用索引排序或者文件排序是否是可接受的。基于索引的排序限定了索引和语句创建的方式。比如,我们不能使用索引的情况是WHERE条件的age在18-25之间,不管语句是否使用了一个索引去排序。
如果MySQL使用了索引的范围查询,它也就不能使用另一个索引来排序了。假如这是个比较常见的查询,我们就得知有很多查询都要用到文件排序。
现在我们需要知道列具有唯一值的数量以及那些经常出现在WHERE条件后的列。在具有许多唯一值的列创建索引,这样做选择性很高。因为MySQL会高效的过滤掉不需要的值。
country不确定是否具有选择性,但是它可能会在大部分查询语句中出现。SEX列不具有选择性,但是它基本会在每个查询语句中出现。根据以上的想法,我们可以使用前缀为(sex,country)来创建一系列不同组合的索引。
按照传统的思路来说,在选择性低的字段加索引没有什么效果。但是我们为什么把一个选择性低的列放在每个索引的开始部分呢?思路错误了?
我们有两个理由要这么使用。第一个理由就是每个查询都会用到sex。我们甚至可以设计让用户仅能通过一个sex来进行搜索。重要的是,添加这一列也不会有什么弊端,因为我们还有一些技巧在里面。
技巧就是:即使一个查询没有对sex进行一些约束,我们也能通过AND sex IN('m', 'f')保证索引的使用。这么做不会过滤任意的行。因此功能上来说和不包含sex查询的语句一样。然而,我们需要包含这一列,因为这样做会使MySQL使用一个更大前缀的索引。这个技巧比较适用于这个需求,但是如果这个值有太多的不同,这样做就不会工作的很好,因为IN()列表太大了些。
这个例子揭示出一个一般准则:全面的考虑整张表。当你设计索引,不要仅仅思考你现有语句所需的索引类型,还要考虑优化语句。如果你需要一个索引,但是你想到一些语句可能会受到不好的影响,问问自己是否能改变这些语句。你应该把语句和索引一起优化来找到折中的一个方案;你没必要设计一个完美的索引schema.
接下来我们要考虑的是其他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列放到最后。这个列有啥不同么,为什么要放到最后?我们要尽可能使MySQL是用索引上所有的列,因为只能使用最左端的前缀,直到发现第一个条件为范围查询为止。我们已经提到的其他列在WHERE中都可以用相等的条件,但是age的条件是个范围(比如18-25之间)。
我们也可以转为IN()列表。如age IN(18, 19, 20, 21, 22, 23, 24, 25),但是这种方法并不适合这个类型的查询。我们举例要说明的一般准则就是把范围条件的列放到索引的最后,因此优化器就会尽可能使用索引。
我们已经说过了你可以添加更多的列到索引中并且使用IN()列表覆盖那些并不是WHERE条件一部分的那些列,但是你可能做的过了头就会导致问题的发生。在IN中使用了过多的值,是这些list的组合变得巨大,优化器就要评估这种情况了,并且,这样也会降低查询的速度。考虑下如下的语句:
WHERE eye_color IN('brown','blue','hazel')
AND hair_color IN('black','red','blonde','brown')
AND sex IN('M','F')
优化器会把这些转换为3*4*2组合,并且WHERE条件必须要对每一个进行校验。24个组合并不是很极端的数字。但是如果这个数字变为上千的话就要小心了。旧的MySQL版本在处理大量IN组合的时候会出现很多问题:查询优化器会用很多事前和消耗很多内存。如果组合变得过大,新的MySQL版本会停止评估这些组合 ,但是仍会限制MySQL使用索引。
假设我们有个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('2008-01-17', INTERVAL 7 DAY)
AND age BETWEEN 18 AND 25
这个查询有个问题:它有两个范围条件。MySQL会使用这两个条件的其中一个,但不能同时使用。如果last_online约束单独出现,或者如果last_online的选择性多余age,我们可能希望添加另一系列的索引,要把last_online放在最后。但是,如果我们不能把age转为IN()列表,并且必须要在这两个条件同时出现来提高查询速度,怎么办?这种情况下,没有什么直接的方法,但是我们可以把一个范围条件转化为一个等值比较。为了这样做,我们添加了个active列,我们要周期性的维护它。当用户登录的时候我们会把这个列设为1,并且如果用户连续7天没有登录,会把它设为0。
这个方法可以让MySQL使用如(active, sex, country, age).这样的索引。这个列可能不是很精确,但是这种类型的查询要求的精确度低。如果我们需要精确地结果,我们可以在WHERE条件中加入last_online,当时不要索引它。这个方法有些像我们上几节说过的模拟HASH索引。这个条件不会使用任何的索引,但是因为它不太可能抛弃那些加上索引并没有太多好处的行。也就是说,没有索引也不太影响查询。
目前为止,你可能看到了一个模式:如果既想看活动的,也要看不活动的用户,我们可以添加IN()列表。我们已经添加了太多的列表了,但是可选择的是可以创建独立的索引来满足不同条件的组合。我们已经用了如下索引:(active,sex,country,age), (active,country,age), (sex,country,age), (country,age).虽然这些索引对于特有的查询都是最优的,但是就有维护的消耗,占有的空间,也可能使这种索引策略变得很失败。
这是个优化方式影响优化索引的例子。如果未来的MySQL版本可以放宽索引的扫描,它应该做到在单独索引上使用多个范围查询,因此我们也不必使用IN()列表了。
这个示例中我们所关心最后一个优化就是排序。排序小量规模的数据使用文件排序是很快的,但是如果有百万级别的呢?比如,查询条件只有个SEX的情况?
对于这种低选择性的情况,我们可以添加特殊的索引。比如,一个索引(sex,rating)可以适用于下列查询:
SELECT <cols> FROM profiles WHERE sex='M' ORDER BY rating LIMIT 10;
这个查询有ORDER BY 和LIMIT,并且它如果没有索引会非常之慢。
即使加上了索引,如果用户接口是分页并且用户请求的页并不是靠近数据开始的位置,查询还是很慢。语句如下:
mysql> SELECT <cols> FROM profiles WHERE sex='M' ORDER BY rating LIMIT 100000, 10;
这样的语句还是有问题,和加不加索引没什么关系。因为太高的数据偏移导致了MySQL不得不扫描没用的行。反规范的,提前计算的,缓存可能是处理这种语句比较好的方法。一个更好点的策略是限制用户可以查看的页数。这不太会影响用户体验,因为不会有人关心10000页以后的查询结果。
另一个优化这种语句比较好的方法是使用一个覆盖索引去重新获取行的主键。你能连接这个返回的表获得所需要的列。这些可以最小化MySQL的工作量,这些工作就是MySQL必须要收集它所要抛弃的数据。下面的例子就是针对这个语句的优化
mysql> SELECT <cols> FROM profiles INNER JOIN (
-> SELECT <primary key cols> FROM profiles
-> WHERE x.sex='M' ORDER BY rating LIMIT 100000, 10
-> ) AS x USING(<primary key cols>);