如何将 LEFT JOIN 限制为 SQL Server 中的第一个结果?

Posted

技术标签:

【中文标题】如何将 LEFT JOIN 限制为 SQL Server 中的第一个结果?【英文标题】:How do I limit a LEFT JOIN to the 1st result in SQL Server? 【发布时间】:2011-03-23 11:18:51 【问题描述】:

我有一点 SQL 几乎可以做我想做的事。我正在使用三个表,即用户、UserPhoneNumbers 和 UserPhoneNumberTypes。我正在尝试获取用户列表及其电话号码以进行导出。

数据库本身很旧并且存在一些完整性问题。我的问题是数据库中每个电话号码应该只有一种类型,但事实并非如此。当我运行这个时,如果每个人包含例如两个“家庭”数字,我会得到多行结果。

如何修改 SQL 以获取列出的第一个电话号码并忽略其余号码?我在 SQL Server 中,我知道 TOP 语句。但是,如果我将“TOP 1”添加到 LEFT JOIN 选择语句中,它只会给我数据库中的第一个条目,而不是每个用户的第一个条目。

这适用于 SQL Server 2000。

谢谢,

SELECT  Users.UserID, 
  Users.FirstName, Users.LastName,
  HomePhone, WorkPhone, FaxNumber

FROM Users

LEFT JOIN
 (SELECT UserID, PhoneNumber AS HomePhone
 FROM UserPhoneNumbers LEFT JOIN UserPhoneNumberTypes ON UserPhoneNumbers.UserPhoneNumberTypeID=UserPhoneNumberTypes.UserPhoneNumberTypeID
 WHERE UserPhoneNumberTypes.PhoneNumberType='Home') AS tmpHomePhone
 ON tmpHomePhone.UserID = Users.UserID
LEFT JOIN
 (SELECT UserID, PhoneNumber AS WorkPhone
 FROM UserPhoneNumbers LEFT JOIN UserPhoneNumberTypes ON UserPhoneNumbers.UserPhoneNumberTypeID=UserPhoneNumberTypes.UserPhoneNumberTypeID
 WHERE UserPhoneNumberTypes.PhoneNumberType='Work') AS tmpWorkPhone
 ON tmpWorkPhone.UserID = Users.UserID
LEFT JOIN
 (SELECT UserID, PhoneNumber AS FaxNumber
 FROM UserPhoneNumbers LEFT JOIN UserPhoneNumberTypes ON UserPhoneNumbers.UserPhoneNumberTypeID=UserPhoneNumberTypes.UserPhoneNumberTypeID
 WHERE UserPhoneNumberTypes.PhoneNumberType='Fax') AS tmpFaxNumber
 ON tmpFaxNumber.UserID = Users.UserID

【问题讨论】:

取决于您正在谈论的 SQL 版本。如果是 SQL Server 2005+,你有很多选择,包括 RANK 查询。 解决方法是[这里][1],将join替换为left join即可。 [1]:***.com/questions/2043259/… 【参考方案1】:

当您只想从左表中选择第一行为右表中的每一行时,您应该考虑使用 APPLY 运算符而不是连接,并将连接条件 移到内部 左连接:

SELECT  u.UserID, 
  u.FirstName, u.LastName,
  hn.PhoneNumber AS HomePhone
FROM Users u
OUTER APPLY (
 SELECT TOP(1) PhoneNumber 
 FROM UserPhoneNumbers upn
 LEFT JOIN UserPhoneNumberTypes upt 
   ON upn.UserPhoneNumberTypeID=upt.UserPhoneNumberTypeID
 WHERE upt.PhoneNumberType='Home'
 AND upn.UserID = u.UserID
 ORDER BY ...) as hn
...

【讨论】:

这在 MS SQL 2000 中有效吗? 不,是 SQL 2005 及之后的版本。我想念你只要求 SQL 2K。【参考方案2】:

由于它是 SQL Server 2000 并且排名功能已经出来,你可以让你的子查询 SELECTs 聚合:

SELECT UserID, MAX(PhoneNumber) AS HomePhone FROM [...] GROUP BY UserID

如果您不在乎返回用户的哪个家庭号码...

【讨论】:

这个很简单,我也经常用这个。 谢谢,对我来说,结果数字不是问题,所以只有一个。【参考方案3】:

假设 SQL Server 2005+,使用 ROW_NUMBER:

LEFT JOIN (SELECT UserID, 
                  PhoneNumber AS HomePhone,
                  ROW_NUMBER() OVER (PARTITION BY userid ORDER BY what?) AS rank
             FROM UserPhoneNumbers  upn
        LEFT JOIN UserPhoneNumberTypes upnt ON upnt.UserPhoneNumberTypeID = upn.UserPhoneNumberTypeID
                                           AND upnt.PhoneNumberType='Home') AS tmpHomePhone
                ON tmpHomePhone.UserID = Users.UserID
               AND tmpHomePhone.rank = 1

注意what? 占位符以确定第一个数字。如果您根本不在乎,请省略 ORDER BY...

【讨论】:

【参考方案4】:

稍等,只是为了理解问题。

你有两张桌子:

用户 (UserID --> x) UserPhones (UserID, PHoneType --> 电话号码) 并且 UserID / PhoneType 不是唯一的。

首先不需要临时表:

Select 
 x
from
 Users
inner join 
 (
   Select 
    top 1 y
   from
    FoneTypes
   where
    UserID = users.UseriD
   and phoneType = 'typex'
 ) as PhoneTypex on phonetypex.UserID = users.userID

根据需要添加内部连接。

还是我错过了什么?

【讨论】:

好的,我想我现在更了解你了……电话桌上有 ID 字段吗?你也许可以加入我知道我以前做过类似的 max(id) 那些不是真正的临时表,只是子查询,但你是对的,你不需要它们。 @Gary - 我在内部连接中使用了“Select top 1”的变体,这帮助了我。谢谢!【参考方案5】:
Select Users.UserID,  Users.FirstName, Users.LastName
    , PhoneNumbers.HomePhone
    , PhoneNumbers.WorkPhone
    , PhoneNumbers.FaxNumber
From Users
    Left Join   (
                Select UPN.UserId
                    , Min ( Case When PN.PhoneNumberType = 'Home' Then UPN.PhoneNumber End ) As HomePhone
                    , Min ( Case When PN.PhoneNumberType = 'Work' Then UPN.PhoneNumber End ) As WorkPhone
                    , Min ( Case When PN.PhoneNumberType = 'Fax' Then UPN.PhoneNumber End ) As FaxPhone
                From UserPhoneNumbers As UPN
                        Join    (
                                Select Min(UPN1.UserPhoneNumberId) As MinUserPhoneNumberId
                                    , UPNT1.PhoneNumberType
                                From UserPhoneNumbers As UPN1
                                    Join UserPhoneNumberTypes As UPNT1
                                        On UPNT1.UserPhoneNumberTypeID = UPN1.UserPhoneNumberTypeID
                                Where UPNT1.PhoneNumberType In('Home', 'Work', 'Fax')
                                Group By UPN1.UserID, UPNT.PhoneNumberType
                                ) As PN
                            On PN.MinUserPhoneNumberId = UPN.UserPhoneNumberId
                Group By UPN.UserId
                ) As PhoneNumbers
    On PhoneNumbers.UserId = Users.UserId

在这个解决方案中,对于每个用户和电话号码类型,我从UserPhoneNumbers 表中选择最小的主键值(我猜该列被命名为UserPhoneNumberId)。

【讨论】:

【参考方案6】:

我假设您在每个连接表上都有一些主键字段,因为 UserID 不是唯一的。我假设您的主键称为 ID。我们将获取 ID 最低的记录。这符合您的“第一”标准。

SELECT  Users.UserID, Users.FirstName, Users.LastName, hp.HomePhone,
        wp.WorkPhone, fn.FaxNumber
FROM Users
LEFT JOIN HomePhone hp ON hp.UserID = Users.UserID
LEFT JOIN HomePhone hp2 ON hp2.UserID = Users.UserID AND hp2.ID < hp.ID
LEFT JOIN WorkPhone wp ON wp.UserID = Users.UserID
LEFT JOIN WorkPhone wp2 ON wp2.UserID = Users.UserID AND wp2.ID < wp.ID
LEFT JOIN FaxNumber fn ON fn.UserID = Users.UserID
LEFT JOIN FaxNumber fn2 ON fn2.UserID = Users.UserID AND fn2.ID < fn.ID
WHERE hp2.ID IS NULL AND wp2.ID IS NULL AND fn2.ID IS NULL

在SQL Antipatterns一书中有一整章关于这类问题,称为“Ambiguous Gruops”。

【讨论】:

【参考方案7】:

当有两个相同类型的数字时,您必须定义“第一个”的含义,然后将条件添加到您的联接中,以便只有正确的记录符合条件。没有其他捷径可做。

【讨论】:

First - select 语句返回的第一行。表中没有其他标准可以限制结果集。【参考方案8】:

您可以只使用 GROUP BY:

SELECT  Users.UserID, 
  Users.FirstName, Users.LastName,
  HomePhone, WorkPhone, FaxNumber

FROM Users

LEFT JOIN
 (SELECT UserID, min(PhoneNumber) AS HomePhone
 FROM UserPhoneNumbers LEFT JOIN UserPhoneNumberTypes ON UserPhoneNumbers.UserPhoneNumberTypeID=UserPhoneNumberTypes.UserPhoneNumberTypeID
 WHERE UserPhoneNumberTypes.PhoneNumberType='Home'
 GROUP BY userID) AS tmpHomePhone
 ON tmpHomePhone.UserID = Users.UserID
LEFT JOIN
 (SELECT UserID, min(PhoneNumber) AS WorkPhone
 FROM UserPhoneNumbers LEFT JOIN UserPhoneNumberTypes ON UserPhoneNumbers.UserPhoneNumberTypeID=UserPhoneNumberTypes.UserPhoneNumberTypeID
 WHERE UserPhoneNumberTypes.PhoneNumberType='Work'
 GROUP BY userID) AS tmpWorkPhone
 ON tmpWorkPhone.UserID = Users.UserID
LEFT JOIN
 (SELECT UserID, min(PhoneNumber) AS FaxNumber
 FROM UserPhoneNumbers LEFT JOIN UserPhoneNumberTypes ON UserPhoneNumbers.UserPhoneNumberTypeID=UserPhoneNumberTypes.UserPhoneNumberTypeID
 WHERE UserPhoneNumberTypes.PhoneNumberType='Fax'
 GROUP BY userID) AS tmpFaxNumber
 ON tmpFaxNumber.UserID = Users.UserID

你也可以使用 max() 来代替 min()。

或者您可以通过以下方式在一组中完成:

SELECT  Users.UserID, 
  Users.FirstName, Users.LastName,
  max(HomePhone) as HomePhone,
  max(WorkPhone) as WorkPhone,
  max(FaxNumber) as FaxNumber

FROM Users

LEFT JOIN
 (SELECT UserID, PhoneNumber AS HomePhone
 FROM UserPhoneNumbers LEFT JOIN UserPhoneNumberTypes ON UserPhoneNumbers.UserPhoneNumberTypeID=UserPhoneNumberTypes.UserPhoneNumberTypeID
 WHERE UserPhoneNumberTypes.PhoneNumberType='Home') AS tmpHomePhone
 ON tmpHomePhone.UserID = Users.UserID
LEFT JOIN
 (SELECT UserID, PhoneNumber AS WorkPhone
 FROM UserPhoneNumbers LEFT JOIN UserPhoneNumberTypes ON UserPhoneNumbers.UserPhoneNumberTypeID=UserPhoneNumberTypes.UserPhoneNumberTypeID
 WHERE UserPhoneNumberTypes.PhoneNumberType='Work') AS tmpWorkPhone
 ON tmpWorkPhone.UserID = Users.UserID
LEFT JOIN
 (SELECT UserID, PhoneNumber AS FaxNumber
 FROM UserPhoneNumbers LEFT JOIN UserPhoneNumberTypes ON UserPhoneNumbers.UserPhoneNumberTypeID=UserPhoneNumberTypes.UserPhoneNumberTypeID
 WHERE UserPhoneNumberTypes.PhoneNumberType='Fax') AS tmpFaxNumber
 ON tmpFaxNumber.UserID = Users.UserID

【讨论】:

以上是关于如何将 LEFT JOIN 限制为 SQL Server 中的第一个结果?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 LEFT JOIN 查询将 RAW SQL 转换为 DQL

使用 SQL JOIN,如何将一张表的结果限制为最近的记录

sql中的inner join ,left join ,right join

如何在 LINQ to SQL 中编写右侧为空的 LEFT OUTER JOIN?

MySql 之 left join 查询结果

SQL——左连接(Left join)右连接(Right join)内连接(Inner join)