bcp 不能调用where 子句_MySQL中IS NULL、IS NOT NULL、!=不能用索引?胡扯!

不知道从什么时候开始,网上流传着这么一个说法:

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

这种说法愈演愈烈,甚至被很多同学奉为真理。咱啥话也不说,举个例子。假如我们有个表s1,结构如下:

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)

下边我们直接贴几个图:

b80447695d7f39e0d620c60289a22893.png

a9676cc2105b627912cc314b8b4140af.png


image

43db63b9c3d73bb576b78022ee03cade.png


image

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

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

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

3807ec161a82a9a41e43a8148a1e850c.png


image

为了故事的顺利发展,我们新建一个称之为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个列和二进制位的对应关系就是这样:

fa76e0cfa5d1b440553ba6743213d96b.png


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

  • 二进制位的值为1时,代表该列的值为NULL
  • 二进制位的值为0时,代表该列的值不为NULL
  1. 设计InnoDB的大叔规定NULL值列表必须用整数个字节的位表示,如果使用的二进制位个数不是整数个字节,则在字节的高位补0。
    record_format_demo只有3个值允许为NULL的列,对应3个二进制位,不足一个字节,所以在字节的高位补0,效果就是这样:

9cddc296028af29e58a56061f0e4555d.png


以此类推,如果一个表中有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个列对应的二进制位的情况就是:

6abc110539e3d379871c27245894ec0c.png

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

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

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

246d1b5545f60507fa39b05eeeda5fb2.png

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

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

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

SELECT * FROM tbl_name WHERE primary_key IS NULL;

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

d336b904a9658c370b1c4dc3b5ddceb0.png


image

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

SELECT * FROM s1 WHERE key1 IS NULL;

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

c3d2cbef0b40fc6159d37119ab0185d4.png


image

从图中可以看出,对于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!=这些条件。大家以后也多多辟谣吧,没那么复杂,只是一个成本而已。欢迎希望文章对你有帮助,喜欢的可以关注作者给个赞哦

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/469043.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于Springboot外卖系统10:公共字段填充功能+ThreadLocal模块改进

1. 公共字段自动填充 1.1 问题分析 在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时需要设置修改时间、修改人等字段。这些字段属于公共字段,也就是也就是在系统中很多表中都会有这些字段,如下: 而…

## __VA_ARGS__ ... 宏和可变参数

在GNU C中,宏可以接受可变数目的参数,就象函数一样,例如: 1 2 #define pr_debug(fmt,arg...) \ printk(KERN_DEBUG fmt, ##arg) 用可变参数宏(variadic macros)传递可变参数表 你可能很熟悉在函数中使用可变参数表,如: 1 vo…

腾讯云挂在和格式化数据盘

新购买了数据盘时,需要格式化才可使用。未购买数据盘的用户可以跳过此步骤。也可以根据需要进行多分区操作。 这里以Windows 2012R2为例进行格式化说明。 1) 通过步骤四介绍的方法登录Windows云服务器。 2) 点击【开始】(Start)-【服务器管理…

基于Springboot外卖系统11:菜品新增类别+类别信息分页查询

1. 新增分类 1.1 需求分析 后台系统中可以管理分类信息,分类包括两种类型,分别是 菜品分类 和 套餐分类 。当我们在后台系统中添加菜品时需要选择一个菜品分类,在后台系统中添加一个套餐时需要选择一个套餐分类,在移动端也会按照…

Linux内核自旋锁

Linux内核自旋锁 自旋锁 自旋锁(spinlock)是用在多个CPU系统中的锁机制,当一个CPU正访问自旋锁保护的临界区时,临界区将被锁上,其他需要访问此临界区的CPU只能忙等待,直到前面的CPU已访问完临界区&#xf…

词云python_诗词名句网 -古诗词大全|诗歌|诗词鉴赏|古诗名句|诗句赏析!

倾酒向涟漪,乘流欲去时。 寸心同尺璧,投此报冯夷。 展开全文 江曲全萦楚,云飞半自秦。 岘山回首望,如别故乡人。 浦烟含夜色,冷日转秋旻。 自有沈碑在,清光不照人。 楚岸云空合,楚城人不来。 只…

基于Springboot外卖系统12:删除菜品套餐类别+修改套餐类别信息

1. 删除分类 1.1 需求分析 在分类管理列表页面,可以对某个分类进行删除操作。需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除。 1.2 前端页面分析 在前端页面中,点击 "删除" 按钮,就会触发定义的方法&…

html5手机移动端三级联动城市选择器

