基于动态列查找匹配记录

11

我有一份宠物列表:
Pets

我需要从主人表中找到每只宠物的正确主人

Owners

为了正确地将每只宠物与其主人匹配,我需要使用一个特殊的匹配表,它看起来像这样:

Matching

因此,对于PetID = 2的宠物,我需要找到一个符合三个字段的匹配的主人:

    Pet.Zip = Owner.Zip 
    and Pet.OwnerName = Owner.Name 
    and Pet.Document = Owner.Document
在我们的例子中,它将像这样工作:
 select top 1 OwnerID from owners
         where Zip = 23456 
         and Name = 'Alex' 
         and Document = 'a.csv'
如果未找到OwnerID,则需要根据两个字段进行匹配(不使用优先级最高的字段)。
在我们的例子中:
 select top 1 OwnerID from owners where
             Name = 'Alex' 
             and Document = 'a.csv'

由于找不到记录,我们需要在较少的字段上进行匹配。以我们的示例为例:

select top 1 OwnerID from owners where Document = 'a.csv'

现在,我们找到了OwnerID = 6的所有者。

现在,我们需要更新OwnerID = 6的宠物,然后才能处理下一个宠物。

目前,我能想到的唯一方法涉及到使用循环或游标+动态SQL。

是否有可能在不使用循环+动态SQL的情况下实现这一点?也许可以用STUFF + Pivot来解决?

sql fiddle: http://sqlfiddle.com/#!18/10982/1/0

样例数据:

create table  temp_builder
(
    PetID int not null,
    Field varchar(30) not null,
    MatchTo varchar(30) not null,
    Priority int not null
)

insert into temp_builder values
(1,'Address', 'Addr',4),
(1,'Zip', 'Zip', 3),
(1,'Country', 'Country', 2),
(1,'OwnerName', 'Name',1),
(2,'Zip', 'Zip',3),
(2,'OwnerName','Name', 2),
(2,'Document', 'Document', 1),
(3,'Country', 'Country', 1)


create table temp_pets
(
    PetID int null,
    Address varchar(100) null,
    Zip int null,
    Country varchar(100) null,
    Document varchar(100) null,
    OwnerName varchar(100) null,
    OwnerID int null,
    Field1 bit null,
    Field2 bit null
)

insert into temp_pets values
(1, '123 5th st', 12345, 'US', 'test.csv', 'John', NULL, NULL, NULL),
(2, '234 6th st', 23456, 'US', 'a.csv', 'Alex', NULL, NULL, NULL),
(3, '345 7th st', 34567, 'US', 'b.csv', 'Mike', NULL, NULL, NULL)

create table temp_owners
(
    OwnerID int null,
    Addr varchar(100) null,
    Zip int null,
    Country varchar(100) null,
    Document varchar(100) null,
    Name varchar(100) null,
    OtherField bit null,
    OtherField2 bit null,
)

insert into temp_owners values
(1, '456 8th st',  45678, 'US', 'c.csv', 'Mike',  NULL, NULL),
(2, '678 9th st',  45678, 'US', 'b.csv', 'John',  NULL, NULL),
(3, '890 10th st', 45678, 'US', 'b.csv', 'Alex',  NULL, NULL),
(4, '901 11th st', 23456, 'US', 'b.csv', 'Alex',  NULL, NULL),
(5, '234 5th st',  12345, 'US', 'b.csv', 'John',  NULL, NULL),
(6, '123 5th st',  45678, 'US', 'a.csv', 'John',  NULL, NULL)

编辑:我收到了很多好的建议和回复,非常感谢。我已经测试了许多解决方案,它们中的许多都对我很有效。不幸的是,我只能奖励一个最佳解决方案。


2
我不理解你的优先级规则。为什么国家比邮政编码具有更高的优先级? - Tim Biegeleisen
@TimBiegeleisen,对于宠物#1,我需要尝试通过地址、邮编、国家和所有者姓名进行匹配。如果没有匹配,则按照邮编、国家、所有者姓名进行匹配;如果还是没有匹配,则按照国家、所有者姓名进行匹配;如果仍然没有匹配,则按照所有者姓名进行匹配。因此,我们从更具体的条件到不那么具体的条件进行匹配。我为这个例子想出了列名,只是为了简化。 - user194076
@MichałTurczyn 我可以使用动态SQL,但我想尝试提高效率(我的表中有超过100万条记录。如果可能的话,我想避免循环)。 - user194076
你可以用一个视图或包含查询列的表格来替换匹配表,这样每个宠物都可以执行查询。这样做会更简单易懂。 - Steve Ford
对于在Z,N,D的“Zip,Name,Document”上与可能性Z,-,D-,-,D进行匹配; 应该返回两行吗?(三列没有匹配项,最后两列没有匹配项,最后一列有两个匹配项。)还是应该只返回Z,-,D,因为它比-,-,D更匹配? - MatBailie
显示剩余6条评论
10个回答

4
可以通过将用于比较的字段视为每个宠物的位集合条目来避免使用游标、循环和动态 SQL。针对每个优先级,基于一种位条目(FieldRank 排名列)计算出一个位集合(FieldSetRank 列)。必须取消枢轴化 Pets 和 Owner 表,以便可以比较字段及其关联值。将已匹配的每个字段和值分配给相应的 FieldRank。然后,根据匹配的值(MatchSetRank),计算出新的位集合。仅返回匹配集合(MatchSetRank)等于所需集合(FieldSetRank)的记录。查询执行最终排名,以返回具有最高 MatchSetRank 的记录(在维护优先级条件的情况下,匹配了最多列的记录)。以下 T-SQL 演示了该概念。
;WITH CTE_Builder
 AS
 (
     SELECT  [PetID]
            ,[Field]
            ,[Priority]
            ,[MatchTo]
            ,POWER(2, [Priority] - 1) AS [FieldRank] -- Define the field ranking as bit set numbered item.
            ,SUM(POWER(2, [Priority] - 1)) OVER (PARTITION BY [PetID] ORDER BY [Priority] ROWS UNBOUNDED PRECEDING) FieldSetRank -- Sum all the bit set IDs to define what constitutes a completed field set ordered by priority.
     FROM   temp_builder
 ),
