文档首页
MySQL 8.4 参考手册
相关文档 下载本手册
PDF (US Ltr) - 39.9Mb
PDF (A4) - 40.0Mb
手册页 (TGZ) - 258.5Kb
手册页 (Zip) - 365.5Kb
信息 (Gzip) - 4.0Mb
信息 (Zip) - 4.0Mb


MySQL 8.4 参考手册  /  ...  /  嵌套连接优化

10.2.1.8 嵌套连接优化

用于表达连接的语法允许嵌套连接。以下讨论指的是 第 15.2.13.2 节“JOIN 子句” 中描述的连接语法。

与 SQL 标准相比,table_factor 的语法进行了扩展。后者只接受 table_reference,而不接受括号内的一组 table_reference 列表。如果我们将 table_reference 列表中的每个逗号都视为等同于内连接,那么这是一个保守的扩展。例如

SELECT * FROM t1 LEFT JOIN (t2, t3, t4)
                 ON (t2.a=t1.a AND t3.b=t1.b AND t4.c=t1.c)

等同于

SELECT * FROM t1 LEFT JOIN (t2 CROSS JOIN t3 CROSS JOIN t4)
                 ON (t2.a=t1.a AND t3.b=t1.b AND t4.c=t1.c)

在 MySQL 中,CROSS JOIN 在语法上等同于 INNER JOIN;它们可以互相替换。在标准 SQL 中,它们并不等同。INNER JOINON 子句一起使用;CROSS JOIN 则在其他情况下使用。

通常,在只包含内连接操作的连接表达式中,可以忽略括号。请看以下连接表达式

t1 LEFT JOIN (t2 LEFT JOIN t3 ON t2.b=t3.b OR t2.b IS NULL)
   ON t1.a=t2.a

删除括号并将操作分组到左侧后,该连接表达式将转换为以下表达式

(t1 LEFT JOIN t2 ON t1.a=t2.a) LEFT JOIN t3
    ON t2.b=t3.b OR t2.b IS NULL

然而,这两个表达式并不等同。为了说明这一点,假设表 t1t2t3 具有以下状态

  • t1 包含行 (1)(2)

  • t2 包含行 (1,101)

  • t3 包含行 (101)

在这种情况下,第一个表达式返回的结果集包含行 (1,1,101,101)(2,NULL,NULL,NULL),而第二个表达式返回的行是 (1,1,101,101)(2,NULL,NULL,101)

mysql> SELECT *
       FROM t1
            LEFT JOIN
            (t2 LEFT JOIN t3 ON t2.b=t3.b OR t2.b IS NULL)
            ON t1.a=t2.a;
+------+------+------+------+
| a    | a    | b    | b    |
+------+------+------+------+
|    1 |    1 |  101 |  101 |
|    2 | NULL | NULL | NULL |
+------+------+------+------+

mysql> SELECT *
       FROM (t1 LEFT JOIN t2 ON t1.a=t2.a)
            LEFT JOIN t3
            ON t2.b=t3.b OR t2.b IS NULL;
+------+------+------+------+
| a    | a    | b    | b    |
+------+------+------+------+
|    1 |    1 |  101 |  101 |
|    2 | NULL | NULL |  101 |
+------+------+------+------+

在以下示例中,外连接操作与内连接操作一起使用

t1 LEFT JOIN (t2, t3) ON t1.a=t2.a

该表达式不能转换为以下表达式

t1 LEFT JOIN t2 ON t1.a=t2.a, t3

对于给定的表状态,这两个表达式返回不同的行集

mysql> SELECT *
       FROM t1 LEFT JOIN (t2, t3) ON t1.a=t2.a;
+------+------+------+------+
| a    | a    | b    | b    |
+------+------+------+------+
|    1 |    1 |  101 |  101 |
|    2 | NULL | NULL | NULL |
+------+------+------+------+

mysql> SELECT *
       FROM t1 LEFT JOIN t2 ON t1.a=t2.a, t3;
+------+------+------+------+
| a    | a    | b    | b    |
+------+------+------+------+
|    1 |    1 |  101 |  101 |
|    2 | NULL | NULL |  101 |
+------+------+------+------+

因此,如果我们在带有外连接运算符的连接表达式中省略括号,则可能会更改原始表达式的结果集。

更准确地说,我们不能忽略左外连接操作的右操作数和右连接操作的左操作数中的括号。换句话说,我们不能忽略外连接操作的内表表达式中的括号。可以忽略另一个操作数(外表的的操作数)的括号。

以下表达式

(t1,t2) LEFT JOIN t3 ON P(t2.b,t3.b)

对于任何表 t1、t2、t3 以及属性 t2.bt3.b 上的任何条件 P,都等同于以下表达式

t1, t2 LEFT JOIN t3 ON P(t2.b,t3.b)

