MySQL 外连接相关属性设置代码分析

本文基于 MySQL 5.7.35 版本的源代码,主要是对 JOIN::make_outerjoin_info() 方法的代码逻辑进行分析

1. 准备工作

1.1 示例 SQL

本文后面的提到的示例 SQL,除有特别说明外,都是指的下面这条 SQL

1SELECT
2    t3.*, t4.* 
3FROM t3 LEFT JOIN (t4 INNER JOIN t1 INNER JOIN t5)
4ON t3.i1 = t4.i1 AND t1.i1 = t3.i1 AND t5.i2 = t3.i1

1.2 一些对象的属性说明

TABLE_LIST::m_qs 的部分属性

m_first_inner:外连接的第 1 个内表

m_last_inner:外连接的最后一个内表,如果外连接只有一个内表,则 m_last_inner 等于 m_first_inner;如果外连接有一组内表,m_first_inner 指向这组内表的第 1 个,m_last_inner 指向这组内表的最后 1 个

m_first_upper:如果外连接是一个子查询,此字段指向父查询层级的外连接的第 1 个内表

TABLE_LIST::cond_equal

TABLE_LIST::cond_equal 中保存了附加到该表上的条件,只涉及到当前表的条件

比如当前表为 t1,附加到 t1 上的查询条件有:t1.a = 1 AND t1.b = t2.b AND t1.c = t3.c AND t1.d > 100,则 cond_equal 为 t1.a = 1 AND t1.d > 100

TABLE_LIST::nested_join 的部分属性

first_nested:外连接的第 1 个内表,如果外连接的有一组内连接表,m_first_inner 的值就是从这个属性里来的 nj_counter:外连接的内表数量,只有外连接有一组内表时,此字段才有值

2. 代码截图

3. 代码分析

见上图中代码,SELECT_LEX::record_join_nest_info() 的主体逻辑就是对已经选定的查询执行计划中的非常量表对应的各个 JOIN_TAB 对应进行循环处理,为 JOIN_TAB::cond_equal,以及 JOIN_TAB::m_qsm_first_innerm_last_innerm_first_upper 赋值

for 循环中的代码逻辑分为两大块

  • 8458 ~ 8476 行处理 JOIN_TAB 对应的表属于没有嵌套的外连接场景(本文中的示例 SQL 是没有对应这种场景的,这种场景本小节后面用其它示例 SQL 来说明)
  • 8477 ~ 8505 行处理 JOIN_TAB 对应的表属于有嵌套的外连接场景,本文的示例 SQL 中的 t5t1t4 都是这种情况

Tips:上面所说的没有嵌套的外连接有嵌套的外连接中的嵌套可以理解成我们四则运算中的括号所表示的那种嵌套,而不是 MySQL 中的 嵌套循环连接中所说的嵌套

8449 行的 for 循环,i = 0 时,TABLE_LIST 对象对应的表 t3,即不属于没有嵌套的外连接,也不属于有嵌套的外连接,所以此轮循环不会处理任何逻辑

3.1 没有嵌套的外连接

本文的 1. 准备工作 中说明的 示例 SQL 没有 8458 ~ 8476 行代码对应的情况,需要 2 条新的 SQL 来说明,如下:

1SELECT * FROM t1 LEFT JOIN (
2    SELECT t3.* FROM t3 LEFT JOIN t4 ON t3.i1 = t4.i1
3) AS tx ON t1.i1 = tx.i1

上面的 SQL 中,没有嵌套的外连接 是指的括号里面的 SQL(SELECT t3.* FROM t3 LEFT JOIN t4 ON t3.i1 = t4.i1),括号里面这条 SQL 的 t4 表满足代码 8458 行的 if (tab->outer_join),能够把自己的 TABLE_LIST::m_qsm_first_innerm_last_inner 设置为当前循环的 i 的值

