在Postgres中将多个子查询折叠为一个

时间:2011-08-23 17:44:23

标签: sql database database-design postgresql

我有两张桌子:

CREATE TABLE items
(
  root_id integer NOT NULL,
  id serial NOT NULL,
  -- Other fields...

  CONSTRAINT items_pkey PRIMARY KEY (root_id, id)
)

CREATE TABLE votes
(
  root_id integer NOT NULL,
  item_id integer NOT NULL,
  user_id integer NOT NULL,
  type smallint NOT NULL,
  direction smallint,

  CONSTRAINT votes_pkey PRIMARY KEY (root_id, item_id, user_id, type),
  CONSTRAINT votes_root_id_fkey FOREIGN KEY (root_id, item_id)
      REFERENCES items (root_id, id) MATCH SIMPLE
      ON UPDATE CASCADE ON DELETE CASCADE,
  -- Other constraints...
)

我正在尝试在单个查询中提取特定root_id的所有项目以及以特定方式投票的用户的几个user_id数组。以下查询可以满足我的需求:

SELECT *,
  ARRAY(SELECT user_id from votes where root_id = i.root_id AND item_id = i.id AND type = 0 AND direction = 1) as upvoters,
  ARRAY(SELECT user_id from votes where root_id = i.root_id AND item_id = i.id AND type = 0 AND direction = -1) as downvoters,
  ARRAY(SELECT user_id from votes where root_id = i.root_id AND item_id = i.id AND type = 1) as favoriters
FROM items i
WHERE root_id = 1
ORDER BY id

问题是我正在使用三个子查询来获取我需要的信息,因为我似乎应该能够在一个中执行相同的操作。我认为Postgres(我使用8.4)可能足够聪明,可以将它们全部折叠成一个查询给我,但是看看pgAdmin中的explain输出看起来没有发生 - 它正在运行多个主键查找而是表。我觉得我可以重写这个查询以提高效率,但我不确定如何。

任何指针?

编辑:更新解释我现在的位置。在pgsql-general邮件列表的建议下,我尝试将查询更改为使用CTE:

WITH v AS (
  SELECT item_id, type, direction, array_agg(user_id) as user_ids
  FROM votes
  WHERE root_id = 5305
  GROUP BY type, direction, item_id
  ORDER BY type, direction, item_id
)
SELECT *,
  (SELECT user_ids from v where item_id = i.id AND type = 0 AND direction = 1) as upvoters,
  (SELECT user_ids from v where item_id = i.id AND type = 0 AND direction = -1) as downvoters,
  (SELECT user_ids from v where item_id = i.id AND type = 1) as favoriters
FROM items i
WHERE root_id = 5305
ORDER BY id

从我的应用程序中对这些中的每一个进行基准测试(我将每个设置为准备好的语句,以避免花时间进行查询规划,然后使用各种root_id运行每个数千次)我的初始方法平均为15毫秒并且CTE接近平均17毫秒。我能够在几次运行中重复这个结果。

当我有一段时间我会用我的测试数据玩jkebinger和Dragontamer5788的方法,看看它们是如何工作的,但我也开始赏金,看看能不能得到更多的建议。

我还应该提一下,如果它可以加快这个查询,我可以改变我的架构(系统尚未投入生产,并且不会持续几个月)。我以这种方式设计了我的投票表,以利用主键的唯一性约束 - 例如,给定的用户既可以喜欢也可以投票赞成一个项目,但不能投票支持它并向下投票 - 但我可以放松/解决这个约束,如果代表这些选项以不同的方式更有意义。

编辑#2:我对所有四种解决方案进行了基准测试。令人惊讶的是,Sequel足够灵活,我能够写入所有四个而不会掉到SQL一次(甚至不用于CASE语句)。像以前一样,我将它们全部作为预处理语句运行,这样查询计划时间就不会成为问题,并且每次都运行数千次。然后我在两种情况下运行所有​​查询 - 最坏情况下有很多行(265项和4911票),其中相关行很快就会在缓存中,所以CPU使用率应该是决定因素而且更多为每次运行选择随机root_id的现实场景。我结束了:

Original query  - Typical: ~10.5 ms, Worst case: ~26 ms
CTE query       - Typical: ~16.5 ms, Worst case: ~70 ms
Dragontamer5788 - Typical: ~15 ms,   Worst case: ~36 ms
jkebinger       - Typical: ~42 ms,   Worst case: ~180 ms