CTE_PetsUnpivoted
AS
(   -- Unpivot pets table and assign Field Rank and Field Set Rank.
    SELECT   [PetsUnPivot].[PetID]
            ,[PetsUnPivot].[Field]
            ,[Builder].[MatchTo]
            ,[PetsUnPivot].[FieldValue]
            ,[Builder].[Priority]
            ,[Builder].[FieldRank]
            ,[Builder].[FieldSetRank]

    FROM 
       (
            SELECT [PetID], [Address], CAST([Zip] AS VARCHAR(100)) AS [Zip], [Country], [Document], [OwnerName]
            FROM temp_pets
        ) [Pets]
    UNPIVOT
       (FieldValue FOR Field IN 
          ([Address], [Zip], [Country], [Document], [OwnerName])
    ) AS [PetsUnPivot]
    INNER JOIN [CTE_Builder] [Builder] ON [PetsUnPivot].PetID = [Builder].PetID AND [PetsUnPivot].Field = [Builder].Field
),
CTE_Owners
AS
(
    -- Unpivot Owners table and join with unpivoted Pets table on field name and field value.  
    -- Next assign Pets field rank then calculated the field set rank (MatchSetRank) based on actual matches made.
    SELECT   [OwnersUnPivot].[OwnerID]
            ,[Pets].[PetID]
            ,[OwnersUnPivot].[Field]
            ,[Pets].Field AS [PetField]
            ,[Pets].FieldValue as PetFieldValue
            ,[OwnersUnPivot].[FieldValue]
            ,[Pets].[Priority]
            ,[Pets].[FieldRank]
            ,[Pets].[FieldSetRank]
            ,SUM([FieldRank]) OVER (PARTITION BY [Pets].[PetID], [OwnersUnPivot].[OwnerID] ORDER BY [Pets].[Priority] ROWS UNBOUNDED PRECEDING) MatchSetRank
    FROM 
       (
            SELECT [OwnerID], [Addr], CAST([Zip] AS VARCHAR(100)) AS [Zip], [Country], [Document], [Name]
            FROM temp_owners
        ) [Owners]
    UNPIVOT
       (FieldValue FOR Field IN 
          ([Addr], [Zip], [Country], [Document], [Name])
    ) AS [OwnersUnPivot]
    INNER JOIN [CTE_PetsUnpivoted] [Pets] ON [OwnersUnPivot].[Field] = [Pets].[MatchTo] AND [OwnersUnPivot].[FieldValue] = [Pets].[FieldValue]
),
CTE_FinalRanking
AS
(
    SELECT   [PetID]
            ,[OwnerID]
            -- -- Calculate final rank, if multiple matches have the same rank then multiple rows will be returned per pet. 
            -- Change the “RANK()” function to "ROW_NUMBER()" to only return on result per pet.
            ,RANK() OVER (PARTITION BY [PetID] ORDER BY [MatchSetRank] DESC) AS [FinalRank] 
    FROM    CTE_Owners
    WHERE   [FieldSetRank] = [MatchSetRank] -- Only return records where the field sets calculated based on 
                                            -- actual matches is equal to desired field set ranks. This will 
                                            -- eliminate matches where the number of fields that meets the 
                                            -- criteria is the same but does not meet priority requirements. 
)
SELECT   [PetID]
        ,[OwnerID]
FROM    CTE_FinalRanking
WHERE   [FinalRank] = 1

我喜欢这种方法;我想到了一个类似的方法。为了提高速度,您可以将未旋转的所有者字段数据存储在临时表中,并在字段名称/字段值上进行索引。 - Nik Shenoy
类似于我下面发布的方法。关键点在于匹配列实际上是静态的,只有优先级是动态的。 - dybzon
谢谢你的回答!看起来很好,但是对我来说静态列不适用,因为它们可能会改变。 - user194076

2

这是一个相当大的任务...以下是我的做法:

首先,您需要添加一个表格,其中包含半where子句,即基于temp_builder表准备好使用的条件。另外,由于您有5列,我假设最多可能有5个条件。以下是创建表格的方法:

CREATE TABLE [dbo].[temp_builder_with_where](
    [petid] [int] NULL,
    [priority1] [bit] NULL,
    [priority2] [bit] NULL,
    [priority3] [bit] NULL,
    [priority4] [bit] NULL,
    [priority5] [bit] NULL,
    [whereClause] [varchar](200) NULL
) 
--it's good to create index, for better performance
create clustered index idx on [temp_builder_with_where]([petid])

insert into temp_builder_with_where
select petid,[priority1],[priority2],[priority3],[priority4],[priority5],
         '[pets].' + CAST(field as varchar(100)) + ' = [owners].' + CAST(matchto as varchar(100)) [whereClause]
from (
select petid, field, matchto, [priority],
        1 Priority1,
        case when [priority] > 1 then 1 else 0 end Priority2,
        case when [priority] > 2 then 1 else 0 end Priority3,
        case when [priority] > 3 then 1 else 0 end Priority4,
        case when [priority] > 4 then 1 else 0 end Priority5       
from temp_builder) [builder]

现在我们将循环遍历该表。您说这个表包含8000行,因此我选择了另一种方式:动态查询现在一次只插入一个petid的结果。
为了做到这一点,我们需要一个存储结果的表:
CREATE TABLE [dbo].[TableWithNewId](
    [petid] [int] NULL,
    [ownerid] [int] NULL,
    [priority] [int] NULL
)

现在动态SQL用于insert语句:

declare @query varchar(1000) = ''
declare @i int, @max int
set @i = 1
select @max = MAX(petid) from temp_builder_with_where

while @i <= @max
begin

    set @query = ''

    select @query = @query + whereClause1 + whereClause2 + whereClause3 + whereClause4 + whereClause5 + ' union all ' from (
    select 'insert into [MY_DATABASE].dbo.TableWithNewId  select ' + CAST(petid as varchar(3)) + ' [petid], [owners].ownerid, 1 [priority] from temp_pets [pets], temp_owners [owners] where (' + [where_petid] + [where1] + ')' [whereClause1],
           case when [where2] is null then '' else ' union all select ' + CAST(petid as varchar(3)) + ' [petid], [owners].ownerid, 2 [priority] from temp_pets [pets], temp_owners [owners] where (' + [where_petid] + [where2] + ')' end [whereClause2], 
           case when [where3] is null then '' else ' union all select ' + CAST(petid as varchar(3)) + ' [petid], [owners].ownerid, 3 [priority] from temp_pets [pets], temp_owners [owners] where (' + [where_petid] + [where3] + ')' end [whereClause3], 
           case when [where4] is null then '' else ' union all select ' + CAST(petid as varchar(3)) + ' [petid], [owners].ownerid, 4 [priority] from temp_pets [pets], temp_owners [owners] where (' + [where_petid] + [where4] + ')' end [whereClause4], 
           case when [where5] is null then '' else ' union all select ' + CAST(petid as varchar(3)) + ' [petid], [owners].ownerid, 5 [priority] from temp_pets [pets], temp_owners [owners] where (' + [where_petid] + [where5] + ')' end [whereClause5]
    from (
            select petid, 'petid = ' + CAST(petid as nvarchar(3)) [where_petid],
               (select ' and ' + whereClause from temp_builder_with_where where petid = t.petid and priority1 = 1 for xml path(''),type).value('(.)[1]', 'varchar(500)') [where1],
               (select ' and ' + whereClause from temp_builder_with_where where petid = t.petid and priority2 = 1 for xml path(''),type).value('(.)[1]', 'varchar(500)') [where2],
               (select ' and ' + whereClause from temp_builder_with_where where petid = t.petid and priority3 = 1 for xml path(''),type).value('(.)[1]', 'varchar(500)') [where3],
               (select ' and ' + whereClause from temp_builder_with_where where petid = t.petid and priority4 = 1 for xml path(''),type).value('(.)[1]', 'varchar(500)') [where4],
               (select ' and ' + whereClause from temp_builder_with_where where petid = t.petid and priority5 = 1 for xml path(''),type).value('(.)[1]', 'varchar(500)') [where5]
       from temp_builder_with_where [t]
       where petid = @i
        group by petid
    ) a
    ) a
    --remove last union all
    set @query = left(@query, len(@query) - 10)
    exec (@query)

    set @i = @i + 1

end

请记住,在上面的代码中,您必须使用您的数据库名称替换[MY_DATABASE]
从您的示例数据中,此查询select * from TableWithNewId的结果如下:
PetId|OwnerId|Priority
1    |6      |4
2    |4      |2
2    |4      |3
3    |1      |1
3    |2      |1
3    |3      |1
3    |4      |1
3    |5      |1
3    |6      |1

根据这个结果,现在你可以根据最低优先级将OwnerId分配给PetId(当然,如果有多个相同优先级的OwnerId时,你没有说明如何处理这种情况)。


这看起来很棒!我刚测试了一下,似乎运行良好。不幸的是,对于我的第一个查询返回了8000个匹配项,而当我执行第二部分查询(生成@query)时它需要很长时间。我等了5分钟,但它从未完成。但是对于小数据集,它可以工作。现在我需要找出如何优化第二部分。如果你有任何想法,请告诉我 :) - user194076
@user194076,我更新了我的答案,你可以试一试。 - Michał Turczyn

2

我想在一开始就告诉您,为了节省时间:

  • 我的解决方案使用动态SQL。Michał Turczyn正确地指出,当比较的列名存储在数据库中时,您无法避免它。
  • 我的解决方案使用循环。我强烈认为,您将无法使用纯SQL查询解决此问题,并且该查询在您声明的数据大小(表具有> 1M记录)上快速运行。您描述的逻辑本质上意味着进行迭代-从匹配字段的更大集合到较小集合。 SQL作为查询语言并不设计来涵盖这些棘手的情况。您可以尝试使用纯SQL查询解决问题,但即使您成功构建了这样的查询,它也将非常棘手,复杂和不清晰。我不喜欢这种解决方案。这就是为什么我甚至没有朝这个方向探索的原因。
  • 另一方面,我的解决方案不需要创建临时表,这是一个优点。

鉴于此,我的方法相当简单:

  1. 外部循环从最大的匹配集合(所有匹配字段)迭代到最小的匹配集合(一个字段)。在第一次迭代时,当我们还不知道数据库中存储了多少个匹配器时,我们读取并使用它们全部。在接下来的迭代中,我们减少所使用的匹配器数量1(删除优先级最高的那个)。

  2. 内部循环遍历当前匹配器集合,并构建比较PetsOwners表之间字段的WHERE子句。

  3. 执行当前查询,如果有某个所有者符合给定的条件,则从外部循环中断。

这是实现此逻辑的代码:

DECLARE @PetId INT = 2;

DECLARE @MatchersLimit INT;
DECLARE @OwnerID INT;

WHILE (@MatchersLimit IS NULL OR @MatchersLimit > 0) AND @OwnerID IS NULL
BEGIN

    DECLARE @CurrMatchFilter VARCHAR(max) = ''
    DECLARE @Field VARCHAR(30)
    DECLARE @MatchTo VARCHAR(30)
    DECLARE @CurrMatchersNumber INT = 0;

    DECLARE @GetMatchers CURSOR;
    IF @MatchersLimit IS NULL
        SET @GetMatchers = CURSOR FOR SELECT Field, MatchTo FROM temp_builder WHERE PetID = @PetId ORDER BY Priority ASC;
    ELSE
        SET @GetMatchers = CURSOR FOR SELECT TOP (@MatchersLimit) Field, MatchTo FROM temp_builder WHERE PetID = @PetId ORDER BY Priority ASC;

    OPEN @GetMatchers;
    FETCH NEXT FROM @GetMatchers INTO @Field, @MatchTo;
    WHILE @@FETCH_STATUS = 0
    BEGIN
        IF @CurrMatchFilter <> '' SET @CurrMatchFilter = @CurrMatchFilter + ' AND ';
        SET @CurrMatchFilter = @CurrMatchFilter + ('temp_pets.' + @Field + ' = ' + 'temp_owners.' + @MatchTo);
        FETCH NEXT FROM @GetMatchers INTO @field, @matchTo;
        SET @CurrMatchersNumber = @CurrMatchersNumber + 1;
    END
    CLOSE @GetMatchers;
    DEALLOCATE @GetMatchers;

    IF @CurrMatchersNumber = 0 BREAK;

    DECLARE @CurrQuery nvarchar(max) = N'SELECT @id = temp_owners.OwnerID FROM temp_owners INNER JOIN temp_pets ON (' + CAST(@CurrMatchFilter AS NVARCHAR(MAX)) + N') WHERE temp_pets.PetID = ' + CAST(@PetId AS NVARCHAR(MAX));
    EXECUTE sp_executesql @CurrQuery, N'@id int OUTPUT', @id=@OwnerID OUTPUT;

    IF @MatchersLimit IS NULL
        SET @MatchersLimit = @CurrMatchersNumber - 1;
    ELSE
        SET @MatchersLimit = @MatchersLimit - 1;

END

SELECT @OwnerID AS OwnerID, @MatchersLimit + 1 AS Matched;

性能考虑

这种方法基本上执行了2个查询:

  1. SELECT Field, MatchTo FROM temp_builder WHERE PetID = @PetId;

    你应该在temp_builder表的PetID字段上添加索引,这样查询将非常快。

  2. SELECT @id = temp_owners.OwnerID FROM temp_owners INNER JOIN temp_pets ON (temp_pets.Document = temp_owners.Document AND temp_pets.OwnerName = temp_owners.Name AND temp_pets.Zip = temp_owners.Zip AND ...) WHERE temp_pets.PetID = @PetId;

    这个查询看起来很吓人,因为它连接了两个大表 - temp_ownerstemp_pets。然而,temp_pets表由PetID列过滤,应该只有一条记录。因此,如果您在temp_pets.PetID列上有一个索引(并且您应该有,因为这个列似乎是一个主键),查询将导致对temp_owners表进行扫描。即使对于具有100万行以上的表,这样的扫描也不会花费太长时间。如果查询仍然太慢,您可以考虑为用于匹配器的temp_owners表列添加索引(例如AddrZip等)。添加索引有缺点,比如更大的数据库和更慢的插入/更新操作。因此,在为temp_owners列添加索引之前,请在没有索引的表上检查查询速度。


2

我不确定我的最终结果是否正确,但我建议使用一些常见的表达式来生成一批更新语句,使用动态SQL(恐怕没有动态SQL就无法完成),然后使用Exec(sql)执行它们。

这种方法的好处是它不涉及循环或游标。

我生成的每个更新语句都使用宠物和所有者表之间的inner join,使用生成器表中的映射作为on子句的基础,将宠物表的所有者ID更新为所有者表的所有者ID。
第一个cte负责从构建器表生成on子句,第二个cte负责生成更新语句。
最后,我将第二个CTE中的所有SQL语句选择到单个nvarchar(max)变量中并执行它。

我解决优先级问题的方式是为每组优先级生成单个更新语句,从包括所有优先级开始,从下一个SQL语句中排除值,最高优先级首先被排除,直到只剩下一个映射到单个列集的on子句。

所以,第一件事是声明一个变量来保存生成的更新语句:

DECLARE @Sql nvarchar(max) = ''

现在,第一个CTE使用CROSS APPLYSTUFFFOR XML来为每对petIdPriority生成on子句:

;WITH OnClauseCTE AS
(
SELECT DISTINCT PetId, Priority, OnClause
FROM temp_builder t0
CROSS APPLY
(
    SELECT STUFF (
    (  
        SELECT ' AND p.'+ Field +' = o.'+ MatchTo
        FROM temp_builder t1
        WHERE PetID = t0.PetId
        AND Priority <= t0.Priority
        FOR XML PATH('')  
    )
    , 1, 5, '') As OnClause
) onClauseGenerator
)

第二个CTE生成每个petIdPriority组合的单个UPDATE语句:
, UpdateStatementCTE AS
(
    SELECT  PetId,
            Priority,
            'UPDATE p 
            SET OwnerID = o.OwnerID 
            FROM temp_pets p 
            INNER JOIN temp_owners o ON ' + OnClause + ' 
            WHERE p.PetId = '+ CAST(PetId as varchar(10)) +'
            AND p.OwnerID IS NULL; -- THIS IS CRITICAL!
            ' AS SQL
    FROM OnClauseCTE
)

最后,从UpdateStatementCTE生成单个批次的更新语句:

SELECT @Sql = @Sql + SQL
FROM UpdateStatementCTE    
ORDER BY PetId, Priority DESC -- ORDER BY Priority is CRITICAL!
order by PetId只是为了方便阅读,当你打印出@Sql的内容时。然而,order by子句中的Priority DESC部分非常重要,因为我们希望先执行最高优先级,最后执行最低优先级。现在,@Sql包含以下内容(缩短版):
UPDATE p 
SET OwnerID = o.OwnerID 
FROM temp_pets p 
INNER JOIN temp_owners o ON p.Address = o.Addr AND p.Zip = o.Zip AND p.Country = o.Country AND p.OwnerName = o.Name 
WHERE p.PetId = 1
AND p.OwnerID IS NULL;

...

UPDATE p 
SET OwnerID = o.OwnerID 
FROM temp_pets p 
INNER JOIN temp_owners o ON p.OwnerName = o.Name 
WHERE p.PetId = 1
AND p.OwnerID IS NULL;

...

UPDATE p 
SET OwnerID = o.OwnerID 
FROM temp_pets p 
INNER JOIN temp_owners o ON p.OwnerName = o.Name AND p.Document = o.Document 
WHERE p.PetId = 2
AND p.OwnerID IS NULL;

...

UPDATE p 
SET OwnerID = o.OwnerID 
FROM temp_pets p 
INNER JOIN temp_owners o ON p.Country = o.Country 
WHERE p.PetId = 3
AND p.OwnerID IS NULL;

如您所见,每个更新语句都在生成器表中表示,并且只有在先前的更新语句没有更改所有者ID时才会更改所有者ID,因为AND p.OwnerID IS NULLwhere子句的一部分。

运行更新语句批处理后,temp_pets表如下所示:

PetID   Address         Zip     Country     Document    OwnerName   OwnerID     Field1  Field2
1       123 5th st      12345   US          test.csv    John        5           NULL    NULL
2       234 6th st      23456   US          a.csv       Alex        6           NULL    NULL
3       345 7th st      34567   US          b.csv       Mike        1           NULL    NUL

您可以在rextester上看到实时演示。

然而,请注意,您的条件越少,联接返回的记录就越多,从而使更新更容易出现不准确的情况。例如,对于PetId 3,我得到了OwnerId 1,因为我只需要匹配记录的Country列,这意味着在这个样本数据中实际上可能是每个OwnerId,因为每个人在Country列中都有相同的值US
根据以下规则,我无法做太多事情。


2

无需动态sql或循环即可完成此操作。关键点在于用于匹配宠物和主人的列是静态的。只有优先级是动态的。但是,性能在很大程度上取决于您的数据。您需要自己测试并考虑您认为最佳的方法。

以下解决方案基本上找到与任何给定宠物匹配的所有主人。然后过滤掉只包括与优先级1、1&2、1&2&3等相匹配的所有者。最后找到与匹配主人中的“最佳”匹配,并将该值更新到宠物表中。

我已经向查询添加了一些说明性注释,但如果有不清楚的地方,请随时问我。