同时,t4 表还满足 8484 行的 if (outer_join_nest),能够把自己的 TABLE_LIST::m_qs->m_first_upper 的值设置为中间表 txTABLE_LIST::m_qs->m_idx(即代码中的 outer_join_nest->nested_join->first_nested

3.2 有嵌套的外连接

有嵌套的外连接比较复杂,单纯从代码层面分析容易混乱,所以要基于一个具体的例子进行分析,先来看下示例 SQL 的 st_select_lex::top_join_list 结构,如下图:

Tips:上面这张图和4. 调用路径分析末尾说明 TABLE_LIST 嵌套其它 TABLE_LIST 时的图中各结点的顺序是不一样的;上面这张图是经过优化阶段之后,最终确定的查询执行计划中的顺序,而4. 调用路径分析中的图是 st_select_lex::top_join_list 初始状态的图

8449 行的 for 循环,i = 1,2,3 时,TABLE_LIST 对象对应的表分别为 t5t1t3,它们的 TABLE_LIST 对象的 embedding 属性(代码中 8477 行的 tbl->embedding)都是上图中的 nest_last_join<TABLE_LIST>

8484 行,NESTED_JOIN *const nested_join= embedding->nested_join 就是上图中 nested_join->join_list 中的 nested_join

从上图可见,nested_join->join_list 列表有 3 个元素,因此,8485 行的 nested_join->nj_counter 的值最终应该等于 3,不过在第二轮循环(i = 1)时,nested_join->nj_counter 还等于 0,此时需要做一些初始化赋值工作(8491 ~ 8497 行)

第二轮循环(i = 1)中,做完初始化赋值工作之后,就是第二轮(i = 1) ~ 第四轮(i = 3)都要做的事情了

8499 ~ 8500 行,如果 tab->m_first_inner 还没有初始化(当然是没有初始化的),则设置 tab->m_first_inner 的值为 nested_join->first_nested

nested_join->first_nested 是在第二轮(i = 1)循环中初始化时赋值的,本文示例 SQL 中值为 1(即循环变量 i 的值),见代码 8491nested_join->first_nested = i

8501 行,判断本轮循环是不是有嵌套的外连接的最后一轮循环(本文示例 SQL 中,第四轮(i = 3)是最后一轮循环)

8504 行,如果本轮循环是有嵌套的外连接的最后一轮循环,需要把嵌套的外连接嵌套的第 1 个表(本文示例 SQL 中,对应 t5 表,见上图)的 TABLE_LIST::m_qs->m_last_inner 设置为 i 的值(本文示例中,此时 i 的值为 3

本方法执行结束时,本文示例 SQL 的各表的 TABLE_LIST::m_qsm_first_innerm_last_innerm_first_upper 属性的值如下:

  • t3 表

  • t1 表

  • t5 表

  • t4 表

通过上面 4 张图可见,外连接的外表 t3TABLE_LIST::m_qsm_first_innerm_last_innerm_first_upper 都是初始值 -2

外连接中嵌套的表 t1t5t4TABLE_LIST::m_qs->m_first_inner 都是 1(见 t1 表对应的图中 m_idx = 1,说明 t1 表是嵌套的这一组表的第一个表)

只有 t1 表中保存了嵌套的这一组表中的最后一个表的 m_idx(t1 表中的 m_last_inner = 3,其中的 3 就是 t4 表的 m_idx,说明 t4 表是嵌套的这一组表的最后一个表)

4. 调用路径分析

Tips:SELECT_LEXst_select_lex 结构体的宏定义,下面的分析中会根据具体的场景使用 SELECT_LEXst_select_lex,这两者是等价的

SELECT_LEX::record_join_nest_info() 的调用栈如下图:

从上图的调用栈可以看到,st_select_lex::apply_local_transforms() 调用了 st_select_lex::record_join_nest_info(),把 st_select_lex::top_join_list 作为参数传给了 tables,此时 tables = st_select_lex::top_join_list,见下图:

select_lex->outer_joinSELECT_LEX::record_join_nest_info() 中赋值,见下图:

从上图代码可以看到,select_lex->outer_join 中保存的是外连接表的 m_map,多个外连接表的 m_map 通过按位或运算累加到 select_lex->outer_join 中,见 1672 行、1690

Tips:m_map 是通过表 ID 进行左移运行得到的,m_map = 1 << 表 ID

1669 行,如果当前循环的 TABLE_LIST 对象没有嵌套其它的 TABLE_LIST 对象,则执行 if (table->nested_join == NULL) 代码块中的逻辑

什么叫 TABLE_LIST 对象中嵌套了其它 TABLE_LIST 对象?什么叫没有嵌套其它 TABLE_LIST 对象?

这个问题在本小节末尾有说明

1671 行,table->join_cond() 判断当前循环的 TABLE_LIST 对象上是否有连接条件,如果有连接条件,就执行 1672 行代码逻辑 outer_join|= table->map() 把当前循环的 TABLE_LIST 对象对应表的 m_map

为什么 if (table->join_cond()) 条件成立,就要把 table->map() 通过按位或累加到 st_select_lex::outer_join 上呢?

因为在 st_select_lex::apply_local_transforms() 里调用 st_select_lex::simplify_joins() 之后,会把一些外连接转换为内连接,然后把原本就是内连接,以及从外连接转换而来的内连接上的 ON 子句 的连接条件移动到 WHERR 条件 上(即从 TABLE_LIST::m_join_cond 转移到 st_select_lex::m_where_cond),然后 TABLE_LIST::m_join_cond 就被设置为 NULL

所以如果 table->join_cond()(即 table->m_join_cond)不为 NULL,说明当前循环的 TABLE_LIST 是属于外连接的

接下来就讲解当前循环的 TABLE_LIST 嵌套了其它 TABLE_LIST 对象并情况了

1676 行,如果代码运行到这里,说明当前循环的 TABLE_LIST 嵌套了其它 TABLE_LIST,递归调用 record_join_nest_info(),并把当前循环的 TABLE_LIST 对象嵌套的 TABLE_LIST 对象的列表(&table->nested_join->join)传给方法的参数 table

1677 行,如果递归调用返回 true,则结束方法的执行,而只有当 TABLE_LIST 对应的表是半连接时,才会返回 true(见上图中 1683 ~ 1687 行的代码,此段逻辑暂时忽略,至于什么是半连接,等后面再专门写文章来阐述)

1689 行,如果当前循环的 TABLE_LIST 是属于外连接的,则把它嵌套的 TABLE_LIST 列表中所有表的 m_maptable->nested_join->used_tables)都通过按位或累加到 st_select_lex::outer_join

关于 TABLE_LIST 对象嵌套其它 TABLE_LIST 对象的说明

基于本文中的示例 SQL 语句,st_select_lex::top_join_list(即 st_select_lex::apply_local_transforms() 调用 SELECT_LEX::record_join_nest_info() 时的 table 参数的值) 的结构如下:

SELECT_LEX::record_join_nest_info()1667while ((table= li++)) 第 1 次循环时,table 为上图中的 nest_last_join,此时 table->nested_join->join_list 为一个列表,列表中包含 3 个元素:t5t1t4 表对应的 TABLE_LIST 对象,此时,TABLE_LIST 对象中就嵌套了其它 TABLE_LIST 对象

第 2 次循环时,table 为上图中的 t3<TABLE_LIST>,此时,table->nested_joinNULLTABLE_LIST 中就没有嵌套其它 TABLE_LIST 对象




欢迎扫码关注公众号,我们一起学习更多 MySQL 知识: