
带你读 MySQL 源码:where 条件怎么过滤记录?
源码分析系列的第 3 篇文章,我们来聊聊 MySQL 是怎么判断一条记录是否匹配 where 条件的。
本文内容基于 MySQL 8.0.32 源码。
目录
- 1. 准备工作
- 2. 整体介绍
- 3. 源码分析
○ 3.1 ExecuteIteratorQuery()
○ 3.2 FilterIterator::Read()
○ 3.3 compare_int_signed()
○ 3.4 Arg_comparator::compare()
○ 3.5 Item_func_gt::val_int()
○ 3.6 Item_cond_and::val_int()
○ 3.7 Item_func_eq::val_int()
○ 3.8 Item_cond_or::val_int()
- 4. 总结
正文
1. 准备工作
创建测试表:
插入测试数据:
示例 SQL:
2. 整体介绍
在源码中,where 条件会形成树状结构,示例 SQL 的 where 条件结构如下:
注意:这里的树状结构
不是
数据结构中的树。
我们可以从图中得到以下信息:
- Item_cond_and 代表 where 条件中的
and
,连接 Item_func_gt 和 Item_cond_or。 - Item_func_gt 代表
i2 > 20
,其中 Item_field 包含 Field_long,代表 i2 字段,Item_int 代表整数 20。 - Item_cond_or 代表 where 条件中的
or
,连接两个 Item_func_eq。 - 第 1 个 Item_func_eq 代表
i1 = 50
,其中 Item_field 包含 Field_long,代表 i1 字段,Item_int 代表整数 50。 - 第 2 个 Item_func_eq 代表
i1 = 80
,其中 Item_field 包含 Field_long,代表 i1 字段,Item_int 代表整数 80。
接下来,我们结合堆栈来看看 where 条件的实现流程:
FilterIterator::Read() 从存储引擎读取一条记录,Item_cond_and::val_int() 判断该记录是否匹配 where 条件。
从堆栈中可以看到,Item_cond_and::val_int() 的下一层有两个 Item::val_bool():
- 第 1 个 Item::val_bool() 代表
i2 > 20
,经过多级调用 Arg_comparator::compare_int_signed() 判断记录的 i2 字段值是否大于
20。 - 第 2 个 Item::val_bool() 代表
i1 = 50 or i1 = 80
。
第 2 个 Item::val_bool() 是复合条件
,它的下层还嵌套了第 3、4 个 Item::val_bool():
- 第 3 个 Item::val_bool() 代表
i1 = 50
,经过多级调用 Arg_comparator::compare_int_signed() 判断记录的 i1 字段值是否等于
50。 - 第 4 个 Item::val_bool() 代表
i1 = 80
,经过多级调用 Arg_comparator::compare_int_signed() 方法判断记录的 i1 字段值是否等于
80。
第 3、4 个 Item::val_bool() 中只要有一个返回 true,第 2 个 Item::val_bool() 就会返回 true,表示记录匹配 i1 = 50 or i1 = 80。
第 1、2 个 Item::val_bool() 必须都返回 true,Item_cond_and::val_int() 才会返回 1,表示记录匹配示例 SQL 的 where 条件。
3. 源码分析
3.1 ExecuteIteratorQuery()
这个方法是 select 语句的入口,属于重量级方法,在源码分析的第 1 篇文章《带你读 MySQL 源码:limit, offset》中也介绍过,但是,本文示例 SQL 的执行计划和之前不一样,这里有必要再介绍下。
m_root_iterator->Read() 从存储引擎读取一条记录,对于示例 SQL 来说,m_root_iterator 是 FilterIterator 迭代器对象,实际执行的方法是 FilterIterator::Read()
。
3.2 FilterIterator::Read()
上面是 FilterIterator::Read() 方法的全部代码,代码量比较少,主要逻辑如下:
m_source->Read() 方法从存储引擎读取一条记录,因为示例 SQL 中 t1 表的访问方式为全表扫描
,所以 m_source 是 TableScanIterator 迭代器对象。
通过 explain 可以确认示例 SQL 中 t1 表的访问方式为全表扫描
(type = ALL):
m_source->Read() 从存储引擎读取一条记录之后,m_condition->val_int() 会判断这条记录是否匹配 where 条件。
m_condition 代表 SQL 的 where 条件,对于示例 SQL 来说,它是 Item_cond_and 对象。
m_condition->val_int() 实际执行的方法是 Item_cond_and::val_int()
,这就是判断记录是否匹配示例 SQL where 条件的入口。
3.3 compare_int_signed()
我们以 id = 2、3 的两条记录和示例 SQL 的 where 条件 i2 > 20
为例介绍 compare_int_signed() 的逻辑:
对于 where 条件 i2 > 20,longlong val1 = (*left)->val_int()
中的 *left
表示 i2 字段。
读取 id = 2 的记录:
i2 字段值为 NULL,if (!(*left)->null_value)
条件不成立,执行流程直接来到 if (set_null) owner->null_value = true
,把 where 条件的 null_value 设置为 true,表示对于当前读取的记录,where 条件包含 NULL 值。
然后,return -1
,compare_int_signed() 方法执行结束。
读取 id = 3 记录:
i2 字段值为 31(即 val1 = 31),if (!(*left)->null_value)
条件成立,执行流程进入该 if 分支。
对于 where 条件 i2 > 20,longlong val2 = (*right)->val_int()
中的 *right
表示大于号右边的 20(即 val2 = 20),if (!(*right)->null_value)
条件成立,进入该 if 分支:
-
if (set_null) owner->null_value = false
,把 where 条件的 null_value 设置为 false,表示对于当前读取的记录,where 条件不包含 NULL 值。 -
if (val1 < val2)
,val1 = 31 大于 val2 = 20,if 条件不成立。 -
if (val1 == val2)
,val1 = 31 大于 val2 20,if 条件不成立。 -
return 1
,因为 val1 = 31 大于 val2 = 20,返回 1,表示当前读取的记录匹配 where 条件 i2 > 20。
3.4 Arg_comparator::compare()
Arg_comparator::compare() 只有一行代码,就是调用 *func
方法,比较两个值的大小。
func 属性保存了用于比较两个值大小的方法的地址,在 Arg_comparator::set_cmp_func(...)
中赋值。
对于示例 SQL 来说,where 条件中的 i1、i2 字段类型都是 int,func 属性保存的是用于比较两个整数大小的 Arg_comparator::compare_int_signed() 方法的地址。(this->*func)()
调用的方法就是 Arg_comparator::compare_int_signed()。
3.5 Item_func_gt::val_int()
这里调用的 cmp.compare() 就是上一小节介绍的 Arg_comparator::compare() 方法。
对于示例 SQL 来说,Arg_comparator::compare() 会调用 Arg_comparator::compare_int_signed() 方法,返回值只有 3 种:
-
-1
:表示 where 条件操作符左边的值小于
右边的值。 -
0
:表示 where 条件操作符左边的值等于
右边的值。 -
1
:表示 where 条件操作符左边的值大于
右边的值。
我们以 id = 3 的记录和示例 SQL 的 where 条件 i2 > 20 为例,介绍 Item_func_gt::val_int() 的逻辑:
i2 字段值为 31,对 where 条件 i2 > 20 调用 cmp.compare(),得到的返回值为 1(即 value = 1)。
value > 0 ? 1 : 0
表达式的值为 1,这就是 Item_func_ge::val_int() 的返回值,表示 id = 3 的记录匹配 where 条件 i2 > 20。
3.6 Item_cond_and::val_int()
Item_cond_and::val_int() 的逻辑:
- 判断当前读取的记录是否匹配 Item_cond_and 对象所代表的 and 连接的 N 个 where 条件(N >= 2)。
- 如果对
每个
条件调用 item->val_bool() 的返回值都是
true,说明记录匹配 and 连接的 N 个 where 条件。 - 如果对
某一个或多个
条件调用 item->val_bool() 的返回值是 false,就说明记录不匹配 and 连接的 N 个 where 条件。
由于 if (ignore_unknown() || !(null_value = item->null_value))
中的 ignore_unknown() 用于控制 where 条件中包含 NULL 值时怎么处理,我们需要展开介绍 Item_cond_and::val_int() 的代码。
想要深入了解 Item_cond_and::val_int() 代码细节的读者朋友,可以做个心理建设:内容有点长(但不会太长)。
首先,我们来看一下 null_value = false
:
null_value 的初始值被设置为 false,表示 and 连接的 N 个 where 条件中,还没出现哪个 where 条件包含 NULL 值的情况(毕竟还啥都没干)。
null_value 比较重要,它有可能最终决定 Item_cond_and::val_int() 的返回值(后面会介绍)。
然后,再来看看 while 循环的逻辑,这块内容会有一点点多:
while 循环迭代 and 连接的 N 个 where 条件。
每迭代一个 where 条件,都调用 item->val_bool() 方法,判断当前读取的记录是否匹配该条件。
如果 val_bool() 的返回值是 true,说明记录匹配
该条件,进入下一轮循环,迭代下一个 where 条件(如果有的话)。
if (current_thd->is_error()),这行代码表示执行过程中出现了错误,我们先忽略它。
如果 val_bool() 的返回值是 false,说明记录不匹配
该条件。
接下来是进入下一轮循环,还是执行 return 0
结束 Item_cond_and::val_int() 方法,就要由 if (ignore_unknown() || !(null_value = item->null_value))
决定了。
展开介绍 if (ignore_unknown() || ...)
之前,先来看看 ignore_unknown() 的定义:
从代码注释可以看到,ignore_unknown() 用于决定是否把 UNKNOWN
当作 FALSE 处理。
那么,什么是 UNKNOWN?
在 MySQL 中,NULL 会被特殊对待。NULL 和任何值(包含 NULL 本身)通过关系操作符(=、>、<、...)比较,得到的结果都是 NULL,这个结果就被认为是 UNKNOWN
。
如果想知道某个值是否为 NULL,只能使用 IS NULL、IS NOT NULL 进行判断。
说完了 ignore_unknown(),我们回到 if (ignore_unknown() || !(null_value = item->null_value))
,它包含两个表达式:
- ignore_unknown()
- !(null_value = item->null_value))
如果 ignore_unknown() 的返回值为 true,if 条件成立,执行流程就会进入 if 分支,执行 return 0
,Item_cond_and::val_int() 方法的执行流程就此结束
,表示当前读取的记录不匹配 and 连接的 N 个 where 条件。
如果 ignore_unknown() 的返回值为 false,那么还需要再判断 !(null_value = item->null_value))
的值是 true 还是 false。
我们先分解一下 !(null_value = item->null_value))
,其中包含 2 个步骤:
- null_value = item->null_value
- !null_value
如果 item->null_value 的值为 false,赋值给 null_value 之后,!null_value
的值为 true
,if 条件成立,执行流程就会进入 if (ignore_unknown() || ...)
分支,执行 return 0
,Item_cond_and::val_int() 方法的执行流程就此结束
,表示当前读取的记录不匹配 and 连接的 N 个 where 条件。
item->null_value = false,表示对于当前读取的记录,where 条件
不包含
NULL 值。
如果 item->null_value 的值为 true,赋值给 null_value 之后,!null_value
的值为 false
,即 !(null_value = item->null_value))
的值为 false
,if 条件不成立,执行流程不会进入 if (ignore_unknown() || ...)
分支,也就不会执行 return 0
了,接下来就会进入下一轮循环,迭代下一个 where 条件(如果有的话)。
item->null_value = true,表示对于当前读取的记录,where 条件
包含
NULL 值。
最后,再来看看 return null_value ? 0 : 1
:
while 循环迭代完 and 连接的 N 个 where 条件之前,如果 Item_cond_and::val_int() 方法的执行流程都没有被 while 代码块中包含的 return 0
提前结束,执行流程就会来到 return null_value ? 0 : 1
。
有两种场景会导致这种情况的出现:
> 场景 1:
while 循环迭代 and 连接的 N 个 where 条件的过程中,对每个条件调用 item->val_bool() 的返回值都是 true。
此时,null_value 属性的值为 false,null_value ? 0 : 1
表达式的值为 1,说明当前读取的记录匹配
and 连接的 N 个 where 条件。
> 场景 2:
while 循环迭代 and 连接的 N 个 where 条件的过程中,某个条件同时满足
以下 4 个要求:
- 调用 item->val_bool() 的返回值是 false,说明当前读取的记录不匹配该条件。
- ignore_unknown() 的返回值也是 false,表示包含 NULL 值的 where 条件的比较结果(
UNKNOWN
)不按 false 处理,而是要等到 while 循环结束之后,根据 null_value 属性的值(true 或 false)算总帐。
这是由 Item_cond_and 对象控制的行为,而不是 and 连接的某个 where 条件控制的行为。
- !(null_value = item->null_value)) 表达式的值为 false,说明该条件包含 NULL 值,那么它就是 ignore_unknown() = false 时需要等到 while 循环结束之后,根据 null_value 属性的值算总帐的条件。
- 该条件之后的其它 where 条件,不会导致 while 循环被提前中止(这样执行流程才能来到
return null_value ? 0 : 1
)。
此时,null_value 属性的值为 true,null_value ? 0 : 1
表达式的值为 0,说明当前读取的记录不匹配
and 连接的 N 个 where 条件。
3.7 Item_func_eq::val_int()
这里调用的 cmp.compare() 就是前面介绍的 Arg_comparator::compare() 方法。
对于示例 SQL 来说,Arg_comparator::compare() 调用的是 Arg_comparator::compare_int_signed() 方法,返回值只有 3 种:
-
-1
:表示 where 条件操作符左边的值小于
右边的值。 -
0
:表示 where 条件操作符左边的值等于
右边的值。 -
1
:表示 where 条件操作符左边的值大于
右边的值。
我们以 id = 5 的记录和示例 SQL 的 where 条件 i1 = 50 为例,介绍 Item_func_eq::val_int() 的逻辑:
i1 字段值为 50,对 where 条件 i1 = 50 调用 cmp.compare(),得到的返回值为 0(即 value = 0)。
value == 0 ? 1 : 0
表达式的值为 1,这就是 Item_func_eq::val_int() 的返回值,表示 id = 5 的记录匹配 where 条件 i1 = 50。
3.8 Item_cond_or::val_int()
我们以 id = 8 的记录和示例 SQL 的 where 条件 i1 = 50 or i1 = 80 为例,介绍 Item_cond_or::val_int() 的逻辑:
Item_cond_or 对象的 list 属性包含 2 个条件:i1 = 50、i1 = 80,List_iterator_fastli(list) 根据 list 构造一个迭代器。
对于 id = 8 的记录,i1 字段值为 80,while 循环每次迭代一个 where 条件:
第 1 次迭代,对 where 条件 i1 = 50 调用 item->val_bool(),返回值为 false,不进入
if (item->val_bool()) 分支。
if (item->null_value) 条件不成立,不执行
null_value = true。
第 2 次迭代,对 where 条件 i1 = 80 调用 item->val_bool(),返回值为 true,进入
if (item->val_bool()) 分支。
设置 Item_cond_or 对象的 null_value 属性值为 false,表示 Item_cond_or 所代表的 or 连接的 where 条件(i1 = 50、i1 = 80)都不包含 NULL 值。
return 1
,这就是 Item_cond_or::val_int() 的返回值,表示 id = 8 的记录匹配 where 条件 i1 = 50 or i1 = 80。
4. 总结
本文介绍了 SQL 的 where 条件中包含 and、or 的实现逻辑:
- 从存储引擎读取一条记录之后,对 and 连接的 N 个 where 条件(N >= 2)调用 item->val_bool() 的返回值
必须全部等于
true,记录才匹配 and 连接的 N 个 where 条件。
Item_cond_and::val_int() 的代码不多,但是这个方法中调用了ignore_known()
用于控制怎么处理 where 条件包含 NULL 值的场景,代码细节并不太好理解,所以花了比较长的篇幅介绍 Item_cond_and::val_int() 方法的逻辑,需要多花点时间去理解其中的逻辑。 - 从存储引擎读取一条记录之后,对 or 连接的 N 个 where 条件(N >= 2)调用 item->val_bool(),只要
其中一个
返回值等于 true,记录就匹配 or 连接的 N 个 where 条件。
文章转载自公众号:一树一溪
