無料PHPプログラム

MySQL 5.1 リファレンスマニュアル :: 6 最適化 :: 6.2 SELECTステートメントおよびその他のクエリの最適化 :: 6.2.10 入れ子結合最適化
« 6.2.9 LEFT JOINとRIGHT JOIN最適化

6.2.11 外側Join 単純化 »
Section Navigation      [Toggle]
  • 6.2 SELECTステートメントおよびその他のクエリの最適化
  • 6.2.1 EXPLAINを使用して、クエリを最適化する
  • 6.2.2 クエリパフォーマンスの推定
  • 6.2.3 SELECTクエリの速度
  • 6.2.4 WHERE 節最適化
  • 6.2.5 Range 最適化
  • 6.2.6 インデックス結合最適化
  • 6.2.7 IS NULL最適化
  • 6.2.8 DISTINCT最適化
  • 6.2.9 LEFT JOINとRIGHT JOIN最適化
  • 6.2.10 入れ子結合最適化
  • 6.2.11 外側Join 単純化
  • 6.2.12 ORDER BY最適化
  • 6.2.13 GROUP BY最適化
  • 6.2.14 LIMITの最適化
  • 6.2.15 テーブルスキャンを避ける方法
  • 6.2.16 INSERTステートメントの速度
  • 6.2.17 UPDATEステートメントの速度
  • 6.2.18 DELETEステートメントの速度
  • 6.2.19 その他の最適化のヒント

6.2.10. 入れ子結合最適化

結合を表す構文は入れ子結合を許可します。以下は項12.2.7.1. 「JOIN 構文」で記述された結合構文に関連します。

table_factor構文はSQL標準と比較して拡張されています。後者は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 JOINはON節と一緒に使用されます。CROSS JOINは他の使用方法があります。

一般的に、inner joinオペレーションを含むjoin 表現のかっこは無視できます。かっこを取り除きグルーピングオペレーションを左に移動させた後、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、t2、そしてt3が以下の状態であるとします。

  • テーブルt1は(1)、(2)を含む

  • テーブルt2は(1,101)行を含む

  • テーブルt3は(101)行を含む

この場合、最初の表現は(1,1,101,101)、そして(2,NULL,NULL,NULL)を含む行の結果セットを返します。2番目の表現は(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 |
+------+------+------+------+

下記の例では、外側 join オペレーションが内側 join オペレーションと一緒に使用されます。

t1 LEFT JOIN (t2, t3) ON t1.a=t2.a

その表現は以下の表現に変換できません。

t1 LEFT JOIN t2 ON t1.a=t2.a, t3.

既存のテーブル状態では、以下の2表現は異なる行セットを返します。

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

よって、外側 join 演算子を含む join 表現のかっこを取り除いた場合、元の表現の結果セットを変える可能性があります。

正確には、左外側 join オペレーションの右演算子のかっこを、そして右側 join オペレーションの左演算子のかっこを無視することができません。言い換えれば、外側 join オペレーションの内側テーブル表現のかっこを無視することはできません。他のオペランド(外側テーブルのオペランド)のかっこは無視できます。

以下の表現:

(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属性t2.b and t3.b.

join 表現(join_table) のjoin オペレーション実行順序が左から右でない場合、入れ子 join の話が出てきます。以下のクエリを考慮してください。

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

上記クエリは以下の入れ子 join が含まれると考えられています。

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

最初のクエリでは、左 join オペレーショを使用して入れ子 join が生成されます。二番目のクエリでは内側 join オペレーションで生成されます。

最初のクエリでは、かっこは取り除いてもかまいません。join 表現の文法構成はjoin オペレーションと同じ実行順序を指令します。2番目のクエリでは、かっこなしでも join 表現があいまいに解釈されますが、かっこは取り除くことができません。(拡張された構文では、理論上はかっこなしでもパースされますが、2番目クエリの(t2, t3)のかっこは必要です。まだ、クエリはあいまいでない構文構成になります。これはLEFT JOINとONが左と右のデリミタの役割りを右の表現(t2,t3)で果たすからです。)

前述の例でこれらの点を証明しています。

  • インナーjoins のみ関する表現(アウターjoins は不可)については、かっこは取り除けます。かっこを取り除いて左から右に評価を行うことができます(あるいは、テーブルの評価は好きな順序で行えます)。

  • 一般的に、そとがわjoinや外がわjoin と併合された内側join にとっては、同じではありません。かっこを取り除くことで結果を変えることがあるかもしれません。

入れ子外側joins を含むクエリは内側join を含むクエリと同じように、パイプライン形式で実行されます。正確には、入れ子ループjoinアルゴリズムが利用されます。入れ子ループjoinがクエリを実行する際利用するアルゴリズムスキーマを思い出してください。例えば、3つのテーブルT1,T2,T3に関するjoinクエリが、以下のフォームであるとします。

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)はjoin 条件です(表現につく)。それに引き換え、P(t1,t2,t3)はテーブルT1,T2,T3カラム上の条件です。

入れ子ループjoinアルゴリズムはこのクエリを次のように実行します。

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はその行の各カラムに使用されることを意味します。例えば、t1||t2||NULLは「t3の各カラム毎のt1 、t2、そしてNULL行のカラムを連鎖させることで作成された行。」

入れ子のある外側join クエリを見てみましょう。

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

一般的に、外側 joinオペレーションの最初の内側テーブル入れ子ループにとって、ループ前に消され、ループ後にチェックされるフラグが導入されます。フラグがオンになるのは、内側オペランドを表すテーブルから外側テーブルの現在行にマッチが見つかったときです。ループサイクルの最後でフラグがOFFの場合は、外側テーブルの現在行でマッチが見つからなかったときです。この場合、行がインナーテーブルのカラムNULL値で補われています。結果行は次の入れ子ループか出力へ、最終確認のため渡されますが、これは行が組み込まれた全ての外側 joinの条件を満たしている場合のみです。

この例では、次の表現で表された外側joinテーブルは組み込まれています。

(T2 LEFT JOIN T3 ON P2(T2,T3))

内側joinを含むクエリにとって、オプティマイザは以下のような異なる順序の入れ子ループが選択できることに注目してください。

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

外側テーブルを含むクエリに関しては、オプティマイザは外側テーブルのループが内側テーブルのループの前にくる順序のみ選択可能です。よって、外側joinのクエリにとって、1つの入れ子順序のみ可能となります。以下のクエリでは、2つの異なる入れ子を評価します。

SELECT * T1 LEFT JOIN (T2,T3) ON P1(T1,T2) AND P2(T1,T3)
  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) {
    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;
    }
  }
}

