SQL Select 来自 table 其中来自第二个 table 的连接值是来自第三个 table 的值的子集

SQL Select from table where joined values from a second table are a subset of values from a third table

我在 MS SQL 服务器中有以下 tables:Tasks、Users、Tags、TaskTags(将任务映射到标签)和 UserTags(将用户映射到标签) .

给定一个用户 U,我想找到所有任务 T,其中 T 的每个标签也是 U 的标签(例如,如果任务的标签是用户标签的子集)。

这是一个带有一些示例数据的 table 脚本(它可以是 运行 at http://sqlfiddle.com/ with MS SQL Server 17):

CREATE TABLE [dbo].[Tasks](
    [TaskId] [int] NOT NULL PRIMARY KEY,
    [TaskName] [nvarchar](MAX) NOT NULL
)

CREATE TABLE [dbo].[Users](
    [UserId] [int] NOT NULL PRIMARY KEY,
    [UserName] [nvarchar](MAX) NOT NULL
)

CREATE TABLE [dbo].[Tags](
    [TagId] [int] NOT NULL PRIMARY KEY,
    [TagName] [nvarchar](MAX) NOT NULL
)

CREATE TABLE [dbo].[TaskTags](
    [TaskId] [int] NOT NULL,
    [TagId] [int] NOT NULL
)

CREATE TABLE [dbo].[UserTags](
    [UserId] [int] NOT NULL,
    [TagId] [int] NOT NULL
)
 
INSERT INTO Tasks VALUES (1,'Task for all SWEs');
INSERT INTO Tasks VALUES (2,'Task for USA SWEs');
INSERT INTO Tasks VALUES (3,'Task for all PMs');
INSERT INTO Tasks VALUES (4,'Task for Europe PMs');

INSERT INTO Users VALUES (1,'Europe SWE');
INSERT INTO Users VALUES (2,'USA SWE');
INSERT INTO Users VALUES (3,'Europe PM');
INSERT INTO Users VALUES (4,'USA PM');

INSERT INTO Tags VALUES (1,'swe');
INSERT INTO Tags VALUES (2,'pm');
INSERT INTO Tags VALUES (3,'usa');
INSERT INTO Tags VALUES (4,'europe');

INSERT INTO TaskTags VALUES (1,1);
INSERT INTO TaskTags VALUES (2,1);
INSERT INTO TaskTags VALUES (2,3);
INSERT INTO TaskTags VALUES (3,2);
INSERT INTO TaskTags VALUES (4,2);
INSERT INTO TaskTags VALUES (4,4);

INSERT INTO UserTags VALUES (1,1);
INSERT INTO UserTags VALUES (1,4);
INSERT INTO UserTags VALUES (2,1);
INSERT INTO UserTags VALUES (2,3);
INSERT INTO UserTags VALUES (3,2);
INSERT INTO UserTags VALUES (3,4);
INSERT INTO UserTags VALUES (4,2);
INSERT INTO UserTags VALUES (4,3);

当给出任务 T 时,我能够找出这个问题的逆。例如。给定任务 T,return 所有用户 U,其中 T 的标签是 U 的子集。这是该查询:

WITH thisTaskTags AS (
    SELECT DISTINCT TaskTags.TagId
    FROM TaskTags
    WHERE TaskTags.TaskId = @taskId
)
SELECT UserTags.UserId
FROM UserTags JOIN thisTaskTags 
    ON UserTags.TagId = thisTaskTags.TagId CROSS JOIN
    (SELECT COUNT(*) AS keycnt FROM thisTaskTags) k
GROUP BY UserTags.UserId
HAVING COUNT(thisTaskTags.TagId) = MAX(k.keycnt)

当@taskId = 1 时,UserIds 1 和 2 被 returned,而当 @taskId = 2 时,只有 UserId 2 被 returned(正确行为)。

然而,当我试图将其转换为 return 给定用户应该拥有的所有任务时,我 运行 遇到了麻烦。我试过这个查询:

WITH thisUserTags AS (
    SELECT DISTINCT UserTags.TagId
    FROM UserTags
    WHERE UserTags.UserId = @userId
)
SELECT TaskTags.TaskId
FROM TaskTags JOIN thisUserTags
    ON thisUserTags.TagId = TaskTags.TagId CROSS JOIN
    (SELECT COUNT(*) AS keycnt FROM thisUserTags) k
GROUP BY TaskTags.TaskId
HAVING COUNT(thisUserTags.TagId) = MAX(k.keycnt);

然而,这只有 returns 个任务,其中所有任务标签都匹配所有用户任务,例如如果你有标签:[a,b,c] 它只会得到带有标签的任务:[a,b,c] 而不是 [a]、[b]、[b,c] 等

对于具体示例,如果您设置@userId = 1,则不会 return 编辑任何任务 ID,当正确的输出将获得 1 行时,任务 ID = 1。当 @userId = 2 时,只有 taskID 2 是 returned,而 taskIDs 1 和 2 都应该是 returned(即,如果一个任务只有“swe”标签,所有“swe”用户都应该得到它,但是如果任务同时具有“swe”和“usa”,只有同时具有这两个标签的用户才能获得它。

我也试过这个查询:

SELECT DISTINCT Tasks.TaskId FROM Tasks
INNER JOIN TaskTags ON TaskTags.TaskId = Tasks.TaskId
WHERE TaskTags.TagId IN (SELECT TagId from UserTags where UserId = @userId)
GROUP BY Tasks.TaskId

但问题是 return 任何具有任何共同标签的任务,所以带有标签的 U 会得到带有标签的 T:[b,d]即使你没有标签 d.

再举个具体的例子,如果@userId = 1,taskIDs 1,2,4 被 returned,而只有 taskIds 1 和 2 应该被 returned(任务 ID 4 应该仅分配给同时具有“europe”和“pm”标签的用户,由于常见的“europe”标签,此处错误地分配给具有“europe”和“swe”标签的用户。

有人可以在这里阐明一下吗?

您可能正在寻找类似以下内容...

declare @userId int = ...;
select Tasks.TaskId
from Tasks
where 0 = (
  select count(1)
  from (
    select TagId from TaskTags where TaskTags.TaskId=Tasks.TaskId
    except
    select TagId from UserTags where UserTags.UserId=@userId
  ) TaskSpecificTags
);

不清楚您是否还想 return 带有 0 个标签的任务,因此您可能还需要测试该条件。

这是一道经典的Relational Division With Remainder题。

你只需要正确构图:

  • 你想要所有 Tasks...
  • ... 其 TaskTags 将所有 UserTags 的集合划分为给定的 User
  • UserTags可以有余数,TaskTags不能有余数所以前者是被除数,后者是除数

一个典型的解决方案(有很多)是将被除数左连接到除数,将其分组,然后确保匹配的被除数与除数的个数相同。换句话说,所有除数都匹配。

因为你似乎只想要 Tasks 而不是他们的 TaskTags,你可以在 EXISTS 子查询中完成所有这些:

DECLARE @userId int = 1;

SELECT *
FROM Tasks t
WHERE EXISTS (SELECT 1
    FROM TaskTags tt
    LEFT JOIN UserTags ut ON ut.TagId = tt.TagId
        AND ut.UserId = @userId
    WHERE tt.TaskId = t.TaskId
    HAVING COUNT(*) = COUNT(ut.UserId)
);

db<>fiddle