我有两张桌子:
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正在做的事情 - 这将是非常酷的。 :)
答案 0 :(得分: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
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版本的讨论。