-- We start off by converting the priority values into int values that are suitable to add up to a bit array
-- I'll save those in a #Temp table to cut that piece of logic out of the final query
IF EXISTS(SELECT 1 FROM #TempBuilder)
BEGIN
    DROP TABLE #TempBuilder
END
SELECT 
    PetID, Field, MatchTo, 
    CASE [Priority] 
    WHEN 1 THEN 16 -- Priority one goes on the 16-bit (10000)
    WHEN 2 THEN 8 -- Priority two goes on the 8-bit (01000)
    WHEN 3 THEN 4 -- Priority three goes on the 4-bit (00100)
    WHEN 4 THEN 2 -- Priority four goes on the 2-bit (00010)
    WHEN 5 THEN 1 END AS [Priority] -- Priority five goes on the 1-bit (00001)
INTO #TempBuilder
FROM dbo.temp_builder;

-- Then we pivot the match priorities to be able to join them on our pets
WITH PivotedMatchPriorities AS (
    SELECT
        PetId,
        [Address], [Zip], [Country], [OwnerName], [Document]
    FROM (SELECT PetId, Field, [Priority] FROM #TempBuilder) tb
        PIVOT 
        (
            SUM([Priority])
            FOR [Field] IN ([Address], [Zip], [Country], [OwnerName], [Document])
        )
        AS PivotedMatchPriorities
),
-- Next we get (for each pet) all owners with ANY matching value
-- We want to filter the matching owners to find these that match priorities 1 (priority sum 10000, i.e. 16), 
    --- or match priorities 1 & 2 (priority sum 11000, i.e. 24)
    --- or match priorities 1 & 2 & 3 (priority sum 11100, i.e. 28)
    --- etc.
MatchingOwners AS (
    SELECT o.*,
        p.PetID,
        pmp.[Address] AS AddressPrio,
        pmp.Country AS CountryPrio,
        pmp.Zip AS ZipPrio,
        pmp.OwnerName AS OwnerPrio,
        pmp.Document AS DocumentPrio,
        CASE WHEN o.Addr = p.[Address] THEN ISNULL(pmp.[Address],0) ELSE 0 END
        + CASE WHEN o.Zip = p.Zip THEN ISNULL(pmp.Zip,0) ELSE 0 END
        + CASE WHEN o.Country = p.Country THEN ISNULL(pmp.Country,0) ELSE 0 END
        + CASE WHEN o.Document = p.Document THEN ISNULL(pmp.[Document],0) ELSE 0 END
        + CASE WHEN o.[Name] = p.OwnerName THEN ISNULL(pmp.OwnerName,0) ELSE 0 END AS MatchValue -- Calculate a match value for each matching owner
    FROM dbo.temp_pets p
        INNER JOIN dbo.temp_owners o 
            ON p.[Address] = o.Addr
            OR p.Country = o.Country
            OR p.Document = o.Document
            OR p.OwnerName = o.[Name]
            OR p.Zip = o.Zip
        INNER JOIN PivotedMatchPriorities pmp ON pmp.PetId = p.PetId
),
-- Now we can get all owners that match the pet, along with a match value for each owner.
-- We want to rank the matching owners for each pet to allow selecting the best ranked owner
-- Note: In the demo data there are multiple owners that match petId 3 equally well. We'll pick a random one in such cases.
RankedValidMatches AS (
    SELECT 
        PetID,
        OwnerID,
        MatchValue,
        ROW_NUMBER() OVER (PARTITION BY PetID ORDER BY MatchValue DESC) AS OwnerRank
    FROM MatchingOwners
    WHERE MatchValue IN (16, 24, 28, 30, 31)
)
-- Finally we can get the best valid match per pet
--SELECT * FROM RankedValidMatches WHERE OwnerRank = 1
-- Or we can update our pet table to reflect our results
UPDATE dbo.temp_pets
SET OwnerID = rvm.OwnerID
FROM dbo.temp_pets tp
    INNER JOIN RankedValidMatches rvm ON rvm.PetID = tp.PetID AND rvm.OwnerRank = 1

2
以下方法基于这样一个事实:选择和排序要匹配的列的不同组合的数量是有限的,可能远少于记录的数量。对于5列,总的组合数为325,但由于不太可能使用每种可能的组合,实际数量可能会少于100。与记录的数量相比(OP提到了>1M),尝试将共享相同列组合的宠物进行组合是值得的。
下面是SQL脚本的特点:
  • 没有动态SQL。
  • 有循环,但没有游标;迭代次数有限,不会随着记录数量成比例增长。
  • 创建两个(索引)辅助表。(请随意将它们变为临时表或表变量。)这极大地加快了匹配过程(INNER JOIN),但会带来一些开销,因为需要填充表格。
  • 只使用简单直观的SQL语句结构(没有旋转、没有FOR XML、甚至没有CTE)。
  • 仅依赖于关键列(PetID、OwnerID)、优先级列和辅助表中的列上的索引。不需要地址、邮政编码、国家、文档、名称的索引。
乍一看,该查询似乎有些过度(在OP提供的少量样本数据上执行47个SQL语句),但对于更大的表格,优势应该很明显。最坏情况下的时间复杂度应该为O(nlogn),这比许多替代方案要好得多。
当然,它仍然需要在实践中证明自己;我还没有用大型数据集进行测试。
示例:http://sqlfiddle.com/#!18/53320/1
-- Adding indexes to OP's tables to optimize the queries that follow.
CREATE INDEX IX_PetID ON temp_builder (PetID)
CREATE INDEX IX_Priority ON temp_builder (Priority)
CREATE INDEX IX_PetID ON temp_pets (PetID)
CREATE INDEX IX_OwnerID ON temp_owners (OwnerID)

-- Helper table for pets. Each column has its own index.
CREATE TABLE PetKey (
    PetID int NOT NULL PRIMARY KEY CLUSTERED,
    KeyNames varchar(200) NOT NULL INDEX IX_KeyNames NONCLUSTERED,
    KeyValues varchar(900) NOT NULL INDEX IX_KeyValues NONCLUSTERED
)

-- Helper table for owners. Each column has its own index.
CREATE TABLE OwnerKey (
    OwnerID int NOT NULL PRIMARY KEY CLUSTERED,
    KeyValues varchar(900) NULL INDEX IX_KeyValues NONCLUSTERED
)

-- For every pet, create a record in table PetKey.
-- (Unless the pet already belongs to someone.)
INSERT INTO PetKey (PetID, KeyNames, KeyValues)
SELECT PetID, '', ''
FROM temp_pets
WHERE OwnerID IS NULL

-- For every owner, create a record in table OwnerKey.
INSERT INTO OwnerKey (OwnerID, KeyValues)
SELECT OwnerID, ''
FROM temp_owners

-- Populate columns KeyNames and KeyValues in table PetKey.
-- Lowest priority (i.e. highest number in column Priority) comes first.
-- We use CHAR(1) as a separator character; anything will do as long as it does not occur in any column values.
-- Example: when a pet has address as prio 1, zip as prio 2, then:
--    KeyNames = 'Zip' + CHAR(1) + 'Address' + CHAR(1)
--    KeyValues = '12345' + CHAR(1) + 'John' + CHAR(1)
-- NULL is replaced by CHAR(2); can be any value as long as it does not match any owner's value.
DECLARE @priority int = 1
WHILE EXISTS (SELECT * FROM temp_builder WHERE Priority = @priority)
BEGIN
    UPDATE pk
    SET KeyNames = b.Field + CHAR(1) + KeyNames,
        KeyValues = ISNULL(CASE b.Field
                               WHEN 'Address' THEN p.Address
                               WHEN 'Zip' THEN CAST(p.Zip AS varchar)
                               WHEN 'Country' THEN p.Country
                               WHEN 'Document' THEN p.Document
                               WHEN 'OwnerName' THEN p.OwnerName
                           END, CHAR(2)) +
                    CHAR(1) + KeyValues
    FROM PetKey pk
    INNER JOIN temp_pets p ON p.PetID = pk.PetID
    INNER JOIN temp_builder b ON b.PetID = pk.PetID
    WHERE b.Priority = @priority

    SET @priority = @priority + 1
END

-- Loop through all distinct key combinations.
DECLARE @maxKeyNames varchar(200), @namesToAdd varchar(200), @index int
SELECT @maxKeyNames = MAX(KeyNames) FROM PetKey
WHILE @maxKeyNames <> '' BEGIN
    -- Populate column KeyValues in table OwnerKey.
    -- The order of the values is determined by the column names listed in @maxKeyNames.
    UPDATE OwnerKey
    SET KeyValues = ''

    SET @namesToAdd = @maxKeyNames
    WHILE @namesToAdd <> '' BEGIN
        SET @index = CHARINDEX(CHAR(1), @namesToAdd)

        UPDATE ok
        SET KeyValues = KeyValues +
                        CASE LEFT(@namesToAdd, @index - 1)
                            WHEN 'Address' THEN o.Addr
                            WHEN 'Zip' THEN CAST(o.Zip AS varchar)
                            WHEN 'Country' THEN o.Country
                            WHEN 'Document' THEN o.Document
                            WHEN 'OwnerName' THEN o.Name
                        END +
                        CHAR(1)
        FROM OwnerKey ok
        INNER JOIN temp_owners o ON o.OwnerID = ok.OwnerID

        SET @namesToAdd = SUBSTRING(@namesToAdd, @index + 1, 200)
    END

    -- Match pets with owners, based on their KeyValues.
    UPDATE p
    SET OwnerID = (SELECT TOP 1 ok.OwnerID FROM OwnerKey ok WHERE ok.KeyValues = pk.KeyValues)
    FROM temp_pets p
    INNER JOIN PetKey pk ON pk.PetID = p.PetID
    WHERE pk.KeyNames = @maxKeyNames

    -- Pets that were successfully matched are removed from PetKey.
    DELETE FROM pk
    FROM PetKey pk
    INNER JOIN temp_pets p ON p.PetID = pk.PetID
    WHERE p.OwnerID IS NOT NULL

    -- For pets with no match, strip off the first (lowest priority) name and value.
    SET @namesToAdd = SUBSTRING(@maxKeyNames, CHARINDEX(CHAR(1), @maxKeyNames) + 1, 200)

    UPDATE pk
    SET KeyNames = @namesToAdd,
        KeyValues = SUBSTRING(KeyValues, CHARINDEX(CHAR(1), KeyValues) + 1, 900)
    FROM PetKey pk
    INNER JOIN temp_pets p ON p.PetID = pk.PetID
    WHERE pk.KeyNames = @maxKeyNames

    -- Next key combination.    
    SELECT @maxKeyNames = MAX(KeyNames) FROM PetKey
END

1

我已经用UNPIVOT写了另一版本,但是以一种更简单的方式对行进行排名和筛选。

;with
-- r: rules table
r as (select * from temp_builder),
-- o0: owners table with all fields unpivotable (varchar)
o0 as (SELECT [OwnerID], [Addr], CAST([Zip] AS VARCHAR(100)) AS [Zip], [Country], [Document], [Name] FROM temp_owners ),
-- o: owners table unpivoted
o as (
    SELECT * FROM o0 
    UNPIVOT (FieldValue FOR Field IN ([Addr], [Zip], [Country], [Document], [Name])) AS p
),
-- p0: pets table with all fields unpivotable (varchar)
p0 as (SELECT [PetID], [Address], CAST([Zip] AS VARCHAR(100)) AS [Zip], [Country], [Document], [OwnerName] FROM temp_pets),
-- p: petstable unpivoted
p as (
    SELECT * FROM p0
    UNPIVOT (FieldValue FOR Field IN ([Address], [Zip], [Country], [Document], [OwnerName])) AS p
),
-- t: join up all data and keep only matching priority
d as (
    select petid, ownerid, priority 
    from (
        select r.*, o.ownerid, ROW_NUMBER() over (partition by r.petid, o.ownerid order by r.petid, o.ownerid, priority) calc_priority
        from r
        join p on (r.field = p.field) and (p.petid = r.petid)
        join o on (r.matchto = o.field) and (p.fieldvalue=o.fieldvalue) 
    ) x
    where calc_priority=priority
),
-- g: group by the matching rows to know the best priority reached for each pet
g as (
    select petid, max(priority) max_priority
    from d
    group by petid
)
-- output only the rows with best priority
select d.*
from d
join g on d.petid = g.petid and d.priority = g.max_priority
order by petid, ownerid, priority

这个版本的性能并不比@EdmondQuinton的版本更好(我投了他的票),我的版本比他慢5%,但我认为对于非专业用户来说,更容易理解和维护。


谢谢!不幸的是,静态枢轴对我没有用处。字段可以改变。 - user194076
你的意思是说,owners和pets的表结构可以改变吗?它们是你在过程中建立的“临时”表吗?你可以将列命名为Col1..Col10(最多需要的列数),并在未使用的列中留下null值,这样你就会拥有静态的列名来进行UNPIVOT操作。 - MtwStark

0

我会采取略有不同的方法,而不是存储要匹配的列,您可以存储要执行的查询:

create table builder
(
    PetID int not null,
    Query varchar(max)
)

INSERT INTO builder
VALUES (1, 'SELECT TOP 1 *
FROM pets
INNER JOIN Owners
    ON Owners.Name = pets.OwnerName 
WHERE petId = 1
ORDER BY 
    CASE WHEN Owners.Country = pets.Country THEN 0 ELSE 1 END,
    CASE WHEN Owners.Zip = pets.Zip THEN 0 ELSE 1 END,
    CASE WHEN Owners.Addr = pets.Address THEN 0 ELSE 1 END'),
(2, 'SELECT TOP 1 *
FROM pets
INNER JOIN Owners
    ON Owners.Name = pets.OwnerName 
WHERE petId = 2
ORDER BY 
    CASE WHEN Owners.Document = pets.Document THEN 0 ELSE 1 END,
    CASE WHEN Owners.Name = pets.OwnerName THEN 0 ELSE 1 END,
    CASE WHEN Owners.Zip = pets.Zip THEN 0 ELSE 1 END'),
(3, 'SELECT TOP 1 *
FROM pets
INNER JOIN Owners
    ON Owners.Name = pets.OwnerName 
WHERE petId = 3
ORDER BY 
    CASE WHEN Owners.Country = pets.Country THEN 0 ELSE 1 END
')

create table pets
(
    PetID int null,
    Address varchar(100) null,
    Zip int null,
    Country varchar(100) null,
    Document varchar(100) null,
    OwnerName varchar(100) null,
    OwnerID int null,
    Field1 bit null,
    Field2 bit null
)

insert into pets values
(1, '123 5th st', 12345, 'US', 'test.csv', 'John', NULL, NULL, NULL),
(2, '234 6th st', 23456, 'US', 'a.csv', 'Alex', NULL, NULL, NULL),
(3, '345 7th st', 34567, 'US', 'b.csv', 'Mike', NULL, NULL, NULL)

create table owners
(
    OwnerID int null,
    Addr varchar(100) null,
    Zip int null,
    Country varchar(100) null,
    Document varchar(100) null,
    Name varchar(100) null,
    OtherField bit null,
    OtherField2 bit null,
)

insert into owners values
(1, '456 8th st',  45678, 'US', 'c.csv', 'Mike',  NULL, NULL),
(2, '678 9th st',  45678, 'US', 'b.csv', 'John',  NULL, NULL),
(3, '890 10th st', 45678, 'US', 'b.csv', 'Alex',  NULL, NULL),
(4, '901 11th st', 23456, 'US', 'b.csv', 'Alex',  NULL, NULL),
(5, '234 5th st',  12345, 'US', 'b.csv', 'John',  NULL, NULL),
(6, '123 5th st',  45678, 'US', 'a.csv', 'John',  NULL, NULL)

现在要找到特定宠物的匹配所有者,只需从表格中找到查询并执行它:

DECLARE @query varchar(max)
SELECT TOP 1 @query = query
FROM builder
WHERE petId =1

EXEC (@query)

0

这是一个严格解决您的问题的答案

  • 遵循您提出的规则无循环,无游标,无动态SQL
  • 同时严格考虑您的问题,因此这不是通用解决方案,而是非常特定于您的问题和您拥有的列

测试数据

declare @Pets table 
(
    PetID int null,
    Address varchar(100) null,
    Zip int null,
    Country varchar(100) null,
    Document varchar(100) null,
    OwnerName varchar(100) null,
    OwnerID int null,
    Field1 bit null,
    Field2 bit null
)

insert into @Pets values
(1, '123 5th st', 12345, 'US', 'test.csv', 'John', NULL, NULL, NULL),
(2, '234 6th st', 23456, 'US', 'a.csv', 'Alex', NULL, NULL, NULL),
(3, '345 7th st', 34567, 'US', 'b.csv', 'Mike', NULL, NULL, NULL)

declare @owners table
(
    OwnerID int null,
    Addr varchar(100) null,
    Zip int null,
    Country varchar(100) null,
    Document varchar(100) null,
    Name varchar(100) null,
    OtherField bit null,
    OtherField2 bit null
)

insert into @owners values
(1, '456 8th st',  45678, 'US', 'c.csv', 'Mike',  NULL, NULL),
(2, '678 9th st',  45678, 'US', 'b.csv', 'John',  NULL, NULL),
(3, '890 10th st', 45678, 'US', 'b.csv', 'Alex',  NULL, NULL),
(4, '901 11th st', 23456, 'US', 'b.csv', 'Alex',  NULL, NULL),
(5, '234 5th st',  12345, 'US', 'b.csv', 'John',  NULL, NULL),
(6, '123 5th st',  45678, 'US', 'a.csv', 'John',  NULL, NULL)

declare @builder table  
(
    PetID int not null,
    Field varchar(30) not null,
    MatchTo varchar(30) not null,
    Priority int not null
)

insert into @builder values
(1,'Address', 'Addr',4),
(1,'Zip', 'Zip', 3),
(1,'Country', 'Country', 2),
(1,'OwnerName', 'Name',1),
(2,'Zip', 'Zip',3),
(2,'OwnerName','Name', 2),
(2,'Document', 'Document', 1),
(3,'Country', 'Country', 1)

解决问题的代码

select distinct p.PetID, min(o.OwnerID) as ownerID from @pets p
inner join @builder b on p.PetID = b.PetID
inner join @owners o on 
( 
   (case when b.Field = 'Address' and b.Priority = 1 then p.Address else '0' end) = (case when b.MatchTo = 'Addr' and b.Priority = 1 then o.Addr else '-1' end)                  
or (case when b.Field = 'Zip' and b.Priority = 1 then p.Zip else '0' end) = (case when b.MatchTo = 'Zip' and b.Priority = 1 then o.Zip else '-1' end)                    
or (case when b.Field = 'Country' and b.Priority = 1 then p.Country else '0' end) = (case when b.MatchTo = 'Country' and b.Priority = 1 then o.Country else '-1' end)                    
or (case when b.Field = 'OwnerName' and b.Priority = 1 then p.OwnerName else '0' end) = (case when b.MatchTo = 'Name' and b.Priority = 1 then o.Name else '-1' end)                  
or (case when b.Field = 'Document' and b.Priority = 1 then p.Document else '0' end) = (case when b.MatchTo = 'Document' and b.Priority = 1 then o.Document else '-1' end)                    
)
AND
( 
   (case when b.Field = 'Address' and b.Priority = 2 then p.Address else '0' end) = (case when b.MatchTo = 'Addr' and b.Priority = 2 then o.Addr else '-1' end)                  
or (case when b.Field = 'Zip' and b.Priority = 2 then p.Zip else '0' end) = (case when b.MatchTo = 'Zip' and b.Priority = 2 then o.Zip else '-1' end)                    
or (case when b.Field = 'Country' and b.Priority = 2 then p.Country else '0' end) = (case when b.MatchTo = 'Country' and b.Priority = 2 then o.Country else '-1' end)                    
or (case when b.Field = 'OwnerName' and b.Priority = 2 then p.OwnerName else '0' end) = (case when b.MatchTo = 'Name' and b.Priority = 2 then o.Name else '-1' end)                  
or (case when b.Field = 'Document' and b.Priority = 2 then p.Document else '0' end) = (case when b.MatchTo = 'Document' and b.Priority = 2 then o.Document else '-1' end)                    
)
AND
( 
   (case when b.Field = 'Address' and b.Priority = 3 then p.Address else '0' end) = (case when b.MatchTo = 'Addr' and b.Priority = 3 then o.Addr else '-1' end)                  
or (case when b.Field = 'Zip' and b.Priority = 3 then p.Zip else '0' end) = (case when b.MatchTo = 'Zip' and b.Priority = 3 then o.Zip else '-1' end)                    
or (case when b.Field = 'Country' and b.Priority = 3 then p.Country else '0' end) = (case when b.MatchTo = 'Country' and b.Priority = 3 then o.Country else '-1' end)                    
or (case when b.Field = 'OwnerName' and b.Priority = 3 then p.OwnerName else '0' end) = (case when b.MatchTo = 'Name' and b.Priority = 3 then o.Name else '-1' end)                  
or (case when b.Field = 'Document' and b.Priority = 3 then p.Document else '0' end) = (case when b.MatchTo = 'Document' and b.Priority = 3 then o.Document else '-1' end)                    
)
AND
( 
   (case when b.Field = 'Address' and b.Priority = 4 then p.Address else '0' end) = (case when b.MatchTo = 'Addr' and b.Priority = 4 then o.Addr else '-1' end)                  
or (case when b.Field = 'Zip' and b.Priority = 4 then p.Zip else '0' end) = (case when b.MatchTo = 'Zip' and b.Priority = 4 then o.Zip else '-1' end)                    
or (case when b.Field = 'Country' and b.Priority = 4 then p.Country else '0' end) = (case when b.MatchTo = 'Country' and b.Priority = 4 then o.Country else '-1' end)                    
or (case when b.Field = 'OwnerName' and b.Priority = 4 then p.OwnerName else '0' end) = (case when b.MatchTo = 'Name' and b.Priority = 4 then o.Name else '-1' end)                  
or (case when b.Field = 'Document' and b.Priority = 4 then p.Document else '0' end) = (case when b.MatchTo = 'Document' and b.Priority = 4 then o.Document else '-1' end)                    
)
AND
( 
   (case when b.Field = 'Address' and b.Priority = 5 then p.Address else '0' end) = (case when b.MatchTo = 'Addr' and b.Priority = 5 then o.Addr else '-1' end)                  
or (case when b.Field = 'Zip' and b.Priority = 5 then p.Zip else '0' end) = (case when b.MatchTo = 'Zip' and b.Priority = 5 then o.Zip else '-1' end)                    
or (case when b.Field = 'Country' and b.Priority = 5 then p.Country else '0' end) = (case when b.MatchTo = 'Country' and b.Priority = 5 then o.Country else '-1' end)                    
or (case when b.Field = 'OwnerName' and b.Priority = 5 then p.OwnerName else '0' end) = (case when b.MatchTo = 'Name' and b.Priority = 5 then o.Name else '-1' end)                  
or (case when b.Field = 'Document' and b.Priority = 5 then p.Document else '0' end) = (case when b.MatchTo = 'Document' and b.Priority = 5 then o.Document else '-1' end)                    
)
group by p.PetID

union
--------------------------

select distinct p.PetID, min(o.OwnerID) as ownerID from @pets p
inner join @builder b on p.PetID = b.PetID
inner join @owners o on 
( 
   (case when b.Field = 'Address' and b.Priority = 1 then p.Address else '0' end) = (case when b.MatchTo = 'Addr' and b.Priority = 1 then o.Addr else '-1' end)                  
or (case when b.Field = 'Zip' and b.Priority = 1 then p.Zip else '0' end) = (case when b.MatchTo = 'Zip' and b.Priority = 1 then o.Zip else '-1' end)                    
or (case when b.Field = 'Country' and b.Priority = 1 then p.Country else '0' end) = (case when b.MatchTo = 'Country' and b.Priority = 1 then o.Country else '-1' end)                    
or (case when b.Field = 'OwnerName' and b.Priority = 1 then p.OwnerName else '0' end) = (case when b.MatchTo = 'Name' and b.Priority = 1 then o.Name else '-1' end)                  
or (case when b.Field = 'Document' and b.Priority = 1 then p.Document else '0' end) = (case when b.MatchTo = 'Document' and b.Priority = 1 then o.Document else '-1' end)                    
)
AND
( 
   (case when b.Field = 'Address' and b.Priority = 2 then p.Address else '0' end) = (case when b.MatchTo = 'Addr' and b.Priority = 2 then o.Addr else '-1' end)                  
or (case when b.Field = 'Zip' and b.Priority = 2 then p.Zip else '0' end) = (case when b.MatchTo = 'Zip' and b.Priority = 2 then o.Zip else '-1' end)                    
or (case when b.Field = 'Country' and b.Priority = 2 then p.Country else '0' end) = (case when b.MatchTo = 'Country' and b.Priority = 2 then o.Country else '-1' end)                    
or (case when b.Field = 'OwnerName' and b.Priority = 2 then p.OwnerName else '0' end) = (case when b.MatchTo = 'Name' and b.Priority = 2 then o.Name else '-1' end)                  
or (case when b.Field = 'Document' and b.Priority = 2 then p.Document else '0' end) = (case when b.MatchTo = 'Document' and b.Priority = 2 then o.Document else '-1' end)                    
)
AND
( 
   (case when b.Field = 'Address' and b.Priority = 3 then p.Address else '0' end) = (case when b.MatchTo = 'Addr' and b.Priority = 3 then o.Addr else '-1' end)                  
or (case when b.Field = 'Zip' and b.Priority = 3 then p.Zip else '0' end) = (case when b.MatchTo = 'Zip' and b.Priority = 3 then o.Zip else '-1' end)                    
or (case when b.Field = 'Country' and b.Priority = 3 then p.Country else '0' end) = (case when b.MatchTo = 'Country' and b.Priority = 3 then o.Country else '-1' end)                    
or (case when b.Field = 'OwnerName' and b.Priority = 3 then p.OwnerName else '0' end) = (case when b.MatchTo = 'Name' and b.Priority = 3 then o.Name else '-1' end)                  
or (case when b.Field = 'Document' and b.Priority = 3 then p.Document else '0' end) = (case when b.MatchTo = 'Document' and b.Priority = 3 then o.Document else '-1' end)                    
)
AND
( 
   (case when b.Field = 'Address' and b.Priority = 4 then p.Address else '0' end) = (case when b.MatchTo = 'Addr' and b.Priority = 4 then o.Addr else '-1' end)                  
or (case when b.Field = 'Zip' and b.Priority = 4 then p.Zip else '0' end) = (case when b.MatchTo = 'Zip' and b.Priority = 4 then o.Zip else '-1' end)                    
or (case when b.Field = 'Country' and b.Priority = 4 then p.Country else '0' end) = (case when b.MatchTo = 'Country' and b.Priority = 4 then o.Country else '-1' end)                    
or (case when b.Field = 'OwnerName' and b.Priority = 4 then p.OwnerName else '0' end) = (case when b.MatchTo = 'Name' and b.Priority = 4 then o.Name else '-1' end)                  
or (case when b.Field = 'Document' and b.Priority = 4 then p.Document else '0' end) = (case when b.MatchTo = 'Document' and b.Priority = 4 then o.Document else '-1' end)                    
)
group by p.PetID

union
--------------------------

select distinct p.PetID, min(o.OwnerID) as ownerID from @pets p
inner join @builder b on p.PetID = b.PetID
inner join @owners o on 
( 
   (case when b.Field = 'Address' and b.Priority = 1 then p.Address else '0' end) = (case when b.MatchTo = 'Addr' and b.Priority = 1 then o.Addr else '-1' end)                  
or (case when b.Field = 'Zip' and b.Priority = 1 then p.Zip else '0' end) = (case when b.MatchTo = 'Zip' and b.Priority = 1 then o.Zip else '-1' end)                    
or (case when b.Field = 'Country' and b.Priority = 1 then p.Country else '0' end) = (case when b.MatchTo = 'Country' and b.Priority = 1 then o.Country else '-1' end)                    
or (case when b.Field = 'OwnerName' and b.Priority = 1 then p.OwnerName else '0' end) = (case when b.MatchTo = 'Name' and b.Priority = 1 then o.Name else '-1' end)                  
or (case when b.Field = 'Document' and b.Priority = 1 then p.Document else '0' end) = (case when b.MatchTo = 'Document' and b.Priority = 1 then o.Document else '-1' end)                    
)
AND
( 
   (case when b.Field = 'Address' and b.Priority = 2 then p.Address else '0' end) = (case when b.MatchTo = 'Addr' and b.Priority = 2 then o.Addr else '-1' end)                  
or (case when b.Field = 'Zip' and b.Priority = 2 then p.Zip else '0' end) = (case when b.MatchTo = 'Zip' and b.Priority = 2 then o.Zip else '-1' end)                    
or (case when b.Field = 'Country' and b.Priority = 2 then p.Country else '0' end) = (case when b.MatchTo = 'Country' and b.Priority = 2 then o.Country else '-1' end)                    
or (case when b.Field = 'OwnerName' and b.Priority = 2 then p.OwnerName else '0' end) = (case when b.MatchTo = 'Name' and b.Priority = 2 then o.Name else '-1' end)                  
or (case when b.Field = 'Document' and b.Priority = 2 then p.Document else '0' end) = (case when b.MatchTo = 'Document' and b.Priority = 2 then o.Document else '-1' end)                    
)
AND
( 
   (case when b.Field = 'Address' and b.Priority = 3 then p.Address else '0' end) = (case when b.MatchTo = 'Addr' and b.Priority = 3 then o.Addr else '-1' end)                  
or (case when b.Field = 'Zip' and b.Priority = 3 then p.Zip else '0' end) = (case when b.MatchTo = 'Zip' and b.Priority = 3 then o.Zip else '-1' end)                    
or (case when b.Field = 'Country' and b.Priority = 3 then p.Country else '0' end) = (case when b.MatchTo = 'Country' and b.Priority = 3 then o.Country else '-1' end)                    
or (case when b.Field = 'OwnerName' and b.Priority = 3 then p.OwnerName else '0' end) = (case when b.MatchTo = 'Name' and b.Priority = 3 then o.Name else '-1' end)                  
or (case when b.Field = 'Document' and b.Priority = 3 then p.Document else '0' end) = (case when b.MatchTo = 'Document' and b.Priority = 3 then o.Document else '-1' end)                    
)
group by p.PetID

union
------------------------

select distinct p.PetID, min(o.OwnerID) as ownerID from @pets p
inner join @builder b on p.PetID = b.PetID
inner join @owners o on 
( 
   (case when b.Field = 'Address' and b.Priority = 1 then p.Address else '0' end) = (case when b.MatchTo = 'Addr' and b.Priority = 1 then o.Addr else '-1' end)                  
or (case when b.Field = 'Zip' and b.Priority = 1 then p.Zip else '0' end) = (case when b.MatchTo = 'Zip' and b.Priority = 1 then o.Zip else '-1' end)                    
or (case when b.Field = 'Country' and b.Priority = 1 then p.Country else '0' end) = (case when b.MatchTo = 'Country' and b.Priority = 1 then o.Country else '-1' end)                    
or (case when b.Field = 'OwnerName' and b.Priority = 1 then p.OwnerName else '0' end) = (case when b.MatchTo = 'Name' and b.Priority = 1 then o.Name else '-1' end)                  
or (case when b.Field = 'Document' and b.Priority = 1 then p.Document else '0' end) = (case when b.MatchTo = 'Document' and b.Priority = 1 then o.Document else '-1' end)                    
)
AND
( 
   (case when b.Field = 'Address' and b.Priority = 2 then p.Address else '0' end) = (case when b.MatchTo = 'Addr' and b.Priority = 2 then o.Addr else '-1' end)                  
or (case when b.Field = 'Zip' and b.Priority = 2 then p.Zip else '0' end) = (case when b.MatchTo = 'Zip' and b.Priority = 2 then o.Zip else '-1' end)                    
or (case when b.Field = 'Country' and b.Priority = 2 then p.Country else '0' end) = (case when b.MatchTo = 'Country' and b.Priority = 2 then o.Country else '-1' end)                    
or (case when b.Field = 'OwnerName' and b.Priority = 2 then p.OwnerName else '0' end) = (case when b.MatchTo = 'Name' and b.Priority = 2 then o.Name else '-1' end)                  
or (case when b.Field = 'Document' and b.Priority = 2 then p.Document else '0' end) = (case when b.MatchTo = 'Document' and b.Priority = 2 then o.Document else '-1' end)                    
)
group by p.PetID

union
------------------------

select distinct p.PetID, min(o.OwnerID) as ownerID from @pets p
inner join @builder b on p.PetID = b.PetID
inner join @owners o on 
( 
   (case when b.Field = 'Address' and b.Priority = 1 then p.Address else '0' end) = (case when b.MatchTo = 'Addr' and b.Priority = 1 then o.Addr else '-1' end)                  
or (case when b.Field = 'Zip' and b.Priority = 1 then p.Zip else '0' end) = (case when b.MatchTo = 'Zip' and b.Priority = 1 then o.Zip else '-1' end)                    
or (case when b.Field = 'Country' and b.Priority = 1 then p.Country else '0' end) = (case when b.MatchTo = 'Country' and b.Priority = 1 then o.Country else '-1' end)                    
or (case when b.Field = 'OwnerName' and b.Priority = 1 then p.OwnerName else '0' end) = (case when b.MatchTo = 'Name' and b.Priority = 1 then o.Name else '-1' end)                  
or (case when b.Field = 'Document' and b.Priority = 1 then p.Document else '0' end) = (case when b.MatchTo = 'Document' and b.Priority = 1 then o.Document else '-1' end)                    
)
group by p.PetID

结果

PetID   OwnerID
1       2
2       6
3       1

0

如果您正在寻找一种简单的解决方案,而不需要使用联合、循环、游标或动态SQL,则下面的查询可以正常工作。

SQL Fiddle: http://sqlfiddle.com/#!18/10982/41

select PetID ,COALESCE(
 (select  top 1 OwnerID from temp_owners
     where Zip = pets.Zip 
     and Name = pets.OwnerName
     and Document = pets.Document) ,
     (select top 1 OwnerID from temp_owners where
         Name = pets.OwnerName 
         and Document = pets.Document)  ,
         (select top 1 OwnerID from temp_owners where
          Document = pets.Document)  ) OwnerId
       from 
temp_pets pets

结果:

PetID   OwnerId
1       (null)
2       6
3       2

这不需要动态SQL,因为它是硬编码的,并且完全忽略了“特殊匹配表”的内容... - MatBailie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接