一、字段为NULL走不走索引?

      我先在本地建了一张叫test的表,用存储过程插入了一百五十多万的数据,并对code字段和name字段分别加了二级索引idx_code和idx_name。根据字段中null的占比,分两种情况讨论:

 1.绝大多数是非NULL

     name字段绝大多数都是非NULL,如下图所示。

not exists走索引吗 is not null会走索引吗_not exists走索引吗

(1)sql为:select * from test where name is null 

用explain查看执行计划,如下图所示:

not exists走索引吗 is not null会走索引吗_not exists走索引吗_02

 可看出,即使用了is null的查询条件,也是走了索引的;

(2)sql为:select * from test where name is not null

用explain查看执行计划,如下图所示: 

not exists走索引吗 is not null会走索引吗_字段_03

  可看出,is not null的查询条件,却没有走索引;

 2.绝大多数是NULL

code字段绝大多数都是NULL,如下图所示。

not exists走索引吗 is not null会走索引吗_not exists走索引吗_04

(3)sql为:select * from test where code is not null

用explain查看执行计划,如下图所示:

not exists走索引吗 is not null会走索引吗_not exists走索引吗_05

   可看出,is not null的查询条件,走了索引;

(4)sql为:select * from test where code is null

用explain查看执行计划,如下图所示:

not exists走索引吗 is not null会走索引吗_字段_06

 可看出,is null的查询条件,竟然走了索引(下面讲原因);

总结:

对以上现象进行总结,可看出对于建了二级索引的字段:

(1)若绝大多数行都是非null, 则查询is null 走二级索引; (2)查询is not null走全表扫描;(3)相反,若绝大多数行都是null,则查询is not null走索引;(4)而is null 也走索引;

(1)、(2)、(3)的原因是:优化器认为,若按查询条件进行,扫描的行记录占总行记录数的比例太大,成本太高,则不走索引;相反,这个占比很小,则成本低,走索引。

(4)的原因是:优化器统计错rows了(rows是一个估算值,在本例中,此rows相当不准),因此它认为若走idx_code索引,扫描的行记录数只有754715,大致是全表的一半(全表有一百五十万的数据),成本更低,所以它选择了走索引。其实经测试,走了索引,运行select * from test where code is null的时间为8.986s;而全表扫描,即运行select * from test的时间只有2.736s,时间还短些。

因此得出最终结论:走不走索引,还得看优化器自己算的成本;有时优化器会算错,即使走索引,时间比全表扫描还要长!

not exists走索引吗 is not null会走索引吗_mysql_07

 

not exists走索引吗 is not null会走索引吗_字段_08

原理分析:

(1)null在B+树是如何存放的?
        因为主键索引的键值必定不为null,所以讨论的索引都是二级索引。首先B+树最底层是叶子节点,叶子节点间由双向链表组织。叶子节点由行记录组成,行记录间由单向链表组织,且行记录是按二级索引的键值从左往右顺序排列的。而在MySQL里null被认为小于任何值,因此键值为null的行记录被排在行记录链表的最左侧,其对应的叶子节点也在所有叶子节点里的最左侧。
(2)如何识别null?
        行记录的结构里,有NULL标识字段。若一行记录的某列值为null,则会被该标识标志。
(3)整个查找过程
        首先在B+树里根据二叉查找法找到第一个键值为null的叶子节点(其实就是在所有叶子节点的最左端),到里面找到第一个键值为null的行记录。再根据第(1)点的链表从左往右挨个查找,直到不为键值null为止。到此,可以统计所有的键值为null的行记录数。

 (4)为什么说有时走了二级索引还不如全表扫描?

        上文提到过,根本原因在于成本!二级索引的叶子结点里,键是二级索引键值,值是主键值;因此用到select * 这样的想查所有字段的sql,还得通过二级索引定位到主键索引的具体行记录(回表);若扫描二级索引行记录数过多,则回表次数也更多,还不如直接全表扫描(扫描所有的主键索引的行记录)的成本低。

二、limit走不走索引?

1.介绍:

      以select * from table_1 order by name limit 5000,1为例,table1表有一百五十多万数据,在name上建了二级索引。

      此sql中有order by,二级索引的键值本身就是排序的,所以order by 的原理很好理解。

      其执行过程如下,以此分析limit的原理

           a) 存储引擎层找在name二级索引找到第一条记录后,因为select * 要查所有字段的原因,得回表查询主键索引的记录,然后将其返回给server层。

           b) server层发现有limit 5000,1要求,就脑残地让存储引擎执行a)步骤5000次,直到找到那第5001条数据。

2.总结:

       存储引擎层并不是一次性到位地跳到第5001条行记录,而是逐个返回5001条行记录给server层。太憨了!这意味着要查询二级索引记录5001次,同时回表5001次!

3.案例及优化:

(1)优化器认为:范围较小,走二级索引可以接受,因为只需扫描5001条二级索引记录、回表5001次!走索引!

sql:select * from table_1 order by name limit 5000,1

运行时间:0.019s

not exists走索引吗 is not null会走索引吗_sql_09

(2)优化器认为:范围较大,走二级索引不可以接受,因为要扫描10001条二级索引记录、回表10001次!还不如直接走全表扫描10001条主键索引的行记录!不走索引!但全表扫描指的是扫描主键索引的所有行记录,代价仍然很大,因此查询效率很低!

sql:select * from table_1 order by name limit 10000,1

运行时间:4.5s

not exists走索引吗 is not null会走索引吗_二级索引_10

(3)人为地强制用索引,扫描10001条二级索引记录、回表10001次!

sql:select * from table_1 force index(idx_name) order by name limit 10000,1

运行时间:0.022s

not exists走索引吗 is not null会走索引吗_sql_11

(4) 最优解:用子查询的方式,更省时间!

sql:select * from table_1, (select id from table_1 order by name limit 5000, 1) as d

where table_1.id = d.id;

运行时间:0.015s, 比上面的都要短

过程分析:

      a)先用子查询,在二级索引上扫描10001条行记录。因为是select id,id可直接在二级索引叶子节点里找出来,无需回表。这些行记录可直接返回给server层;

      b)server层取得第10001条二级索引行记录后,再向存储引擎层讨要对应的主键索引的行记录;

      c)存储引擎层回表主键索引,向server层返回第10001条行记录。

简而言之,二级索引扫描记录数仍是10001条,但回表只需要回1次!

not exists走索引吗 is not null会走索引吗_mysql_12