你可能不够了解的NULL和IN

Updated on with 0 views and 1 comments

1 NULL值是空值吗?

1.1 NULL值代表什么意思?

The NULL value means “no data”.
NULL means “a missing unknown value”.

NULL值的意思就是未知的(UNKNOWN),不要想当然地认为NULL是空字符串、0值、最小值或者默认值!!!

1.2 NULL值的比较和包含NULL值的表达式

7aa312b8f0864a99af86646f6541b30a.png

  • 在MySQL中,0或NULL表示false,其他表示true。

  • NULL值与任何值做算数比较均返回NULL,也就是false的意思,所以不要再傻乎乎地写:

    • 【a is not null and a>0】,请直接写【a>0】
    • 【a is not null and a<>''】,请直接写【a<>''】
  • 包含NULL的表达式总是产生NULL值,除非在表达式中涉及的操作符和函数的文档中另有说明

  • 当使用DISTINCT、GROUP BY或ORDER BY时,所有NULL值都被视为相等

  • 使用ORDER BY … ASC时NULL值在前,ORDER BY … DESC时NULL值在后

  • 要判断一个值是否为NULL,请使用 IS [NOT] NULL 或者 IFNULL() 函数

1.3 聚合函数对NULL值的处理

  • 聚合函数,如COUNT()、MIN()和SUM()会忽略NULL值

  • 例外情况是COUNT(*),它计数行而不是单个列值。例如,下面的语句将产生两个计数,第一个是表中行数的计数,第二个是年龄列中非null值的计数
    d09de5981c3941f690e0b6c7f1112519.png

1.4 INSERT对NULL值的处理

一般情况INSERT什么值就存储什么值,但是INSERT某些数据类型,MySQL会特别处理NULL值:

  • 在TIMESTAMP列中插入NULL,则插入当前日期和时间

  • 将NULL插入具有AUTO_INCREMENT属性的整数或浮点列,则插入序列中的下一个数值

1.5 索引对NULL值的处理

MySQL5.7官方文档 IS NULL Optimization 明确提到:

MySQL can perform the same optimization on col_name IS NULL that it can use for col_name = constant_value. For example, MySQL can use indexes and ranges to search for NULL with IS NULL.

请看下面具体栗子:

88728757883e4751a86b66aacf634bca.png

注意,t01这张表只有4条记录!!!

MySQL can also optimize the combination col_name = expr OR col_name IS NULL, a form that is common in resolved subqueries. EXPLAIN shows ref_or_null when this optimization is used.

The optimization can handle only one IS NULL level. In the following query, MySQL uses key lookups only on the expression (t1.a=t2.a AND t2.a IS NULL) and is not able to use the key part on b:

420cb8154bca4c95b4f6755dce88bfd6.png

我们再来看看IS NOT NULL是否使用索引:

0f1c9b205dae4e6e88e01a56f3f1dd92.png

注意:

  • 表dim.dmp_meta_data有1573条记录,从explain的结果可以看到使用了索引,并且我们也看到了【ref_or_null】

  • 大家看看key_len这一列,截图中圈出来的195是怎么算出来的?字段type_en类型是VARCHAR(64),编码是UTF-8,也就是1个字符最多占用3个字节,64*3=192,还有3字节是怎么来的呢?因为type_en是允许为NULL的(+1字节),还有VARCHAR是可变长度的(+2字节)

  • 对比上面两个explain截图会发现,如果有【IS NOT NULL】,那么type就会是【range】,为什么会这样?联想下ORDER BY是怎么处理NULL值的

  • 上图最后一个explain为什么没有用到索引?前面已经说了【IS NOT NULL】会被转化成范围查询,由于范围查询的阻断,后续字段不能走索引,详见 Range Optimization
    6a1978d332414907b7fc9694483f1069.png

更多EXPLAIN细节参考 Optimizing Queries with EXPLAIN

1.6 NULL值的存储

先来看看InnoDB逻辑存储结构:

48d21109dfc446c6bb03ebefbf672cba.png

再来看看InnoDB文件格式和行格式:

29c24efcc9f943dcb856842d5e72f061.png

参考 InnoDB Row Formats

8de4e26e4b7e4a8490035b5442ab70f6.png

直接抛结论:

  • 使用Redundant行格式,VARCHAR类型NULL不占用任何存储空间,而CHAR类型将会占用可能存放的最大字节数

  • 使用Compact行格式,不管是VARCHAR类型还是CHAR类型,NULL值都不占用任何存储空间

请注意与key_len区分开!!!

2 使用IN会用到索引吗?

当与子查询一起使用时,IN是(=ANY)的别名。

与表达式列表一起使用时,IN和(=ANY)不是同义词。IN可以接受表达式列表,但是(=ANY)不行。

NOT IN不是(<>ANY)的别名,而是(<>ALL)的别名。

为了方便大家理解(<>ANY),MySQL增加了SOME关键字,下面两条SQL是等价的:

