mysql null索引失效 mysql 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;
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.


 

这个表里有10000条记录:
1.


mysql> SELECT COUNT(*) FROM s1;
+----------+
| COUNT(*) |
+----------+
|    10000 |
+----------+
1 row in set (0.00 sec)
1.
2.
3.
4.
5.
6.
7.


 

下边我们直接贴几个图:

上边几个查询语句的WHERE子句中用了IS NULL、IS 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;
1.
2.
3.
4.
5.
6.


 

NULL
NULL值列表
NULL
1.
2.
3.
首先统计表中允许存储NULL的列有哪些。
我们前边说过,主键列、被NOT NULL修饰的列都是不可以存储NULL值的,所以在统计的时候不会把这些列算进去。比方说表record_format_demo的3个列c1、c3、c4都是允许存储NULL值的,而c2列是被NOT NULL修饰,不允许存储NULL值。
如果表中没有允许存储NULL的列,则NULL值列表也不存在了,否则将每个允许存储NULL的列对应一个二进制位,二进制位按照列的顺序逆序排列,二进制位表示的意义如下:
因为表record_format_demo有3个值允许为NULL的列,所以这3个列和二进制位的对应关系就是这样:
再一次强调,二进制位按照列的顺序逆序排列,所以第一个列c1和最后一个二进制位对应。

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

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

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

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


1.
这条记录的c1、c3、c4这3个列中c3和c4的值都为NULL,所以这3个列对应的二进制位的情况就是:

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

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

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

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

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

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

SELECT * FROM s1 WHERE key1 IS NULL;
1.
那它的查询示意图就如下所示:

从图中可以看出,对于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 NULL、IS NOT NULL、!=这些条件都可能使用到索引,那到底什么时候索引,什么时候采用全表扫描呢?

答案很简单:成本。当然,关于如何定量的计算使用某个索引执行查询的成本比较复杂,我们在小册中花了很大的篇幅来唠叨了。不过因为篇幅有限,我们在这里只准备定性的分析一下。对于使用二级索引进行查询来说,成本组成主要有两个方面:

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

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

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

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

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

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

不信谣,不传谣
大家可以看到,MySQL中决定使不使用某个索引执行查询的依据很简单:就是成本够不够小。而不是是否在WHERE子句中用了IS NULL、IS NOT NULL、!=这些条件。大家以后也多多辟谣吧,没那么复杂,只是一个成本而已。
-----------------------------------
mysql null索引失效 mysql null对索引的影响
https://blog.51cto.com/u_75269/7348188

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 你可以使用以下SQL语句,以标记表中某一列的重复: ```sql UPDATE your_table t1 SET t1.duplicate_column = 'duplicate' WHERE t1.primary_key NOT IN ( SELECT MIN(t2.primary_key) FROM your_table t2 GROUP BY t2.duplicate_column ) ``` 以上SQL语句会将 `your_table` 表中的 `duplicate_column` 列中重复的标记为 `'duplicate'`。这里假设 `your_table` 中有一个主键 `primary_key`,你需要将其替换为实际的主键名称。 ### 回答2: 在MySQL中,可以通过使用GROUP BY子句和HAVING子句来标记表中某一列的重复。 例如,假设我们有一个名为"students"的表,其中有两列:id和name。我们想要找出name列中重复的姓名,并将其标记。 下面是实现这个目标的步骤: 1. 使用GROUP BY子句和HAVING子句来找出重复。可以通过以下SQL查询语句找出重复的姓名: SELECT name, COUNT(*) as duplicate_count FROM students GROUP BY name HAVING duplicate_count > 1; 这个查询语句将返回一个结果集,其中包含name列中重复的姓名以及它们的重复次数。 2. 为了在表中标记这些重复的姓名,我们可以使用UPDATE语句来更新表中的数据。根据查询结果,我们可以使用以下SQL语句来标记重复的姓名: UPDATE students SET is_duplicate = 1 WHERE name IN( SELECT name FROM students GROUP BY name HAVING COUNT(*) > 1 ); 这个UPDATE语句将把is_duplicate列的设置为1,表示这些姓名是重复的。 通过以上步骤,我们可以在表中标记出name列中的重复。 ### 回答3: 在MySQL中,可以使用以下步骤来标记表中某一列的重复: 1. 创建一个临时表来存储标记结果。 ```mysql CREATE TABLE tmp_table AS SELECT your_column, COUNT(*) AS count FROM your_table GROUP BY your_column HAVING count > 1; ``` 这将创建一个临时表`tmp_table`,其中存储了重复的列和它们的数量。 2. 更新原始表,添加一个标记列来区分重复。 ```mysql ALTER TABLE your_table ADD COLUMN is_duplicate INT DEFAULT 0; ``` 这将在原始表中添加一个新列`is_duplicate`,用于标记重复。 3. 使用UPDATE语句来更新标记列。 ```mysql UPDATE your_table SET is_duplicate = 1 WHERE your_column IN ( SELECT your_column FROM tmp_table ); ``` 这将把具有重复的行的`is_duplicate`列设置为1,以便标记它们。 4. 最后,您可以通过查询具有标记列为1的行来找到重复。 ```mysql SELECT * FROM your_table WHERE is_duplicate = 1; ``` 这将返回包含重复的行作为结果。 通过以上步骤,您可以标记表中某一列的重复,并且能够轻松地找到它们。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值