很多时候,我们在mysql中创建了索引,但是某些查询还是很慢,根本就没有使用到索引!
一般来说,可能是某些字段没有创建索引,或者是组合索引中字段的顺序与查询语句中字段的顺序不符。

看下面的例子:
假设有一张订单表(orders),包含order_id和product_id二个字段。
一共有31条数据。符合下面语句的数据有5条。

执行下面的sql语句:
select product_id 
from orders
where order_id in (123, 312, 223, 132, 224);

这条语句要mysql去根据order_id进行搜索,然后返回匹配记录中的product_id。

所以组合索引应该按照以下的顺序创建:

create index orderid_productid on orders(order_id, product_id)
 mysql> explain select product_id from orders where order_id in (123, 312, 223, 132, 224) \G
 *************************** 1. row ***************************
            id: 1
   select_type: SIMPLE
         table: orders
          type: range
 possible_keys: orderid_productid
           key: orderid_productid
       key_len: 5
           ref: NULL
          rows: 5
         Extra: Using where; Using index
 1 row in set (0.00 sec)



可以看到,这个组合索引被用到了,扫描的范围也很小,只有5行。

如果把组合索引的顺序换成product_id, order_id的话,
mysql就会去索引中搜索 *123 *312 *223 *132 *224,必然会有些慢了。

mysql> create index orderid_productid on orders(product_id, order_id);                                                       
 Query OK, 31 rows affected (0.01 sec)
 Records: 31  Duplicates: 0  Warnings: 0

 mysql> explain select product_id from orders where order_id in (123, 312, 223, 132, 224) \G

 *************************** 1. row ***************************

            id: 1
   select_type: SIMPLE
         table: orders
          type: index
 possible_keys: NULL
           key: orderid_productid
       key_len: 10
           ref: NULL
          rows: 31
         Extra: Using where; Using index
 1 row in set (0.00 sec)


这次索引搜索的性能显然不能和上次相比了。

rows:31,我的表中一共就31条数据。

索引被使用部分的长度:key_len:10,比上一次的key_len:5多了一倍。

不知道是这样在索引里面查找速度快,还是直接去全表扫描更快呢?

mysql> alter table orders add modify_a char(255) default 'aaa';
 Query OK, 31 rows affected (0.01 sec)
 Records: 31  Duplicates: 0  Warnings: 0

 mysql> 
 mysql> 
 mysql> explain select modify_a from orders where order_id in (123, 312, 223, 132, 224) \G          
 *************************** 1. row ***************************
            id: 1
   select_type: SIMPLE
         table: orders
          type: ALL
 possible_keys: NULL
           key: NULL
       key_len: NULL
           ref: NULL
          rows: 31
         Extra: Using where
 1 row in set (0.00 sec)



这样就不会用到索引了。 刚才是因为select的product_id与where中的order_id都在索引里面的。

为什么要创建组合索引呢?这么简单的情况直接创建一个order_id的索引不就行了吗?
如果只有一个order_id索引,没什么问题,会用到这个索引,然后mysql要去磁盘上的表里面取到product_id。

如果有组合索引的话,mysql可以完全从索引中取到product_id,速度自然会快。

再多说几句组合索引的最左优先原则:
组合索引的第一个字段必须出现在查询组句中,这个索引才会被用到。
如果有一个组合索引(col_a,col_b,col_c)

下面的情况都会用到这个索引:

col_a = "some value";
 col_a = "some value" and col_b = "some value";
 col_a = "some value" and col_b = "some value" and col_c = "some value";
 col_b = "some value" and col_a = "some value" and col_c = "some value";



对于最后一条语句,mysql会自动优化成第三条的样子~~。

下面的情况就不会用到索引:
col_b = "aaaaaa";
col_b = "aaaa" and col_c = "cccccc";

通过实例理解单列索引、多列索引以及最左前缀原则

