查找并处理重复的用户

时间:2012-03-02 21:00:35

标签: mysql sql

在具有以下格式和示例数据的大型用户数据库中,我们正在尝试识别重复的人:

id   first_name    last_name   email
---------------------------------------------------
 1   chris         baker       
 2   chris         baker       chris@gmail.com
 3   chris         baker       chris@hotmail.com
 4   chris         baker       crayzyguy@crazy.com  
 5   carl          castle      castle@npr.org
 6   mike          rotch       fakeuser@sample.com  

我使用以下查询:

SELECT 
    GROUP_CONCAT(id) AS "ids",
    CONCAT(UPPER(first_name), UPPER(last_name)) AS "name",
    COUNT(*) AS "duplicate_count" 
FROM 
    users 
GROUP BY 
    name 
HAVING 
    duplicate_count > 1

这很有效;我得到一个重复列表,其中包含所涉及行的ID号。

我们会将与重复相关的任何相关数据重新分配给实际的人(set user_id = 2 where user_id = 3),然后删除重复的用户行。

在我们第一次发布此报告之后出现问题,因为我们在手动验证它们确实是重复之后清理列表 - 有些不重复。有两个Chris Bakers是合法用户。

我们不希望在随后的重复报告中看到Chris Baker,直到时间结束,所以我正在寻找一种方法来标记用户ID 1和用户ID 4不是彼此重复的未来报告,但是他们可以被以后添加的新用户复制。

我尝试了什么

我在用户表中添加了is_not_duplicate字段,但是如果新的重复“Chris Baker”被添加到数据库中,则会导致此情况不会显示在重复的报告上; is_not_duplicate不正确地排除其中一个帐户。在有两个重复的Chris Baker加上标记为HAVING的“真实”之前,我的> 1语句将不会达到is_not_duplicate阈值。

总结的问题

如何在不循环结果或多个查询的情况下在上述查询中构建异常?

子查询很好,但数据集的大小使每个查询都很重要,我希望解决方案尽可能高效。

16 个答案:

答案 0 :(得分:7)

尝试添加is_not_duplicate布尔字段并修改代码,如下所示:

SELECT 
    GROUP_CONCAT(id) AS "ids",
    CONCAT(UPPER(first_name), UPPER(last_name)) AS "name",
    COUNT(*) AS "duplicate_count",
    SUM(is_not_duplicate) AS "real_count"
FROM 
    users 
GROUP BY 
    name 
HAVING 
    duplicate_count > 1
AND
    duplicate_count - real_count > 0

新添加的重复项将有is_not_duplicate=0,因此该名称的real_count将小于duplicate_count,并且该行将显示

答案 1 :(得分:2)

我的大脑太油腻了,目前无法提出实际的查询,但我可能会在一条应该有效的路径上给你一个推动:)

如果你确实添加了另一列(也许是一个有效的重复用户表?...两者都会完成相同的事情),并运行一个子查询来计算所有有效的重复项,然后你可以比较当前查询中的计数。您将排除任何具有匹配计数的用户,并且会使用更高的计数来提取任何用户。希望这是有道理的;我将创建一个用例:

  • 身份1和4的Chris Baker被标记为valid_duplicates
  • 系统中有4个Chris Baker
  • 你得到了有效的Chris Baker的
  • 你得到了所有克里斯贝克的统计数据
  • valid_count<> total_count,所以返回Chris Baker

*您甚至可以修改查询,使其甚至不会列出重复的ID(即使您只获得1个ID的重复标记)。而不是必须重新检查哪些是valids。这会有点复杂。没有它,至少你会忽略Chris Baker,直到另一个人进入系统

我已经写了基本查询,处理排除特定ID我将尝试在今晚滚动。但是,这至少解决了你的初始需求。如果您不需要更复杂的查询,请告诉我,以便我不会浪费时间在上面:)

