Использование IS NULL или NOT NULL в условиях соединения - вопрос теории - PullRequest
35 голосов
/ 09 июля 2011

Теория вопроса здесь:

Почему указание table.field IS NULL или table.field IS NOT NULL не работает с условием соединения (например, с левым или правым соединением), а только с условием where?

нерабочий пример:

- это должно вернуть все отгрузки с отфильтрованным возвратом (ненулевыми значениями). Тем не менее, это возвращает все поставки независимо от того, соответствует ли что-либо выражению [r.id is null].

SELECT
  *
FROM 
  shipments s
LEFT OUTER JOIN returns r  
  ON s.id = r.id
  AND r.id is null
WHERE
  s.day >= CURDATE() - INTERVAL 10 DAY 

Рабочий пример:

-Это возвращает правильное количество строк, которое представляет собой общее количество отправлений, за вычетом любых связанных с возвратами (ненулевых значений).

SELECT
  *
FROM 
  shipments s
LEFT OUTER JOIN returns r  
  ON s.id = r.id
WHERE
  s.day >= CURDATE() - INTERVAL 10 DAY
  AND r.id is null

Почему это так? Все остальные условия фильтрации между двумя объединяемыми таблицами работают нормально, но по некоторым причинам фильтры IS NULL и IS NOT NULL не работают, если только в операторе where.

В чем причина этого?

Ответы [ 6 ]

81 голосов
/ 09 июля 2011

Пример с таблицами A и B:

 A (parent)       B (child)    
============    =============
 id | name        pid | name 
------------    -------------
  1 | Alex         1  | Kate
  2 | Bill         1  | Lia
  3 | Cath         3  | Mary
  4 | Dale       NULL | Pan
  5 | Evan  

Если вы хотите найти родителей и их детей, вы делаете INNER JOIN:

SELECT id,  parent.name AS parent
     , pid, child.name  AS child

FROM
        parent  INNER JOIN  child
  ON   parent.id     =    child.pid

Результатом является то, что каждое совпадение parent 'id из левой таблицы и child' pid из второй таблицы будет отображаться в виде строки в результате:

+----+--------+------+-------+
| id | parent | pid  | child | 
+----+--------+------+-------+
|  1 | Alex   |   1  | Kate  |
|  1 | Alex   |   1  | Lia   |
|  3 | Cath   |   3  | Mary  |
+----+--------+------+-------+

