看面试题的时候,总能看到MySQL在什么情况下用不上索引,如下:

MySQL的WHERE子句中包含 IS NULL、IS NOT NULL、!= 这些条件时便不能使用索引查询,只能使用全表扫描。

不耽误大家时间,告诉大家结论:

MySQL中决定使不使用某个索引执行查询的依据就是成本够不够小,如果null值很多,还是会用到索引的。

自己做了个验证:

一个大概3万数据的表,如果只有10多个记录是null值,is null走索引,not null!=没走索引,如果大部分都是null值,只有部分几条数据有值,is nullnot null!=都走索引。

以下是搬过来网上的验证,让大家看看,结构如下:

CREATE TABLE s1 (
id INT NOT NULL AUTO_INCREMENT,
key1 VARCHAR(100),
key2 VARCHAR(100),
key3 VARCHAR(100),
key_part1 VARCHAR(100),
key_part2 VARCHAR(100),
key_part3 VARCHAR(100),
common_field VARCHAR(100),
PRIMARY KEY (id),
KEY idx_key1 (key1),
KEY idx_key2 (key2),
KEY idx_key3 (key3),
KEY idx_key_part(key_part1, key_part2, key_part3)
) Engine=InnoDB CHARSET=utf8;

这个表里有10000条记录:

mysql> SELECT COUNT(*) FROM s1;
+----------+
| COUNT(*) |
+----------+
| 10000 |
+----------+
1 row in set (0.00 sec)

下边贴几个图:



上边几个查询语句的WHERE子句中用了IS NULLIS NOT NULL!=这些条件,但是从它们的执行计划中可以看出来,这些语句都采用了相应的二级索引执行查询,而不是使用所谓的全表扫描,谣言不攻自破。我们来更细致的分析一下这些查询到底是怎么执行的。

NULL值是怎么在记录中存储的

在MySQL中,每一条记录都有它固定的格式,我们以InnoDB存储引擎的Compact行格式为例,来看一下NULL值是怎样存储的。在Compact行格式下,一条记录是由下边这几个部分构成的:

新建一个称之为record_format_demo的表:

CREATE TABLE record_format_demo (
c1 VARCHAR(10),
c2 VARCHAR(10) NOT NULL,
c3 CHAR(10),
c4 VARCHAR(10)
) CHARSET=ascii ROW_FORMAT=COMPACT;

因为我们的重点是NULL值是如何存储在记录中的,所以重点唠叨一下行格式的NULL值列表部分。存储NULL值的过程如下:

1.首先统计表中允许存储NULL的列有哪些。

我们前边说过,主键列、被NOT NULL修饰的列都是不可以存储NULL值的,所以在统计的时候不会把这些列算进去。比方说表record_format_demo的3个列c1c3c4都是允许存储NULL值的,而c2列是被NOT NULL修饰,不允许存储NULL值。

2.如果表中没有允许存储NULL的列,则NULL值列表也不存在了,否则将每个允许存储NULL的列对应一个二进制位,二进制位按照列的顺序逆序排列,二进制位表示的意义如下:

因为表record_format_demo有3个值允许为NULL的列,所以这3个列和二进制位的对应关系就是这样:



再一次强调,二进制位按照列的顺序逆序排列,所以第一个列c1和最后一个二进制位对应。

  • 二进制位的值为1时,代表该列的值为NULL
  • 二进制位的值为0时,代表该列的值不为NULL

3.设计InnoDB的大叔规定NULL值列表必须用整数个字节的位表示,如果使用的二进制位个数不是整数个字节,则在字节的高位补0。

record_format_demo只有3个值允许为NULL的列,对应3个二进制位,不足一个字节,所以在字节的高位补0,效果就是这样:



以此类推,如果一个表中有9个允许为NULL,那这个记录的NULL值列表部分就需要2个字节来表示了。

假设我们现在向record_format_demo表中插入一条记录:

INSERT INTO record_format_demo(c1, c2, c3, c4)
VALUES('eeee', 'fff', NULL, NULL);

这条记录的c1c3c4这3个列中c3c4的值都为NULL,所以这3个列对应的二进制位的情况就是:



所以这记录的NULL值列表用十六进制表示就是:0x06

键值为NULL的记录是怎么在B+树中存放的

对于InnoDB存储引擎来说,记录都是存储在页面中的(一个页面默认是16KB大小),这些页面可以作为B+树的节点而组成一个索引,类似这种样子(只是用下边的图举个B+树的例子而已,跟我们上边列举的表没关系):

聚簇索引和二级索引都对应着像上图一样的B+树(也就是说有多少个索引就有多少棵对应的B+树),不过:

  • 对于聚簇索引索引来说,页面中的记录是按照主键值进行排序的;而对于二级索引来说,页面中的记录是按照给定的索引列的值进行排序的。

  • 对于聚簇索引来说,B+树每一层节点(页面)都是按照页中记录的主键值大小进行排序的;而对于二级索引来说,B+树每一层节点(页面)都是按照页中记录的给定的索引列的值进行排序的。

  • 对于聚簇索引来说,B+树叶子节点对应的页面中存储的是完整的用户记录(就是一条记录中包含我们定义的所有列值,还包含一些InnoDB自己添加的一些隐藏列);而对于二级索引来说,B+树叶子节点对应的页面中存储的只是索引列的值 + 主键值