SELECT 
    GROUP_CONCAT(id) AS "ids",
    CONCAT(UPPER(first_name), UPPER(last_name)) AS "name",
    COUNT(*) AS "duplicate_count" 
FROM 
    users 
WHERE NOT EXISTS
    (
        SELECT 1 
        FROM
        (
            SELECT 
                CONCAT(UPPER(first_name), UPPER(last_name)) AS "name",
                COUNT(*) AS "valid_duplicate_count" 
            FROM 
                users
            WHERE 
                is_valid_duplicate = 1 --true
            GROUP BY 
               name 
            HAVING 
               valid_duplicate_count > 1 
        ) AS duplicate_users
        WHERE 
            duplicate_users.name = users.name 
                AND valid_duplicate_count = duplicate_count
    )    
GROUP BY 
    name 
HAVING 
    duplicate_count > 1

以下是执行上述操作的查询,但最终列表将仅打印不在有效列表中的ID。实际上这比我想象的要简单得多。并且,它与上面的大致相同,但我保持上面的唯一原因是保留两个选项,以防我搞砸了上面的...它确实变得复杂,因为它是许多嵌套查询。如果您可以使用CTE,甚至临时表。它可能会使查询更具表现力,将其分解为临时表:)。希望这有助于您正在寻找

SELECT GROUP_CONCAT(id) AS "ids", 
    CONCAT(UPPER(first_name), UPPER(last_name)) AS "name",
    COUNT(*) AS "final_duplicate_count" 
    --This count could actually be 1 due to the nature of the query 
FROM 
    users
--get the list of duplicated user names
WHERE EXISTS
    (
        SELECT 
            CONCAT(UPPER(first_name), UPPER(last_name)) AS "name",
            COUNT(*) AS "total_duplicate_count"
        FROM 
            users AS total_dup_users
        --ignore valid_users whose count still matches
        WHERE NOT EXISTS
            (
                SELECT 1 
                FROM
                (
                    SELECT 
                        CONCAT(UPPER(first_name), UPPER(last_name)) AS "name",
                        COUNT(*) AS "valid_duplicate_count" 
                    FROM 
                        users AS valid_users
                    WHERE 
                        is_valid_duplicate = 1 --true
                    GROUP BY 
                        name 
                    HAVING 
                        valid_duplicate_count > 1 
                ) AS duplicate_users
                WHERE 
                    --join inner table to outer table
                    duplicate_users.name = total_dup_users.name  
                        --valid count check
                        AND valid_duplicate_count = total_duplicate_count
            )   
            --join inner table to outer table
            AND total_dup_users.Name = users.Name 
        GROUP BY 
            name 
        HAVING 
            duplicate_count > 1
    ) 
    --ignore users that are valid when doing the actual counts
    AND NOT EXISTS
    (
        SELECT 1
        FROM users AS valid
        WHERE 
            --join inner table to outer table
            users.name = 
                CONCAT(UPPER(valid.first_name), UPPER(valid.last_name))
            --only valid users
            AND valid.is_valid_duplicate = 1 --true
    )
GROUP BY 
    FinalDuplicates.Name

答案 2 :(得分:2)

由于这基本上是多对多关系,我会添加一个新表not_duplicate,其中包含字段user1user2

我可能会为每个not_duplicate关系添加两行,这样我有2 -> 3的一行和3 -> 2的对称行来简化查询,但这可能会导致数据不一致确保同时删除这两行(或只有一行并在脚本中进行正确的查询)。

答案 3 :(得分:1)

在我看来,is_not_duplicate列不够复杂,无法容纳您想要存储的信息 - 据我所知,您希望手动告诉您的检测,两个不同的用户不是彼此重复的。所以你要么创建一个像is_not_duplicate_of = other-user-id这样的列,要么你想保持开放的可能性,即一个用户可以手动定义而不是多个用户的副本,你需要一个带有两个user-id列的单独表。

查询告诉你非重写的重复项可能必须比你建议的重复一点复杂,我想不出一个适用于group by和逻辑的那个。我想到的唯一一件事就是

