如何在 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_id
和type
都是NOT NULL
,否则你需要做更多。)
合格行的模式
这仅根据过滤后的集合(使用type = 'customer'
)计算模式。您将获得“客户”中每组最流行的颜色。
一个普通的JOIN
(在这种情况下没有LEFT
和LATERAL
)中的子查询可以完成这项工作 - 每组计算模式一次,而不是每个单独的行:
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 中选择不超过某个值的行