Теперь, выше не показаны родители без детей (потому что их идентификаторы не совпадают с идентификаторами детей, так что вы делаете? Вместо этого вы делаете внешнее соединение. Есть три типа внешних объединений, слева, правое и полное внешнее соединение. Нам нужно левое, так как нам нужны «лишние» строки из левой таблицы (родитель):

SELECT id,  parent.name AS parent
     , pid, child.name  AS child

FROM
        parent  LEFT JOIN  child
  ON   parent.id    =    child.pid

Результат состоит в том, что кроме предыдущих матчей также отображаются все родители, у которых нет совпадений (читай: у них нет ребенка):

+----+--------+------+-------+
| id | parent | pid  | child | 
+----+--------+------+-------+
|  1 | Alex   |   1  | Kate  |
|  1 | Alex   |   1  | Lia   |
|  3 | Cath   |   3  | Mary  |
|  2 | Bill   | NULL | NULL  |
|  4 | Dale   | NULL | NULL  |
|  5 | Evan   | NULL | NULL  |
+----+--------+------+-------+

Откуда взялись все эти NULL? Что ж, MySQL (или любая другая СУБД, которую вы можете использовать) не будет знать, что туда поместить, поскольку у этих родителей нет совпадений (ребенок), поэтому нет pid или child.name для сопоставления с этими родителями. Таким образом, это специальное ненулевое значение называется NULL.

Я хочу сказать, что эти NULLs создаются (в наборе результатов) во время LEFT OUTER JOIN.


Итак, если мы хотим показать только родителей, которые НЕ имеют ребенка, мы можем добавить WHERE child.pid IS NULL к LEFT JOIN выше. Предложение WHERE оценивается (проверяется) после выполнения JOIN. Итак, из приведенного выше результата ясно, что будут показаны только последние три строки, в которых pid равен NULL:

SELECT id,  parent.name AS parent
     , pid, child.name  AS child

FROM
        parent  LEFT JOIN  child
  ON   parent.id    =    child.pid

WHERE child.pid IS NULL

Результат:

+----+--------+------+-------+
| id | parent | pid  | child | 
+----+--------+------+-------+
|  2 | Bill   | NULL | NULL  |
|  4 | Dale   | NULL | NULL  |
|  5 | Evan   | NULL | NULL  |
+----+--------+------+-------+

Теперь, что произойдет, если мы переместим этот IS NULL чек из WHERE в объединяющее предложение ON?

SELECT id,  parent.name AS parent
     , pid, child.name  AS child

FROM
        parent  LEFT JOIN  child
  ON   parent.id    =    child.pid
  AND  child.pid IS NULL

В этом случае база данных пытается найти строки из двух таблиц, которые соответствуют этим условиям. То есть строки, где parent.id = child.pid И child.pid IN NULL. Но он может найти нет такого совпадения , потому что никакие child.pid не могут быть равны чему-то (1, 2, 3, 4 или 5) и быть равными NULL одновременно

Итак, условие:

ON   parent.id    =    child.pid
AND  child.pid IS NULL

эквивалентно:

ON   1 = 0

, который всегда False.

Итак, почему он возвращает ВСЕ строки из левой таблицы? Поскольку это ЛЕВОЕ СОЕДИНЕНИЕ! И левые соединения возвращают строк, которые соответствуют (в данном случае ни одной) , а также строк из левой таблицы, которые не соответствуют проверке ( все в этом случае ):

+----+--------+------+-------+
| id | parent | pid  | child | 
+----+--------+------+-------+
|  1 | Alex   | NULL | NULL  |
|  2 | Bill   | NULL | NULL  |
|  3 | Cath   | NULL | NULL  |
|  4 | Dale   | NULL | NULL  |
|  5 | Evan   | NULL | NULL  |
+----+--------+------+-------+

Надеюсь, приведенное выше объяснение понятно.



Sidenote (не имеет прямого отношения к вашему вопросу): почему же Pan не появляется ни в одном из наших JOIN? Поскольку его pid равен NULL, а NULL в (не распространенной) логике SQL не равен ничему, поэтому он не может совпадать ни с одним из родительских идентификаторов (1,2,3,4 и 5). , Даже если бы там был NULL, он все равно не соответствовал бы, потому что NULL не равно ничему, даже самому NULL (это действительно очень странная логика!). Вот почему мы используем специальный чек IS NULL, а не = NULL чек.

Итак, Pan появится, если мы сделаем RIGHT JOIN? Да, это будет! Поскольку RIGHT JOIN покажет все соответствующие результаты (первое INNER JOIN, которое мы сделали), а также все строки из RIGHT таблицы, которые не совпадают (в нашем случае это единица, строка (NULL, 'Pan').

SELECT id,  parent.name AS parent
     , pid, child.name  AS child

FROM
        parent  RIGHT JOIN  child
  ON   parent.id     =    child.pid

Результат:

+------+--------+------+-------+
| id   | parent | pid  | child | 
+---------------+------+-------+
|   1  | Alex   |   1  | Kate  |
|   1  | Alex   |   1  | Lia   |
|   3  | Cath   |   3  | Mary  |
| NULL | NULL   | NULL | Pan   |
+------+--------+------+-------+

К сожалению, MySQL не имеет FULL JOIN. Вы можете попробовать его в других РСУБД, и он покажет:

+------+--------+------+-------+
|  id  | parent | pid  | child | 
+------+--------+------+-------+
|   1  | Alex   |   1  | Kate  |
|   1  | Alex   |   1  | Lia   |
|   3  | Cath   |   3  | Mary  |
|   2  | Bill   | NULL | NULL  |
|   4  | Dale   | NULL | NULL  |
|   5  | Evan   | NULL | NULL  |
| NULL | NULL   | NULL | Pan   |
+------+--------+------+-------+
6 голосов
/ 09 июля 2011

Часть NULL вычисляется ПОСЛЕ фактического соединения, поэтому она должна быть в предложении where.

3 голосов
/ 09 июля 2011

Фактически фильтр NULL не игнорируется.Дело в том, как работает объединение двух таблиц.

Я попытаюсь пройтись по шагам, выполняемым сервером базы данных, чтобы это понять.Например, когда вы выполняете запрос, который вы сказали, игнорирует условие NULL.SELECT * FROM отгрузки s LEFT OUTER JOIN возвращает r
ON s.id = r.id И r.id является нулевым, ГДЕ s.day> = CURDATE () - ИНТЕРВАЛ 10 ДНЯ

1-я вещь произошлавсе строки из таблицы SHIPMENTS будут выбраны

на следующем шаге. Сервер базы данных начнет выбирать одну запись из второй (RETURNS) таблицы.

на третьем шаге будет подтверждена запись из таблицы RETURNS.против условий соединения, которые вы указали в запросе (в данном случае (s.id = r.id и r.id равен NULL)

обратите внимание, что эта квалификация, примененная на третьем шаге, решает только то, должен ли сервер принятьили отклоните текущую запись таблицы RETURNS для добавления к выбранной строке таблицы SHIPMENT.Это никоим образом не может повлиять на выбор записи из таблицы SHIPMENT.

И когда сервер завершит соединение двух таблиц, содержащих все строки таблицы SHIPMENT и выбранные строки таблицы RETURNS, он применяет предложение where кпромежуточный результат.поэтому, когда вы помещаете условие (r.id равно NULL) в предложение where, все записи из промежуточного результата с r.id = null отфильтровываются.

2 голосов
/ 09 июля 2011

Вы делаете LEFT OUTTER JOIN, который указывает, что вы хотите, чтобы каждый кортеж из таблицы в левой части оператора, независимо от того, имеет ли соответствующую запись в ПРАВЫЙ таблице. В этом случае ваши результаты удаляются из таблицы RIGHT, но в итоге вы получаете те же результаты, как если бы вы вообще не включали AND в предложение ON.

Выполнение оператора AND в предложении WHERE приводит к тому, что обрезка происходит после ЛЕВОГО СОЕДИНЕНИЯ.

2 голосов
/ 09 июля 2011

Предложение WHERE оценивается после обработки условий JOIN.

1 голос
/ 09 июля 2011

Ваш план выполнения должен прояснить это;JOIN имеет приоритет, после чего результаты фильтруются.

...