【问题标题】:Effective query merging more than 2 subqueries合并两个以上子查询的有效查询
【发布时间】:2012-04-23 00:59:26
【问题描述】:

我有一个数据库

books          (primary key: bookID)
characterNames (foreign key: books.bookID) 
locations      (foreign key: books.bookID)

字符名称和位置的文本位置保存在相应的表中。
我正在使用 psycopg2 编写 Pythonscript,查找书中给定字符名称和位置的所有出现。我只想要书中的出现,同时找到角色名称和位置。
Here我已经找到了搜索一个位置和一个角色的解决方案:

WITH b AS (  
    SELECT bookid  
    FROM   characternames  
    WHERE  name = 'XXX'  
    GROUP  BY 1  
    INTERSECT  
    SELECT bookid  
    FROM   locations  
    WHERE  l.locname = 'YYY'  
    GROUP  BY 1  
    )  
SELECT bookid, position, 'char' AS what  
FROM   b  
JOIN   characternames USING (bookid)  
WHERE  name = 'XXX'  
UNION  ALL  
SELECT bookid, position, 'loc' AS what  
FROM   b  
JOIN   locations USING (bookid)  
WHERE  locname = 'YYY'  
ORDER  BY bookid, position;  

CTE 'b' 包含所有 bookid,其中出现了字符名称 'XXX' 和位置 'YYY'。

现在我还想知道搜索 2 个地点和一个名称(或分别搜索 2 个名称和一个地点)。如果所有搜索的实体都必须出现在一本书中,这很简单,但是这样呢:
搜索:Tim、Al、Toolshop 结果:书籍包括
(蒂姆、艾尔、工具店)或
(蒂姆,艾尔)或
(蒂姆,工具店)或
(艾尔,工具店)

问题可能会在 4、5、6...条件下重复出现。
我想 INTERSECTing 更多子查询,但这行不通。
相反,我会将找到的 bookID 合并,将它们分组并选择 bookid 的出现不止一次:

WITH b AS (  
    SELECT bookid, count(bookid) AS occurrences  
    FROM  
        (SELECT DISTINCT bookid  
        FROM characterNames  
        WHERE name='XXX'  
        UNION  
        SELECT DISTINCT bookid  
        FROM characterNames  
        WHERE name='YYY'  
        UNION  
        SELECT DISTINCT bookid  
        FROM locations  
        WHERE locname='ZZZ'  
        GROUP BY bookid)  
    WHERE occurrences>1)  

我认为这可行,目前无法测试,但这是最好的方法吗?

【问题讨论】:

  • +1 你付出了努力。 SO上的每个人都喜欢一个格式清晰、措辞清晰的问题。
  • @ErwinBrandstetter:谢谢(=

标签: python postgresql psycopg2


【解决方案1】:

对一般情况使用计数的想法是合理的。不过,对语法进行了一些调整:

WITH b AS (  
   SELECT bookid
   FROM  (
      SELECT DISTINCT bookid  
      FROM   characterNames  
      WHERE  name='XXX'  

      UNION ALL  
      SELECT DISTINCT bookid  
      FROM   characterNames  
      WHERE  name='YYY'  

      UNION ALL
      SELECT DISTINCT bookid  
      FROM   locations  
      WHERE  locname='ZZZ'  
      ) x
   GROUP  BY bookid
   HAVING count(*) > 1
   )
SELECT bookid, position, 'char' AS what
FROM   b
JOIN   characternames USING (bookid)
WHERE  name = 'XXX'

UNION  ALL
SELECT bookid, position, 'loc' AS what
FROM   b
JOIN   locations USING (bookid)
WHERE  locname = 'YYY'
ORDER  BY bookid, position;

注意事项

  • 使用UNION ALL(不是UNION)来保留子查询之间的重复。在这种情况下,您希望它们能够计数。

  • 子查询应该产生不同的值。它以您拥有的方式与DISTINCT 一起使用。您可能想改用GROUP BY 1,看看它是否表现更好(我不希望它这样做。)

  • GROUP BY 必须超出子查询。它只会应用于最后一个子查询,并且没有任何意义,因为您已经拥有DISTINCT bookid

  • 检查一本书是否有多个点击必须进入HAVING子句:

     HAVING count(*) > 1
    

    您不能在WHERE 子句中使用聚合值。


在一张表上组合条件

您不能简单地在一张表上组合多个条件。您将如何计算发现的数量?但是有一种更复杂的方法。可能会或可能不会提高性能,您必须进行测试(使用EXPLAIN ANALYZE)。这两个查询都需要对表characterNames 至少进行两次索引扫描。至少它缩短了语法。

考虑我如何计算characterNames 的点击次数,以及如何在外部SELECT 中更改为sum(hits)

WITH b AS (  
   SELECT bookid
   FROM  (
      SELECT bookid
           , max((name='XXX')::int)
           + max((name='YYY')::int) AS hits
      FROM   characterNames  
      WHERE  (name='XXX'
           OR name='YYY')
      GROUP  BY bookid

      UNION ALL
      SELECT DISTINCT bookid, 1 AS hits  
      FROM   locations  
      WHERE  locname='ZZZ'  
      ) x
   GROUP  BY bookid
   HAVING sum(hits) > 1
   )
...

boolean 转换为integer 得到0 对应FALSE1 对应TRUE。这有帮助。


EXISTS 更快

骑自行车去公司时,这件事一直在我的后脑勺上踢。我有理由相信这个查询可能会更快。请试一试:

WITH b AS (  
   SELECT bookid

        , (EXISTS (
            SELECT *
            FROM   characterNames c
            WHERE  c.bookid = b.bookid
            AND    c.name = 'XXX'))::int
        + (EXISTS (
            SELECT *
            FROM   characterNames c
            WHERE  c.bookid = b.bookid
            AND    c.name = 'YYY'))::int AS c_hits

        , (EXISTS (
            SELECT *
            FROM   locations l
            WHERE  l.bookid = b.bookid
            AND    l.locname='ZZZ'))::int AS l_hits
   FROM   books b  
   WHERE  (c_hits + l_hits) > 1
   )
SELECT c.bookid, c.position, 'char' AS what
FROM   b
JOIN   characternames c USING (bookid)
WHERE  b.c_hits > 0
AND    c.name IN ('XXX', 'YYY')

UNION  ALL
SELECT l.bookid, l.position, 'loc' AS what
FROM   b
JOIN   locations l USING (bookid)
WHERE  b.l_hits > 0
AND    l.locname = 'YYY'
ORDER  BY 1,2,3;
  • EXISTS 半连接可以在第一次匹配时停止执行。由于我们只对 CTE 中全有或全无的答案感兴趣,因此这可能会更快更快地完成这项工作。

  • 这样我们也不需要聚合(不需要GROUP BY)。

  • 我还记得是否找到任何个字符或位置,并且只重新访问实际匹配的表格。

【讨论】:

  • 啊,是的...HAVING 语句,我记得(= HAVING 语句是否需要'count(*) AS 出现次数'?再说一次,我只需要ID
  • @EarlGrey:如果您只需要 ID,则也不需要 CTE。我修剪了查询中不需要的部分。
  • 抱歉,我的意思是我只需要 CTE 中的 ID。我的脚本仍然需要 ID 和出现的位置,但不需要“出现”
  • @EarlGrey:啊,我明白了。回滚了我的最后一次编辑并进行了相应的调整。
  • +1 好的,非常感谢。我在编写 (postgre)SQL 查询方面的技能受益匪浅。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-06-16
相关资源
最近更新 更多