从每组的第一行和最后一行获取值
Posted
技术标签:
【中文标题】从每组的第一行和最后一行获取值【英文标题】:Get values from first and last row per group 【发布时间】:2014-09-29 23:15:04 【问题描述】:我是 Postgres 的新手,来自 mysql,希望大家能够帮助我。
我有一个包含三列的表:name
、week
和 value
。该表记录了姓名、他们记录身高的星期以及他们的身高值。
像这样的:
Name | Week | Value
------+--------+-------
John | 1 | 9
Cassie| 2 | 5
Luke | 6 | 3
John | 8 | 14
Cassie| 5 | 7
Luke | 9 | 5
John | 2 | 10
Cassie| 4 | 4
Luke | 7 | 4
我想要的是每个用户在最短周和最长周的值的列表。像这样的:
Name |minWeek | Value |maxWeek | value
------+--------+-------+--------+-------
John | 1 | 9 | 8 | 14
Cassie| 2 | 5 | 5 | 7
Luke | 6 | 3 | 9 | 5
在 Postgres 中,我使用这个查询:
select name, week, value
from table t
inner join(
select name, min(week) as minweek
from table
group by name)
ss on t.name = ss.name and t.week = ss.minweek
group by t.name
;
但是,我收到一个错误:
列“w.week”必须出现在 GROUP BY 子句中或用于聚合函数 职位:20
这在 MySQL 中对我来说很好,所以我想知道我在这里做错了什么?
【问题讨论】:
那么如果第二列的结果只知道AFTER分组,你怎么期望GROUP BY
第二列呢?
这句话在逻辑上没有任何意义。从 MySql 到 Postgres,你将不得不习惯这样一个事实,即你不能再做没有意义的事情。
“这对我来说在 MySQL 中工作得很好,所以我想知道我在这里做错了什么?” - MySQL 不能很好地处理分组,并且会在不返回错误的情况下做错事,而 Postgres 足够聪明,可以返回错误。它在 MySQL 上不能正常工作,它做错事没有错误
在 MySQL 中我得到ERROR 1052 (23000): Column 'name' in field list is ambiguous
与您的查询。
【参考方案1】:
这有点痛苦,因为 Postgres 有很好的窗口函数 first_value()
和 last_value()
,但这些不是聚合函数。所以,这是一种方法:
select t.name, min(t.week) as minWeek, max(firstvalue) as firstvalue,
max(t.week) as maxWeek, max(lastvalue) as lastValue
from (select t.*, first_value(value) over (partition by name order by week) as firstvalue,
last_value(value) over (partition by name order by week) as lastvalue
from table t
) t
group by t.name;
【讨论】:
它有效,但它不优雅,不友好......并且可能会失去性能(不需要max()
比较)。为什么 PostgreSQL 不使用(或其社区不喜欢)第一个/最后一个作为内置聚合函数?有external lib for fast first/last,有问题吗?
你认为这值得一些特殊的索引来更好地工作吗?我有一个索引,这里是“名称”和“周”(在我的例子中是日期),但查询在 60M 行表中需要很长时间。也许是按名称和日期的复合索引?
(name, week, value)
上的索引可能有助于查询。
注意:这并不一定适用于所有窗口函数(即总和),当某些/所有值为负数时,MAX 可能是最终记录。为了修复它,您需要将row_number()
与分区一起使用,然后使用另一个窗口获取最后一行(最高行号)。来自 erwin 的 2x Distinct 解决方案更好 imo。
@pstanton 。 . .我只是不明白你的评论。【参考方案2】:
有各种更简单、更快捷的方法。
2x DISTINCT ON
SELECT *
FROM (
SELECT DISTINCT ON (name)
name, week AS first_week, value AS first_val
FROM tbl
ORDER BY name, week
) f
JOIN (
SELECT DISTINCT ON (name)
name, week AS last_week, value AS last_val
FROM tbl
ORDER BY name, week DESC
) l USING (name);
或更短:
SELECT *
FROM (SELECT DISTINCT ON (1) name, week AS first_week, value AS first_val FROM tbl ORDER BY 1,2) f
JOIN (SELECT DISTINCT ON (1) name, week AS last_week , value AS last_val FROM tbl ORDER BY 1,2 DESC) l USING (name);
简单易懂。在我的旧测试中也是最快的。 DISTINCT ON
详解:
2x 窗口函数,1x DISTINCT ON
SELECT DISTINCT ON (name)
name, week AS first_week, value AS first_val
, first_value(week) OVER w AS last_week
, first_value(value) OVER w AS last_value
FROM tbl t
WINDOW w AS (PARTITION BY name ORDER BY week DESC)
ORDER BY name, week;
显式的WINDOW
子句只会缩短代码,对性能没有影响。
复合类型的first_value()
aggregate functions min()
or max()
不接受复合类型作为输入。您必须创建自定义聚合函数(这并不难)。
但是window functions first_value()
and last_value()
做。在此基础上,我们可以设计简单的解决方案:
简单查询
SELECT DISTINCT ON (name)
name, week AS first_week, value AS first_value
,(first_value((week, value)) OVER (PARTITION BY name ORDER BY week DESC))::text AS l
FROM tbl t
ORDER BY name, week;
输出包含所有数据,但上周的值被填充到匿名记录中(可选地转换为text
)。您可能需要分解的值。
机会主义使用表类型的分解结果
为此,我们需要一个众所周知的复合类型。修改后的表定义将允许直接使用表类型本身:
CREATE TABLE tbl (week int, value int, name text); -- optimized column order
week
和 value
排在第一位,所以现在我们可以按表类型本身进行排序:
SELECT (l).name, first_week, first_val
, (l).week AS last_week, (l).value AS last_val
FROM (
SELECT DISTINCT ON (name)
week AS first_week, value AS first_val
, first_value(t) OVER (PARTITION BY name ORDER BY week DESC) AS l
FROM tbl t
ORDER BY name, week
) sub;
用户自定义行类型的分解结果
这在大多数情况下可能是不可能的。使用CREATE TYPE
(永久)或CREATE TEMP TABLE
(在会话期间)注册复合类型:
CREATE TEMP TABLE nv(last_week int, last_val int); -- register composite type
SELECT name, first_week, first_val, (l).last_week, (l).last_val
FROM (
SELECT DISTINCT ON (name)
name, week AS first_week, value AS first_val
, first_value((week, value)::nv) OVER (PARTITION BY name ORDER BY week DESC) AS l
FROM tbl t
ORDER BY name, week
) sub;
自定义聚合函数first()
& last()
为每个数据库创建一次函数和聚合:
CREATE OR REPLACE FUNCTION public.first_agg (anyelement, anyelement)
RETURNS anyelement
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS
'SELECT $1;'
CREATE AGGREGATE public.first(anyelement) (
SFUNC = public.first_agg
, STYPE = anyelement
, PARALLEL = safe
);
CREATE OR REPLACE FUNCTION public.last_agg (anyelement, anyelement)
RETURNS anyelement
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS
'SELECT $2';
CREATE AGGREGATE public.last(anyelement) (
SFUNC = public.last_agg
, STYPE = anyelement
, PARALLEL = safe
);
然后:
SELECT name
, first(week) AS first_week, first(value) AS first_val
, last(week) AS last_week , last(value) AS last_val
FROM (SELECT * FROM tbl ORDER BY name, week) t
GROUP BY name;
可能是最优雅的解决方案。使用提供 C 实现的 additional module first_last_agg
更快。
比较instructions in the Postgres Wiki。
相关:
Calculating follower growth over time for each influencerdbfiddle here(显示全部)旧 sqlfiddle
在使用EXPLAIN ANALYZE
对具有 50k 行的表进行快速测试时,这些查询中的每一个都比当前接受的答案快得多。
还有更多方法。根据数据分布,不同的查询样式可能会(很多)更快。见:
Optimize GROUP BY query to retrieve latest row per user【讨论】:
在上面问过,但在这里重复这个特定的答案:你认为这值得一些特定的索引来更好地工作吗?我有一个索引,这里是“名称”和“周”(在我的例子中是日期),但查询在 60M 行表中需要很长时间。也许是按名称和日期按顺序排列的复合索引? @FerminSilva:考虑这个相关答案,深入讨论“每组最大 n 个”问题的性能:"Optimize GROUP BY query to retrieve latest record per user" - 包括优化和索引的方法。 “2x 窗口函数”看起来最优雅。有什么理由不使用它吗?您所说的“对性能没有影响”是什么意思? @Bergi:与每个窗口函数(重复)使用OVER (...)
拼写窗口子句相比,显式的WINDOW
子句“对性能没有影响”。 “2x 窗口函数”很优雅,但首先为每一行计算窗口函数的成本更高,只是为了在下一步中消除每组的重复项。其他变体更快。
@ErwinBrandstetter 可惜不能一起做:-/ 我想这需要first_value
作为聚合函数,对吧? (就像在您的链接答案中一样)是否有将其添加为本机功能的动议?以上是关于从每组的第一行和最后一行获取值的主要内容,如果未能解决你的问题,请参考以下文章