递归查询中不允许使用聚合函数。是否有其他方法来编写此查询?

Aggregate functions are not allowed in a recursive query. Is there an alternative way to write this query?

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);

我们想要一个包含其他类别的 'super' 类别:

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

对于 'super' 类别,运动员的排名应基于他们在各个类别中的平均排名,而不是这些类别中的基础运动。即它应该只考虑它的直接子代,而不是扩展所有的运动。

我尽力写了一个查询来计算这些排名。这是一个递归查询,从底部的运动开始,通过成员资格向上计算类别和 'super' 类别的排名。这是我的查询:

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,查询将执行并且结果对于运动来说看起来是正确的,它包括类别的预期行数。

我想过也许可以尝试使用嵌套查询将递归展开到两个或三个级别,因为这对我的用例来说很好,但我想在尝试之前我会先在这里问一下。

另一种选择可能是更改架构,使其灵活性降低,从而使运动不能属于多个类别。我不确定查询在那种情况下会是什么样子,但它可能更简单?

提前致谢,非常感谢。

不太好,但我找到了解决方案:

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),而是使用在相同列上分区的 window 函数。这与计算平均排名的效果相同。

一个缺点是此计算发生的次数超过了必要的次数。如果我们可以 GROUP BY 然后 avg(r.rank),那将比 avg(r.rank) 然后 GROUP BY.

更有效率

由于嵌套查询的结果中现在有重复项,我使用 DISTINCT 过滤掉这些,然后外部查询计算每个 [=] 中所有运动员的 RANK() 19=] 基于这些平均值。

我仍然很想知道是否有人知道更好的方法。谢谢

正如您所描述的,聚合函数可以通过 distinct + analytics 来模仿。 此外,同样可以仅通过分析来完成 - 通过为每个组过滤 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 也不允许在递归成员中使用 distinct。