SQL Left Join 仅第一个匹配项

Posted

技术标签:

【中文标题】SQL Left Join 仅第一个匹配项【英文标题】:SQL Left Join first match only 【发布时间】:2013-11-18 11:03:52 【问题描述】:

我对具有多个连接的大量大表(行和列)进行了查询,但是其中一个表有一些重复的数据行,导致我的查询出现问题。由于这是来自另一个部门的只读实时提要,我无法修复该数据,但我正在努力防止我的查询出现问题。

鉴于此,我需要将此废话数据作为左联接添加到我的良好查询中。数据集如下:

IDNo    FirstName   LastName    ...
-------------------------------------------
uqx     bob     smith
abc     john        willis
ABC     john        willis
aBc     john        willis
WTF     jeff        bridges
sss     bill        doe
ere     sally       abby
wtf     jeff        bridges
...

(大约 2 打列,10 万行)

我的第一直觉是执行一个 distinct 给了我大约 80K 行:

SELECT DISTINCT P.IDNo
FROM people P

但是当我尝试以下操作时,我得到了所有行:

SELECT DISTINCT P.*
FROM people P

SELECT 
    DISTINCT(P.IDNo) AS IDNoUnq 
    ,P.FirstName
    ,P.LastName
    ...etc.    
FROM people P

然后我想我会在所有列上执行一个 FIRST() 聚合函数,但是这也感觉不对。从语法上讲,我在这里做错了吗?

更新: 只是想注意:这些记录是基于上面列出的 ID 的非键/非索引字段的重复项。 ID 是一个文本字段,虽然具有相同的值,但它与导致问题的其他数据不同。

【问题讨论】:

听起来这些行不是真正的重复项。某些列必须不同,否则 distinct * 不会返回所有行。你能澄清一下是什么让一行重复吗? 请为此表提供所需的输出declare @t table(id int, name char(1)) insert @t values (1, 'a'), (1, 'b'), (1, 'a'), (2, 'a'), (2, 'c') 您可以使用带有 ROW_NUMBER() OVER (PARTITION BY FirstName, LastName ORDER BY ID/DATE DESC) As orderID 的 CTE,然后从 CTE 中选择该列等于 1 的位置。 @acfrancis 是对的;您需要定义输出的主键并将其用于按列分区。 @acfrancis 正确,它们是基于上面列出的 ID 的非键/非索引字段的重复项。 ID 是一个文本字段,虽然具有相同的值,但它与其他数据的大小写不同。 CTE 不是专有的(例如,在许多 rdbms Oracle、DB2、SQL Server/Sybase、Postgres 中可用) 【参考方案1】:

distinct不是函数。它总是对选择列表的所有列进行操作。

您的问题是典型的“每组最大 N”问题,可以使用窗口函数轻松解决:

select ...
from (
  select IDNo,
         FirstName,
         LastName,
         ....,
         row_number() over (partition by lower(idno) order by firstname) as rn 
  from people 
) t
where rn = 1;

使用order by 子句,您可以选择要选择的重复项。

上面可以在左连接中使用,见下文:

select ...
from x
  left join (
    select IDNo,
           FirstName,
           LastName,
           ....,
           row_number() over (partition by lower(idno) order by firstname) as rn 
    from people 
  ) p on p.idno = x.idno and p.rn = 1
where ...

【讨论】:

如果例如people 表比 x 表大得多? @HansHarhoff:检查执行计划。但是,如果一个表比另一个更大 - 如果要求是从 x 返回所有行并且只从 people 返回“最新”,则没有其他方法可以做到这一点。【参考方案2】:

添加一个标识列 (PeopleID),然后使用相关子查询返回每个值的第一个值。

SELECT *
FROM People p
WHERE PeopleID = (
    SELECT MIN(PeopleID) 
    FROM People 
    WHERE IDNo = p.IDNo
)

【讨论】:

这不允许空行【参考方案3】:

事实证明我做错了,我需要先执行嵌套选择,首先是重要列,然后再执行不同的选择,以防止“唯一”数据的垃圾列破坏我的好数据。以下似乎已经解决了这个问题......但我稍后会尝试完整的数据集。

SELECT DISTINCT P2.*
FROM (
  SELECT
      IDNo
    , FirstName
    , LastName
  FROM people P
) P2

这里是一些要求的播放数据:http://sqlfiddle.com/#!3/050e0d/3

CREATE TABLE people
(
       [entry] int
     , [IDNo] varchar(3)
     , [FirstName] varchar(5)
     , [LastName] varchar(7)
);

INSERT INTO people
    (entry,[IDNo], [FirstName], [LastName])
VALUES
    (1,'uqx', 'bob', 'smith'),
    (2,'abc', 'john', 'willis'),
    (3,'ABC', 'john', 'willis'),
    (4,'aBc', 'john', 'willis'),
    (5,'WTF', 'jeff', 'bridges'),
    (6,'Sss', 'bill', 'doe'),
    (7,'sSs', 'bill', 'doe'),
    (8,'ssS', 'bill', 'doe'),
    (9,'ere', 'sally', 'abby'),
    (10,'wtf', 'jeff', 'bridges')
