从SQL中几乎相似的记录中删除重复项

时间:2015-02-16 11:10:59

标签: sql sql-server

我有一个包含三列的表:名称,地址,城市。这张表大约有一百万条记录。名称和地址字段可能有重复项。

重复名称的一个例子是:

XYZ foundation Coorporation
XYZ foundation Corp
XYZ foundation Co-orporation

或另一个例子

XYZ Center
XYZ Ctr

地址重复的一个例子是

60909 East 34TH STREET BAY #1
60909 East 34TH ST. BAY #1
60909 East 34TH ST. BAY 1

如您所见,名称和地址字段是重复的,但仅限于人眼,因为我们理解缩写和简短形式。如何在SQL Server中将其构建为select语句?如果不是SQL Server,是否有其他方法可以扫描并删除此类重复项?

2 个答案:

答案 0 :(得分:3)

我使用的方法更适合姓氏,但我也将它用于公司名称。很可能它不适用于地址。

第1阶段

在存储"规范化"的表中添加一列。公司名。在我的情况下,我编写了一个通过触发器填充列的函数。该函数有一组规则,如下所示:

  • 在前面添加一个空格,在后面添加一个
  • 替换单个字符符号〜!@#$%^& *()= _ + [] {} |;':",。<>?空间(除了/ - 之外)
  • 用空格替换多字符标记:T / A C / - P / L
  • 替换单个字符符号 - /用空格
  • 用空格替换多字符代币:PTY PTE INC INCORPORATED LTD LIMITED CO COMPANY DR AND THE' TRADING AS' ' TRADE AS' '操作AS'
  • 以CORP取代公司
  • 修剪所有前导和尾随空格
  • 用单个空格替换多个连续的空格
  • 注意:处理多字符代币时,用空格包围它们

我查看了我的数据并制定了这些规则。根据你的情况调整它们。

第2阶段

我使用所谓的Jaro-Winkler指标来计算两个规范化公司名称之间的距离。我实现了在CLR中计算此指标的函数。

就我而言,我的目标是在系统中添加新条目时检查重复项。用户输入公司名称,程序对其进行标准化并计算给定名称与所有现有名称之间的Jaro-Winkler距离。距离越近1,匹配越近。用户看到按相关性排序的现有记录,可以决定他刚刚输入的公司名称是否已存在于数据库中,或者他仍然想要创建一个新名称。

还有其他指标尝试执行fuzzy search,例如Levenshtein distance。最有可能的是,您必须对名称和地址使用不同的指标,因为错误的类型对他们来说是显着不同的。

SQL Server具有内置函数来进行模糊搜索,但我没有使用它们,我不确定它们是在标准版本中还是仅在企业中可用,例如CONTAINSTABLE

  

返回这些列的零行,一行或多行的表   包含精确或模糊(不太精确)匹配单个单词和   短语,一定距离内的单词的接近程度   另一个或加权匹配。

注意

当我调查这个主题时,我得出的结论是,所有这些指标(Jaro-Winkler,Levenstein等)都会寻找简单的错误类型,例如遗漏/额外的字母或交换的两个字母。在我和你的情况下,这种方法原样会表现不佳,因为你首先会有一个收缩字典,然后可以有简单的错误类型。这就是我最终分两个阶段进行的原因 - 规范化,然后应用模糊搜索指标。

为了制作我上面提到的规则列表,我制作了一个字典,列出了我的数据中出现的所有单词。基本上,取每个Name并按空格将其拆分为多行。然后通过找到令牌进行分组并计算它们出现的次数。手动查看令牌列表。从列表中删除稀有令牌时,此列表不应太长。希望常见的词汇和收缩很容易被发现。我会想象Corporation和" Corp"会出现很多次,而不是实际的公司名称XYZ。那些奇怪的错误类似于" Coorporation"应该稍后通过模糊指标来选择。

以类似的方式为地址创建单独的字典,您会看到StreetSt.多次出现。对于地址你可以"作弊"从一些城市地图的索引(街道/街道,道路/ rd,高速公路/高速公路,格罗夫/ gv等)获取常用单词列表

这是我对Jaro-Winkler指标的实现:

using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;

public partial class UserDefinedFunctions
{
    /*
    The Winkler modification will not be applied unless the percent match
    was at or above the WeightThreshold percent without the modification.
    Winkler's paper used a default value of 0.7
    */
    private static readonly double m_dWeightThreshold = 0.7;

    /*
    Size of the prefix to be concidered by the Winkler modification.
    Winkler's paper used a default value of 4
    */
    private static readonly int m_iNumChars = 4;

    [Microsoft.SqlServer.Server.SqlFunction(DataAccess = DataAccessKind.None, SystemDataAccess = SystemDataAccessKind.None, IsDeterministic = true, IsPrecise = true)]
    public static SqlDouble StringSimilarityJaroWinkler(SqlString string1, SqlString string2)
    {
        if (string1.IsNull || string2.IsNull)
        {
            return 0.0;
        }

        return GetStringSimilarityJaroWinkler(string1.Value, string2.Value);
    }

