Documentation Home
MySQL 8.3 Reference Manual
Related Documentation Download this Manual
PDF (US Ltr) - 40.8Mb
PDF (A4) - 40.9Mb
Man Pages (TGZ) - 294.0Kb
Man Pages (Zip) - 409.0Kb
Info (Gzip) - 4.0Mb
Info (Zip) - 4.0Mb
Excerpts from this Manual

10.2.1.8 嵌套连接优化

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

与 SQL 标准相比,table_factor 的语法扩展了。后者仅接受 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 JOIN 用于 ON 子句;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)

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 |
+------+------+------+------+

在这种情况下,第一个表达式返回结果集,包括行 (1,1,101,101)(2,NULL,NULL,NULL),而第二个表达式返回行 (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 LEFT JOIN t3 ON P(t2.b,t3.b)

对于任何表 t1,t2,t3 和任何条件 P 都等同于以下表达式:

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

每当连接操作的执行顺序在连接表达式 (joined_table) 中不是从左到右时,我们谈论嵌套连接。考虑以下查询:

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

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

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

在第一个查询中,括号可以省略:连接表达式的语法结构规定了相同的执行顺序。但是,对于第二个查询,括号不能省略,尽管查询可以在没有它们的情况下被解析:我们仍然具有明确的语法结构,因为 LEFT JOINON 扮演着左和右分隔符的角色 (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) 是一个非常 restrict 的条件,那么该条件的推送可能会大大减少从表 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 条件中的谓词引起的,那么该访问将被禁止。