如何在多对多表中查找缺失对?

时间:2019-12-13 12:15:21

标签: sql-server tsql

给出:“用户和组”以及“多对多GroupUsers”表。

每对用户都必须有自己的配对组,前提是可以有更多或更少用户的组。

如何检查和创建丢失的配对组?

Link to SQL Fiddle

create table dbo.Users (
    Id   int not null,
    Name nvarchar(50) not null,
    constraint PK_Users primary key clustered (Id)
);
create table dbo.Groups (
    Id   int not null,
    Name nvarchar(50) not null, 
    constraint PK_Groups primary key clustered (Id)
);
create table dbo.GroupUsers (
    GroupId int not null ,
    UserId  int not null ,
    constraint PK_GroupUsers primary key clustered (GroupId, UserId),
    constraint FK_GroupUsers_GroupId foreign key (GroupId) references dbo.Groups (Id),
    constraint FK_GroupUsers_UserId foreign key (UserId) references dbo.Users (Id)
);

insert into dbo.Users values (1, 'Anna'), (2, 'Berta'), (3, 'Carlie'), (4, 'Dana'), (5, 'Emil');
insert into dbo.Groups values (1, 'Anna-Berta'), (2, 'Anna-Carlie'), (3, 'Anna-Berta-Carlie');
insert into dbo.GroupUsers values 
(1,1), (1, 2), -- 'Anna-Berta' group
(2,1), (2, 3), -- 'Anna-Carlie' group
(3,1), (3, 2), (3, 3); -- 'Anna-Berta-Carlie' group

如何为用户Anna查找和创建丢失的“配对组”?

  • 因此,安娜与其他所有用户都有一个配对组
  • 所有“安娜的配对组”必须恰好有两个用户,
  • 尽管包括Anna在内的任何用户都可能有一个包含多于或少于两个用户的组。

更新2019-12-15

因此(Link to SQL Fiddle)是迄今为止我最喜欢的解决方案,它如何查找丢失的配对组及其用户(感谢answer@Kari F.

declare @UserId int = 1;

with cte as 
(
    select
        VirtualGroupId = row_number() over(order by p.Id desc) * -1,
        GroupName = concat( u.Name, '_', p.Name),
        CurrentUserId = u.Id, 
        OtherUserId = p.Id
    from 
        dbo.Users u inner join dbo.Users p on u.Id = @UserId and p.Id <> @UserId
    where       
        concat('_', u.Id, '_', p.Id) not in 
        (
            select
                (
                    select concat('_', gu.UserId) 
                    from dbo.GroupUsers gu 
                    where gu.GroupId = g.Id
                    order by case when gu.UserId = @UserId then 0 else 1 end
                    for xml path ('')
                )
            from                         
                dbo.Groups g
         )
)
select VirtualGroupId, GroupName, UserId = CurrentUserId from cte
union all
select VirtualGroupId, GroupName, UserId = OtherUserId from cte 
order by VirtualGroupId, UserId   

2 个答案:

答案 0 :(得分:1)

我尝试使用循环方法解决问题,这是因为必须提供groups表中的id,并且它不是一个Identity列,否则实现会变得更简单。

我还使用了keyuser变量来处理以下事实:必须配对的是“ Anna”,而不是其他用户。这样可以避免简单地对用户ID值进行硬编码。

create table #Users (
    Id   int not null,
    Name nvarchar(50) not null,
    constraint PK_Users primary key clustered (Id)
);
create table #Groups (
    Id   int not null,
    Name nvarchar(50) not null, 
    constraint PK_Groups primary key clustered (Id)
);
create table #GroupUsers (
    GroupId int not null ,
    UserId  int not null ,
    constraint PK_GroupUsers primary key clustered (GroupId, UserId),
    constraint FK_GroupUsers_GroupId foreign key (GroupId) references #Groups (Id),
    constraint FK_GroupUsers_UserId foreign key (UserId) references #Users (Id)
);

insert into #Users values (1, 'Anna'), (2, 'Berta'), (3, 'Carlie'), (4, 'Dana'), (5, 'Emil');
insert into #Groups values (1, 'Anna-Berta'), (2, 'Anna-Carlie'), (3, 'Anna-Berta-Carlie');
insert into #GroupUsers values 
(1,1), (1, 2), -- 'Anna-Berta' group
(2,1), (2, 3), -- 'Anna-Carlie' group
(3,1), (3, 2), (3, 3); -- 'Anna-Berta-Carlie' group

declare @sql nvarchar(max) = '';
declare @nextgroupid int = 0;
declare @keyuser int = 0;

select @keyuser = ID from #Users where [Name]='Anna';

while exists (select 1 from #Users u inner join #Users u2 on u.Id=1 where u2.Id not in (select userid from #GroupUsers))
begin

select @nextgroupid = MAX(id)+1 from #Groups;

set @sql = 'insert #groups (id, [name]) select top 1 ' + CAST(@nextgroupid as nvarchar(3)) + ', u.[name] + ''-'' + u2.[name] as missingusergroup from #Users u inner join #Users u2 on u.Id= ' + CAST(@keyuser as nvarchar(3)) + ' where u2.Id not in (select userid from #GroupUsers) order by u2.id;';
exec(@sql);
set @sql = 'insert #groupusers (groupid, userid) select top 1 ' + CAST(@nextgroupid as nvarchar(3)) + ', u2.id from #Users u inner join #Users u2 on u.Id= ' + CAST(@keyuser as nvarchar(3)) + ' where u2.Id not in (select userid from #GroupUsers) union select top 1 '+ CAST(@nextgroupid as nvarchar(3)) +', u.id from #Users u where u.id= ' + CAST(@keyuser as nvarchar(3)) + ' order by u2.id;';
exec(@sql);
end

select * from #GroupUsers;

select * from #Groups;

drop table #Groups, #GroupUsers, #Users;

enter image description here

答案 1 :(得分:1)

这个简单的解决方案呢?

 select u.Id, p.Id
  from dbo.Users u 
  cross join dbo.Users p
  where u.Id = 1 and p.Id <> 1
    and u.Name + '-' + p.Name not in (select g.Name from dbo.Groups g);

http://sqlfiddle.com/#!18/3c30f/16

第二种解决方案,考虑到您的评论

with required_groups as (
   select u.Id as userId, p.Id pairId
    from dbo.Users u 
    cross join dbo.Users p
    where u.Id = 1 and p.Id <> 1
 ),
 existing_groups as (
   select annas_groups.UserId as userId, pairs_groups.UserId as pairId
     from (
       select *
         from GroupUsers gu
         where gu.UserId = 1
           and gu.GroupId in (
             select x.GroupId from GroupUsers x
               group by x.GroupId
               having count(*) = 2
         )
      ) annas_groups
      inner join (
       select *
         from GroupUsers gu
         where 
           gu.UserId <> 1
           and gu.GroupId in (
             select x.GroupId from GroupUsers x
               group by x.GroupId
               having count(*) = 2
         )        
      ) pairs_groups on annas_groups.GroupId = pairs_groups.GroupId
 )

 select * 
   from required_groups rg
   where not exists (
       select 1 
         from existing_groups eg 
         where eg.UserId = rg.UserId 
           and eg.PairId = rg.PairId
   )

http://sqlfiddle.com/#!18/3c30f/34

我认为它有点复杂,但仍然可读。