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_qs
的 m_first_inner
、m_last_inner
、m_first_upper
赋值
for 循环中的代码逻辑分为两大块
8458 ~ 8476
行处理JOIN_TAB
对应的表属于没有嵌套的外连接
场景(本文中的示例 SQL 是没有对应这种场景的,这种场景本小节后面用其它示例 SQL 来说明)8477 ~ 8505
行处理JOIN_TAB
对应的表属于有嵌套的外连接
场景,本文的示例 SQL 中的t5
、t1
、t4
都是这种情况
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_qs
的 m_first_inner
、m_last_inner
设置为当前循环的 i
的值
同时,t4
表还满足 8484
行的 if (outer_join_nest)
,能够把自己的 TABLE_LIST::m_qs->m_first_upper
的值设置为中间表 tx
的 TABLE_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
对象对应的表分别为 t5
、t1
、t3
,它们的 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 的值),见代码8491
行nested_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_qs
的 m_first_inner
、m_last_inner
、m_first_upper
属性的值如下:
-
t3 表
-
t1 表
-
t5 表
-
t4 表
通过上面 4 张图可见,外连接的外表 t3
的 TABLE_LIST::m_qs
的 m_first_inner
、m_last_inner
、m_first_upper
都是初始值 -2
外连接中嵌套的表 t1
、t5
、t4
的 TABLE_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_LEX
是st_select_lex
结构体的宏定义,下面的分析中会根据具体的场景使用SELECT_LEX
和st_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_join
在 SELECT_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_map
(table->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()
中 1667
行 while ((table= li++))
第 1 次循环时,table 为上图中的 nest_last_join
,此时 table->nested_join->join_list
为一个列表,列表中包含 3 个元素:t5
、t1
、t4
表对应的 TABLE_LIST
对象,此时,TABLE_LIST
对象中就嵌套了其它 TABLE_LIST
对象
第 2 次循环时,table 为上图中的 t3<TABLE_LIST>
,此时,table->nested_join
为 NULL
,TABLE_LIST
中就没有嵌套其它 TABLE_LIST
对象
欢迎扫码关注公众号,我们一起学习更多 MySQL 知识: