计算每个影响者随时间的追随者增长

Calculating follower growth over time for each influencer

我每天都有一个 table 有影响力的人和他们的追随者计数器:

influencer_id |     date     |    followers
     1        | 2020-05-29   |      7361
     1        | 2020-05-28   |      7234
                    ...
     2        | 2020-05-29   |       82
     2        | 2020-05-28   |       85
                    ...
     3        | 2020-05-29   |      3434
     3        | 2020-05-28   |      2988
     3        | 2020-05-27   |      2765
                    ...

假设我想计算每个有影响力的人在过去 7 天内获得了多少粉丝,并得到以下 table:

influencer_id |                       growth
     1        |  <num followers last day - num followers first day>
     2        |                         "
     3        |                         "

作为第一次尝试,我这样做了:

SELECT influencer_id,
      (MAX(followers) - MIN(followers)) AS growth
FROM influencer_follower_daily
WHERE date < '2020-05-30'
AND date >= '2020-05-23'
GROUP BY influencer_id;

这有效并显示了每个影响者在一周内的增长。但它假设关注者人数始终增加并且人们永远不会取消关注!

那么有没有一种方法可以通过对原始 table 的 SQL 查询来实现我想要的?或者我是否必须使用计算每个日期之间的 +/- 关注者更改列的 FOR 循环生成一个全新的 table?

Postgres 没有 first()/last() 聚合函数。一种方法是:

SELECT DISTINCT influencer_id,
       ( FIRST_VALUE(followers) OVER (PARTITION BY influencer_id ORDER BY DATE DESC) -
         FIRST_VALUE(followers) OVER (PARTITION BY influencer_id ORDER BY DATE ASC)
       ) as growth
FROM influencer_follower_daily
WHERE date < '2020-05-30' AND date >= '2020-05-23';

另一种方法是使用数组:

SELECT influencer_id,
       ( ARRAY_AGG(followers ORDER BY DATE DESC) )[1] -
         ARRAY_AGG(followers ORDER BY DATE ASC) )[1]
       ) as growth
FROM influencer_follower_daily
WHERE date < '2020-05-30' AND date >= '2020-05-23'
GROUP BY influencer_id;

标准 Postgres 中未实现简单聚合函数 first()last()。但见下文。

1。 array_agg()

使用 array_agg() 的查询,但这比必要的要昂贵,尤其是每组有很多行。调用两次时更是如此,并且每个聚合使用 ORDER BY。这个等效的替代方案应该显着更快:

SELECT influencer_id, arr[array_upper(arr, 1)] - arr[1]
FROM  (
   SELECT influencer_id, array_agg(followers) AS arr
   FROM  (
      SELECT influencer_id, followers
      FROM   influencer_follower_daily
      WHERE  date >= '2020-05-23'
      AND    date <  '2020-05-30'
      ORDER  BY influencer_id, date
      ) sub1
   GROUP  BY influencer_id
   ) sub2;

因为它排序一次并聚合一次。内部子查询 sub1 的排序顺序被带到下一级。参见:

索引 事项:

  • 如果您查询整个 table 或其中的大部分,(influencer_id, date, followers) 上的 index 可以帮助(很多)仅索引扫描。

  • 如果您只查询 table 的一小部分,(date)(date, influencer_id, followers) 上的 index 可以帮助(很多)。

2。 DISTINCT & window 函数

Gordon 还展示了 DISTINCT 和 window 功能。同样,可以显着更快:

SELECT DISTINCT ON (influencer_id)
       influencer_id
     , last_value(followers) OVER (PARTITION BY influencer_id ORDER BY date
                                   ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
     - followers AS growth
FROM   influencer_follower_daily
WHERE  date >= '2020-05-23'
AND    date <  '2020-05-30'
ORDER  BY influencer_id, date;

使用 single window 函数,使用与主查询相同的排序顺序 (!)。为此,我们需要非默认 window 定义 ROWS BETWEEN ... 请参阅:

DISTINCT ON 而不是 DISTINCT。参见:

  • Select first row in each GROUP BY group?

3。自定义聚合函数

first()last()

你可以自己添加,很简单。参见 instructions in the Postgres Wiki
或者安装 additional module first_last_agg 在 C 中实现更快

相关:

那么你的查询就变得更简单了:

SELECT influencer_id, last(followers) - first(followers) AS growth
FROM  (
   SELECT influencer_id, followers
   FROM   influencer_follower_daily 
   WHERE  date >= '2020-03-02'
   AND    date <  '2020-05-09'
   ORDER  BY influencer_id, date
   ) z
GROUP  BY influencer_id
ORDER  BY influencer_id;

自定义聚合growth()

您可以将 first()last() 组合在一个聚合函数中。这样速度更快,但调用两个 C 函数的性能仍优于一个自定义 SQL 函数。

基本上将我的第一个查询的逻辑封装在自定义聚合中:

CREATE OR REPLACE FUNCTION f_growth(anyarray)
  RETURNS anyelement LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE AS
'SELECT [array_upper(, 1)] - [1]';

CREATE OR REPLACE AGGREGATE growth(anyelement) (
   SFUNC     = array_append
 , STYPE     = anyarray
 , FINALFUNC = f_growth
 , PARALLEL  = SAFE
);

适用于任何数字类型(或任何带有运算符 type - type 返回相同类型的类型)。查询更简单,但是:

SELECT influencer_id, growth(followers)
FROM  (
   SELECT influencer_id, followers
   FROM   influencer_follower_daily 
   WHERE  date >= '2020-05-23'
   AND    date <  '2020-05-30'
   ORDER  BY influencer_id, date
   ) z
GROUP  BY influencer_id
ORDER  BY influencer_id;

还是慢一点,但最终还是短了:

SELECT influencer_id, growth(followers ORDER BY date)
FROM   influencer_follower_daily 
WHERE  date >= '2020-05-23'
AND    date <  '2020-05-30'
GROUP  BY 1
ORDER  BY 1;

db<>fiddle here

4。每组许多行的性能优化

每个组/分区有 许多 行,其他查询技术可以(很多)更快。沿着这些方向的技术:

  • Optimize GROUP BY query to retrieve latest row per user

如果适用,我建议您提出一个新问题,披露确切的 table 定义和基数 ...


密切相关:

  • Get values from first and last row per group
  • PostgreSQL: joining arrays within group by clause
  • Best performance in sampling repeated value from a grouped column