;

【讨论】:

如果您不控制最终数据库,您会发现它区分大小写 (***.com/questions/1411161/…)。我的建议是将IDNo 放在UCASE() 中,以保持安全 在进行完整测试后,看起来小提琴中的 POC 并没有达到预期的效果。这是因为任何具有不同值的列都会使行不同。 @mucio 好点子,当我解决这个问题时,我肯定会将键大写。 这正是DISTINCT 的重点,把所有东西都放在UCASE() 或者TRIM() @mucio 所以考虑到这一点,我已经搞砸了,还不如做一堆聚合函数来保持数据的完整性,因为我不能把所有数据都改成大写.我会考虑这里的选项,看看我能想出什么替代解决方案。【参考方案4】:

经过仔细考虑,这个困境有几个不同的解决方案:

聚合一切 在每一列上使用聚合来获取最大或最小的字段值。这就是我正在做的,因为它需要 2 条部分填写的记录并“合并”数据。

http://sqlfiddle.com/#!3/59cde/1

SELECT
  UPPER(IDNo) AS user_id
, MAX(FirstName) AS name_first
, MAX(LastName) AS name_last
, MAX(entry) AS row_num
FROM people P
GROUP BY 
  IDNo

获取第一条(或最后一条记录)

http://sqlfiddle.com/#!3/59cde/23

-- ------------------------------------------------------
-- Notes
-- entry: Auto-Number primary key some sort of unique PK is required for this method
-- IDNo:  Should be primary key in feed, but is not, we are making an upper case version
-- This gets the first entry to get last entry, change MIN() to MAX()
-- ------------------------------------------------------

SELECT 
   PC.user_id
  ,PData.FirstName
  ,PData.LastName
  ,PData.entry
FROM (
  SELECT 
      P2.user_id
     ,MIN(P2.entry) AS rownum
  FROM (
    SELECT
        UPPER(P.IDNo) AS user_id 
      , P.entry 
    FROM people P
  ) AS P2
  GROUP BY 
    P2.user_id
) AS PC
LEFT JOIN people PData
ON PData.entry = PC.rownum
ORDER BY 
   PData.entry

【讨论】:

【参考方案5】:

试试这个

 SELECT *
 FROM people P 
 where P.IDNo in (SELECT DISTINCT IDNo
              FROM people)

【讨论】:

好主意。试过了,它仍然显示重复。看来 IN 比较不区分大小写。我试图通过将 P.IDNo 包装在 Upper() 和不同的 IDNo 中来作弊,但它仍然显示了欺骗。呵呵。 这显然行不通。假设人们的 IDno 是 (1, 2, 2, 3, 4, 5, 5)。不同的 ID 号是 (1,2,3,4,5)。然后,您将所有 IDno 位于 (1,2,3,4,5) 中的人,但所有 IDno 位于 (1, 2, 2, 3, 4, 5, 5) 中的人也在 (1, 2、3、4、5),反之亦然。除了使查询变得更复杂之外,您在这里没有做任何事情。【参考方案6】:

根据重复行的性质,您似乎只想对这些列进行区分大小写。在这些列上设置排序规则应该是您所追求的:

SELECT DISTINCT p.IDNO COLLATE SQL_Latin1_General_CP1_CI_AS, p.FirstName COLLATE SQL_Latin1_General_CP1_CI_AS, p.LastName COLLATE SQL_Latin1_General_CP1_CI_AS
FROM people P

http://msdn.microsoft.com/en-us/library/ms184391.aspx

【讨论】:

【参考方案7】:

使用 Cross Apply 或 Outer Apply,这样您可以限制从表中与重复项连接到第一次匹配的数据量。

Select 
    x.*,
    c.*
from 
    x
Cross Apply 
    (
        Select 
            Top (1)
            IDNo,
            FirstName,
            LastName,
            ...., 
        from 
            people As p
        where 
            p.idno = x.idno
        Order By 
            p.idno //unnecessary if you don't need a specific match based on order
    ) As c

Cross Apply 的行为类似于内连接,Outer Apply 的行为类似于左连接

SQL Server CROSS APPLY and OUTER APPLY

【讨论】:

以上是关于SQL Left Join 仅第一个匹配项的主要内容,如果未能解决你的问题,请参考以下文章

SQL SERVER LEFT JOIN, INNER JOIN, RIGHT JOIN

sql中left join、right join、inner join有啥区别

SQL LEFT JOIN 与部分字符串匹配

SQL的JOIN语法解析(inner join, left join, right join, full outer join的区别)

MySQL 的 2 个字段上的 SQL LEFT-JOIN

SQL 中left joinright joininner join的区别