上一篇文章MySQL中IS NULL、IS NOT NULL、!=不能用索引?胡扯!中唠叨了在WHERE子句中出现IS NULL、IS NOT NULL、!=这些条件时仍然可能使用索引,强调了一个查询成本的问题,很多同窗反映对这个查询成本仍是没啥概念,咱们今天再来稍微深刻的唠叨一下。算法
咱们说对于InnoDB
存储引擎来讲,表中的数据都存储在所谓的B+树中,咱们每多创建一个索引,就至关于多创建一棵B+树。bash
对于聚簇索引对应的B+树来讲,叶子节点处存储了完整的用户记录(所谓完整用户记录,就是指一条聚簇索引记录中包含全部用户定义的列已经一些内建的列),而且这些聚簇索引记录按照主键值从小到大排序。post
对于二级索引对应的B+树来讲,叶子节点处存储了不完整的用户记录(所谓不完整用户记录,就是指一条二级索引记录只包含索引列和主键),而且这些二级索引记录按照索引列的值从小到大排序。优化
咱们向表中存储了多少条记录,每一棵B+树的叶子节点中就包含多少条记录(注意是“每一棵”,包括聚簇索引对应的B+树以及二级索引对应的B+树)。ui
咱们举个例子:spa
CREATE TABLE t (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
key1 INT,
common_field VARCHAR(100),
PRIMARY KEY (id),
KEY idx_key1 (key1)
) Engine=InnoDB CHARSET=utf8;
复制代码
这个表就包含2个索引(也就是2棵B+树):设计
以id
列为主键对应的聚簇索引。3d
为key1
列创建的二级索引idx_key1
。code
咱们向表中插入一些记录:orm
INSERT INTO t VALUES
(1, 30, 'b'),
(2, 80, 'b'),
(3, 23, 'b'),
(4, NULL, 'b'),
(5, 11, 'b'),
(6, 53, 'b'),
(7, 63, 'b'),
(8, NULL, 'b'),
(9, 99, 'b'),
(10, 12, 'b'),
(11, 66, 'b'),
(12, NULL, 'b'),
(13, 66, 'b'),
(14, 30, 'b'),
(15, 11, 'b'),
(16, 90, 'b');
复制代码
因此如今s1
表的聚簇索引示意图就是这样:
s1
表的二级索引示意图就是这样:
从图中能够看出,值为NULL
的二级索引记录都被放到了B+树的最左边,这是由于设计InnoDB的大叔们有规定:
We define the SQL null to be the smallest possible value of a field.
也就是认为NULL
值是最小的。
小贴士: 原谅咱们把B+树的结构作了一个如此这般的简化,咱们省略了页面的结构,省略了全部的内节点(只画了了三角形替代),省略了记录之间的链表,由于这些不是本文的重点,画成若是所示的样子只是为了突出叶子节点处的记录是按照给定索引的键值进行排序的。
比方说咱们如今执行下边这个查询语句:
SELECT * FROM t WHERE key1 = 53;
复制代码
那么语句的执行过程就以下图所示:
用文字描述一下这个过程也就是:
先经过二级索引idx_key1
对应的B+
树快速定位到key1
列值为53
的那条二级索引记录。
而后经过二级索引记录上的主键值,也就是6
到执行回表
操做,也就是到聚簇索引中再找到id
列值为6
的聚簇索引记录。
小贴士: B+树叶子节点中的记录都是按照键值按照从小到大的顺序排好序的,经过B+树索引定位到叶子节点中的一条记录是很是快速的。不过因为咱们并无唠叨内节点、页目录这些东西,因此经过B+树索引定位到叶子节点中的一条记录的过程就不详细唠叨了,这些东西其实都在《MySQL是怎样运行的:从根儿上理解MySQL》的掘金小册里详细讲述过。
像下边这个查询:
SELECT * FROM t WHERE key1 > 20 AND key1 < 50;
复制代码
它的执行示意图就是这样:
用文字表述就是这样:
先经过二级索引idx_key1
对应的B+
树快速定位到知足key1 > 20
的第一条记录,也就是咱们图中所示的key1
值为23
的那条记录,而后根据该二级索引中的主键值3
执行回表操做,获得完整的用户记录后发送到客户端。
而后根据上一步骤中获取到的key1
列值为23
的二级索引记录的next_record
属性,找到紧邻着的下一条二级索引记录,也就是key1
列值为30
的记录,而后执行回表操做,获得完整用户记录后发送到客户端。
而后再找上一步骤中获取到的key1
列值为30
的二级索引记录的下一条记录,该记录的key1
列值也为30
,继续执行回表操做将完整的用户记录发送到客户端。
而后再找上一步骤中获取到的key1
列值为30
的二级索引记录的下一条记录,该记录的key1
列值为53
,不知足key1 < 50
的条件,因此查询就此终止。
从上边的步骤中也能够看出来:须要扫描的二级索引记录越多,须要执行的回表操做也就越多。若是须要扫描的二级索引记录占所有记录的比例达到某个范围,那优化器就可能选择使用全表扫描的方式执行查询(一个极端的例子就是扫描所有的二级索引记录,那么将对全部的二级索引记录执行回表操做,显然还不如直接全表扫描)。
小贴士: 咱们这里仍是定型的分析成本,而不定量分析。定量分析的过程比较复杂,不太小册里有写,有兴趣的同窗能够去看。
因此如今的结论就是:断定某个查询是否可使用索引的条件就是须要扫描的二级索引记录占所有记录的比例是否比较低,较低的话说明成本较低,那就可使用二级索引来执行查询,不然要采用全表扫描。
咱们分别看一下WHERE子句中出现IS NULL
、IS NOT NULL
、!=
这些条件时优化器是怎么作决策的。
比方说这个查询:
SELECT * FROM t WHERE key1 IS NULL;
复制代码
优化器在真正执行查询前,会首先少许的访问一下索引,调查一下key1
在[NULL, NULL]
这个区间的记录有多少条:
小贴士: [NULL, NULL]这个区间表明区间里只有一个NULL值。
优化器通过调查得知,须要扫描的二级索引记录占总记录条数的比例是3/16
,它以为这个查询使用二级索引来执行比较靠谱,因此在执行计划中就显示使用这个idx_key1
来执行查询:
比方说这个查询:
SELECT * FROM t WHERE key1 IS NOT NULL;
复制代码
优化器在真正执行查询前,会首先少许的访问一下索引,调查一下key1
在(NULL, +∞)
这个区间内记录有多少条:
小贴士: 咱们这里把NULL看成是最小值对待,你能够认为它比-∞都小。另外注意区间(NULL, +∞)是开区间,也就意味这不包括NULL值。
优化器通过调查得知,须要扫描的二级索引记录占总记录条数的比例是13/16
,跟显然这个比例已经很是大了,因此优化器决定使用全表扫描的方式来执行查询:
那怎么才能让使用IS NOT NULL
条件的查询使用到二级索引呢?这还不简单,让表中符合IS NOT NULL
条件的记录少不就好了,咱们能够执行一下:
UPDATE t SET key1 = NULL WHERE key1 < 80;
复制代码
这样再去执行这个查询:
SELECT * FROM t WHERE key1 IS NOT NULL;
复制代码
优化器在真正执行查询前,会首先少许的访问一下索引,调查一下key1
在(NULL, +∞)
这个区间内记录有多少条::
优化器通过调查得知,须要扫描的二级索引记录占总记录条数的比例是3/16
,它以为这个查询使用二级索引来执行比较靠谱,因此在执行计划中就显示使用这个idx_key1
来执行查询:
比方说这个查询:
SELECT * FROM t WHERE key1 != 80;
复制代码
优化器在真正执行查询前,会首先少许的访问一下索引,调查一下key1
在(NULL, 80)
和(80, +∞)
这两个区间内记录有多少条:
优化器通过调查得知,须要扫描的二级索引记录占总记录条数的比例是2/16
,它以为这个查询使用二级索引来执行比较靠谱,因此在执行计划中就显示使用这个idx_key1
来执行查询:
且慢!为啥执行计划的rows
列的值为3呢???这是个什么鬼,明明只有2条记录符合条件嘛。哈哈,咱们罗列一下每一个区间找到的符合条件的记录数量:
(NULL, 80)
区间中有0条记录知足条件key1 != 80
。
(80, +∞)
区间中有2条记录知足条件key1 != 80
。
但是设计优化器的大叔在这里有个规定:当某个范围区间符合给定条件的记录数量为0时,硬生生的把它掰成1。也就是说实际优化器认为在(NULL, 80)
这个范围区间中有1条记录符合条件key1 != 80
。因此执行计划的rows
列才显示了3
。
小贴士: 下边是设计优化器的大叔本身对当某个范围区间符合给定条件的记录数量为0时硬生生的把它掰成1的解释(能看懂的就看,看不懂赶忙跳过): The MySQL optimizer seems to believe an estimate of 0 rows is always accurate and may return the result 'Empty set' based on that. The accuracy is not guaranteed, and even if it were, for a locking read we should anyway perform the search to set the next-key lock. Add 1 to the value to make sure MySQL does not make the assumption!
至此,咱们分别分析了拥有IS NULL
、IS NOT NULL
、!=
这三个条件的查询是在什么状况下使用二级索引来执行的,核心结论就是:成本决定执行计划,跟使用什么查询条件并无什么关系。优化器会首先针对可能使用到的二级索引划分几个范围区间,而后分别调查这些区间内有多少条记录,在这些范围区间内的二级索引记录的总和占总共的记录数量的比例达到某个值时,优化器将放弃使用二级索引执行查询,转而采用全表扫描。
小贴士: 其实范围区间划分的太多也会影响优化器的决策,比方说IN条件中有太多个参数,将会下降优化器决定使用二级索引执行查询的概率。 另外,优化器调查在某个范围区间内的索引记录的条数的方式有两种,一种是所谓的index dive(这种方式在数据少的时候是精确的,在数据多时会有些误差),一种是依赖index statistics,也就是统计数据来作调查(这种方式的统计是很不精确的,有时候误差是超级巨大的),反正不论采用哪一种方式,优化器都会将各个范围区间中的索引记录数量给计算出来。关于这两种调查方式在小册中都给出了详细的算法,固然都占用了至关大的篇幅,写在公众号文章里就有点杀鸡用牛刀了。
写文章挺累的,有时候你以为阅读挺流畅的,那实际上是背后无数次修改的结果。若是你以为不错请帮忙转发一下,万分感谢~ 这里是个人公众号「咱们都是小青蛙」,里边有更多技术干货,时不时扯一下犊子,欢迎关注: