文件首頁
MySQL 9.0 參考手冊
相關文件 下載本手冊
PDF (美式信紙) - 40.0Mb
PDF (A4) - 40.1Mb
Man Pages (TGZ) - 258.2Kb
Man Pages (Zip) - 365.3Kb
Info (Gzip) - 4.0Mb
Info (Zip) - 4.0Mb


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

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 條件中的述詞引導的,則會被禁止。