The NULL value means “no data”.
NULL means “a missing unknown value”.
NULL值的意思就是未知的(UNKNOWN),不要想当然地认为NULL是空字符串、0值、最小值或者默认值!!!
在MySQL中,0或NULL表示false,其他表示true。
NULL值与任何值做算数比较均返回NULL,也就是false的意思,所以不要再傻乎乎地写:
包含NULL的表达式总是产生NULL值,除非在表达式中涉及的操作符和函数的文档中另有说明
当使用DISTINCT、GROUP BY或ORDER BY时,所有NULL值都被视为相等
使用ORDER BY … ASC时NULL值在前,ORDER BY … DESC时NULL值在后
要判断一个值是否为NULL,请使用 IS [NOT] NULL
或者 IFNULL()
函数
聚合函数,如COUNT()、MIN()和SUM()会忽略NULL值
例外情况是COUNT(*),它计数行而不是单个列值。例如,下面的语句将产生两个计数,第一个是表中行数的计数,第二个是年龄列中非null值的计数
一般情况INSERT什么值就存储什么值,但是INSERT某些数据类型,MySQL会特别处理NULL值:
在TIMESTAMP列中插入NULL,则插入当前日期和时间
将NULL插入具有AUTO_INCREMENT属性的整数或浮点列,则插入序列中的下一个数值
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.
请看下面具体栗子:
注意,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:
我们再来看看IS NOT NULL
是否使用索引:
注意:
表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:
更多EXPLAIN细节参考 Optimizing Queries with EXPLAIN
先来看看InnoDB逻辑存储结构:
再来看看InnoDB文件格式和行格式:
直接抛结论:
使用Redundant行格式,VARCHAR类型NULL不占用任何存储空间,而CHAR类型将会占用可能存放的最大字节数
使用Compact行格式,不管是VARCHAR类型还是CHAR类型,NULL值都不占用任何存储空间
请注意与key_len区分开!!!
当与子查询一起使用时,IN是(=ANY)的别名。
与表达式列表一起使用时,IN和(=ANY)不是同义词。IN可以接受表达式列表,但是(=ANY)不行。
NOT IN不是(<>ANY)的别名,而是(<>ALL)的别名。
为了方便大家理解(<>ANY),MySQL增加了SOME关键字,下面两条SQL是等价的:
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
我们来看一个具体的栗子:
从上面截图可以看到,列表数小于eq_range_index_dive_limit时用到了索引,但是列表数等于10却没有用到。简言之,eq_range_index_dive_limit这个参数会影响COST计算,进而会导致Mysql选择的索引不准确。
子查询可以按两种方式进行分类:
按照期望值的数量,可以将子查询分为标量子查询和多值子查询
按照查询对外部查询的依赖可分为独立子查询和相关子查询
标量子查询和多值子查询可以是独立子查询,也可以是相关子查询。
翻阅MySQL官方文档找不到独立子查询的相关介绍,这是因为大多数情况下,MySQL数据库都将独立子查询转换为相关子查询。
我们来回顾下MySQL的执行过程,如下图所示:
其中逻辑优化和物理优化统称为查询优化:
逻辑优化也称为基于规则的查询优化(Rule Based Optimization,简称RBO),主要是对查询进行逻辑上的等价变换,目的是通过这些变换提高查询的性能
物理优化也称为基于代价的查询优化(Cost-based Optimization,简称CBO),主要是通过一些模型,预测一个查询使用某种执行计划时的成本,并选择其中成本最小的一个
MySQL针对子查询的具体优化见下图:
相关子查询(Dependent Subquery或Correlated Subquery)是指引用了外部查询列的子查询,即子查询会对外部查询的每行进行一次计算。
从EXPLAIN的select_type列我们可以看到 DEPENDENT SUBQUERY
,相关子查询的一个优化思路是:尽量避免相关子查询与外部查询的多次比较操作!想知道原因,请往下看。
MySQL文档Optimizing Subqueries with the EXISTS Strategy讲到使用EXISTS优化子查询,先看看下图这样的子查询:
可以等价转化成EXISTS查询,如下图:
转化成EXISTS后,可以向子查询的WHERE子句下推一个适当的等值过滤条件,以使其更具限制性。
但是,这并不代表使用EXISTS查询效率优于IN,请看下图:
从上图可以看到EXISTS查询时间比IN查询时间长,为什么呢?我们看下这两条SQL的EXPLAIN情况:
从EXPLAIN的结果可以看到,IN查询被改写成【JOIN】(下文会分析),而EXISTS未被改写,再加上EXISTS子查询里的是小表,即使有下推的过滤条件,对整个查询影响不大,真正拖慢查询的是外部大表与EXISTS子查询小表需要进行大约339931次比较。
上面提到IN查询可以改写成EXISTS查询,那【NOT IN】可以直接改写成【NOT EXISTS】吗?
如果表达式列表里包含NULL,那么NOT IN只会返回FALSE或者UNKNOWN,不会返回TRUE,而NOT EXISTS是可以返回TRUE和FALSE的。因此,使用NOT IN的时候一定要注意NULL值!!!
半联接是一种准备时转换,它可以实现多种执行策略,如table pullout、duplicate weedout、first match、loose scan和materialization,优化器使用半联接策略来改善子查询的执行情况。
半连接和常规连接之间存在两个非常重要的区别:
在半连接中,内表不会导致重复的结果
此操作不会有内表中的字段添加到结果中去
因此,半连接的结果常常是来自外表记录的一个子集。从有效性上看,半连接的优化在于有效的消除了来自内表的重复项。
半连接需满足以下条件:
Table Pullout的作用就是根据主键或者唯一索引将子查询重写为JOIN语句。一个常见的SQL优化建议是“如果可能,将你的子查询重写为JOIN”,而Table Pullout正是这样做的,所以不必手动重写。
Duplicate Weedout是指外部查询条件的列是唯一的,优化器会先将子查询查出来的结果进行去重。
看一个具体的栗子:
如果直接转换成常规inner join,示意图如下:
从上图可以看到常规inner join会产生重复数据,我们再来看看Duplicate Weedout处理流程:
在Duplicate Weedout策略中,MySQL就像执行一个常规内表连接一样执行半连接,并在接下来利用一个临时表消除重复记录。当此策略需要在临时表中执行查找操作时,相较于那些常规表所消耗的成本会更低廉,因为临时表是存储在内存中的。
如果使用Duplicate Weedout优化,Extra列会出现 Start temporary
和 End temporary
:
FirstMatch策略执行join只挑选第一次匹配的项,非常类似于In语义。看一个栗子:
常规inner join先遍历Country再遍历City,执行流程示意图如下:
如果使用FirstMatch策略,执行流程示意图如下:
如果Extra列出现FirstMatch(tbl_name),说明使用FirstMatch策略:
LooseScan策略利用索引来扫描一个子查询表可以从每个子查询的值群组中选出一个单一的值。看下面栗子:
假设Satellite.country_code上有索引,示意图如下:
LooseScan策略并不真正需要排序,它需要的是分组。在上图中,Satellite是按Country分组的。例如,所有由AUS拥有的卫星都在一起,没有与其他Country的卫星混在一起。这使得很容易从每个组中只选择一颗卫星,把它和Country关联起来,得到一个没有重复的国家名单,示意图如下:
如果Extra列中出现LooseScan(m..n)表示使用LooseScan策略,m和n是索引部分的编号:
LooseScan策略支持下面子查询:
如果子查询是独立子查询,那么优化器可以选择将独立子查询产生的结果写到一张物化临时表中。看下面这个栗子:
Materialization策略实现原理如下图所示:
物化临时表(该表包含一个索引,该索引既可以删除重复元素,也可用于后面执行Join运算)与outer表Join过程,采用类似NestedLoop形式,根据先遍历临时表还是outer表,Materialization优化可以分为:
Materialization Scan:先循环遍历物化表,并在outer表查找
Materialization Lookup:先循环遍历outer表,并在物化表查找
在MySQL 5.6.7之前,如果使用单个表,临时表的物化使用在Extra列中由 Materialize
表示,如果使用多个表,由 Start materialize
和 End materialize
表示。如果出现Scan,在读表时不使用临时表的索引;否则,将使用临时表的索引查找。
在MySQL 5.6.7之后,用于物化的临时表由select_type列值为 MATERIALIZED
的行和table列值为<subqueryN>的行表示。
Materialization策略不只应用在子查询的优化上,更多Materialization优化内容详见 Optimizing Derived Tables and View References with Merging or Materialization
The best Christmas present ever.