递归查询中不允许使用聚合函数。有没有另一种方法来编写这个查询?
Posted
技术标签:
【中文标题】递归查询中不允许使用聚合函数。有没有另一种方法来编写这个查询?【英文标题】:Aggregate functions are not allowed in a recursive query. Is there an alternative way to write this query? 【发布时间】:2019-12-09 11:58:09 【问题描述】:TL;DR 我不知道如何编写在递归部分不使用聚合函数的递归 Postgres 查询。有没有另一种方法来编写如下所示的递归查询?
假设我们有一些运动:
CREATE TABLE sports (id INTEGER, name TEXT);
INSERT INTO sports VALUES (1, '100 meter sprint');
INSERT INTO sports VALUES (2, '400 meter sprint');
INSERT INTO sports VALUES (3, '50 meter swim');
INSERT INTO sports VALUES (4, '100 meter swim');
以及参加这些运动的运动员的一些圈速:
CREATE TABLE lap_times (sport_id INTEGER, athlete TEXT, seconds NUMERIC);
INSERT INTO lap_times VALUES (1, 'Alice', 10);
INSERT INTO lap_times VALUES (1, 'Bob', 11);
INSERT INTO lap_times VALUES (1, 'Claire', 12);
INSERT INTO lap_times VALUES (2, 'Alice', 40);
INSERT INTO lap_times VALUES (2, 'Bob', 38);
INSERT INTO lap_times VALUES (2, 'Claire', 39);
INSERT INTO lap_times VALUES (3, 'Alice', 25);
INSERT INTO lap_times VALUES (3, 'Bob', 23);
INSERT INTO lap_times VALUES (3, 'Claire', 24);
INSERT INTO lap_times VALUES (4, 'Alice', 65);
INSERT INTO lap_times VALUES (4, 'Bob', 67);
INSERT INTO lap_times VALUES (4, 'Claire', 66);
我们想创建一些任意类别:
CREATE TABLE categories (id INTEGER, name TEXT);
INSERT INTO categories VALUES (1, 'Running');
INSERT INTO categories VALUES (2, 'Swimming');
INSERT INTO categories VALUES (3, '100 meter');
让我们的运动成为这些类别的成员:
CREATE TABLE memberships (category_id INTEGER, member_type TEXT, member_id INTEGER);
INSERT INTO memberships VALUES (1, 'Sport', 1);
INSERT INTO memberships VALUES (1, 'Sport', 2);
INSERT INTO memberships VALUES (2, 'Sport', 3);
INSERT INTO memberships VALUES (2, 'Sport', 4);
INSERT INTO memberships VALUES (3, 'Sport', 1);
INSERT INTO memberships VALUES (3, 'Sport', 4);
我们想要一个包含其他类别的“超级”类别:
INSERT INTO categories VALUES (4, 'Running + Swimming');
INSERT INTO memberships VALUES (4, 'Category', 1);
INSERT INTO memberships VALUES (4, 'Category', 2);
现在是棘手的一点。
我们希望根据运动员在每项运动中的单圈时间对他们进行排名:
SELECT sport_id, athlete,
RANK() over(PARTITION BY sport_id ORDER BY seconds)
FROM lap_times lt;
但我们也希望在类别级别上做到这一点。当我们这样做时,运动员的排名应该基于他们在该类别中所有运动的平均排名。例如:
Alice is 1st in 100 meter sprint and 3rd in 400 meter sprint
-> average rank: 2
Bob is 2nd in 100 meter sprint and 1st in 400 meter sprint
-> average rank: 1.5
Claire is 3rd in 100 meter sprint and 2nd in 400 meter sprint
-> average rank: 2.5
Ranking for running: 1st Bob, 2nd Alice, 3rd Claire
对于“超级”类别,运动员的排名应该基于他们在各个类别中的平均排名,而不是这些类别中的基础运动。即它应该只考虑它的直接孩子,而不是扩展所有的运动。
我尽力编写查询来计算这些排名。这是一个递归查询,从运动的底部开始,然后通过会员资格向上计算类别和“超级”类别的排名。这是我的查询:
WITH RECURSIVE rankings(rankable_type, rankable_id, athlete, value, rank) AS (
SELECT 'Sport', sport_id, athlete, seconds, RANK() over(PARTITION BY sport_id ORDER BY seconds)
FROM lap_times lt
UNION ALL
SELECT 'Category', category_id, athlete, avg(r.rank), RANK() OVER (PARTITION by category_id ORDER BY avg(r.rank))
FROM categories c
JOIN memberships m ON m.category_id = c.id
JOIN rankings r ON r.rankable_type = m.member_type AND r.rankable_id = m.member_id
GROUP BY category_id, athlete
)
SELECT * FROM rankings;
但是,当我运行它时,我收到以下错误:
ERROR: aggregate functions are not allowed in a recursive query's recursive term
这是由查询的递归部分中的avg(r.rank)
引起的。 Postgresql 不允许在查询的递归部分调用聚合函数。有没有其他的写法?
如果我将avg(r.rank), RANK() ...
换成NULL, NULL
,查询就会执行,结果对于运动来说看起来是正确的,并且它包括预期的类别行数。
我考虑过可能尝试使用嵌套查询将递归展开到两个或三个级别,因为这对我的用例来说很好,但我想在尝试之前我会先在这里询问。
另一种选择可能是更改架构,使其不太灵活,因此运动不能属于多个类别。我不确定在这种情况下查询会是什么样子,但它可能更简单?
提前致谢,非常感谢。
【问题讨论】:
我可能会先构建类别树,然后在单独的 CTEwith recursive cat_tree as (...), aggregates as (...) select * from aggregates
中进行聚合
【参考方案1】:
正如您所描述的,聚合函数可以通过 distinct + 分析来模仿。 此外,仅分析也可以做到这一点 - 通过为每个组过滤 1 行。
WITH RECURSIVE rankings(rankable_type, rankable_id, athlete, value, rank) AS (
SELECT 'Sport', sport_id, athlete, seconds, RANK() over(PARTITION BY sport_id ORDER BY seconds)
FROM lap_times lt
UNION ALL
SELECT 'Category', category_id, athlete, avg_rank, rank() OVER(PARTITION by category_id ORDER BY avg_rank) FROM (
SELECT category_id, athlete, avg(r.rank) OVER (PARTITION by category_id, athlete) AS avg_rank,
row_number() over (partition by category_id, athlete order by '') rn
FROM categories c
JOIN memberships m ON m.category_id = c.id
JOIN rankings r ON r.rankable_type = m.member_type AND r.rankable_id = m.member_id
) _
where rn = 1
)
SELECT * FROM rankings;
这几乎是相同的方法,但看起来有点尴尬。
我看不出聚合函数不能在引用递归成员的查询块中使用的根本原因,但这不仅是 PG 的限制。 MSSQL 和 Oracle 中存在相同的限制,但与 PG 不同,这两个 RBDMS 也不允许递归成员中的不同。
【讨论】:
谢谢你——虽然几年后我忘记了很多细节。很高兴您只能选择一行来避免我描述的缺点。【参考方案2】:不漂亮,但我找到了解决办法:
WITH RECURSIVE rankings(rankable_type, rankable_id, athlete, value, rank) AS (
SELECT 'Sport', sport_id, athlete, seconds, RANK() over(PARTITION BY sport_id ORDER BY seconds)
FROM lap_times lt
UNION ALL
SELECT 'Category', *, rank() OVER(PARTITION by category_id ORDER BY avg_rank) FROM (
SELECT DISTINCT category_id, athlete, avg(r.rank) OVER (PARTITION by category_id, athlete) AS avg_rank
FROM categories c
JOIN memberships m ON m.category_id = c.id
JOIN rankings r ON r.rankable_type = m.member_type AND r.rankable_id = m.member_id
) _
)
SELECT * FROM rankings;
在查询的递归部分,我没有调用GROUP BY
并计算avg(r.rank)
,而是使用在相同列上分区的窗口函数。这与计算平均排名的效果相同。
一个缺点是这种计算发生的次数超出了必要的次数。如果我们可以GROUP BY
然后avg(r.rank)
,那将比avg(r.rank)
然后GROUP BY
更有效。
由于现在嵌套查询的结果中有重复项,我使用DISTINCT
将它们过滤掉,然后外部查询根据这些平均值计算每个category_id
中所有运动员的RANK()
。
我仍然很想知道是否有人知道更好的方法来做到这一点。谢谢
【讨论】:
以上是关于递归查询中不允许使用聚合函数。有没有另一种方法来编写这个查询?的主要内容,如果未能解决你的问题,请参考以下文章
在 mysql 中不允许使用 FETCH 子句。改用啥替代方法