SELECT u1.* FROM users u1
INNER JOIN users u2
ON u1.id <> u2.id
AND u2.name = u1.name
WHERE NOT EXISTS (
  SELECT *
  FROM users_non_dups un
  WHERE (un.id1 = u1.id AND un.id2 = u2.id)
  OR (un.id1 = u2.id AND un.id2 = u1.id)
)

答案 4 :(得分:1)

如果您每次运行报告时都要更正所有重复项,那么一个非常简单的解决方案可能是修改查询:

SELECT 
    GROUP_CONCAT(id) AS "ids",
    MAX(id) AS "max_id",
    CONCAT(UPPER(first_name), UPPER(last_name)) AS "name",
    COUNT(*) AS "duplicate_count" 
FROM 
    users 
GROUP BY 
    name 
HAVING 
    duplicate_count > 1
    AND
    max_id > MAX_ID_LAST_TIME_DUPLICATE_REPORT_WAS_GENERATED;

答案 5 :(得分:0)

我会继续制作“confirmed_unique”专栏,默认为“False”。

为了避免你提到的问题,

然后我会选择所有看起来像重复的元素,并为“confirmed_unique”添加“False”条目。

答案 6 :(得分:0)

我不确定这是否有效,但您能否考虑添加* is_duplicate_of *列的相反逻辑?这样,您可以通过在此列输入第一个记录的ID来标记重复项,该记录将大于零。您希望保留的记录在此字段中的值为0。您可以将默认(未经检查的记录)设置为-1,以跟踪每条记录的验证状态。

之后,您可以继续执行一个SQL,该SQL只会将新记录与 is_duplicate_of = 0 正确记录进行比较。

答案 7 :(得分:0)

如果您可以稍微更改报告的格式。你可以像这样进行自我加入 -

SELECT 
    CONCAT(u1.id,",", u2.id) AS "ids",
    CONCAT(UPPER(u1.first_name), UPPER(u1.last_name)) AS "name"
FROM 
    users u1, users u2
WHERE
    u1.id < u2.id AND
    UPPER(u1.first_name) = UPPER(u2.first_name) AND
    UPPER(u1.last_name) = UPPER(u2.last_name) AND
    CONCAT(u1.id,",", u2.id) NOT IN (SELECT ids from not_dupe)

报告重复项如下:

ids | name
----|--------
1,2 | CHRISBAKER
1,3 | CHRISBAKER
...

并且not_dupe表将具有如下行:

ids
------
1,2
3,4
...

答案 8 :(得分:0)

我认为创建一个存储不重复的ID的查找表是有意义的。因此,删除了已确认的非重复项,查询只需要查找在查找表上找到的重复项。

例如在这个例子中我们有

id 1 | id 2

 2      4

如果crayzyguy@crazy.com和chris@gmail.com是不同的人。

答案 9 :(得分:0)

如果我是你,我会在我的数据库架构中添加一些地理定位表/字段。

两个最终用户具有相同名称且生活在同一个地方的可能性非常低 - 除非在非常大的城镇 - 但您也可以将地理定位分割为小区域 - 这与粒度有关。

祝你好运。

答案 10 :(得分:0)

我建议你创建一些东西:

  1. 标记已确认用户的布尔列
  2. 用于保存ID的字符串列
  3. 一个触发器,它将检查第一个名称和姓氏是否已经存在以填充该标志,并在字符串列中保存可能重复的所有ID。
  4. 然后构建一个报告,查找重复的true并解码字符串字段以匹配可能的重复

答案 11 :(得分:0)

我给Justin Pihony +1作为第一个建议将重复计数与非重复计数进行比较,而Hrant Khachatrian +1则表示第一个显示有效的方法。

这是一个稍微不同的方法,加上一些重命名使一切更加自我解释,加上查询中的一些额外列,以明确哪些记录需要作为潜在的重复进行比较。

