【问题标题】:Using IS NULL or IS NOT NULL on join conditions - Theory question在连接条件上使用 IS NULL 或 IS NOT NULL - 理论问题
【发布时间】:2011-07-08 21:48:13
【问题描述】:

这里的理论问题:

为什么指定 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 语句中。

这是什么原因?

【问题讨论】:

    标签: mysql oracle theory left-join


    【解决方案1】:

    以表 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
    

    结果是左表中的parentid 和第二个表中的childpid 的每次匹配都会在结果中显示为一行:

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

    现在,上面没有显示没有孩子的父母(因为他们的 id 与孩子的 id 不匹配,所以你怎么办?你做一个外连接。有三种类型的外连接,左边,右连接和完全外连接。我们需要左连接,因为我们想要左表(父表)中的“额外”行:

    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(或您可能使用的任何其他 RDBMS)将不知道该放什么,因为这些父母没有匹配(孩子),因此没有 pidchild.name 与这些父母匹配。所以,它把这个特殊的非值称为NULL

    我的意思是这些NULLs 是在LEFT OUTER JOIN 期间创建的(在结果集中)。


    所以,如果我们只想显示没有孩子的父母,我们可以在上面的LEFT JOIN 中添加一个WHERE child.pid IS NULLJOIN 完成后评估(检查)WHERE 子句。因此,从上面的结果可以清楚地看出,只有最后三行 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 AND 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  |
    +----+--------+------+-------+
    

    希望上面的解释清楚。



    旁注(与您的问题没有直接关系):为什么 Pan 没有出现在我们的任何 JOIN 中?因为他的pidNULL 并且SQL(不常见)逻辑中的NULL 不等于任何东西,所以它不能与任何父ID(即1、2、3、4 和5)匹配.即使那里有一个 NULL,它仍然不匹配,因为 NULL 不等于任何东西,甚至不等于 NULL 本身(这确实是一个非常奇怪的逻辑!)。这就是为什么我们使用特殊检查 IS NULL 而不是 = NULL 检查。

    那么,如果我们执行RIGHT JOINPan 会出现吗?是的,它会!因为 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。您可以在其他 RDBMS 中尝试,它会显示:

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

    【讨论】:

    • 您可以在 MySQL 中通过合并 LEFT JOINRIGHT JOIN 之间的联合来伪造 FULL JOIN,其中 id 是 NULL。这有一些限制——例如,你不能更新或删除——而且可能比它的价值更麻烦。
    【解决方案2】:

    NULL 部分是在实际连接之后计算的,因此它需要位于 where 子句中。

    【讨论】:

    • 所以如果我理解正确的话,RDMS 软件会忽略空计算,除非它们在 WHERE 子句中,但在表连接时执行其他连接条件?
    • @JoshG ,我认为你说得对。为了让 RDMS 确定列值是否为 NULL,它将首先将它们连接在一起。加入它们后,它将查看 WHERE 子句并根据该子句过滤记录。这正是为什么 SQL 大师说明智的做法是考虑您的联接,看看是否有任何 WHERE 子句部分可以移动到 JOIN 条件,因为这样联接将发生在更少的记录上并且会更快。跨度>
    【解决方案3】:

    实际上 NULL 过滤器并没有被忽略。问题是这就是连接两个表的工作原理。

    我将尝试了解数据库服务器执行的步骤以使其理解。 例如,当您执行您所说的忽略 NULL 条件的查询时。 选择 * 从 出货量 左外连接返回 r
    ON s.id = r.id AND r.id 为空 在哪里 s.day >= CURDATE() - 间隔 10 天

    发生的第一件事是选中了 SHIPMENTS 表中的所有行

    下一步数据库服务器将开始从第二个(RETURNS)表中逐一选择记录。

    在第三步中,来自 RETURNS 表的记录将根据您在查询中提供的连接条件进行限定,在本例中为 (s.id = r.id and r.id is NULL)

    请注意,在第三步中应用的此限定仅决定服务器是否应接受或拒绝 RETURNS 表的当前记录以附加到 SHIPMENT 表的选定行。它绝不会影响从 SHIPMENT 表中选择记录。

    一旦服务器完成连接两个表,其中包含 SHIPMENT 表的所有行和 RETURNS 表的选定行,它会在中间结果上应用 where 子句。 因此,当您在 where 子句中放置 (r.id is NULL) 条件时,中间结果中 r.id = null 的所有记录都会被过滤掉。

    【讨论】:

      【解决方案4】:

      WHERE 子句在处理完JOIN 条件后进行评估。

      【讨论】:

      • 感谢您的回复。为什么在处理其他连接条件时会忽略“IS NULL”连接条件?
      • @JoshG:因为 NULL/NOT NULL 状态直到 JOIN 被评估后才存在。
      【解决方案5】:

      您正在执行LEFT OUTTER JOIN,这表明您想要语句左侧表中的每个元组,而不管它在右侧表中是否有匹配记录。在这种情况下,您的结果将从 RIGHT 表中删除,但您最终得到的结果与您在 ON 子句中根本没有包含 AND 相同。

      在 WHERE 子句中执行 AND 会导致修剪发生在 LEFT JOIN 发生之后。

      【讨论】:

      • 感谢您的回复。这是有道理的,只是这个逻辑似乎只影响 IS NULL AND IS NOT NULL 过滤器,这很奇怪。我可以在连接条件上放置任何其他过滤器,它会正常工作。知道这是为什么吗?
      • join时检查null;因此,您所做的只是检查当前存在于正确表中且 id 为空的行。不是以左表 + 右表元组结尾的后连接值(在右表中没有匹配的情况下,使用 NULL 元组)。因此,通过在 ON 子句中执行 r.id is not NULL 你只是在现有的 r 表中寻找空值。
      【解决方案6】:

      你的执行计划应该明确这一点; JOIN 优先,然后过滤结果。

      【讨论】:

      • 感谢您的回复。那么join + all join过滤条件是计算出来的,但是加入的时候不是Nulls?为什么它会忽略 NULL 过滤器而不是其他过滤器?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-09-04
      • 1970-01-01
      • 2021-05-12
      • 2013-02-04
      • 1970-01-01
      • 2023-04-03
      相关资源
      最近更新 更多