实例:现在我们想查出满足以下条件的用户id:
mysql>SELECT `uid` FROM people WHERE lname`='Liu'  AND `fname`='Zhiqun' AND `age`=26
因为我们不想扫描整表,故考虑用索引。

单列索引:
ALTER TABLE people ADD INDEX lname (lname);
将lname列建索引,这样就把范围限制在lname='Liu'的结果集1上,之后扫描结果集1,产生满足fname='Zhiqun'的结果集2,再扫描结果集2,找到 age=26的结果集3,即最终结果。

由 于建立了lname列的索引,与执行表的完全扫描相比,效率提高了很多,但我们要求扫描的记录数量仍旧远远超过了实际所需 要的。虽然我们可以删除lname列上的索引,再创建fname或者age 列的索引,但是,不论在哪个列上创建索引搜索效率仍旧相似。

2.多列索引:
ALTER TABLE people ADD INDEX lname_fname_age (lame,fname,age);
为了提高搜索效率,我们需要考虑运用多列索引,由于索引文件以B-Tree格式保存,所以我们不用扫描任何记录,即可得到最终结果。

注:在mysql中执行查询时,只能使用一个索引,如果我们在lname,fname,age上分别建索引,执行查询时,只能使用一个索引,mysql会选择一个最严格(获得结果集记录数最少)的索引。

3.最左前缀:顾名思义,就是最左优先,上例中我们创建了lname_fname_age多列索引,相当于创建了(lname)单列索引,(lname,fname)组合索引以及(lname,fname,age)组合索引。

注:在创建多列索引时,要根据业务需求,where子句中使用最频繁的一列放在最左边。


复合索引对排序的优化:

复合索引只对和索引中排序相同或相反的order by 语句优化。
在创建复合索引时,每一列都定义了升序或者是降序。如定义一个复合索引:


Sql代码  

    1. CREATE INDEX
    2. ON table1 (col1 ASC, col2 DESC, col3 ASC)


    其中 有三列分别是:col1 升序,col2 降序, col3 升序。现在如果我们执行两个查询

    1:Select col1, col2, col3 from table1 order by col1 ASC, col2 DESC, col3 ASC


      和索引顺序相同


    2:Select col1, col2, col3 from table1 order by col1 DESC, col2 ASC, col3 DESC

     和索引顺序相反

    查询1,2 都可以别复合索引优化。


    如果查询为:


    Select col1, col2, col3 from table1 order by col1 ASC, col2 ASC, col3 ASC


      排序结果和索引完全不同时,此时的 查询不会被复合索引优化。


    下面是一些常见的索引限制问题

    1、使用不等于操作符(<>, !=)
    下面这种情况,即使在列dept_id有一个索引,查询语句仍然执行一次全表扫描
    select * from dept where staff_num <> 1000;
    但是开发中的确需要这样的查询,难道没有解决问题的办法了吗?
    有!
    通过把用 or 语法替代不等号进行查询,就可以使用索引,以避免全表扫描:上面的语句改成下面这样的,就可以使用索引了。


    Sql代码  

    1. select * from dept shere staff_num < 1000 or

    2、使用 is null 或 is not null


    使用 is null 或is nuo null也会限制索引的使用,因为数据库并没有定义null值。如果被索引的列中有很多null,就不会使用这个索引(除非索引是一个位图索引,关于位图索引,会在以后的blog文章里做详细解释)。在sql语句中使用null会造成很多麻烦。


    解决这个问题的办法就是:建表时把需要索引的列定义为非空(not null)



    3、使用函数


    如果没有使用基于函数的索引,那么where子句中对存在索引的列使用函数时,会使优化器忽略掉这些索引。下面的查询就不会使用索引:


    Sql代码  

    1. select * from staff where trunc(birthdate) = '01-MAY-82';

    但是把函数应用在条件上,索引是可以生效的,把上面的语句改成下面的语句,就可以通过索引进行查找。


    Sql代码  

    1. select * from staff where birthdate < (to_date('01-MAY-82') + 0.9999);

    4、比较不匹配的数据类型


    比较不匹配的数据类型也是难于发现的性能问题之一。


    下面的例子中,dept_id是一个varchar2型的字段,在这个字段上有索引,但是下面的语句会执行全表扫描。


    Sql代码  


    1. select * from dept where

    这是因为oracle会自动把where子句转换成to_number(dept_id)=900198,就是3所说的情况,这样就限制了索引的使用。


    把SQL语句改为如下形式就可以使用索引


    Sql代码  

    1. select * from dept where dept_id = '900198';


    恩,这里还有要注意的:


    比方说有一个文章表,我们要实现某个类别下按时间倒序列表显示功能:

    SELECT * FROM articles WHERE category_id = ... ORDER BY created DESC LIMIT ...

    这样的查询很常见,基本上不管什么应用里都能找出一大把类似的SQL来,学院派的读者看到上面的SQL,可能会说SELECT *不好,应该仅仅查询需要的字段,那我们就索性彻底点,把SQL改成如下的形式:


    SELECT id FROM articles WHERE category_id = ... ORDER BY created DESC LIMIT ...

    我们假设这里的id是主键,至于文章的具体内容,可以都保存到memcached之类的键值类型的缓存里,如此一来,学院派的读者们应该挑不出什么毛病来了,下面我们就按这条SQL来考虑如何建立索引:

    不考虑数据分布之类的特殊情况,任何一个合格的WEB开发人员都知道类似这样的SQL,应该建立一个”category_id, created“复合索引,但这是最佳答案不?不见得,现在是回头看看标题的时候了:MySQL里建立索引应该考虑数据库引擎的类型!

    如果我们的数据库引擎是InnoDB,那么建立”category_id, created“复合索引是最佳答案。让我们看看InnoDB的索引结构,在InnoDB里,索引结构有一个特殊的地方:非主键索引在其BTree的叶节点上会额外保存对应主键的值,这样做一个最直接的好处就是Covering Index,不用再到数据文件里去取id的值,可以直接在索引里得到它。

    如果我们的数据库引擎是MyISAM,那么建立"category_id, created"复合索引就不是最佳答案。因为MyISAM的索引结构里,非主键索引并没有额外保存对应主键的值,此时如果想利用上Covering Index,应该建立"category_id, created, id"复合索引。

    唠完了,应该明白我的意思了吧。希望以后大家在考虑索引的时候能思考的更全面一点,实际应用中还有很多类似的问题,比如说多数人在建立索引的时候不从Cardinality(SHOW INDEX FROM ...能看到此参数)的角度看是否合适的问题,Cardinality表示唯一值的个数,一般来说,如果唯一值个数在总行数中所占比例小于20%的话,则可以认为Cardinality太小,此时索引除了拖慢insert/update/delete的速度之外,不会对select产生太大作用;还有一个细节是建立索引的时候未考虑字符集的影响,比如说username字段,如果仅仅允许英文,下划线之类的符号,那么就不要用gbk,utf-8之类的字符集,而应该使用latin1或者ascii这种简单的字符集,索引文件会小很多,速度自然就会快很多。这些细节问题需要读者自己多注意,我就不多说了。