【问题标题】:Matching sets in SQL when cross-referencing交叉引用时 SQL 中的匹配集
【发布时间】:2013-08-11 21:14:21
【问题描述】:

我有以下表格:

  • 书籍:书籍列表
  • 标签:标签列表(小说、传记等)
  • xrefbookstags : 交叉引用表(一本书有多个标签)

我想要做的是给定一组标签,找到标签与该组完全匹配的书籍。这是我尝试过的:

SELECT B.name, B.author, B.id, B.finished, B.manual
    FROM books B INNER JOIN xrefbookstags XRBT 
        ON XRBT.idBooks=B.id JOIN tags T
        ON XRBT.idTags=T.id
    WHERE T.name IN ('novel','biography')

这给了我一个包含而不是集合的相等性。所以我想我应该计算集合中的标签数量并匹配集合的大小。

SELECT B.name, B.author, B.id, B.finished, B.manual
    FROM books B INNER JOIN xrefbookstags XRBT 
        ON XRBT.idBooks=B.id JOIN tags T
        ON XRBT.idTags=T.id
    WHERE T.name IN ('novel','biography') AND count(T.id)=2

但这失败了。所以我的问题来了:我可以在这个交叉引用上下文中匹配集合吗?

【问题讨论】:

    标签: sql select count relational-division cross-reference


    【解决方案1】:

    你想要那些书

    a) 那本书没有缺少必需的标签,AND b) 该书没有标签不在所需标签列表中。

    将这些标签名称的 id 放入一个表中(这里是即时完成的)并执行此操作。 (没有 SQL Fiddle 或 CREATE TABLE/INSERT 示例数据,所以我没有运行它来检查每个细节)

    WITH TagIDs(id) AS ( --required tag ids
      SELECT id
      FROM tags
      WHERE name in ('novel', 'biography')
    )
    
      SELECT B.name, B.author, B.id, B.finished, B.manual --select those books
      FROM books B
    
      WHERE --a)
      NOT EXISTS (  -- there is no
        SELECT * FROM TagIDs --  required tag
        WHERE TagIDs.id NOT IN ( -- that's missing
          SELECT XRBT.idTags FROM xrefbookstags XRBT -- from the list of tags
          WHERE XRBT.idBooks=B.id -- for this particular book
        )
      )
      AND --b)
      NOT EXISTS ( -- there is no
        SELECT * FROM xrefbookstags XRBT --tag
        WHERE XRBT.idBooks=B.id --for this particular book
        AND XRBT.idTags NOT IN ( --that's missing
          SELECT id FROM TagIDs --from the list of required tags
        )
      )
    

    【讨论】:

      【解决方案2】:

      你可以结合EXISTSNOT EXISTS

      SELECT B.name, B.author, B.id, B.finished, B.manual
      FROM books B
      WHERE EXISTS(
          SELECT 1 FROM xrefbookstags XRBT 
          INNER JOIN Tags T ON XRBT.idTags=T.id
          WHERE XRBT.idBooks=B.id
          AND T.name = 'novel'
      )
      AND EXISTS(
          SELECT 1 FROM xrefbookstags XRBT 
          INNER JOIN Tags T ON XRBT.idTags=T.id
          WHERE XRBT.idBooks=B.id
          AND T.name = 'biography'
      ) 
      AND NOT EXISTS(
          SELECT 1 FROM xrefbookstags XRBT 
          INNER JOIN Tags T ON XRBT.idTags=T.id
          WHERE XRBT.idBooks=B.id
          AND T.name NOT IN ('novel','biography')
      )
      

      【讨论】:

      • 实际上我无法控制输入标签,它来自一个变量。这里('novel','biography') 只是一个例子。我可以输出 COUNT(T.id) 并在最后添加 GROUP BY T.id 。但是这样我对 SQL 输出进行后处理。我想只使用 SQL 并可能作为一个紧凑的请求来获得所有结果:)
      【解决方案3】:

      这是“set-within-sets”查询的示例。最通用的方法是使用group by 并将逻辑放在having 子句中。对于您的情况:

      SELECT B.name, B.author, B.id, B.finished, B.manual
      FROM books B INNER JOIN
           xrefbookstags XRBT 
           ON XRBT.idBooks = B.id JOIN
           tags T
           ON XRBT.idTags = T.id
      group by B.name, B.author, B.id, B.finished, B.manual
      having sum(case when t.name = 'novel' then 1 else 0 end) > 0 and
             sum(case when t.name = 'biography' then 1 else 0 end) > 0 and
             sum(case when t.name not in ('novel', 'biography') then 1 else 0 end) = 0;
      

      逻辑如下。当至少一个标签是'novel' 时,第一个子句为真。当至少有一个子句为'biography' 时,第二个子句为真,当没有其他标签时,第三个子句为真。

      这很容易概括。如果您想要具有这两个标签但可以有其他标签的书籍,只需省略第三个子句:

      having sum(case when t.name = 'novel' then 1 else 0 end) > 0 and
             sum(case when t.name = 'biography' then 1 else 0 end) > 0;
      

      如果您想要有其中之一的书籍:

      having sum(case when t.name = 'novel' then 1 else 0 end) > 0 or
             sum(case when t.name = 'biography' then 1 else 0 end) > 0;
      

      如果您想要有这两个加“历史”的书籍,那么您只需将其添加到:

      having sum(case when t.name = 'novel' then 1 else 0 end) > 0 and
             sum(case when t.name = 'biography' then 1 else 0 end) > 0 and
             sum(case when t.name = 'historical' then 1 else 0 end) > 0;
      

      而且,如果你想要,但不想做饭:

      having sum(case when t.name = 'novel' then 1 else 0 end) > 0 and
             sum(case when t.name = 'biography' then 1 else 0 end) > 0 and
             sum(case when t.name = 'historical' then 1 else 0 end) > 0 and
             sum(case when t.name = 'cooking' then 1 else 0 end) = 0;
      

      编辑:

      如果您有一个逗号分隔的要匹配的标签列表,那么您可以这样做:

      having sum(case when ','+@List+',' not like '%,'+t.name+',%' then 1 else 0 end) = 0 and
             count(distinct t.name) = 1 + len(@list) - len(replace(@list, ',', ''))
      

      第一个子句表示所有标签都在列表中。第二个说标签的长度就是列表的长度。

      这本质上是伪代码。不同的数据库对len() 函数有不同的名称,将字符串连接在一起的方式不同,表示变量值的方式也不同。但意图应该很清楚。

      【讨论】:

      • 与@Tim Schmelter 的评论相同,我只有一个包含标签的变量,因此大小可以是任何值。我可以生成 SQL,但我想保持轻松。 SQL 中是否有任何内置功能可以实现这一点?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-06-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多