FULL JOIN и MySQL

Полное внешнее соединение (FULL JOIN) не поддерживается в MySQL. Можно считать, что это – «избыточная» операция, т.к. она представляется через объединение левого и правого внешних соединений. Например, запрос

--(1)--
SELECT * 
FROM Income_o I 
    FULL JOIN Outcome_o O ON I.point = O.point 
                         AND I.date = O.date;
🚫
[[ error ]]
[[ column ]]
NULL [[ value ]]
который на каждый рабочий день по каждому пункту выводит в одну строку приход и расход (схема «Вторсырье»), можно переписать в виде:

--(2)--
SELECT * 
FROM Income_o I 
    LEFT JOIN Outcome_o O ON I.point = O.point 
                         AND I.date = O.date   
UNION  
SELECT * 
FROM Income_o I 
    RIGHT JOIN Outcome_o O ON I.point = O.point 
                          AND I.date = O.date;
🚫
[[ error ]]
[[ column ]]
NULL [[ value ]]

С логической точки зрения эти запросы эквивалентны; оба они выводят как дни, когда был и приход, и расход, так и дни, когда отсутствовала одна из операций (отсутствующие значения заменяются NULL). Однако с точки зрения производительности второй запрос проигрывает первому вдвое по оценке стоимости плана. Это связано с тем, что операция UNION приводит к выполнению сортировки, которая отсутствует в плане первого запроса. Сортировка же необходима для процедуры исключения дубликатов, т.к. левое и правое соединения оба содержат строки, соответствующие внутреннему соединению, т.е. случаю, когда есть как приход, так и расход. Поэтому, если вместо UNION написать UNION ALL, то такие строки будут присутствовать в результирующем наборе в двух экземплярах.

Тем не менее, чтобы получить план, близкий по стоимости FULL JOIN, нужно избавиться от сортировки. Например, использовать UNION ALL, но в одном из объединяемых запросов исключить строки, соответствующие внутреннему соединению:

--(3)--
SELECT * 
FROM Income_o I 
    LEFT JOIN Outcome_o O ON I.point = O.point 
                         AND I.date = O.date   
UNION ALL  
SELECT NULL, NULL, NULL,* 
FROM Outcome_o O   
WHERE NOT EXISTS (SELECT 1 
                  FROM Income_o I   
                  WHERE I.point = O.point 
                     AND I.date = O.date
                 );
🚫
[[ error ]]
[[ column ]]
NULL [[ value ]]

Обратите внимание, что заведомо отсутствующие значения, которые появлялись в правом соединении решения (2), здесь формируются явным заданием NULL-значений. Если по каким-то причинам, явное задание NULL вместо соединения вам не подходит, можно оставить соединение, но это даст более дорогой план, хотя и он будет дешевле плана с сортировкой (2):

SELECT * 
FROM Income_o I 
    LEFT JOIN Outcome_o O ON I.point = O.point 
                         AND I.date = O.date   
UNION ALL  
SELECT * 
FROM Income_o I 
    RIGHT JOIN Outcome_o O ON I.point = O.point 
                          AND I.date = O.date  
WHERE NOT EXISTS (SELECT 1 
                  FROM Income_o I   
                  WHERE I.point = O.point 
                      AND I.date = O.date
                 );
🚫
[[ error ]]
[[ column ]]
NULL [[ value ]]