文件首頁
MySQL 8.4 參考手冊
相關文件 下載本手冊
PDF (US Ltr) - 39.9Mb
PDF (A4) - 40.0Mb
Man Pages (TGZ) - 258.5Kb
Man Pages (Zip) - 365.5Kb
Info (Gzip) - 4.0Mb
Info (Zip) - 4.0Mb


MySQL 8.4 參考手冊  /  ...  /  巢狀聯結最佳化

10.2.1.8 巢狀聯結最佳化

用於表達聯結的語法允許巢狀聯結。以下討論是指第 15.2.13.2 節「JOIN 子句」中所述的聯結語法。

與 SQL 標準相比,table_factor 的語法已擴充。後者僅接受 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 條件中的謂詞引起的,則禁止從同一個巢狀連接中的一個內部表格透過鍵存取另一個內部表格。