【问题标题】:How do I efficiently do the intersection of joins in SQL?如何有效地在 SQL 中进行连接的交集?
【发布时间】:2010-01-13 22:20:53
【问题描述】:

我有三个表,bookstagstaggings (books-xref-tags):

books
id | title |      author     
 1 | Blink | Malcolm Gladwell
 2 |  1984 |    George Orwell

taggings
book_id | tag_id
      1 |      1
      1 |      2
      2 |      1
      2 |      3

tags
id | name
 1 | interesting
 2 |  nonfiction
 3 |     fiction

我想搜索所有标记为“有趣”“小说”的书籍。我想出的最好的是

select books.* from books, taggings, tags
 where taggings.book_id = books.id
   and taggings.tag_id  = tag.id
   and tag.name = "interesting"
intersect
select books.* from books, taggings, tags
 where taggings.book_id = books.id
   and taggings.tag_id  = tag.id
   and tag.name = "fiction"

这似乎可行,但我不确定它将如何缩放,无论是在行还是标签数量。也就是说,当我添加数百本书、数百个标签和数千个标签时会发生什么?当搜索变成“‘有趣’‘小说’‘水生’‘石匠’”时会发生什么?

如果没有更好的方法直接在 SQL 中进行查询,我会考虑另一种方法:

  1. 选择带有第一个标签的所有书籍,以及所有这些书籍的标签
  2. 从列表中删除任何未查询到所有标签的标签

【问题讨论】:

  • 我在发帖前四处寻找类似的问题,我保证。我的似乎非常接近彼得朗的答案。完全重复?不确定。

标签: sql join intersection


【解决方案1】:

如果您想保留使用两个以上标签的选项,this answer 对您来说可能会很有趣。

它使用 MySQL 语法(不确定你用的是什么),但它非常简单,你应该可以将它与其他数据库一起使用。

这对你来说是这样的(使用 MySQL 语法):

SELECT books.id, books.title, books.author
FROM books
INNER JOIN taggings ON ( taggings.book_id = books.book_id )
INNER JOIN tags ON ( tags.tag_id = taggings.tag_id )
WHERE tags.name IN ( @tag1, @tag2, @tag3 )
GROUP BY books.id, books.title, books.author
HAVING COUNT(*) = @number_of_tags

来自我的另一篇文章:

如果你有 3 个标签 例如 number_of_tags 将有 为 3,连接将导致 每个 id 匹配 3 行。

您可以创建该查询 动态地,或者定义它,比如说, 10 个标签并用 不会出现在标签中的值。

【讨论】:

  • 这是一种天才的做法。 另外它让我可以做“7 个中的任何 5 个”,这非常棒。
【解决方案2】:

我会推荐 ALL 而不是 intersect 因为 mysql 实际上知道如何更好地加入这个,尽管我缺乏适当的基准。

select books.* from books, taggings, tags
 where taggings.book_id = books.id
   and taggings.tag_id  = tag.id
   and tag.name ALL("interesting", "fiction");

至于它的扩展性,有数百万本书和标签表上的低基数,您最终要做的是将标签 id 迁移到代码/内存中,以便您使用 taggings.tag_id ALL( 3, 7, 105) 什么的。获取标签表的最后一个连接不会使用索引,除非您超过 1k 个标签,因此您每次都要进行表扫描。

根据我的经验,连接、交叉点和联合对性能来说是巨大的弊端。大多数连接是我们经常遇到的问题。您拥有的连接越少,您最终获得的速度就越快。

【讨论】:

    【解决方案3】:

    这里有点“老派”的 SQL 方言,但它的语法更紧凑,仍然是内部连接。

    select * from books, taggings tg1, tags t1, taggings tg2, tags t2 
     where tg1.book_id = books.id
       and tg1.tag_id  = t1.id
       and t1.name = 'interesting'
       and tg2.book_id = books.id
       and tg2.tag_id  = t2.id
       and t2.name = 'fiction'
    

    编辑:哇,堆垛机非常讨厌在一个查询中加入太多内容。使用exists 子查询可以进行更多优化:

    select * from books
     where exists (select * from taggings, tags
                    where tags.name = 'fiction'
                      and taggings.tag_id = tags.id
                      and taggings.book_id = books.id)
       and exists (select * from taggings, tags
                    where tags.name = 'interesting'
                      and taggings.tag_id = tags.id
                      and taggings.book_id = books.id)
    

    【讨论】:

    • 不知道为什么你在这里得到了负面评论。我猜它并没有真正回答关于扩展或效率的问题,但它真的应该投反对票吗?
    • 添加连接维度的效果如何?
    • -1 表示过时的连接语法、不正确的字符串文字引号以及每本书返回两次。
    • 已编辑查询修复了原始查询中的所有问题,因此删除了反对票,但它的效率可能低于 OP 已有的。
    • 在您发表评论之前我注意到了引号问题 :-) 但我见过的大多数 SQL 实现都没有古老的内部连接语法问题。除了语法,原始查询如何返回重复的书籍,而表中没有重复的行?
    【解决方案4】:
    with
      tt as
      (
          select id
          from tags
          where name in ('interesting', 'fiction')
      ),
      mm as
      (
          select book_id
          from taggings join tt on taggings.tag_id = tt.id
          group by taggings.book_id having count(*) = 2
      )
    select books.*
    from books join mm on books.id = mm.book_id
    

    这种变体似乎比 Peter Lang 的解决方案产生了更好的执行计划(至少在 Oracle 上),原因如下(转述自 EXPLAIN PLAN):

    • tagstaggings 之间的连接是执行表到索引而不是表到表的。我不知道这是否真的会影响大型数据集的查询性能。

    • 该计划在使用 books 执行最终连接之前对数据集进行分组和计数。这肯定会影响大型数据集的性能。

    【讨论】:

    • with 无法在 MySQL 上运行,但如果真的成功了,我当然不介意切换到支持它的 PostgreSQL。另一个非常好的答案!
    • mm 子查询更改如下是否是获取匹配任何 个标签但按匹配数排序的书籍的有效方法? mm as (select book_id, COUNT(*) as taggings_count from taggings join tt on taggings.tag_id = tt.id group by taggings.book_id) select books.*, mm.taggings_count ... order by mm.taggings_count
    • James,您对mm 的更改会产生您描述的结果。顺便说一句,您可以通过在from 子句中使用嵌套子查询让 MySQL 生成类似的执行计划。
    【解决方案5】:

    什么数据库?这将稍微改变答案。例如,这适用于 sql server,并且应该更快,因为它消除了两次访问 tags 表的需要,但在 mysql 上会失败,因为 mysql 不做 CTE:

    WITH taggingNames
    AS
    (
        SELECT tag.Name, tag.tag_id, tagging.book_id
        FROM tags
        INNER JOIN taggings ON tags.tag_id = taggings.tagid
    ) 
    SELECT b.* 
    FROM books b
    INNER JOIN (
      SELECT t1.book_id
       FROM taggingNames 
       INNER JOIN taggingNames t2 ON t2.book_id = t1.book_id AND t2.Name='fiction'
       WHERE t1.Name='interesting' 
       GROUP BY t1.book_id
     ) ids ON b.book_id = ids.book_id
    

    现在我觉得我也喜欢 Peter Lang 的回答。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-02-04
      • 2021-11-03
      • 1970-01-01
      • 2013-04-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多