1. 程式人生 > >MySQL查詢優化之巢狀連線

MySQL查詢優化之巢狀連線

 

原文地址:https://dev.mysql.com/doc/refman/5.7/en/nested-join-optimization.html

譯文:

8.2.1.7巢狀連線優化

連線的語法允許巢狀連線。下面討論中涉及到的連線語法可以參考Section 13.2.9.2, “JOIN Syntax”

與SQL標準相比,table_factor的語法得到了擴充套件。SQL標準只接受table_reference,不接受括號內的列表。如果我們認為table_referenceitems列表中的每個逗號等價於內連線,那麼這是一個保守的擴充套件。

例如:

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是不跟on子句的連線(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

在左移括號和分組操作後,該join表示式轉換為如下表達式:

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

然而,這兩個表達是不相等的。為了瞭解這一點,假設表t1、t2和t3具有以下狀態:

    1)表t1包含行 (1)(2);

    2)表t2包含行(1,101);

    3)表t3包含行(101)。

這種情況下,前面的第一個表示式返回的結果集包含行(1,1,101,102)和行(2,NULL,NULL,NULL),而第二個表示式的返回結果返回行(1,1,101,102)和行(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 LEFT JOIN t3 ON P(t2.b,t3.b)

當連線表示式(join_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 join和on扮演了表示式(t2,t3)左右分隔符的角色。

前面的例子證明了以下觀點:

    1)對於只包含內部連線(而不是外部連線)的連線表示式,可以刪除括號,並從左到右計算連線。事實上,表可以按任何順序計算;

    2)一般來說,對於外部連線或混合了內部連線的外部連線,情況並非如此。去掉括號可能會改變結果。

具有巢狀外部連線的查詢以與具有內部連線的查詢相同的管道方式執行。更準確地說,它利用了巢狀迴圈連線演算法的一種變體。回想一下以巢狀迴圈連線演算法執行查詢 (可以參考Section 8.2.1.6, “Nested-Loop Join Algorithms”)。假設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表示將行t1、t2和t3的列串聯起來構造的行。在下面的一些示例中,如果表名出現,則表示對該表的每一列使用NULL。例如,t1||t2||NULL表示通過連線行t1和行t2的列構造的行,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必須在外部迴圈中處理,因為它在外部連線中使用。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;
    }
  }
}

在討論內連線的巢狀迴圈演算法時,我們忽略了一些對查詢執行的效能可能有巨大影響的細節。我們沒有提及所謂的"下推條件"。假設我們的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)中提取出來。在這種情況下,下推謂詞還是由一個標誌保護,該標誌阻止檢查由對應外連線操作產生的空補行的謂詞。

如果訪問方法是由來自where條件的謂詞引發的,則禁止索引鍵在同一個巢狀連線中從一個內表訪問另一個內表。

PS:由於水平有限,譯文中難免存在謬誤,歡迎批評指正。