如何在 Postgres 的窗口函数中获取 mode()?

Posted

技术标签:

【中文标题】如何在 Postgres 的窗口函数中获取 mode()?【英文标题】:How to get mode() in a window function in Postgres? 【发布时间】:2019-08-27 18:46:06 【问题描述】:

我试图为分组数据集获取mode(),但没有对结果进行分组。 (使用 Postgres 9.5,如果需要可以升级。)

例如用户有一个“最喜欢的颜色”,并且属于一个组。获取组内具有mode()“最喜欢的颜色”的用户列表。

窗口函数适用于大多数聚合,但mode() 似乎是一个与窗口函数不兼容的例外。还有其他方法可以解决这个问题吗?到目前为止,这是我一直在玩的东西......

有效但给出分组结果,我正在寻找未分组的结果:

SELECT group_id, 
    mode() WITHIN GROUP (ORDER BY color)
FROM users
GROUP BY group_id;

无效的语法(只是我想要完成的一个例子):

SELECT id, color, group_id, 
    mode(color) OVER (PARTITION BY group_id)
FROM users;

或者:

SELECT id, color, group_id, 
    mode() WITHIN GROUP (ORDER BY color) OVER (PARTITION BY group_id)
FROM users;

我尝试使用横向连接,但如果不在连接内部和外部重新迭代我的 WHERE 子句,就无法使其正常工作(当此查询变得更复杂时,我不希望这样做):

SELECT u1.id, u1.group_id, u1.color, mode_color
FROM users u1
LEFT JOIN LATERAL
    (SELECT group_id, mode() WITHIN GROUP (ORDER BY color) as mode_color
     FROM users
     WHERE group_id = d1.group_id
     GROUP BY group_id)
    u2 ON u1.group_id = u2.group_id
WHERE u1.type = 'customer';

重要的是WHERE u1.type = 'customer' 保留在子查询之外,因为它稍后会在前半部分已经写入之后附加到查询中。

【问题讨论】:

您按type 过滤,但按group_id 聚合。这使事情变得复杂,这将有助于了解两者之间的关系。一个是另一个的子组吗?它还提出了一个问题:究竟是哪种模式?过滤之后的那个,还是之前的那个(对于同一组中的 all 行)?请始终公开您的 Postgres 版本。 【参考方案1】:

我们谈论的是 ordered-set 聚合函数 mode(),它在 Postgres 9.4 中引入。你可能看到了这个错误信息:

ERROR:  OVER is not supported for ordered-set aggregate mode

我们可以解决它。 但究竟是哪种模式?

(所有假设group_idtype都是NOT NULL,否则你需要做更多。)

合格行的模式

这仅根据过滤后的集合(使用type = 'customer')计算模式。您将获得“客户”中每组最流行的颜色。

一个普通的JOIN(在这种情况下没有LEFTLATERAL)中的子查询可以完成这项工作 - 每组计算模式一次,而不是每个单独的行:

SELECT u1.id, u1.group_id, u1.color, u2.mode_color
FROM   users u1
JOIN  (                            -- not LATERAL
   SELECT group_id, type           -- propagate out for the join
        , mode() WITHIN GROUP (ORDER BY color) AS mode_color
   FROM   users 
   WHERE  type = 'customer'        -- place condition in subquery (cheap)
   GROUP  BY group_id, type
   ) u2 USING (group_id, type);    -- shorthand syntax for matching names
-- WHERE  type = 'customer'        -- or filter later (expensive)

为避免重复您的条件,请将其放在子查询中并将其传播到连接子句中的外部查询 - 在我的示例中,我选择了匹配的列名并与 USING 连接。

可以将条件移至外部查询,甚至移至后面的步骤。但是,这将不必要地增加成本,因为必须计算(group_id, type)每个组合的模式,然后在后面的步骤中排除所有其他类型的结果。

有多种方法可以参数化您的查询。准备好的语句,PL/pgSQL 函数,见:

Split given string and prepare case statement

或者,如果基础表没有太大变化,则可以选择使用每个(group_id, type) 的所有预计算模式替换子查询的物化视图。

另一种选择:首先使用 CTE 过滤符合条件的行,然后 WHERE 条件可以保留在子查询之外,就像您要求的那样:

WITH cte AS (  -- filter result rows first
   SELECT id, group_id, color
   FROM   users u1
   WHERE  type = 'customer'        -- predicate goes here
   )
SELECT *
FROM   cte u1
LEFT   JOIN (                      -- or JOIN, doesn't matter here
   SELECT group_id
        , mode() WITHIN GROUP (ORDER BY color) AS mode_color
   FROM   cte                      -- based on only qualifying rows
   GROUP  BY 1
   ) u2 USING (group_id);

我们可以使用SELECT * 进行简化,因为USING 在结果集中只放置了一个 group_id

所有行的模式

如果您希望模式基于所有行(包括那些type = 'customer' 不为真的行),您需要一个不同的查询。您将获得所有成员中每组最受欢迎的颜色。

WHERE 子句移至外部查询:

SELECT u1.id, u1.group_id, u1.color, u2.mode_color
FROM   users u1
LEFT   JOIN (                      -- or JOIN, doesn't matter here
   SELECT group_id
        , mode() WITHIN GROUP (ORDER BY color) AS mode_color
   FROM   users
   GROUP  BY group_id
   ) u2 USING (group_id)
WHERE  u1.type = 'customer';

如果您的谓词 (type = 'customer') 有足够的选择性,那么计算所有组的模式可能是一种浪费。首先过滤小子集,只计算包含组的模式。为此添加 CTE:

WITH cte AS (  -- filter result rows first
   SELECT id, group_id, color
   FROM   users u1
   WHERE  type = 'customer'
   )
SELECT *
FROM   cte u1
LEFT   JOIN (        -- or JOIN
   SELECT group_id
        , mode() WITHIN GROUP (ORDER BY color) AS mode_color
   FROM  (SELECT DISTINCT group_id FROM cte) g  -- only relevant groups
   JOIN   users USING (group_id)                -- but consider all rows for those
   GROUP  BY 1
   ) u2 USING (group_id);

类似于上面的 CTE 查询,但基于基表中的所有组成员。

【讨论】:

感谢您的出色回复,我正在寻找“合格行的模式”,但我确实需要将条件保留在子查询之外。这就是为什么我正在研究横向连接(但也许我误解了这些连接的用途)已经写好了。 @PeanutsMcgee:已更新以解决该问题,见上文。 对不起,我应该更清楚,但我不认为这对我来说是一个解决方案,我可能只是要求一些不可能的事情。条件是动态的,它并不总是使用“类型”,它可能是“created_at”或“last_name”或其他十几种可过滤选项的任意组合。这就是为什么我强调条件不在子查询中。将子查询条件从 WHERE 移动到 GROUP BY 没有帮助。非常感谢你的努力,我学到了很多东西。 @PeanutsMcgee:我添加了一个带有 CTE 的选项。不会比这更好的了。使用横向连接计算每行将使 Postgres 完成的工作成倍增加。

以上是关于如何在 Postgres 的窗口函数中获取 mode()?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用窗口函数仅在 POSTGRES 中选择不超过某个值的行

获取表的 ID 及其模尊重 Postgres 中同一表中的总行数

Postgres - 如何对窗口函数列的每 x 行求和?

如何使用窗口函数枚举 Postgres 表中的分区组?

如何如何使用MOD函数?

使用'parititon by'和窗口函数在postgres中返回多于一行?