我会调用新列“CONFIRMED_UNIQUE”而不是“IS_NOT_DUPLICATE”。和Hrant一​​样,我会把它设为布尔值(tinyint(1),0 = FALSE,1 = TRUE)。

“potential_duplicate_count”是必须删除的最大记录数。

select
    group_concat(case when not confirmed_unique then id end) as potential_duplicate_ids,
    group_concat(case when confirmed_unique then id end) as confirmed_unique_ids,
    concat(upper(first_name), upper(last_name)) as name,
    sum( case when not confirmed_unique then 1 end ) - (not max(confirmed_unique)) as potential_duplicate_count
from
    users
group by
    name
having
    potential_duplicate_count > 0

答案 12 :(得分:0)

我看到其他人因为合并的建议而被投票否决了,但是你的问题陈述没有说数据需要到位。 OP跟进了他们的解决方案,这恰好是一个put SQL,这并不意味着每个解决方案都需要限制在那个。

据我所知,问题在于数据库中有多个相似但不一定相同的记录,这些记录具有成本和声誉影响,因此您需要对这些记录进行重复数据删除。

我会编写一个批处理作业来搜索可能的重复项(这可能会像你想的那样复杂或简单),然后关闭它找到的两个记录是dupes并创建一个新记录。

要启用它,您需要四个新列:

  • 状态,可以是“打开”,“合并”,“拆分”
  • RelatedId,它将保存记录与谁合并的值
  • ChainId,新记录ID
  • DateStatusChanged,显而易见

打开将是默认状态 合并将合并记录(有效关闭和替换) 如果合并被颠倒,拆分

因此,作为示例,请浏览所有记录,例如,具有相同的名称。将它们成对合并。因此,如果你有三个Chris Bakers,记录1,2和3,合并1和2来制作记录4然后3和4来制作记录5.你的表最终会像:

ID  NAME        STATUS  RELATEDID  CHAINID DATESTATUSCHANGED [other rows omitted]
 1  Chris Baker MERGED          2        4       27-AUG-2012
 2  Chris Baker MERGED          1        4       27-AUG-2012
 3  Chris Baker MERGED          4        5       28-AUG-2012
 4  Chris Baker MERGED          3        5       28-AUG-2012
 5  Chris Baker   OPEN

通过这种方式,您可以完整记录您的数据发生了什么,可以通过取消合并来反转任何更改,如果例如联系人1和2不相同则反转合并3和4,反转合并1 2,你最终会得到这个:

ID  NAME        STATUS  RELATEDID  CHAINID DATESTATUSCHANGED
 1  Chris Baker  SPLIT          2        4       29-AUG-2012
 2  Chris Baker  SPLIT          1        4       29-AUG-2012
 3  Chris Baker  SPLIT          4        5       29-AUG-2012
 4  Chris Baker CLOSED          3        5       29-AUG-2012
 5  Chris Baker CLOSED                           29-AUG-2012

然后您可以手动合并,因为您可能不希望您的作业自动重新合并拆分记录。

答案 13 :(得分:-1)

是否有充分的理由不将重复的帐户合并到一个帐户中?

从评论中看,信息似乎主要用于联系信息,因此合并应该相对轻松,风险低。合并用户后,他们将不再出现在您的重复报告中。此外,您的用户表实际上会缩小,这有助于提高性能。

答案 14 :(得分:-1)

将数据类型位is_not_duplicate添加到您的表中,并在设置is_not_duplicate数据值后使用以下查询:

SELECT  GROUP_CONCAT(id) AS "ids",
        CONCAT(UPPER(first_name), UPPER(last_name)) AS "name"
FROM users 
GROUP BY name 
HAVING COUNT(*) > SUM(CAST(is_not_duplicate AS INT))

以上查询将总重复行与总有效重复行进行比较。

答案 15 :(得分:-1)

为什么不在这种情况下将电子邮件列设置为唯一标识符,并且在您清理一次记录后,您不允许从那里开始重复?