如何优化 3 个表的 m:n 关系查询

Posted

技术标签:

【中文标题】如何优化 3 个表的 m:n 关系查询【英文标题】:How to optimize m:n relation query on 3 tables 【发布时间】:2009-05-27 17:47:36 【问题描述】:

这是我的 sql 问题 - 有 3 个表:

名称列表 ListHasNames Id 名称 Id 描述 ListsId NamesId =-------- ------------ ---------------- 1 保罗 1 足球 1 1 2 乔 2 篮球 1 2 3 珍妮 3 乒乓球 2 1 4 蒂娜 4 早餐俱乐部 2 3 5 午夜俱乐部 3 2 3 3 4 1 4 2 4 3 5 1 5 2 5 3 5 4

这意味着 Paul (Id=1) 和 Joe (Id=2) 在足球队 (Lists.Id=1),Paul 和 Jenny 在篮球队,等等...

现在我需要一个返回特定名称组合的 Lists.Id 的 SQL 语句: Paul、Joe 和 Jenny 在哪些列表中是该列表中唯一的成员?只回答 Lists.Id=4(早餐俱乐部) - 但不回答 5(午夜俱乐部),因为 Tina 也在该列表中。

我已经尝试过使用 INNER JOINS 和 SUB QUERIES:

选择 Q1.Lists_id FROM ( 选择 Lists_Id FROM 名称为 T1, 列表名称为 T2 在哪里 (T1.Name='Paul') 和 (T1.Id=T2.Names_ID) 和 ( ( 选择计数(*)从 列表名称为 Z1 其中(Z1.lists_id = T2.lists_Id) ) = 3) ) 作为第一季度 内部联接 ( 选择 Lists_Id FROM 名称为 T1, 列表名称为 T2 在哪里 (T1.Name='Joe') 和 (T1.Id=T2.Names_ID) 和 ( (选择计数(*)来自 列表名称为 Z1 哪里(Z1.Lists_id = T2.lists_id) ) = 3) ) 作为第二季度 开(Q1.Lists_id=Q2.Lists_id) 内部联接 ( 选择 Lists_Id FROM 名称为 T1, 列表名称为 T2 在哪里 (T1.Name='Jenny') 和 (T1.Id=T2.Names_ID) 和 ( (选择计数(*)来自 列表名称为 Z1 哪里(Z1.Lists_id = T2.lists_id) ) = 3) ) 作为第三季度 开(Q1.Lists_id=Q3.Lists_id)

看起来有点复杂,嗯?如何优化它? 我只需要包含特定名称的 Lists.Id(并且只有这些名称,没有其他人)。也许用 SELECT IN?

问候, 丹尼斯

【问题讨论】:

(这是评论,不是答案。)我有点好奇。为什么表名使用复数形式? (我们通常使用单数名称来命名表中的一行。)如果您要使用复数,为什么第三个表不命名(更恰当) ListsHaveNames ? (我可能还建议将第三个表命名为 Membership 或 ListMembership。)Carl Manaster 的答案返回您指定的结果集。 复数与否:***.com/questions/808992/… 第三个表名为 ListHasNames,因为如果您选择 n:m 关系,mysql Workbench 会默认分配该名称。这只是一个例子.. 【参考方案1】:
SELECT ListsId
FROM ListHasNames a
WHERE NamesId in (1, 2, 3)
AND NOT EXISTS
(SELECT * from ListHasNames b 
WHERE b.ListsId = a.ListsId 
AND b.NamesId not in (1, 2, 3))
GROUP BY ListsId
HAVING COUNT(*) = 3;

编辑:感谢 Chris Gow 的评论更正;子选择对于排除其他人的列表是必要的。 编辑 2 感谢 Dennis 的评论更正了表格名称

【讨论】:

这似乎在 postgreSQL 中不起作用。我试过了,我得到了 4 和 5。 OP 只希望返回的列表 id 只包含这三个人而没有其他人 你是对的——它在任何 DBMS 中都不起作用;我添加了一个子选择来纠正问题。谢谢。 (SELECT * from ListsId b 应该是 (SELECT * from ListHasNames b 但我还是明白了!;-)【参考方案2】:

我以 Carl Manaster 的解决方案为出发点:

SELECT listsid 
FROM listhasnames 
GROUP BY listsid HAVING COUNT(*) = 3
INTERSECT
SELECT x.listsid 
FROM listhasnames x, names n 
WHERE n.name IN('Paul', 'Joe', 'Jenny') 
AND n.id = x.namesid

【讨论】:

这是排除包含额外人员的列表的另一种好方法。 从未听说过 INTERSECT,看起来像是 MSSQL2005 的新手。但是,这个查询实际上不会返回不正确的结果吗?第一个查询返回具有 3 个名称的所有列表,第二个查询返回包含 Paul、Joe 或 Jenny 的所有列表。如果有一个列表总共包含 3 个成员,但其中只有 1 或 2 个成员是 Paul、Joe 或 Jenny,则此查询将返回根据原始问题不正确的查询。【参考方案3】:

更新:

select a.ListsId from
(
    --lists with three names only
    select lhn.ListsId, count(*) as count
    from ListHasNames  lhn
    inner join Names n on lhn.NamesId = n.Id 
    group by lhn.ListsId
    having count(*) = 3
) a
where a.ListsId in (select ListsId from ListHasNames lhn where NamesId = (select NamesId from names where Name = 'Paul'))
and a.ListsId in (select ListsId from ListHasNames lhn where NamesId = (select NamesId from names where Name = 'Joe'))
and a.ListsId in (select ListsId from ListHasNames lhn where NamesId = (select NamesId from names where Name = 'Jenny'))

【讨论】:

也许你应该告诉他 Count = 3 是因为他要求 3 个名字 由于名称列表是固定的并且“计数”将始终为 3,因此我将从选择列表中删除最后两列并改为执行“HAVING COUNT(*) = 3”。跨度> 【参考方案4】:

我最近刚刚解决了一个可能对您的情况也很有效的问题。这可能是矫枉过正。

我采用的方法是创建一个可能是正确解决方案的候选关联列表,然后使用游标或队列表来检查可能正确的解决方案以进行全面验证。

在我的例子中,这是通过类似的方式实现的

select
ParentId
count(*) as ChildCount
checksum_agg(checksum(child.*) as ChildAggCrc
from parent join child on parent.parentId = child.parentId

然后,您可以将计数和聚合校验和与您的查找数据(即要检查的 3 个名称)进行比较。如果没有行匹配,则保证没有匹配项。如果任何行匹配,您就可以通过并执行该特定 ParentId 的连接,以验证行集之间是否存在任何差异。

一清二楚? :)

【讨论】:

以上是关于如何优化 3 个表的 m:n 关系查询的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 3 个表在 SQL 查询中获得完整结果,其中 1 个表保持 2 个表的关系?

如何优化这个 MySql 查询 - 连接 3 个表?

优化集合包含查询

mysql 千万级数据库如何进行多张结构相同的表联合查询?如何优化或设置提高查询速度?

50 万条记录的 SQL 查询性能优化

优化联合 sql 查询