我想从现在开始的教训是,Postgres的查询规划器非常聪明,可能在表面上做了一些聪明的事情。我不认为我会花更多的时间来解决它。如果有人想提交另一个查询尝试,我会很乐意对其进行基准测试,但除此之外,我认为Dragontamer是赏金和正确(或最接近正确)答案的赢家。除非其他人能够了解Postgres正在做的事情 - 这将是非常酷的。 :)

3 个答案:

答案 0 :(得分:3)

有两个问题被问到:

  1. 将多个子查询合并为一个子语句的语法。
  2. 优化。
  3. 对于#1,我无法将“完整”的东西变成单个Common Table Expression,因为您在每个项目上使用了相关的子查询。但是,如果使用公用表表达式,则可能会有一些好处。显然,这将取决于数据,所以请进行基准测试以确定它是否会有所帮助。

    对于#2,因为表中有三个常用的“类”项,我希望partial indexes可以提高查询速度,无论您是否能够提高速度到#1。

    首先,简单的东西然后。要为此表添加部分索引,我会这样做:

    CREATE INDEX upvote_vote_index ON votes (type, direction)
    WHERE (type = 0 AND direction = 1);
    
    CREATE INDEX downvote_vote_index ON votes (type, direction)
    WHERE (type = 0 AND direction = -1);
    
    CREATE INDEX favoriters_vote_index ON votes (type)
    WHERE (type = 1);
    

    这些索引越小,查询的效率就越高。不幸的是,在我的测试中,它们似乎没有帮助:-(尽管如此,也许你可以找到它们的使用,它在很大程度上取决于你的数据。


    至于整体优化,我会以不同的方式解决问题。我将查询“展开”到这个表单中(使用内部联接并使用conditional expressions“分割”三种类型的投票),然后使用“Group By”和“array”聚合运算符结合他们。 IMO,我宁愿更改我的应用程序代码以“展开”形式接受它,但如果你不能更改应用程序代码,那么“group by”+聚合函数应该可以工作。

    SELECT array_agg(v.user_id), -- array_agg(anything else you needed), 
        i.root_id, i.id, -- I presume you needed the primary key?
    CASE
        WHEN v.type = 0 AND v.direction = 1
            THEN 'upvoter'
        WHEN v.type = 0 AND v.direction = -1
            THEN 'downvoter'
        WHEN v.type = 1
            THEN 'favoriter'
    END as vote_type
    FROM items i 
        JOIN votes v ON i.root_id = v.root_id AND i.id = v.item_id
    WHERE i.root_id = 1 
      AND ((type=0 AND (direction=1 OR direction=-1)) 
           OR type=1)
    GROUP BY i.root_id, i.id, vote_type
    ORDER BY id
    

    与您的代码相比,它仍然“一步展开”(vote_type是垂直的,而在您的情况下,它是水平的,跨列)。但这似乎更有效率。

答案 1 :(得分:0)

只是一个猜测,但也许值得尝试:

如果您创建VIEW

,也许sql可以优化查询

SELECT user_id from votes where root_id = i.root_id AND item_id = i.id

然后从那里选择3次,其中有关于类型和方向的不同where子句。

如果没有帮助,也许你可以将3种类型作为额外的布尔列获取,然后只使用一个查询?

如果您找到解决方案,将有兴趣听到。祝你好运。

答案 2 :(得分:0)

这是另一种方法。它具有(可能)在数组中包含NULL值的不良结果,但它在一次传递中起作用,而不是三次。我发现以map-reduce方式思考一些SQL查询很有帮助,而case语句对此非常有用。

select
v.root_id, v.item_id,
array_agg(case when type = 0 AND direction = 1 then user_id else NULL end) as upvoters,
array_agg(case when type = 0 AND direction = -1 then user_id else NULL end) as downvoters,
array_agg(case when type = 1 then user_id else NULL end) as favoriters
from items i
join votes v on i.root_id = v.root_id AND i.id = v.item_id
group by 1, 2

通过一些示例数据,我得到了这个结果集:

 root_id | item_id |    upvoters    |    downvoters    |    favoriters    
---------+---------+----------------+------------------+------------------
       1 |       2 | {100,NULL,102} | {NULL,101,NULL}  | {NULL,NULL,NULL}
       2 |       4 | {100,NULL,101} | {NULL,NULL,NULL} | {NULL,100,NULL}

我相信你需要postgres 8.4来获取array_agg,但是之前有一个array_accum函数的配方。

如果您有兴趣,有关postgres-hackers list关于如何构建一个删除NULL的array_agg版本的讨论。