MySQL索引作为数据库优化的常用手段之一在项目优化中经常会被用到, 但是如何建立高效索引,有效的使用索引以及索引优化的背后到底是什么原理?这次我们深入数据库索引,从索引的数据结构开始说起.
索引原理
索引为什么能提高查询效率?当我们有一个索引index(a)
之后,写一个查询语句where a = 4
.索引是怎么工作的.在学数据结构的时候学过红黑树,这是现如今使用的最广泛了的数据结构之一了,原因就在于它查询高效.
上图就是一颗红黑树.它有一个基本特性 :
-
某个节点的左子树的值必然都小于当前节点的值
-
某个节点的右子树的值必然都大于当前节点的值
所以当我们想要找值为6的节点的时候,即类似:where node=6
.它从节点13开始,
-
只要查询的值小于当前节点就顺着左子数查找
-
要查询的节点大于当前节点就顺着当前节点的右子树查找.
所以只要经过4次的查找(13,8,1,6)就可以找到6.由于红黑树的神奇特性,所以在查找的时候可以在O(log n)的时间内做查找操作.现在我们考虑一下,如果我们索引使用了红黑树之后,它怎么帮助我们提高查询效率.
首先,每个节点必然是一个结构体,包含如下属性:
node {
node *left, //指向其左子树的指针
node *right, //指向其右子树的指针
Point *p //指向表中某一行的指针.
int data // 当前节点的值
}
当我们对某个建立一个索引的时候,MySQL就会把这个字段的所有值建立起一个红黑树,红黑树中的每个节点会有一个指针,这个指针指向当前数据的地址,比如A21是13所在行的指针,那么红黑树中值为13的节点就有一个指针指向这一行.
现在我们来模拟一遍数据库查找的流程:SQL语句为select * from table_name where a = 25
,并且a字段建立了索引,那么查询的时候就不用遍历表了,只要查询红黑树,只要经过三次查找(13,17,25)就可以得到a=25所在行的指针.有了指针就可以读取到一整行的数据.
以上就是索引优化的原理.但是一般情况下,数据库中的数据是存放在磁盘中,读取磁盘中的数据要考虑到磁臂移动花费的时间给查询带来的影响,所以数据库中使用的索引一般不是红黑树,而是B树.虽然说采用的数据结构不一样,但是其原理都是一致的,所以即使是B树,我们仍然可以按照上面的方式来理解索引查询优化原理.
MyISAM && InnoDB
MyISAM和InnoDB是MySQL常用的两个存储引擎,两个也都是采用B树作为索引的数据结构,但是虽然如此,两者还是有很大区别的.
-
MyISAM索引中的指针就是指向数据所在地址
-
当有两个索引的时候,就分别有两个B树,两个索引的指针都是指向数据所在地址
MyISAM的这种索引方式被称为非聚集索引.而在InnoDB中,索引结构跟MyISAM有比较大的区别.
-
InnoDB中,整个数据表就是按照B树来存储着,整个表就是一颗巨大的B树.整个b树是按照表的主键索引的,所以一般InnoDB必须要求表有一个主键,如果没有的话,InnoDB会隐式的生成一个6个字节的整数型作为索引.
-
如果建立了其他索引的话,那么其索引的指针不是像MyISAM保存指向数据所在地址,而是保存主键值.这意味着InnoDB在使用非主键索引的时候需要遍历两次索引,一次遍历索引找到主键值,根据主键再次遍历主键索引找到数据行.
InnoDB中每个节点的数据类似:
node {
node *left //左子树指针
node *right //右子树指针
int index //索引值
Data data //index所在行的一整行数据
}
根据InnoDB索引的特殊性.所以可以得出一些使用InnoDB存储引擎的注意方式:
-
表最好要有一个主键.避免MySQL为我们隐式生成一个主键.
-
主键除了确保唯一性,而且占用的空间要少,因为非主键索引都是保存主键值的,如果主键为类似身份证这类数据,那么会导致索引过大.
-
主键最好有auto_increment,不然再每次插入非单调数据的时候为了保持B树的结构会频繁分裂树结构导致性能降低
联合索引
要想高效使用索引,就得知道什么样的查询语句会使用到索引.对于单列索引,只要where字句包含了索引就可以使用到索引.但是很多人很容易忽略联合索引,以为联合索引只是单纯的几个单列索引叠加在一起.其实不然,如果能够建立优秀的联合索引,效率会比建立多个单列索引好上很多.这里我们使用MySQL的Explain命令来分析SQL执行的过程,所以在深入索引之前来看看Explain怎么使用
explain
使用Explain只要在查询语句前面加上explain就可以了.
mysql> explain select * from t1 where a=1 and b=2 and c=3;
+----+-------------+-------+------+---------------+---------+---------+-------------------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+---------+---------+-------------------+------+-------+
| 1 | SIMPLE | t1 | ref | idx_abc | idx_abc | 15 | const,const,const | 1 | NULL |
+----+-------------+-------+------+---------------+---------+---------+-------------------+------+-------+
1 row in set (0.00 sec)
如上 .执行Explain之后会返回给我们类似上面的数据.这里挑几个我们下面重点用到的字段.注意,有些没有提到的字段并不是不重要,只是我们重点在于使用这些字段分析索引.所以可以略过一些字段.
Extra
当值包含using index
的时候说明使用了覆盖索引.什么是覆盖索引,即select要查询的字段正好就是当做索引的字段.比如上面当查询为select a,b,c from t1 where a=1 and b=2 and c=3
.而我们正好有一个索引idx_abc(a,b,c)
.所以这个查询就使用到了覆盖索引.使用覆盖索引有什么好处?回忆一下上面索引的原理,查询的时候查询到了B树的某一个节点,要得到这个节点中保存的主键的值,然后再次遍历主键索引才能查询到数据.如果我们要查询的数据就是索引字段,那么就避免了第二次查找索引.
当值包含Using filesort
说明MySQL在查询完要的数据之后,还要对数据进行一次排序才能返回结果.
key_len
表示当前查询使用到索引的字节数.通过这个字段我们可以分析出在查询的时候是否有效的使用了联合索引.那么如何计算ken_len?,这里有几个可以遵循的法则:
-
key_len 等于索引列类型字节长度,如int占据4个字节.
-
如果索引列为变长类型的数据需要 +2 字节,这个变长类型包括varchar,以及text等长数据建立的部分索引.
-
如果索引列允许为空,那么需要 +1 字节
-
字符类型的索引长度跟字符集有关.
根据上面的法则,如果有一个索引a,其字段定义为a varchar(10)
,并且a保存的是utf-8的数据,那么:
-
varchar为变长数据 需要 +2 字节
-
字段没有定义
not null
,说明允许为空, 需要 +1 字节 -
保存的是utf-8数据,一个字符占据3字节, 需要 10*3 字节
所以,这个索引的长度为:2 + 10*3=32字节
key&&possible_key
-
key字段表示当前搜索使用到的索引
-
possible_可以表示搜索之前MySQL估计可能使用到的索引
type
这个字段说明了MySQL是如何查找表中的行.这个字段有如下的可能值,这些值从上到下依次表明了本次查询从最差到最优.
-
ALL : 表示MySQL必须扫描整张表才能找到所需要的行.注意这里的扫描是指从表的第一行数据开始扫描.
-
index : 跟ALL的一样 需要扫描全表才能找到需要的行,但是不同的是扫描的顺序不是从表的第一行开始,而是根据索引的顺序扫描的.这个有一点好处是避免了排序,因为索引本身已经是有序的.
-
range : 这种查询跟index不同的地方在于不必进行全表扫描这种类型的查询一般在where字句中带有
<, between
的时候出现.所以这种情况下,只要扫描部分索引就可以了. -
ref : 这种查找一般在使用复合索引的时候会出现,具体含义在下文给出.
除了这些之外, 还有另外的eq_ref,const,system,null
等可能.由于下文没有出现这些可能性,所以就不一一说明了.
好了,懂得根据Explain分析SQL执行过程之后,我们来看看如何使用复合索引才是最高效的.
CREATE TABLE `t1` (
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
`c` int(11) DEFAULT NULL,
KEY `idx_abc` (`a`,`b`,`c`)
) ENGINE=InnoDB DEFAULT CHARSET=utf-8
有上面的表结构,我们建立了一个复合索引idx_abc
,其实相当于建立了三个索引:idx(a),idx(a,b),idx(a,b,c)
三个索引.但是在具体查询的时候,是否能够用上这些索引还需要深入分析.
全匹配查询
mysql> explain select * from t1 where a=1 and b=2 and c=3;
+----+-------------+-------+------+---------------+---------+---------+-------------------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+---------+---------+-------------------+------+-------+
| 1 | SIMPLE | t1 | ref | idx_abc | idx_abc | 15 | const,const,const | 1 | NULL |
+----+-------------+-------+------+---------------+---------+---------+-------------------+------+-------+
1 row in set (0.00 sec)
当查询精确匹配索引中的每一个字段的时候,就会使用到(a,b,c)索引.所以这里的key_len等于15,即 4+1+4+1+4+1=15.这里所谓的精确匹配搜索指的是查询类型为 = 或者 in
的时候.
而且这里要注意一点,MySQL对索引的顺序是敏感的,即定义索引的时候顺序为idx_abc(a,b,c)
,那么查询时候字句where的出现顺序也应该是a,b,c才可以用到索引.但是由于MySQL优化器会在查询之前帮我们调整顺序.所以,即使你查询写成select * from t1 where b=1 and a=2 and c=3;
仍然可以使用到索引.
最左前缀
mysql> explain select * from t1 where a =2 and c=4;
+----+-------------+-------+------+---------------+---------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+---------+---------+-------+------+--------------------------+
| 1 | SIMPLE | t1 | ref | idx_abc | idx_abc | 5 | const | 1 | Using where; Using index |
+----+-------------+-------+------+---------------+---------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
这里where字句少了b字段,发现在查询时候只能用到索引a,这里就引出了一个最左前缀原理的概念.如果在查询的时候只是包含了索引定义顺序的某些字段,那么就只能用到复合索引的一部分.比如上面的查询,索引的定义顺序为(a,b,c).但是在查询的时候少了b字段,虽然Explain的key字段说明了使用到idx_abc
索引,但是从ken_len
中发现只是使用到了索引的第一列前缀.即只使用到了a.
当查询为:
mysql> explain select * from t1 where a=4 and b=4;
+----+-------------+-------+------+---------------+---------+---------+-------------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+---------+---------+-------------+------+-------+
| 1 | SIMPLE | t1 | ref | idx_abc | idx_abc | 10 | const,const | 1 | NULL |
+----+-------------+-------+------+---------------+---------+---------+-------------+------+-------+
这个时候(a,b)是按照索引定义顺序出现的,所以这里可以用到(a,b)索引.即key_len=10
诡异的查询
根据上面说的,如果查询的时候没有按照索引定义的顺序来使用索引,那么只有左边的部分可以使用到索引.现在我们来看一个查询.
mysql> explain select * from t1 where b=4;
+----+-------------+-------+-------+---------------+---------+---------+------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+------+------+--------------------------+
| 1 | SIMPLE | t1 | index | NULL | idx_abc | 15 | NULL | 1 | Using where; Using index |
+----+-------------+-------+-------+---------------+---------+---------+------+------+--------------------------+
1 row in set (0.00 sec)
这个查询只使用到了b,并没有使用a.按照上面的最左前缀原理,应该是没办法使用索引才对,但是这里的 ken_len
为15说明使用到了索引.这个时候我们需要看type字段,发现这个查询type=index
.而上一个为type=ref
.这两个有什么区别?
-
type=index
在解释Explain的使用的时候有讲到这种查询MySQL会对该索引进行扫描.这种只要where字句后面的字段为索引,或者复合索引的一部分,那么MySQL就会以index的方式进行扫描.但是这种扫描是很耗费性能的,因为他要从索引的第一个值开始扫描整张表.而且这种扫描是随机IO,因为这种扫描不是按照表的顺序扫描,而是按照索引的顺序扫描.这种查询的好处在于避免了排序.所以这里ken_len表示的不是使用索引进行查找数据,而是根据索引顺序进行扫描整张表 -
type=ref
,这种查找就是我们平常说的使用索引能提高查询效率的查找,他是查找和扫描的混合体.这样讲可能有点难以理解.上栗子.
有索引idx_ac(a,b,c)
,那么索引在MySQL内部的构造类似上图,这有点类似 order by a b c
的感觉,A字段是绝对有序的,只有在A字段一样的情况下,才对B进行排序.最后才是C.
当我们查询where a=2 and c=5
的时候,由于a是绝对有序的,所以查询时候通过索引可以马上查询到2,3行.之后c=5
由于没有了中间的B,所以C相当于是无序的.这个时候,MySQL只能扫描2,3行来找到所需要的数据.这样子,我们也就解释了为什么查询where a=3 and c=5
的时候只能用到索引a了.但是这个查询的的确确使用到了索引来加快查询速度.说着这个查询的type=ref
.而不是type=index
现在,我们对上面的表增加一个字段alter table t1 add other int
.这样子,表的结构就变成
CREATE TABLE `t1` (
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
`c` int(11) DEFAULT NULL,
`other` int(11) DEFAULT NULL,
KEY `idx_abc` (`a`,`b`,`c`)
)
这个时候我们再执行查询语句
mysql> explain select * from t1 where c=4;
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| 1 | SIMPLE | t1 | ALL | NULL | NULL | NULL | NULL | 1 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
发现,这里type=ALL
,按照上面Explain提到的,这个说明MySQL会去扫描全表,这是效率最低的情况.可是和上面的查询相比, 为什么我们查询的语句一样,查询的方式却从type=index
变成了type=all
?问题就在于我们添加了一个字段,添加一个字段之后让select * from t1 where c=4
变成非覆盖索引的查询.在非覆盖索引查询的时候,没有遵循最左前缀原则的查询,只能扫描全表查询.
范围查询
mysql> explain select * from t1 where a=4 and b<5 and c=4;
+----+-------------+-------+-------+---------------+---------+---------+------+------+-----------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-----------------------+
| 1 | SIMPLE | t1 | range | idx_abc | idx_abc | 10 | NULL | 1 | Using index condition |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-----------------------+
1 row in set (0.00 sec)
上面这种查询,虽然满足最左前缀,但是中间的B为范围查询(<, between),那么范围查询之后的字段是用不到索引的,所以可以看出.这里的ken_len
为10.其原理跟上面的一样, 由于B是范围查询,所以相当于C是无序的.这个时候只能使用扫描的方式来查找C.
索引与排序
索引在排序中的作用还是跟上面的一样.我们通过一个例子来分析下.
表结构如下
CREATE TABLE `t1` (
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
`c` int(11) DEFAULT NULL,
`other` int(11) DEFAULT NULL,
KEY `idx_abc` (`a`,`b`,`c`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
查询语句为:
mysql> explain select * from t1 where a=1 order by b;
+----+-------------+-------+------+---------------+---------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+---------+---------+-------+------+-------------+
| 1 | SIMPLE | t1 | ref | idx_abc | idx_abc | 5 | const | 1 | Using where |
+----+-------------+-------+------+---------------+---------+---------+-------+------+-------------+
1 row in set (0.00 sec)
这里,我们where字句只有a=1
一个,所以key_len=5
.但是Extra并没有出现using filesort
的字段,说明这里没有进行排序.原因跟上面的类似:MySQL通过索引 查找到a=1
的行数之后,由于a字段都相等,那么B字段已经是有序的,所以就不必再进行排序了.
如果查询换成:
mysql> explain select * from t1 where a=1 and b <3 order by c;
+----+-------------+-------+-------+---------------+---------+---------+------+------+---------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+------+------+---------------------------------------+
| 1 | SIMPLE | t1 | range | idx_abc | idx_abc | 10 | NULL | 1 | Using index condition; Using filesort |
+----+-------------+-------+-------+---------------+---------+---------+------+------+---------------------------------------+
1 row in set (0.00 sec)
这个时候在extra字段多了Using filesort
,说明MySQL查询到所需要的字段之后还要进行排序,因为这个SQL语句B是范围查询,所以C是无序的.