8.2.1.7 Оптимизация вложенного соединения
Синтаксис выражения соединений допускает вложенные соединения. Далее рассматривается синтаксис соединения, описанный в разделе 13.2.9.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).
Предыдущие примеры демонстрируют эти моменты:
Для выражений соединения, включающих только внутренние соединения (и не внешние), скобки можно удалить, и соединения оцениваются слева направо. Фактически, таблицы могут быть оценены в любом порядке.
Это неверно в общем случае для внешних соединений или для внешних соединений, смешанных с внутренними соединениями. Удаление скобок может изменить результат.
Запросы с вложенными внешними соединениями выполняются таким же способом, как и запросы с внутренними соединениями. Точнее, используется вариант алгоритма вложенного соединения.
Напомним алгоритм, с помощью которого вложенное соединение выполняет запрос (см. раздел 8.2.1.6, «Алгоритмы вложенного соединения»).
Предположим, что запрос соединения по 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, и 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.