//我的地址 var area1 new LArea(); area1.init({ trigger: #demo1, //触发选择控件的文本框,同时选择完毕后name属性输出到该位置 valueTo: #value1, //选择完毕后id属性输出到该位置 keys: { id: id, name: name }, //绑定数据源相关字段 id对应valueTo的value属…

我的师傅是风清扬

1 珍惜那些处处帮助你的人 国庆假期就要过去了,国庆这几天一直在考虑一个问题(先保留是什么问题),也咨询了几个比较信任的朋友,都没有得到肯定的答案。 回家路上跟了一个大哥哥(陈哥)&#xf…

要多大内存才满足_佛龛的尺寸要多大?

佛龛是用于供奉佛像或者牌位的小阁子,大多数是木制家具。佛龛在一般是仿中国古代的房子等工程建筑制做而成,在其纹样层面有很高的要求,有关纹样今日姑且先不谈。除此之外佛龛的尺寸大小多少才算吉祥也是很有讲究的,那麼这个问题跟…

【YOLOV5-6.x讲解】数据配置文件 data/XXX.yaml

主干目录: 【YOLOV5-6.x 版本讲解】整体项目代码注释导航现在YOLOV5已经更新到6.X版本,现在网上很多还停留在5.X的源码注释上,因此特开一贴传承开源精神!5.X版本的可以看其他大佬的帖子本文章主要从6.X版本出发,主要解…

printf 宏 调试技巧

1 前言 printf调试是嵌入式调试的基本手段,而且是非常重要的手段,我认为相比单步调试更加有用有效,特别是单片机之后跑系统,单步调试效率更加低下了,我们在工作遇到bug的时候,我们第一时间就想知道那些该死…

自定义控件--实现步骤

前言: 在android开发中我们常常会用到自定义控件,可是为什么要自定义呢?自定义控件又该怎么实现呢?下面我们来了解下自定义控件的常用知识。1:为什么要自定义控件 用到自定义控件的几种情况如下:1> 通常…

python print 输出到txt_(Python基础教程之七)Python字符串操作

Python基础教程在SublimeEditor中配置Python环境Python代码中添加注释Python中的变量的使用Python中的数据类型Python中的关键字Python字符串操作Python中的list操作Python中的Tuple操作Pythonmax()和min()–在列表或数组中查找最大值和最小值Python找到最大的N个(前N个)或最小…

【YOLOV5-6.x讲解】常用工具类 models/common.py

主干目录: 【YOLOV5-6.x 版本讲解】整体项目代码注释导航现在YOLOV5已经更新到6.X版本,现在网上很多还停留在5.X的源码注释上,因此特开一贴传承开源精神!5.X版本的可以看其他大佬的帖子本文章主要从6.X版本出发,主要解…

Ps钢笔操作技巧

鼠标左键单击新建锚点;Ctrl鼠标左键移动锚点/移动调节点; Alt鼠标左键锚点/角点转换; 方向键微调锚点位置; shift鼠标左键新建水平/垂直锚点;CtrlAlt鼠标左键选中所有锚点; 【重点内容】:锚点断…

Linux 内核自旋锁

为什么需要内核自旋锁? 现在很多CPU都是几核几核的了,如果有一个变量A,CPU-X正在访问,突然CPU-Y也过来访问他,这时候就可能出现问题,因为这个A非常重要,可能导致系统崩溃,中断异常等。 我们来看之前说的TP驱动里面的代码 void gtp_irq_enable(struct goodix_ts_data…

【YOLOV5-6.x讲解】DIY实验文件 models/experimental.py

主干目录: 【YOLOV5-6.x 版本讲解】整体项目代码注释导航现在YOLOV5已经更新到6.X版本,现在网上很多还停留在5.X的源码注释上,因此特开一贴传承开源精神!5.X版本的可以看其他大佬的帖子本文章主要从6.X版本出发,主要解…

mysql 触发器_MySQL入门之触发器

触发器作用当操作了某张表时,希望同时触发一些动作/行为,可以使用触发器完成!!例如: 当向员工表插入一条记录时,希望同时往日志表插入数据。首先创建日志表-- 日志表CREATE TABLE test_log(id INT PRIMARY …

【YOLOV5-6.x讲解】模型搭建模块 models/yolo.py

主干目录: 【YOLOV5-6.x 版本讲解】整体项目代码注释导航现在YOLOV5已经更新到6.X版本,现在网上很多还停留在5.X的源码注释上,因此特开一贴传承开源精神!5.X版本的可以看其他大佬的帖子本文章主要从6.X版本出发,主要解…