首先给定一个数据库表:
mysql>
create table t(
id int primary key,
k int not null default 0,
s varchar(16) not null default '',
index k(k)
)engine=innodb;
insert into t values(100,1,'aa'),(200,2,'bb'),(500,5,'ee'),(600,6,'ff');
我们就可以得到数据表t对应的索引结构:
那么我们如果执行下面这条sql语句,需要进行多少次索引树搜索,会扫描多少行?
mysql>
select * from t where k between 3 and 5
下面我们来分析这条sql语句的执行流程:
- 在k索引树上找到k=3的记录,取得id=300
- 再到id索引树查到id=300对应的r3
- 在k索引树取下一个值5,取得id=500
- 再回到id索引树查到id=500对应的r4
- 在k索引树取下一个值6,不满足条件,退出循环。
从上面的过程中可以观察到,读了k索引树3次(步骤1、3、5),回表2次(步骤2、4)。
但是对于mysql的server层来说,它只从引擎中拿到两条记录,因此mysql默认的扫描行数是2。
如果执行下面的语句:
mysql>
select id from t where k between 3 and 5
因为语句只是查询id,而id已经在k索引树上了,所以可以直接查到结果,不需要回表。
在这个查询里面,索引k已经覆盖了我们的查询的请求,我们称为覆盖索引。由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以覆盖索引是常用的一个优化手段。
又如下面这个例子,建立(name,age)的联合索引。当利用名字取查询对应的年龄时,就可以走这个索引。比如查询“张三”的年龄,根据索引找到名字为张三的叶子节点,节点内部就有对应的年龄,这样可以减少一次回表操作。
接下来思考一个问题:在一个市民信息表中,是否有必要将身份证号和名字建立联合索引?
这个问题需要根据实际情况来考虑。
- 如果有一个高频请求要根据身份证查询姓名,那这个身份证号和名字的联合索引就有意义。因为能够避免回表查询
- 但是,索引的维护也总是有代价的
最左前缀匹配匹配原则就是最左边的优先,优先走最左边的索引。
最左前缀原则的原理
mysql建立多列索引(联合索引)有最左前缀匹配的原则,即最左优先:
如果有一个2列的索引(a,b),则实际对(a),(a,b)建立了索引
如果有一个3列的索引(a,b,c),则实际对(a),(a,b),(a,b,c)建立了索引
比较常见的例子有以下几个,方便理解:
事先建立索引(a,b,c)
select * from table where a = '1'; --符合前缀匹配,会走索引
select * from table where a = '1' and b = '2'; --符合前缀匹配,会走索引
select * from table where a = '1' and b = '2' and c = '3'; --符合前缀匹配,会走索引
select * from table where a = '1' and b = '2' and c = '3'; --优化器会调整顺序,还是符合前缀匹配,会走索引
select * from table where c = '1'; --不符合前缀匹配原则,不会走索引
select * from table where b = '1' and c = '2'; -- 不符合前缀匹配原则,不会走索引
但是有些情况下,最左匹配原则会出现失效的情况,例如下面这个语句,会出现,a与b会走索引,但是c不会走索引:
select * from table where a = 2 and b > 1000 and c = 'yy'
对于上面这种类似的语句,mysql会一直向右匹配,直到碰到范围查询(>,<,between,like)就停止匹配。
具体的原因是因为:
如下表所示:索引(a,b,c)是先基于a排序,再基于b排序,最后基于c排序
因此索引(a,b,c)是一个,以a字段绝对有序而b与c相对有序的b 树,存储引擎可以通过二分查找定位到a=2的
数据,b在a=2的情况下是有序的(所以b的有序是相对的),依然可以通过二分查找来实现。但这些b字段有可
能有很多个不同的值,所以c字段是无序的,因此就不走索引了,直接进行扫描。
a | b | c |
---|---|---|
1 | 1 | 4 |
2 | 1 | 3 |
2 | 2 | 4 |
3 | 1 | 1 |
前面说到会有些情况无法符合最左前缀匹配原则,那么怎么处理呢?
以下面的语句为例,检索出“名字第一个字是张,且年龄是10岁的所有孩子”
事先建立索引(name,age)
mysql>select * from tuser where name like '张%' and age = 10 and ismale = 1;
在mysql 5.6之前,对于age是没法走索引的, 只能通过id进行回表。那就如下图需要四次回表。
但是在mysql 5.6之后,引入了索引下推优化(index condition pushdown),可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
有了索引下推,innodb在(name,age)索引内部就判断了age是否等于10,对于不等于10的记录就直接跳过,可以减少回表次数。如下图所示: