2

带你读 MySQL 源码:Where 条件怎么过滤记录?

 11 months ago
source link: https://www.51cto.com/article/755772.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

带你读 MySQL 源码:Where 条件怎么过滤记录?

作者:操盛春 2023-05-26 07:45:18
从存储引擎读取一条记录之后,对 Or 连接的 N 个 Where 条件(N >= 2)调用 Item->val_bool(),只要其中一个返回值等于True,记录就匹配 Or 连接的 N 个 Where 条件。
916b38b404eed527baf293088bb2ef1d2c62e2.png

我们来聊聊 MySQL 是怎么判断一条记录是否匹配 where 条件的。

本文内容基于 MySQL 8.0.32 源码。

创建测试表:

CREATE TABLE `t1` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `str1` varchar(255) DEFAULT '',
  `i1` int DEFAULT '0',
  `i2` int DEFAULT '0',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

插入测试数据:

INSERT INTO t1(str1, i1, i2) VALUES
('s1', NULL, NULL),
('s2', 20, NULL),
('s3', 30, 31),
('s4', 40, 41),
('s5', 50, 51),
('s6', 60, 61),
('s7', 70, 71),
('s8', 80, 81);

示例 SQL:

select * from t1
where i2 > 20 and (i1 = 50 or i1 = 80)

在源码中,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 条件的实现流程:

| > mysql_execute_command(THD*, bool) sql/sql_parse.cc:4688
| + > Sql_cmd_dml::execute(THD*) sql/sql_select.cc:578
| + - > Sql_cmd_dml::execute_inner(THD*) sql/sql_select.cc:778
| + - x > Query_expression::execute(THD*) sql/sql_union.cc:1823
| + - x = > Query_expression::ExecuteIteratorQuery(THD*) sql/sql_union.cc:1770
| + - x = | > FilterIterator::Read() sql/iterators/composite_iterators.cc:79
| + - x = | + > Item_cond_and::val_int() sql/item_cmpfunc.cc:5973
| + - x = | + - > // 第 1 个 Item::val_bool()
| + - x = | + - > // 代表 i2 > 20
| + - x = | + - > Item::val_bool() sql/item.cc:218
| + - x = | + - x > Item_func_gt::val_int() sql/item_cmpfunc.cc:2686
| + - x = | + - x = > Arg_comparator::compare() sql/item_cmpfunc.h:210
| + - x = | + - x = | > Arg_comparator::compare_int_signed() sql/item_cmpfunc.cc:1826
| + - x = | + - x = | + > Item_field::val_int() sql/item.cc:3013
| + - x = | + - x = | + - > Field_long::val_int() const sql/field.cc:3763 // i2
| + - x = | + - x = | + > Item_int::val_int() sql/item.h:4934 // 20
| + - x = | + - > // 第 2 个 Item::val_bool()
| + - x = | + - > // 代表 i1 = 50 or i1 = 80
| + - x = | + - > Item::val_bool() sql/item.cc:218
| + - x = | + - x > Item_cond_or::val_int() sql/item_cmpfunc.cc:6017
| + - x = | + - x = > // 第 3 个 Item::val_bool()
| + - x = | + - x = > // 代表 i1 = 50
| + - x = | + - x = > Item::val_bool() sql/item.cc:218
| + - x = | + - x = | > Item_func_eq::val_int() sql/item_cmpfunc.cc:2493
| + - x = | + - x = | + > Arg_comparator::compare() sql/item_cmpfunc.h:210
| + - x = | + - x = | + - > Arg_comparator::compare_int_signed() sql/item_cmpfunc.cc:1826
| + - x = | + - x = | + - x > Item_field::val_int() sql/item.cc:3013
| + - x = | + - x = | + - x = > Field_long::val_int() const sql/field.cc:3763 // i1
| + - x = | + - x = | + - x > Item_int::val_int() sql/item.h:4934 // 50
| + - x = | + - x = > // 第 4 个 Item::val_bool()
| + - x = | + - x = > // 代表 i1 = 80
| + - x = | + - x = > Item::val_bool() sql/item.cc:218
| + - x = | + - x = | > Item_func_eq::val_int() sql/item_cmpfunc.cc:2493
| + - x = | + - x = | + > Arg_comparator::compare() sql/item_cmpfunc.h:210
| + - x = | + - x = | + - > Arg_comparator::compare_int_signed() sql/item_cmpfunc.cc:1826
| + - x = | + - x = | + - x > Item_field::val_int() sql/item.cc:3013
| + - x = | + - x = | + - x = > Field_long::val_int() const sql/field.cc:3763 // i1
| + - x = | + - x = | + - x > Item_int::val_int() sql/item.h:4934 // 80

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 条件。

ExecuteIteratorQuery()

// sql/sql_union.cc
bool Query_expression::ExecuteIteratorQuery(THD *thd) {
  ...
  {
    ...
    for (;;) {
      // 从存储引擎读取一条记录
      int error = m_root_iterator->Read();
      DBUG_EXECUTE_IF("bug13822652_1", thd->killed = THD::KILL_QUERY;);

      // 读取出错,直接返回
      if (error > 0 || thd->is_error())  // Fatal error
        return true;
      // error < 0
      // 表示已经读完了所有符合条件的记录
      // 查询结束
      else if (error < 0)
        break;
      // SQL 被客户端干掉了
      else if (thd->killed)  // Aborted by user
      {
        thd->send_kill_message();
        return true;
      }
      ...
      // 发送数据给客户端
      if (query_result->send_data(thd, *fields)) {
        return true;
      }
      ...
    }
  }
  ...
}