そして

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

両の入れ子にとって、T1は外側joinで使用されているため、外側ループでプロセスされなければいけません。 T2とT3は内側joinで使用されているため、そのjoinは内側ループで処理されなければいけません。ただし、joinが内側joinのため、T2とT3はどちらの順序でも処理できます。

内側joinの入れ子ループアルゴリズムについては、クエリ実行性能に関する、重大な詳細を省きました。いわゆる、「後入れ先出し」条件に関することには触れませんでした。例えば、WHERE条件P(T1,T2,T3)が接続法によって表されるとします。

P(T1,T2,T2) = C1(T1) AND C2(T2) AND C3(T3).

この場合、MySQLは内側joinを含むクエリの実行には以下の入れ子ループスキーマを使用します。

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から内側ループに渡される行の数を大幅に減らします。結果的に、クエリの実行時間が大きく短縮できます。

外側joinを含むクエリについては、外側テーブルの現在行に内側テーブルからのマッチがあることが確認できてからWHERE条件がチェックされます。よって、内側入れ子ループの後だし先入れ条件最適化は外側joinを含むクエリには直接適用できません。ここではマッチが見つかった時に起動するフラグに守られた、条件つき後出し先入れの述語を紹介しなければいけません。

例えば、外側joinでは

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)といったjoin条件から抽出できます。この場合、後だし先入れ述語は対応する外側joinオペレーションNULL-に補われた行のチェックを妨げるフラグによって守られています。

ここで、1つの内側テーブルから同じ入れ子joinへのアクセスキーは、WHERE条件からの述語に誘導されている場合、禁止されています。(この場合条件つきのキーアクセスを使用することはできますが、MySQLではこのテクニックはまだ使われていません5.1。)

Copyright c 1997, 2010, Oracle and/or its affiliates. All rights reserved. Legal Notices
Top / Previous / Next / Up / Table of Contents
© 2010, Oracle Corporation and/or its affiliates

無料CGI PHPスクリプト | 新着情報スクリプト | 営業日カレンダー | PHPマニュアル | MySQLマニュアル | PEARマニュアル

Copyright (c) 2010 jmcodex.com All rights reserved.