47bd1f072a2442d69fcd8fde96c82ffb.png

2.1 场景一:IN + 表达式列表

IN()列表中的值的数量仅受 max_allowed_packet 值的限制。

如果没有超出max_allowed_packet的限制,那么IN列表里的值都会走索引吗?

IN查询在MySQL底层是通过 n*m 的方式去搜索,类似UNION,但是效率比UNION高。IN查询在进行COST代价计算时(代价 = 元组数 * IO平均值),是通过将IN包含的数值,一条条去查询获取元组数的,因此这个计算过程会比较的慢,所以MySQL设置了一个临界值 eq_range_index_dive_limit,默认值是200,简单来说就是根据这个阈值来按照不同算法预估影响行数:

  • 如果列表数小于eq_range_index_dive_limit,则使用index dives来预估,针对每个列表值dive到index中使用索引完成元组数的估算,类似于使用索引进行实际查询得到影响行数

  • 如果列表数不小于eq_range_index_dive_limit,则使用index statistics来预估,即根据索引的统计数值进行估算,例如索引统计信息计算出每个等值影响100条数据,那么IN条件中包含5个等值则影响5*100条记录

更多细节参考 Equality Range Optimization of Many-Valued Comparisons

我们来看一个具体的栗子:

99a8d6168cb74f3b9fe1b3bd534392e8.png

从上面截图可以看到,列表数小于eq_range_index_dive_limit时用到了索引,但是列表数等于10却没有用到。简言之,eq_range_index_dive_limit这个参数会影响COST计算,进而会导致Mysql选择的索引不准确

2.2 场景二:IN + 子查询

子查询可以按两种方式进行分类:

  • 按照期望值的数量,可以将子查询分为标量子查询多值子查询
    1407339a3b4c43b6bf067c1b0f33b33b.png

  • 按照查询对外部查询的依赖可分为独立子查询相关子查询

标量子查询和多值子查询可以是独立子查询,也可以是相关子查询。

翻阅MySQL官方文档找不到独立子查询的相关介绍,这是因为大多数情况下,MySQL数据库都将独立子查询转换为相关子查询

我们来回顾下MySQL的执行过程,如下图所示:

25d8a0189fc04104946a78d040204d2b.png

其中逻辑优化和物理优化统称为查询优化:

  • 逻辑优化也称为基于规则的查询优化(Rule Based Optimization,简称RBO),主要是对查询进行逻辑上的等价变换,目的是通过这些变换提高查询的性能

  • 物理优化也称为基于代价的查询优化(Cost-based Optimization,简称CBO),主要是通过一些模型,预测一个查询使用某种执行计划时的成本,并选择其中成本最小的一个

MySQL针对子查询的具体优化见下图:

0974f149de55435990a084bcdf177fec.png

2.2.1 相关子查询

相关子查询(Dependent Subquery或Correlated Subquery)是指引用了外部查询列的子查询,即子查询会对外部查询的每行进行一次计算。

f11dfaeed20d41d0b00ccd62857a392d.png

从EXPLAIN的select_type列我们可以看到 DEPENDENT SUBQUERY,相关子查询的一个优化思路是:尽量避免相关子查询与外部查询的多次比较操作!想知道原因,请往下看。

2.2.2 EXISTS的查询效率比IN高?

MySQL文档Optimizing Subqueries with the EXISTS Strategy讲到使用EXISTS优化子查询,先看看下图这样的子查询:

fe700ef0c97845b7b9ef77c5542cc708.png

可以等价转化成EXISTS查询,如下图:

c4d8a656168a4e2eab49f44a3f079333.png

转化成EXISTS后,可以向子查询的WHERE子句下推一个适当的等值过滤条件,以使其更具限制性

但是,这并不代表使用EXISTS查询效率优于IN,请看下图:

14c33766bd5347b29f261fb4c7d93c77.png

从上图可以看到EXISTS查询时间比IN查询时间长,为什么呢?我们看下这两条SQL的EXPLAIN情况:

5c59e1e8c65844d08bb793b02a6bf648.png

从EXPLAIN的结果可以看到,IN查询被改写成【JOIN】(下文会分析),而EXISTS未被改写,再加上EXISTS子查询里的是小表,即使有下推的过滤条件,对整个查询影响不大,真正拖慢查询的是外部大表与EXISTS子查询小表需要进行大约339931次比较。

2.2.3 NOT IN 与 NOT EXISTS 的误区

上面提到IN查询可以改写成EXISTS查询,那【NOT IN】可以直接改写成【NOT EXISTS】吗?

4e265bcdcdf44468aba0c95f2b965ecb.png

如果表达式列表里包含NULL,那么NOT IN只会返回FALSE或者UNKNOWN,不会返回TRUE,而NOT EXISTS是可以返回TRUE和FALSE的。因此,使用NOT IN的时候一定要注意NULL值!!!

9fe0694df623402c9d79bb07b4b45432.png

2.2.4 半连接SEMIJOIN