这个方法是 select 语句的入口,属于重量级方法,在源码分析的第 1 篇文章《带你读 MySQL 源码:limit, offset》中也介绍过,但是,本文示例 SQL 的执行计划和之前不一样,这里有必要再介绍下。

m_root_iterator->Read() 从存储引擎读取一条记录,对于示例 SQL 来说,m_root_iterator 是 FilterIterator 迭代器对象,实际执行的方法是 FilterIterator::Read()。

FilterIterator::Read()

int FilterIterator::Read() {
  for (;;) {
    int err = m_source->Read();
    if (err != 0) return err;

    bool matched = m_condition->val_int();

    if (thd()->killed) {
      thd()->send_kill_message();
      return 1;
    }

    /* check for errors evaluating the condition */
    if (thd()->is_error()) return 1;

    if (!matched) {
      m_source->UnlockRow();
      continue;
    }

    // Successful row.
    return 0;
  }
}

上面是 FilterIterator::Read() 方法的全部代码,代码量比较少,主要逻辑如下:

m_source->Read() 方法从存储引擎读取一条记录,因为示例 SQL 中 t1 表的访问方式为全表扫描,所以 m_source 是 TableScanIterator 迭代器对象。

通过 explain 可以确认示例 SQL 中 t1 表的访问方式为全表扫描(type = ALL):

explain select * from t1
where i2 > 20 and (i1 = 50 or i1 = 80)\G

***************************[ 1. row ]***************************
id            | 1
select_type   | SIMPLE
table         | t1
partitions    | <null>
type          | ALL
possible_keys | <null>
key           | <null>
key_len       | <null>
ref           | <null>
rows          | 8
filtered      | 12.5
Extra         | Using where

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 条件的入口。

compare_int_signed()

// sql/item_cmpfunc.cc
int Arg_comparator::compare_int_signed() {
  // 获取 where 条件操作符左边的值
  // 例如:i2 > 20
  // 获取当前读取记录的 i2 字段值
  longlong val1 = (*left)->val_int();
  if (current_thd->is_error()) return 0;
  // where 条件操作符左边的值不为 NULL
  // 才进入 if 分支
  if (!(*left)->null_value) {
    // 获取 where 条件操作符右边的值
    // 例如:i2 > 20
    // val2 的值就等于 20
    longlong val2 = (*right)->val_int();
    if (current_thd->is_error()) return 0;
    // where 条件操作符右边的值不为 NULL
    // 才进入 if 分支
    if (!(*right)->null_value) {
      // 到这里,where 条件操作符左右两边的值都不为 NULL
      // 把 where 条件的 null_value 设置为 false
      if (set_null) owner->null_value = false;
      // 接下来 3 行代码
      // 比较 where 条件操作符左右两边的值的大小
      if (val1 < val2) return -1;
      if (val1 == val2) return 0;
      return 1;
    }
  }
  // 如果执行到下面这行代码
  // 说明 where 条件操作符左右两边的值
  // 至少有一个是 NULL
  // 把 where 条件的 null_value 设置为 true
  if (set_null) owner->null_value = true;
  return -1;
}

我们以 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。

Arg_comparator::compare()

// sql/item_cmpfunc.h
inline int compare() { return (this->*func)(); }

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()。

Item_func_gt::val_int()

// sql/item_cmpfunc.cc
longlong Item_func_gt::val_int() {
  assert(fixed == 1);
  int value = cmp.compare();
  return value > 0 ? 1 : 0;
}

这里调用的 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。

Item_cond_and::val_int()

// sql/item_cmpfunc.cc
longlong Item_cond_and::val_int() {
  assert(fixed == 1);
  // and 连接的 N 个 where 条件都保存到 list 中
  // 根据 list 构造迭代器
  List_iterator_fast<Item> li(list);
  Item *item;
  null_value = false;
  // 迭代 where 条件
  while ((item = li++)) {
    if (!item->val_bool()) {
      if (ignore_unknown() || !(null_value = item->null_value))
        return 0;  // return false
    }
    if (current_thd->is_error()) return error_int();
  }
  return null_value ? 0 : 1;
}

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() 的定义:

class Item_cond : public Item_bool_func {
  ...
  /// Treat UNKNOWN result like FALSE 
  /// because callers see no difference
  bool ignore_unknown() const { return abort_on_null; }
  ...
}

从代码注释可以看到,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。

有两种场景会导致这种情况的出现:

while 循环迭代 and 连接的 N 个 where 条件的过程中,对每个条件调用 item->val_bool() 的返回值都是 true。

此时,null_value 属性的值为 false,null_value ? 0 : 1 表达式的值为 1,说明当前读取的记录匹配 and 连接的 N 个 where 条件。

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 条件。

Item_func_eq::val_int()

// sql/item_cmpfunc.cc
longlong Item_func_eq::val_int() {
  assert(fixed == 1);
  int value = cmp.compare();
  return value == 0 ? 1 : 0;
}

这里调用的 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。

Item_cond_or::val_int()

// sql/item_cmpfunc.cc
longlong Item_cond_or::val_int() {
  assert(fixed == 1);
  List_iterator_fast<Item> li(list);
  Item *item;
  null_value = false;
  while ((item = li++)) {
    if (item->val_bool()) {
      null_value = false;
      return 1;
    }
    if (item->null_value) null_value = true;
    ...
  }
  return 0;
}

我们以 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。

本文介绍了 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 条件。

本文转载自微信公众号「一树一溪」,可以通过以下二维码关注。转载本文请联系一树一溪公众号。

f38ec33571d9af82ee9145b7b7b329192d801a.png

责任编辑:姜华 来源: 一树一溪

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK