办公室里有一个更资深的SQL开发人员(DBA),他告诉我,在我的脚本的所有LEFT JOINS中,我必须处理左表的连接列可能为空的情况,否则,我必须使用INNER JOINs。现在,作为一个菜鸟,我在这里可能是错的,但我看不出他的意思,让我不必要地困惑。
他的解释是,除非该列不可为空,否则我必须
- 在 ON 子句上使用
ISNULL(LeftTable.ColumnA,<replacement value here>)
,或 - 处理 ON 子句中的空值或 WHERE 子句,通过添加
AND LeftTable.ColumnA IS NOT NULL
或AND LeftTable.ColumnA IS NULL
。
我认为这些是不必要的,因为如果不介意从右表返回空行,如果右表连接列的值与左表连接列不匹配,无论是使用相等还是不相等,都可以使用 LEFT JOIN。我的目的是它不必等于正确的表连接列值。如果左表连接列为 null,我可以在右表上返回空行,因为 null 不等于任何东西。
我在这里没有看到什么?
主要编辑:
所以我正在添加表定义和脚本。这些不是确切的脚本,只是为了说明问题。我已经删除了早期的编辑,这些编辑不正确,因为之前没有在脚本前面。
CREATE TABLE dbo.Contact (
ContactID int NOT NULL, --PK
FirstName varchar(10) NULL,
LastName varchar(10) NULL,
StatusID int NULL,
CONSTRAINT PK_Contact_ContactID
PRIMARY KEY CLUSTERED (ContactID)
);
GO
CREATE TABLE dbo.UserGroup (
UserGroupID int NOT NULL, --PK
UserGroup varchar(50) NULL,
StatusID int NULL,
CONSTRAINT PK_UserGroup_UserGroupID
PRIMARY KEY CLUSTERED (UserGroupID)
);
GO
CREATE TABLE dbo.UserGroupContact (
UserGroupID int NOT NULL, --PK,FK
ContactID int NOT NULL, --PK,FK
StatusID int NULL
CONSTRAINT PK_UserGroupContact_UserGroupContactID
PRIMARY KEY CLUSTERED (UserGroupID, ContactID),
CONSTRAINT FK_UserGroupContact_UserGroupId
FOREIGN KEY (UserGroupId)
REFERENCES [dbo].[UserGroup](UserGroupId),
CONSTRAINT FK_UserGroupContact_ContactId
FOREIGN KEY (ContactId)
REFERENCES [dbo].[Contact](ContactId)
);
GO
CREATE TABLE dbo.Account (
AccountID int NOT NULL, --PK
AccountName varchar(50) NULL,
AccountManagerID int NULL, --FK
Balance int NULL,
CONSTRAINT PK_Account_AccountID
PRIMARY KEY CLUSTERED (AccountID),
CONSTRAINT FK_Account_AccountManagerID
FOREIGN KEY (AccountManagerID)
REFERENCES [dbo].[Contact](ContactId),
);
GO
我的原始查询如下所示。当我说"左表"时,我指的是连接中 ON 子句左侧的表。如果是"右表",则为 ON 子句右侧的表。
SELECT
a.AccountId,
a.AccountName,
a.Balance,
ug.UserGroup,
ugc.UserGroupID,
a.AccountManagerID,
c.FirstName,
c.LastName
FROM dbo.Account a
LEFT JOIN dbo.Contact c
ON a.AccountManagerID = c.ContactID
AND c.StatusID=1
LEFT JOIN dbo.UserGroupContact ugc
ON a.AccountManagerID = ugc.ContactID
AND ugc.StatusID=1
LEFT JOIN dbo.UserGroup ug
ON ugc.UserGroupID = ug.UserGroupID
AND ug.StatusID=1
WHERE
a.Balance > 0
AND ugc.UserGroupID = 10
AND a.AccountManagerID NOT IN (20,30)
请注意,在上面的示例脚本中,第一个和第二个左连接在左侧表上具有可为空的列,在右侧表上具有不可为空的列。第三个左连接在左侧和右侧表上具有可为 null 的列。
建议是"更改为内部连接或在 where 子句中处理 NULL 条件"或"使用了 LEFT JOIN,但在 WHERE 子句中引用了非空条件"。
建议根据意图执行以下任一操作:
a) 转换为内部连接(不可能,因为我想要客户表中不匹配的行)
SELECT
a.AccountId,
a.AccountName,
a.Balance,
ug.UserGroup,
ugc.UserGroupID,
a.AccountManagerID,
c.FirstName,
c.LastName
FROM dbo.Account a
INNER JOIN dbo.Contact c
ON a.AccountManagerID = c.ContactID
AND c.StatusID=1
INNER JOIN dbo.UserGroupContact ugc
ON a.AccountManagerID = ugc.ContactID
AND ugc.StatusID=1
INNER JOIN dbo.UserGroup ug
ON ugc.UserGroupID = ug.UserGroupID
AND ug.StatusID=1
WHERE
a.Balance > 0
AND ugc.UserGroupID = 10
AND a.AccountManagerID NOT IN (20,30)
b) 处理 WHERE 子句中的空值(不可能,因为我想在 a.AccountManagerID 列和 ugc 上返回带有空值的行。用户组 ID)
SELECT
a.AccountId,
a.AccountName,
a.Balance,
ug.UserGroup,
ugc.UserGroupID,
a.AccountManagerID,
c.FirstName,
c.LastName
FROM dbo.Account a
LEFT JOIN dbo.Contact c
ON a.AccountManagerID = c.ContactID
AND c.StatusID=1
LEFT JOIN dbo.UserGroupContact ugc
ON a.AccountManagerID = ugc.ContactID
AND ugc.StatusID=1
LEFT JOIN dbo.UserGroup ug
ON ugc.UserGroupID = ug.UserGroupID
AND ug.StatusID=1
WHERE
a.Balance > 0
AND ugc.UserGroupID = 10
AND a.AccountManagerID NOT IN (20,30)
AND a.AccountManagerID IS NOT NULL
AND ugc.UserGroupID IS NOT NULL
c) 处理 ON 子句中的空值(我解决了这个我认为没有意义,因为它是多余的)
SELECT
a.AccountId,
a.AccountName,
a.Balance,
ug.UserGroup,
ugc.UserGroupID,
a.AccountManagerID,
c.FirstName,
c.LastName
FROM dbo.Account a
LEFT JOIN dbo.Contact c
ON a.AccountManagerID = c.ContactID
AND c.StatusID=1
AND a.AccountManagerID IS NOT NULL
LEFT JOIN dbo.UserGroupContact ugc
ON a.AccountManagerID = ugc.ContactID
AND ugc.StatusID=1
AND a.AccountManagerID IS NOT NULL
LEFT JOIN dbo.UserGroup ug
ON ugc.UserGroupID = ug.UserGroupID
AND ug.StatusID=1
AND ugc.UserGroupID IS NOT NULL
WHERE
a.Balance > 0
AND ugc.UserGroupID = 10
AND a.AccountManagerID NOT IN (20,30)
我没有提供 ISNULL() 的示例。另外,我认为他指的不是隐式内部连接。
回顾一下,我如何处理这个建议:"使用了 LEFT JOIN,但在 WHERE 子句中引用了非空条件。他评论说,这是一个"有问题的左加入逻辑"。
您的问题没有涉及的一件事是 ANSI NULL,无论它们是打开还是关闭。 如果 ANSI NULL 处于打开状态,则比较 NULL = NULL 将返回 false,但如果它们处于关闭状态,则 NULL = NULL 返回 true。
您可以在此处阅读有关 ANSI NULL 的更多信息:https://learn.microsoft.com/en-us/sql/t-sql/statements/set-ansi-nulls-transact-sql
因此,如果 ANSI NULL 处于 OFF 状态,则您非常关心将 NULL 外键与连接中缺失的行进行匹配。 具有 NULL 外键的行将匹配左侧表均为 NULL 的每一行。
如果 ANSI NULL 处于打开状态,则左外部联接将按预期运行,并且 NULL 外键将不会与其他缺失行的 NULL 主键匹配。
如果另一个开发人员告诉您需要小心外部联接中的 NULL,这可能是一个很好的迹象,表明您正在使用的数据库已关闭 ANSI NULL。
如果不介意从右表中返回空行,则使用左连接
左表 左联接右表 ON条件返回内联接行以及由空值扩展的不匹配的左表行。
如果这是人们想要的,则可以使用左连接。
左侧表的"连接"列
联接不在"联接列"上 - 无论这意味着什么。这是有条件的。
例如,这可能是左侧表中的一列等于右侧表中的同名列。或者是左侧表中的一列等于右侧同名列的函数。或者是同名列的布尔函数。或涉及/包括其中任何一个。或者是任何输入列的任何布尔函数。
如果左表连接列为 null,我可以在右表上返回空行,因为 null 不等于任何东西。
看来你正遭受着一种根本性的误解。唯一"我可以返回"的是你被告知返回的行,对于某些可能的输入。
这不是,比如说,在某些表上编码一些条件的问题,因为我们想要某些内部连接行,然后接受我们得到的任何 null 扩展行。如果我们使用左连接,那是因为它返回正确的内部连接行和正确的 null 扩展行;否则,我们想要一个不同的表达式。
例如,这不是一个具有 null 的左表行的问题,这意味着该行不能是内部连接的一部分,并且必须是 null 扩展的。我们有一些投入;我们想要一些输出。如果我们希望在某个条件下内连接两个表,无论该条件如何使用 null 或任何其他输入值以及不匹配的左侧表行,那么我们就在该条件下保留连接这些表;否则,我们想要一个不同的表达式。
(您的问题使用但不解释"句柄"。对于某些可能的输入,您不会告诉我们您被告知要返回的行。您甚至没有向我们提供示例输入所需的输出或某些查询的实际输出。因此,我们无法补充你的DBA的批评试图说你应该做什么或你正在做什么查询。
将在这里扩展一下我的评论; 然而,这是基于我们目前所拥有的猜测工作。
根据你目前的措辞,你所说的是错误的。让我们以这些简单的表格为例:
USE Sandbox;
GO
CREATE TABLE Example1 (ID int NOT NULL, SomeValue varchar(10));
GO
CREATE TABLE Example2 (ID int NOT NULL, ParentID int NOT NULL, SomeOtherValue varchar(10));
GO
INSERT INTO Example1
VALUES (1,'abc'),(2,'def'),(3,'bcd'),(4,'zxy');
GO
INSERT INTO Example2
VALUES (1,1,'sadfh'),(2,1,'asdgfkhji'),(3,3,'sdfhdfsbh');
现在,让我们用LEFT JOIN
进行一个简单的查询:
SELECT *
FROM Example1 E1
LEFT JOIN Example2 E2 ON E1.ID = E2.ParentID
ORDER BY E1.ID, E2.ID;
请注意,返回 5 行。不需要处理NULL
。如果您在ON
中添加了OR
,那将是无意义的,因为ParentID
不能具有NULL
的值。
但是,如果我们在WHERE
中添加一些内容,例如:
SELECT *
FROM Example1 E1
LEFT JOIN Example2 E2 ON E1.ID = E2.ParentID
WHERE LEFT(E2.SomeOtherValue,1) = 's'
ORDER BY E1.ID, E2.ID;
现在,这会将LEFT JOIN
变成隐式INNER JOIN
。因此,以上最好写成:
SELECT *
FROM Example1 E1
JOIN Example2 E2 ON E1.ID = E2.ParentID
WHERE LEFT(E2.SomeOtherValue,1) = 's'
ORDER BY E1.ID, E2.ID;
但是,这可能不是预期的输出;您可能希望不匹配的行(以及为什么您最初使用LEFT JOIN
。有两种方法可以做到这一点。首先是在ON
子句中添加条件:
SELECT *
FROM Example1 E1
LEFT JOIN Example2 E2 ON E1.ID = E2.ParentID
AND LEFT(E2.SomeOtherValue,1) = 's'
ORDER BY E1.ID, E2.ID;
另一个是添加一个OR
(不要使用ISNULL
,它会影响SARGability!
SELECT *
FROM Example1 E1
LEFT JOIN Example2 E2 ON E1.ID = E2.ParentID
WHERE LEFT(E2.SomeOtherValue,1) = 's'
OR E2.ID IS NULL
ORDER BY E1.ID, E2.ID;
我想这就是你们前辈在说的。
不过要重复一遍:
SELECT *
FROM Example1 E1
LEFT JOIN Example2 E2 ON E1.ID = E2.ParentID OR E2.ID IS NULL
ORDER BY E1.ID, E2.ID;
毫无意义。E2.ID
不能有NULL
的值,所以子句不会对查询进行任何更改,除了可能使其运行速度变慢。
清理:
DROP TABLE Example1;
DROP TABLE Example2;
在我看来,据我所知,这很简单。
让我们尝试一个例子。 假设有 2 个表,一个主表和一个详细信息表。
主表"大师">
身份证名称 1 福1 2 福2 3 福3 4 福4 5 福5 6 Foo6
详细信息表 "详细信息">
ID ID_FK TheDetailValue 1 1 3 2 1 5 3 3 3 4 5 2 5 5 9 6 3 6 7 1 4
TheDetails表通过字段ID_FK链接到TheMaster表。 现在,假设要运行一个查询,您需要在其中对列 TheDetailValue 的值求和。我会选择这样的东西:
SELECT TheMaster.ID, TheMaster.NAME, Sum(TheDetails.TheDetailValue) AS SumOfTheDetailValue
FROM TheMaster INNER JOIN TheDetails ON TheMaster.ID = TheDetails.ID_FK
GROUP BY TheMaster.ID, TheMaster.NAME;
你会得到这样的列表:
ID 名称 SumOfTheDetailValue 1 Foo1 12 3 福3 9 5 Foo5 11
但是,您的查询使用左联接而不是内联接是什么?例如:
SELECT TheMaster.ID, TheMaster.NAME, Sum(TheDetails.TheDetailValue) AS SumOfTheDetailValue
FROM TheMaster LEFT JOIN TheDetails ON TheMaster.ID = TheDetails.ID_FK
GROUP BY TheMaster.ID, TheMaster.NAME;
结果将是:
ID 名称 SumOfTheDetailValue 1 Foo1 12 2 福2 3 福3 9 4 福4 5 福5 11 6 Foo6
您将为明细表中没有值的每个主字段获取一个 NULL。 如何排除这些值?使用 ISNULL!
SELECT TheMaster.ID, TheMaster.NAME, Sum(TheDetails.TheDetailValue) AS SumOfTheDetailValue
FROM TheMaster LEFT JOIN TheDetails ON TheMaster.ID = TheDetails.ID_FK
WHERE (((TheDetails.ID_FK) Is Not Null))
GROUP BY TheMaster.ID, TheMaster.NAME;
。这将带我们得到这些结果:
ID 名称 SumOfTheDetailValue 1 Foo1 12 3 福3 9 5 福5 11
...这正是我们在使用内部连接之前获得的。
所以,最后,我想你的同事正在谈论 ISNULL 函数的使用,以排除在另一个表中没有关系的记录。
就是这样。
例如,只有查询是使用 MS Access(快速测试)进行的,因此 ISNULL 函数使用"Is Null"实现,它可以变为"Is Not Null"。在你的情况下,它可能类似于ISNULL()和/或NOT ISNULL()