每当连接表达式 (joined_table) 中连接操作的执行顺序不是从左到右时,我们就称之为嵌套连接。请看以下查询

SELECT * FROM t1 LEFT JOIN (t2 LEFT JOIN t3 ON t2.b=t3.b) ON t1.a=t2.a
  WHERE t1.a > 1

SELECT * FROM t1 LEFT JOIN (t2, t3) ON t1.a=t2.a
  WHERE (t2.b=t3.b OR t2.b IS NULL) AND t1.a > 1

这些查询被认为包含以下嵌套连接

t2 LEFT JOIN t3 ON t2.b=t3.b
t2, t3

在第一个查询中,嵌套连接是使用左连接操作形成的。在第二个查询中,它是使用内连接操作形成的。

在第一个查询中,可以省略括号:连接表达式的语法结构规定了连接操作的相同执行顺序。对于第二个查询,不能省略括号,尽管这里的连接表达式在没有括号的情况下也可以明确地解释。在我们扩展的语法中,第二个查询的 (t2, t3) 中的括号是必需的,尽管理论上可以在没有括号的情况下解析查询:我们仍然可以为查询提供明确的语法结构,因为 LEFT JOINON 起到了表达式 (t2,t3) 的左右分隔符的作用。

前面的示例说明了以下几点

  • 对于只涉及内连接(不涉及外连接)的连接表达式,可以删除括号并从左到右计算连接。实际上,可以按任何顺序计算表。

  • 通常,对于外连接或外连接与内连接混合使用的情况,情况并非如此。删除括号可能会更改结果。

带有嵌套外连接的查询与带有内连接的查询以相同的管道方式执行。更确切地说,利用了嵌套循环连接算法的一种变体。回顾嵌套循环连接执行查询的算法(参见第 10.2.1.7 节“嵌套循环连接算法”)。假设对 3 个表 T1、T2、T3 的连接查询具有以下形式

SELECT * FROM T1 INNER JOIN T2 ON P1(T1,T2)
                 INNER JOIN T3 ON P2(T2,T3)
  WHERE P(T1,T2,T3)

这里,P1(T1,T2)P2(T3,T3) 是一些连接条件(在表达式上),而 P(T1,T2,T3) 是表 T1、T2、T3 的列上的条件。

嵌套循环连接算法将按以下方式执行此查询

FOR each row t1 in T1 {
  FOR each row t2 in T2 such that P1(t1,t2) {
    FOR each row t3 in T3 such that P2(t2,t3) {
      IF P(t1,t2,t3) {
         t:=t1||t2||t3; OUTPUT t;
      }
    }
  }
}

符号 t1||t2||t3 表示通过连接行 t1t2t3 的列构造的行。在以下某些示例中,表名出现 NULL 表示该表中每一列都使用 NULL 的行。例如,t1||t2||NULL 表示通过连接行 t1t2 的列以及 t3 的每一列的 NULL 构造的行。这样的行被称为 NULL 补全的。

现在考虑一个带有嵌套外连接的查询

SELECT * FROM T1 LEFT JOIN
              (T2 LEFT JOIN T3 ON P2(T2,T3))
              ON P1(T1,T2)
  WHERE P(T1,T2,T3)

对于此查询,修改嵌套循环模式以获得

FOR each row t1 in T1 {
  BOOL f1:=FALSE;
  FOR each row t2 in T2 such that P1(t1,t2) {
    BOOL f2:=FALSE;
    FOR each row t3 in T3 such that P2(t2,t3) {
      IF P(t1,t2,t3) {
        t:=t1||t2||t3; OUTPUT t;
      }
      f2=TRUE;
      f1=TRUE;
    }
    IF (!f2) {
      IF P(t1,t2,NULL) {
        t:=t1||t2||NULL; OUTPUT t;
      }
      f1=TRUE;
    }
  }
  IF (!f1) {
    IF P(t1,NULL,NULL) {
      t:=t1||NULL||NULL; OUTPUT t;
    }
  }
}

通常,对于外连接操作中第一个内表的任何嵌套循环,都会引入一个标志,该标志在循环之前关闭并在循环之后检查。当在外表中找到与表示内部操作数的表的匹配行的当前行时,该标志将打开。如果在循环周期结束时该标志仍处于关闭状态,则表示在外表的当前行中未找到匹配项。在这种情况下,该行由内部表的列的 NULL 值补全。结果行将传递到最终检查以输出或进入下一个嵌套循环,但前提是该行满足所有嵌入式外连接的连接条件。

在本例中,嵌入了由以下表达式表示的外连接表

(T2 LEFT JOIN T3 ON P2(T2,T3))

对于带有内连接的查询,优化器可以选择不同的嵌套循环顺序,例如以下顺序