按规定,一条记录的主键值不允许存储NULL值,所以下边语句中的WHERE子句结果肯定为FALSE

SELECT * FROM tbl_name WHERE primary_key IS NULL;

像这样的语句优化器自己就能判定出WHERE子句必定为NULL,所以压根儿不会去执行它,不信我们看(Extra信息提示WHERE子句压根儿不成立):

对于二级索引来说,索引列的值可能为NULL。那对于索引列值为NULL的二级索引记录来说,它们被放在B+树的哪里呢?答案是:放在B+树的最左边。比方说我们有如下查询语句:

SELECT * FROM s1 WHERE key1 IS NULL;

那它的查询示意图就如下所示:



从图中可以看出,对于s1表的二级索引idx_key1来说,值为NULL的二级索引记录都被放在了B+树的最左边,这是因为设计InnoDB的大叔有这样的规定:

We define the SQL null to be the smallest possible value of a field.

也就是说他们把SQL中的NULL值认为是列中最小的值。

在通过二级索引idx_key1对应的B+树快速定位到叶子节点中符合条件的最左边的那条记录后,也就是本例中id值为521的那条记录之后,就可以顺着每条记录都有的next_record属性沿着由记录组成的单向链表去获取记录了,直到某条记录的key1列不为NULL。

小贴士: 通过B+树快速定位到叶子节点的记录的过程是靠一个所谓的页目录(Page Directory)做到的,不过这不是本文的重点,大家可以到小册中翻看,都有详细解释。

使不使用索引的依据到底是什么?

那既然IS NULLIS NOT NULL!=这些条件都可能使用到索引,那到底什么时候索引,什么时候采用全表扫描呢?

答案很简单:成本。因为篇幅有限,我们在这里只准备定性的分析一下。对于使用二级索引进行查询来说,成本组成主要有两个方面:

  • 读取二级索引记录的成本

  • 将二级索引记录执行回表操作,也就是到聚簇索引中找到完整的用户记录的操作所付出的成本。

很显然,要扫描的二级索引记录条数越多,那么需要执行的回表操作的次数也就越多,达到了某个比例时,使用二级索引执行查询的成本也就超过了全表扫描的成本(举一个极端的例子,比方说要扫描的全部的二级索引记录,那就要对每条记录执行一遍回表操作,自然不如直接扫描聚簇索引来的快)。

所以MySQL优化器在真正执行查询之前,对于每个可能使用到的索引来说,都会预先计算一下需要扫描的二级索引记录的数量,比方说对于下边这个查询:

SELECT * FROM s1 WHERE key1 IS NULL;

优化器会分析出此查询只需要查找key1值为NULL的记录,然后访问一下二级索引idx_key1,看一下值为NULL的记录有多少(如果符合条件的二级索引记录数量较少,那么统计结果是精确的,如果太多的话,会采用一定的手段计算一个模糊的值,当然算法也比较麻烦,我们就不展开说了,小册里有说),这种在查询真正执行前优化器就率先访问索引来计算需要扫描的索引记录数量的方式称之为index dive。当然,对于某些查询,比方说WHERE子句中有IN条件,并且IN条件中包含许多参数的话,比方说这样:

SELECT * FROM s1 WHERE key1 IN ('a', 'b', 'c', ... , 'zzzzzzz');

这样的话需要统计的key1值所在的区间就太多了,这样就不能采用index dive的方式去真正的访问二级索引idx_key1,而是需要采用之前在背地里产生的一些统计数据去估算匹配的二级索引记录有多少条(很显然根据统计数据去估算记录条数比index dive的方式精确性差了很多)。

反正不论采用index dive还是依据统计数据估算,最终要得到一个需要扫描的二级索引记录条数,如果这个条数占整个记录条数的比例特别大,那么就趋向于使用全表扫描执行查询,否则趋向于使用这个索引执行查询。

理解了这个也就好理解为什么在WHERE子句中出现IS NULLIS NOT NULL!=这些条件仍然可以使用索引,本质上都是优化器去计算一下对应的二级索引数量占所有记录数量的比值而已。

结论

大家可以看到,MySQL中决定使不使用某个索引执行查询的依据很简单:就是成本够不够小。而不是是否在WHERE子句中用了IS NULLIS NOT NULL!=这些条件。