半联接是一种准备时转换,它可以实现多种执行策略,如table pullout、duplicate weedout、first match、loose scan和materialization,优化器使用半联接策略来改善子查询的执行情况。

半连接和常规连接之间存在两个非常重要的区别:

  • 在半连接中,内表不会导致重复的结果

  • 此操作不会有内表中的字段添加到结果中去

因此,半连接的结果常常是来自外表记录的一个子集。从有效性上看,半连接的优化在于有效的消除了来自内表的重复项。

半连接需满足以下条件:

bcf5581dca80444286eb407eeb1c9281.png

2.2.5 Table Pullout

Table Pullout的作用就是根据主键或者唯一索引将子查询重写为JOIN语句。一个常见的SQL优化建议是“如果可能,将你的子查询重写为JOIN”,而Table Pullout正是这样做的,所以不必手动重写。

5202513321af4f138e38810066b86df2.png

2.2.6 Duplicate Weedout

Duplicate Weedout是指外部查询条件的列是唯一的,优化器会先将子查询查出来的结果进行去重。

看一个具体的栗子:

3f701ccdfce64df7afbb507d44fee856.png

如果直接转换成常规inner join,示意图如下:

8f8e4143ffc74f31af971fa1f49f67ca.png

从上图可以看到常规inner join会产生重复数据,我们再来看看Duplicate Weedout处理流程:

e29ec0441b7449caac34cd62cab3eaff.png

在Duplicate Weedout策略中,MySQL就像执行一个常规内表连接一样执行半连接,并在接下来利用一个临时表消除重复记录。当此策略需要在临时表中执行查找操作时,相较于那些常规表所消耗的成本会更低廉,因为临时表是存储在内存中的。

如果使用Duplicate Weedout优化,Extra列会出现 Start temporaryEnd temporary

8b872995945747d9bc1b0c1423b11af3.png

2.2.7 FirstMatch

FirstMatch策略执行join只挑选第一次匹配的项,非常类似于In语义。看一个栗子:

c88675f221834415b50919ec882827b2.png

常规inner join先遍历Country再遍历City,执行流程示意图如下:

e69d31b5819c406fac7ed3e2971301b5.png

如果使用FirstMatch策略,执行流程示意图如下:

96e7993fbf59455bab30802a1f2d163d.png

如果Extra列出现FirstMatch(tbl_name),说明使用FirstMatch策略:

977e3e3aa29742f1b11cdd5d6c794dfd.png

2.2.8 LooseScan

LooseScan策略利用索引来扫描一个子查询表可以从每个子查询的值群组中选出一个单一的值。看下面栗子:

304129222529453cae14442b2fa755f4.png

假设Satellite.country_code上有索引,示意图如下:

77897db8639e4d72ad711ab083fb27ba.png

LooseScan策略并不真正需要排序,它需要的是分组。在上图中,Satellite是按Country分组的。例如,所有由AUS拥有的卫星都在一起,没有与其他Country的卫星混在一起。这使得很容易从每个组中只选择一颗卫星,把它和Country关联起来,得到一个没有重复的国家名单,示意图如下:

de483d0eae3b43c0acc6d50999ddceb4.png

如果Extra列中出现LooseScan(m..n)表示使用LooseScan策略,m和n是索引部分的编号:

3ddf858eb59c4a42845b157792a0e48d.png

LooseScan策略支持下面子查询:

c361df83df934189a7a5f832c72b8bab.png

2.2.9 Materialization

如果子查询是独立子查询,那么优化器可以选择将独立子查询产生的结果写到一张物化临时表中。看下面这个栗子:

b3445c7baf094a699ab9505273354e92.png

Materialization策略实现原理如下图所示:

6385de6587474a6ba34934d6499f4415.png

物化临时表(该表包含一个索引,该索引既可以删除重复元素,也可用于后面执行Join运算)与outer表Join过程,采用类似NestedLoop形式,根据先遍历临时表还是outer表,Materialization优化可以分为:

  • Materialization Scan:先循环遍历物化表,并在outer表查找

  • Materialization Lookup:先循环遍历outer表,并在物化表查找

在MySQL 5.6.7之前,如果使用单个表,临时表的物化使用在Extra列中由 Materialize 表示,如果使用多个表,由 Start materializeEnd materialize 表示。如果出现Scan,在读表时不使用临时表的索引;否则,将使用临时表的索引查找。

在MySQL 5.6.7之后,用于物化的临时表由select_type列值为 MATERIALIZED 的行和table列值为<subqueryN>的行表示。

0b2a7b4fffdf46acbefd1b6bcae27c85.png

619a4f8f7ec8408a9e7c1025c9f327e3.png

Materialization策略不只应用在子查询的优化上,更多Materialization优化内容详见 Optimizing Derived Tables and View References with Merging or Materialization


标题:你可能不够了解的NULL和IN
作者:yanghao
地址:http://solo.fancydigital.com.cn/articles/2021/12/24/1640328145075.html