10.MySQL优化Nested Join Optimization

介绍

table_factor是对SQL标准的一个扩展。后者只接受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不需要。

CROSS JOIN是笛卡尔积,而INNER 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
  • 表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和任何条件P在属性t2.b 和t3.b:

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

这里,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表示通过连接行t1,t2和t3的列构成的行。在某些例子中,假如其中某一个是NULL,则代表他在那个表中的每一列都是NULL。

这点不是特别好理解,我用下面的SQL说明

P(t2.b,t3.b)代表条件,这里表示可以添加多个关于t2、t3表的条件,例如:

T2.B IS NOT NULL,
T2.B > 3,
T2.C <= T1.C,
T2.B < 2 OR T2.C > 1

举个具体例子

SELECT * FROM T1 LEFT JOIN T2 ON T2.A=T1.A
                 LEFT JOIN T3 ON T3.B=T1.B
  WHERE T3.C > 0

可以改写为

SELECT * FROM T1 LEFT JOIN
              (T2, T3)
              ON T2.A=T1.A AND T3.C=T1.C AND T3.B=T2.B
  WHERE T3.D > 0

现在考虑使用嵌套外连接的查询:

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必须在外部循环中处理,因为它用于外部连接。T2和T3用于内连接,因此必须在内循环中处理连接。但是,由于连接是内连接,因此可以按任意顺序处理T2和T3。

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;
    }
  }
}

在讨论内部联接的嵌套循环算法时,我们省略了一些细节,这些细节对查询执行性能的影响可能很大。我们没有提到所谓的“下推”条件。

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补充行的谓词。

猜你喜欢

转载自blog.csdn.net/ciqingloveless/article/details/83625899