MySQL中 IS NULL、IS NOT NULL、!= 能用上索引吗?的更多相关文章

  1. mysql中,一个数字加上null,结果为null

    在mysql中,一个数字加上null,结果为null. 这个问题是我用update语句时遇见的,就像下边的例子 update tableName set number = number + x 这里的 ...

  2. Mysql中使用聚合函数对null值的处理

    平时因为对于数据库研习的不深,所以在面试的时候问了一些平常遇到过的问题居然没法很肯定地回答出来,实在让自己很恼怒! 这次让我记忆深刻的一个问题是: 在mysql中使用聚合函数的时候比如avg(t),t ...

  3. 【MySQL】MySQL中针对大数据量常用技术_创建索引+缓存配置+分库分表+子查询优化(转载)

    原文地址:http://blog.csdn.net/zwan0518/article/details/11972853 目录(?)[-] 一查询优化 1创建索引 2缓存的配置 3slow_query_ ...

  4. MySQL中NOT IN语句对NULL值的处理

    与使用in时不同: 在使用in 时: SELECT COUNT(name) FROM CVE WHERE name NOT IN ('CVE-1999-0001', 'CVE-1999-0002'); ...

  5. MySQL中对字段内容为Null的处理

    使用如下指令,意思就是 select IFNULL(jxjy,0) AS jxjy from yourTable ifnull(a,b) 意思是指:如果字段a为null,就等于b if( sex = ...

  6. MySQL中order by中关于NULL值的排序问题

    MySQL中order by 排序遇到NULL值的问题 MySQL数据库,在order by排序的时候,如果存在NULL值,那么NULL是最小的,ASC正序排序的话,NULL值是在最前面的. 如果我们 ...

  7. Mysql中Union和OR性能对比

    博客已搬家,更多内容查看https://liangyongrui.github.io/ Mysql中Union和OR性能对比 在leetcode上看到一篇文章,整理一下 参考:https://leet ...

  8. MySQL 中NULL和空值的区别

    平时我们在使用MySQL的时候,对于MySQL中的NULL值和空值区别不能很好的理解.注意到NULL值是未知的,且占用空间,不走索引,DBA建议建表的时候最好设置字段是NOT NULL 来避免这种低效 ...

  9. MySQL 中NULL和空值的区别 (转载 http://blog.sina.com.cn/s/blog_3f2a82610102v4dn.html)

    平时我们在使用MySQL的时候,对于MySQL中的NULL值和空值区别不能很好的理解.注意到NULL值是未知的,且占用空间,不走索引,DBA建议建表的时候最好设置字段是NOT NULL 来避免这种低效 ...

  10. mysql中的null字段值的处理及大小写问题

    在MySQL中,NULL字段的处理,需要注意,当在处理查询条件中有NULL,很有可能你得到的值不是想要的,因为,在MySQL中,判断NULL值相等(=)或者不等(!=)都会返回false.主要出现在常 ...

随机推荐

  1. Laravel通过用户名和密码查询

    一.如果要检查要验证的用户数据是否正确,可以使用: if (Auth::validate($credentials)) { // } 二.但是如果您想通过用户和密码从数据库中获取用户,您可以使用: / ...

  2. 右键添加IDEA打开

    在安装IntelliJ IDEA时可能没有选择文件用idea打开的选项,现在有这个需求. 下面就演示一下,如何添加文件右键用idea打开! 1. 打开注册表 win+R键输入regedit 2. 找到 ...

  3. Oracle 12c中CDB与PDB实例参数更改影响实验

    基础知识单薄的同学,请逐字逐句阅读以下概念,来自于博客园AskScuti. 预备知识:什么是参数文件.存放位置.参数文件的分类和参数文件的命名方式.参数文件如何创建.参数文件加载顺序.参数分类.参数修 ...

  4. mybatis(五):源码分析 - config文件加载流程

    详细的可以参考https://blog.csdn.net/weixin_33850890/article/details/88112849

  5. OpenGL 编程指南 (1)

    1.在OpenGL3.0(包含3.0)前,或者使用兼容模式(compatibility profile)环境,OpenGL还包含一个固定功能管线(fixed-function pipeline),这时 ...

  6. [codeigniter4]Upgrading from 3.x to 4.x

    CodeIgniter 4 is a rewrite of the framework, and is not backwards compatible. It is more appropriate ...

  7. Java的三种循环:1、for循环 2、while循环 3、do...while循环

    Java的三种循环 Java三种循环结构: 1.for循环 2.while循环 3.do...while循环 循环结构组成部分:1.条件初始化语句,2.条件判断语句 , 3.循环体语句,4.条件控制语 ...

  8. 16day 路径信息系列

    ../ 上一级目录 ./ 当前路径 ~ 返回到家目录 - 两个目录之间进行快速切换 An argument of - is equivalent to $OLDPWD(环境变量) 补充说明: [roo ...

  9. C语言-指针深度分析

    1.变量回顾 程序中的变量只是—段存储空间的别名,那么是不 是必须通过这个别名才能使用这段存储空间? 2.思考 下面的程序输出什么?为什么? ;    int* p = &i;       p ...

  10. VMware上Linux虚拟机和Windows共享文件夹

    参考文章:https://blog.csdn.net/qq_19004627/article/details/78689641 操作环境:主机:Windows10,VMware Workstation ...