    private static double GetStringSimilarityJaroWinkler(string string1, string string2)
    {
        int iLen1 = string1.Length;
        int iLen2 = string2.Length;
        if (iLen1 == 0)
        {
            return iLen2 == 0 ? 1.0 : 0.0;
        }

        int iSearchRange = Math.Max(0, Math.Max(iLen1, iLen2) / 2 - 1);

        bool[] Matched1 = new bool[iLen1];
        for (int i = 0; i < Matched1.Length; ++i)
        {
            Matched1[i] = false;
        }
        bool[] Matched2 = new bool[iLen2];
        for (int i = 0; i < Matched2.Length; ++i)
        {
            Matched2[i] = false;
        }

        int iNumCommon = 0;
        for (int i = 0; i < iLen1; ++i)
        {
            int iStart = Math.Max(0, i - iSearchRange);
            int iEnd = Math.Min(i + iSearchRange + 1, iLen2);
            for (int j = iStart; j < iEnd; ++j)
            {
                if (Matched2[j]) continue;
                if (string1[i] != string2[j]) continue;

                Matched1[i] = true;
                Matched2[j] = true;
                ++iNumCommon;
                break;
            }
        }
        if (iNumCommon == 0) return 0.0;

        int iNumHalfTransposed = 0;
        int k = 0;
        for (int i = 0; i < iLen1; ++i)
        {
            if (!Matched1[i]) continue;
            while (!Matched2[k])
            {
                ++k;
            }
            if (string1[i] != string2[k])
            {
                ++iNumHalfTransposed;
            }
            ++k;
            // even though length of Matched1 and Matched2 can be different,
            // number of elements with true flag is the same in both arrays
            // so, k will never go outside the array boundary
        }
        int iNumTransposed = iNumHalfTransposed / 2;

        double dWeight =
            (
                (double)iNumCommon / (double)iLen1 +
                (double)iNumCommon / (double)iLen2 +
                (double)(iNumCommon - iNumTransposed) / (double)iNumCommon
            ) / 3.0;

        if (dWeight > m_dWeightThreshold)
        {
            int iComparisonLength = Math.Min(m_iNumChars, Math.Min(iLen1, iLen2));
            int iCommonChars = 0;
            while (iCommonChars < iComparisonLength && string1[iCommonChars] == string2[iCommonChars])
            {
                ++iCommonChars;
            }
            dWeight = dWeight + 0.1 * iCommonChars * (1.0 - dWeight);
        }
        return dWeight;
    }

};

答案 1 :(得分:0)

您可以寻找更加个性化的解决方案,例如DIFFERENCE功能。 (见:DIFFERENCE function, SQL Server

Name和City是否可能在逻辑上相似但又不同?

由于此处有很多变化空间,只有您可以访问真实数据,因此只有您可以检查哪些有效,以及您基本上有哪些例外。

但希望这会让你开始。

-- Creating the test set
DECLARE @TESTTABLE TABLE (Name VARCHAR(256), City VARCHAR(256), Address VARCHAR(256))

INSERT INTO @TESTTABLE VALUES ('Billy bob' ,'New York' ,'Baker street 125')
INSERT INTO @TESTTABLE VALUES ('Billy bob' ,'New York' ,'Baker street 120')
INSERT INTO @TESTTABLE VALUES ('Billy bob' ,'New York' ,'Baker st 125')
INSERT INTO @TESTTABLE VALUES ('Billy bob' ,'New York' ,'Mallroad 1')
INSERT INTO @TESTTABLE VALUES ('James Dean' ,'Washington DC' ,'Primadonna road 15 c 100')
INSERT INTO @TESTTABLE VALUES ('James Dean' ,'Washington DC' ,'Primadonna r 15')
INSERT INTO @TESTTABLE VALUES ('Got Nuttin' ,'Philly' ,'Mystreet 1500') -- Doesn't show, since no real duplicates

然后,在测试数据之后,实际查询。

-- The query
;WITH CTE AS 
    (SELECT DISTINCT SRC.RN, T1.*, DIFFERENCE(T1.Address, T2.Address) DIFF_FACTOR
    FROM @TESTTABLE T1
    JOIN @TESTTABLE T2 ON T1.Name = T1.Name AND T2.City = T1.City AND T1.Address <> T2.Address
    JOIN (SELECT DENSE_RANK() OVER (ORDER BY Name, City) RN, Name, City FROM @TESTTABLE T3 GROUP BY Name, City HAVING COUNT(*) > 1) SRC
        ON SRC.City = T1.City AND SRC.Name = T1.Name)
SELECT DISTINCT RN, Name, City, COUNT(DISTINCT C.Address) Address_CT
    , STUFF((SELECT ','+B.Address
    FROM CTE B
    WHERE B.RN = C.RN AND B.DIFF_FACTOR = C.DIFF_FACTOR
    ORDER BY B.Address ASC
    FOR XML PATH('')),1,1,'') AllAdresses
    , DIFF_FACTOR
FROM CTE C
WHERE DIFF_FACTOR > 1 -- Comment this row to see that 'Mallroad 1' was considered to be too different from the rest, so this filter prevents us from considering that in the result set
GROUP BY RN, Name, City, DIFF_FACTOR
ORDER BY RN ASC, DIFF_FACTOR DESC

这可能不是最有效或最有效的方法,但这是一个开始并展示可以做的事情的好地方。如果Name和City也有可能与人眼有所不同,那么您可以修改查询以匹配任何两个相同的列值,比较第三个。但是在你有一个识别列的情况下自动进行比较变得非常困难,而且其他两个在不同程度上可能彼此不同。

我怀疑你需要先做几个查询才能找出最大的混乱,并最终手动找到最后最难以回避的“重复”,一次一些。