创建高性能的索引
使用索引扫描来做排序
MySQL有两种方式可以生成有序的结果:通过排序操作;或者按索引顺序扫描(MySQL有两种排序算法);如果EXPLAIN出来的type列的值为"index",则说明使用了索引扫描来做排序(不要和Extra列的"Using index")搞混淆了。扫描索引本身是很快的。因为只需要从一条索引记录移动到紧接着的下一条记录。但如果索引不能覆盖查询所需的全部列,那就不得不每扫描一条索引记录就都回表查询一次对应的行。这基本上都是随机IO,因此按索引顺序读取数据的速度通常要比顺序地全表扫描慢,尤其是在IO密集型地工作负载时。MySQL可以使用同一个索引即满足排序,又用于查找行。因此,如果可能,设计索引时应该尽可能地同时满足这两种任务,这样是最好地。只有当索引的列顺序和ORDER BY子句的顺序完全一致,并i企鹅所有列的排序方向(倒序或正序)都一样时,MySQL才能够使用索引来对结果排序。如果查询需要关联多张表,则只有当ORDER BY子句引用的字段全部为第一个表时,才能使用索引做排序。ORDER BY子句和查找型查询的限制是一样的:需要满足索引的最左前缀的要求,否则MySQL都需要执行排序操作,而无法利用索引排序。有一种情况ORDER BY子句可以不满足索引的最左前缀的要求,就是前导列为常量的时候。如果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`),....
);
MySQL可以使用rental_date索引为下面的查询做排序,从EXPLAIN张可以看到没有出现文件排序(filesort)操作(MySQL这里称其为文件排序(filesort),其实并不一定使用磁盘文件):
mysql> EXPLAIN SELECT rental_id, staff_id FROM sakila.rental WHERE rental_date = '2005-05-25' ORDER BY inventory_id,customer_id\G
*************************** 1. row ***************************id: 1select_type: SIMPLEtable: rentalpartitions: NULLtype: ref
possible_keys: rental_datekey: rental_datekey_len: 5ref: constrows: 1filtered: 100.00Extra: Using index condition
1 row in set, 1 warning (0.00 sec)
即使ORDER BY 子句不满足索引的最左前缀的要求,也可以用于查询排序,这是因为索引的第一列被指定为一个常数。还有更多可以使用索引做排序的查询示例。下面这个查询可以利用索引排序,是因为查询为索引的第一列提供了常量条件,而使用第二列进行排序,将两列组合在一起,就形成了索引的最左前缀:
.... WHERE rental_date = '2005-05-25' ORDER BY inventory_id DESC;
下面这个查询也没问题,因为ORDER BY 使用的两列就是索引的最左前缀:
... WHERE rental_date > '2005-05-25' ORDER BY rental_date,inventory_id;
下面是一些不能使用索引做排序的查询:
- 1.下面这个查询使用了两种不同的排序方向,但是索引列都是正序排序的
... WHERE rental_date = '2005-05-25' ORDER BY inventory_id DESC,customer_id ASC;
- 2.下面这个查询的ORDER BY 子句中引用了一个不在索引中的列:
... WHERE rental_date = '2005-05-25' ORDER BY inventory_id,staff_id;
- 3.下面这个查询的WHERE和ORDER BY中的列无法组合成索引的最左前缀:
... WHERE rental_date = '2025-05-25' ORDER BY customer_id;
- 4.下面这个查询在索引列的第一个列上是范围查询,所以MySQL无法使用索引的其余列:
... WHERE rental_date > '2005-05-25' ORDER BY inventory_id, customer_id;
- 5.这个查询在inventory_id列上有多个等于条件,对于排序来说,这也是一种范围查询:
... WHERE rental_date='2005-05-25' AND inventory_id IN (1,2) ORDER BY customer_id;
下面这个例子理论上是可以使用索引进行关联排序的,但由于优化器在优化时将film_acotr表当作关联的第二张表,所以实际上无法使用索引:
mysql> EXPLAIN SELECT actor_id,title FROM sakila.film_actor INNER JOIN sakila.film USING(film_id) ORDER BY actor_id\G
*************************** 1. row ***************************id: 1select_type: SIMPLEtable: filmpartitions: NULLtype: index
possible_keys: PRIMARYkey: idx_titlekey_len: 514ref: NULLrows: 1000filtered: 100.00Extra: Using index; Using temporary; Using filesort
*************************** 2. row ***************************id: 1select_type: SIMPLEtable: film_actorpartitions: NULLtype: ref
possible_keys: idx_fk_film_idkey: idx_fk_film_idkey_len: 2ref: sakila.film.film_idrows: 5filtered: 100.00Extra: Using index
2 rows in set, 1 warning (0.00 sec)
使用索引做排序的一个最重要的用法是查询同时有ORDER BY 和LIMIT子句的时候
压缩(前缀压缩)索引
MyISAM使用前缀压缩来减少索引的大小,从而让更多的索引可以放入内存中,这在某些情况下能极大地提高性能。默认只压缩字符串,但通过参数设置也可以对整数做压缩。MyISAM压缩每个索引块的方法是,先完全保存索引块中的第一个值,然后将其他值和第一个值进行比较得到相同前缀的字节数和剩余不同后缀部分,把这部分存储起来即可。例如,索引块中的第一个值是"perform",第二个值是"performance",那么第二个值得前缀压缩后存储得是类似"7,ance"这样的形式。MyISAM对行指针也采用类似的压缩方式。
压缩块使用更少的空间,代价是某些操作可能更慢,因为每个值得压缩前缀都依赖前面的值,所以MyISAM查找时无法在索引块使用二分查找而只能从头开始扫描。正序的扫描速度还不错,但是如果时倒序扫描——例如ORDER BY DESC——就不是很好了,所有在块中查找某一行的操作平均都需要扫描半个索引块。
测试表明,对于CPU密集型应用,因为扫描需要随机查找,压缩索引使得MyISAM在索引查找上要慢好几倍。压缩索引的倒序扫描就更慢了。压缩索引需要在CPU内存资源与磁盘之间做权衡。压缩索引可能只需要十分之一大小的磁盘空间,如果时IO密集型应用,对某些查询带来的好处会比成本多很多。