Spec-Zone.ru › MySQL 9.2

10.2.1.8 Оптимизация вложенных соединений

Синтаксис выражения соединений допускает вложенные соединения. В последующем обсуждении используется синтаксис соединения, описанный в разделе 15.2.13.2, «Оператор 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 используется в противном случае.

В общем случае, скобки можно игнорировать в выражениях соединений, содержащих только операции внутреннего соединения. Рассмотрим это выражение соединения:

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), в то время как второе выражение возвращает строки (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)

Всякий раз, когда порядок выполнения операций соединения в выражении соединения (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 JOIN и ON играют роль левого и правого разделителей для выражения (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 обозначает строку, построенную путем конкатенации столбцов строк t1, t2 и t3. В некоторых последующих примерах NULL, где появляется имя таблицы, означает строку, в которой NULL используется для каждого столбца этой таблицы. Например, t1||t2||NULL обозначает строку, построенную путем конкатенации столбцов строк t1 и t2, и NULL для каждого столбца таблицы t3. Такая строка называется 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). В этом случае выталкиваемый предикат защищен также флагом, который предотвращает проверку предиката для NULL-дополненной строки, генерируемой соответствующей операцией внешнего соединения.

Доступ по ключу из одной внутренней таблицы к другой в том же вложенном соединении запрещен, если он индуцируется предикатом из условия WHERE.

© 2025 Oracle
Licensed under the GPLv2 License.
https://docs.oracle.com/cd/E17952_01/mysql-8.4-en/nested-join-optimization.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API