从每组的第一行和最后一行获取值

Posted

技术标签:

【中文标题】从每组的第一行和最后一行获取值【英文标题】:Get values from first and last row per group 【发布时间】:2014-09-29 23:15:04 【问题描述】:

我是 Postgres 的新手,来自 mysql,希望大家能够帮助我。

我有一个包含三列的表:nameweekvalue。该表记录了姓名、他们记录身高的星期以及他们的身高值。 像这样的:

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详解:

Select first row in each GROUP BY group?

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

weekvalue 排在第一位,所以现在我们可以按表类型本身进行排序:

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 influencer

dbfiddle 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 作为聚合函数,对吧? (就像在您的链接答案中一样)是否有将其添加为本机功能的动议?

以上是关于从每组的第一行和最后一行获取值的主要内容,如果未能解决你的问题,请参考以下文章

获取每组的最后一行

获取第n个连续组的第一行/最后一行

窗口函数从每个组中获取第一行和最后一行

如何将熊猫数据框值除以每组的第一行?

DolphinDB:如何获取每个滑动组的最大值的第一行?

BASH如何从每一行中获取最小值