FOR each row t3 in T3 {
  FOR each row t2 in T2 such that P2(t2,t3) {
    FOR each row t1 in T1 such that P1(t1,t2) {
      IF P(t1,t2,t3) {
         t:=t1||t2||t3; OUTPUT t;
      }
    }
  }
}

对于带有外连接的查询,优化器只能选择外表的循环先于内表的循环的顺序。因此,对于我们带有外连接的查询,只有一种可能的嵌套顺序。对于以下查询,优化器评估两种不同的嵌套。在这两种嵌套中,T1 都必须在外循环中处理,因为它用于外连接。T2T3 用于内连接,因此该连接必须在内循环中处理。但是,由于该连接是内连接,因此可以按任意顺序处理 T2T3

SELECT * T1 LEFT JOIN (T2,T3) ON P1(T1,T2) AND P2(T1,T3)
  WHERE P(T1,T2,T3)

一种嵌套先评估 T2,然后评估 T3

FOR each row t1 in T1 {
  BOOL f1:=FALSE;
  FOR each row t2 in T2 such that P1(t1,t2) {
    FOR each row t3 in T3 such that P2(t1,t3) {
      IF P(t1,t2,t3) {
        t:=t1||t2||t3; OUTPUT t;
      }
      f1:=TRUE
    }
  }
  IF (!f1) {
    IF P(t1,NULL,NULL) {
      t:=t1||NULL||NULL; OUTPUT t;
    }
  }
}

另一种嵌套先评估 T3,然后评估 T2

FOR each row t1 in T1 {
  BOOL f1:=FALSE;
  FOR each row t3 in T3 such that P2(t1,t3) {
    FOR each row t2 in T2 such that P1(t1,t2) {
      IF P(t1,t2,t3) {
        t:=t1||t2||t3; OUTPUT t;
      }
      f1:=TRUE
    }
  }
  IF (!f1) {
    IF P(t1,NULL,NULL) {
      t:=t1||NULL||NULL; OUTPUT t;
    }
  }
}

在讨论内连接的嵌套循环算法时,我们省略了一些细节,这些细节对查询执行的性能可能有很大影响。我们没有提到所谓的“下推”条件。假设我们的 WHERE 条件 P(T1,T2,T3) 可以由一个合取公式表示

P(T1,T2,T2) = C1(T1) AND C2(T2) AND C3(T3).

在这种情况下,MySQL 实际上使用以下嵌套循环算法来执行带有内连接的查询

FOR each row t1 in T1 such that C1(t1) {
  FOR each row t2 in T2 such that P1(t1,t2) AND C2(t2)  {
    FOR each row t3 in T3 such that P2(t2,t3) AND C3(t3) {
      IF P(t1,t2,t3) {
         t:=t1||t2||t3; OUTPUT t;
      }
    }
  }
}

您可以看到每个合取 C1(T1)C2(T2)C3(T3) 都从最内层循环推送到可以对其进行评估的最外层循环。如果 C1(T1) 是一个非常严格的条件,则此条件下推可以大大减少传递到内循环的表 T1 中的行数。结果,查询的执行时间可能会大大提高。

对于带有外连接的查询,只有在发现外表中的当前行在内表中具有匹配项之后,才需要检查 WHERE 条件。因此,将条件从内部嵌套循环中推出的优化不能直接应用于带有外连接的查询。在这里,我们必须引入由标志保护的条件下推谓词,当遇到匹配时,这些标志将打开。

回想一下这个带有外连接的示例

P(T1,T2,T3)=C1(T1) AND C(T2) AND C3(T3)

对于该示例,使用受保护的下推条件的嵌套循环算法如下所示

FOR each row t1 in T1 such that C1(t1) {
  BOOL f1:=FALSE;
  FOR each row t2 in T2
      such that P1(t1,t2) AND (f1?C2(t2):TRUE) {
    BOOL f2:=FALSE;
    FOR each row t3 in T3
        such that P2(t2,t3) AND (f1&&f2?C3(t3):TRUE) {
      IF (f1&&f2?TRUE:(C2(t2) AND C3(t3))) {
        t:=t1||t2||t3; OUTPUT t;
      }
      f2=TRUE;
      f1=TRUE;
    }
    IF (!f2) {
      IF (f1?TRUE:C2(t2) && P(t1,t2,NULL)) {
        t:=t1||t2||NULL; OUTPUT t;
      }
      f1=TRUE;
    }
  }
  IF (!f1 && P(t1,NULL,NULL)) {
      t:=t1||NULL||NULL; OUTPUT t;
  }
}

通常,可以从连接条件(例如 P1(T1,T2)P(T2,T3))中提取下推谓词。在这种情况下,下推谓词也由一个标志保护,该标志防止为由相应外连接操作生成的 NULL 补全的行检查谓词。

如果从 WHERE 条件中的谓词引入了对同一个嵌套连接中一个内表到另一个内表的访问,则禁止这种访问。