【问题标题】:Calculating follower growth over time for each influencer计算每个影响者随时间推移的追随者增长
【发布时间】:2020-09-21 04:15:47
【问题描述】:

我每天都有一张表,上面有影响者和他们的追随者计数器:

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 天内获得的关注者数量,并得到下表:

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;

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

那么有没有办法在原始表上使用 SQL 查询来实现我想要的?或者我是否必须使用 FOR 循环生成一个全新的表,该循环计算每个日期之间的 +/- 追随者更改列?

【问题讨论】:

    标签: sql database postgresql aggregate-functions greatest-n-per-group


    【解决方案1】:

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

    1。 array_agg()

    Gordon demonstratedarray_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 的排序顺序被传递到下一个级别。见:

    索引很重要:

    • 如果您查询整个表或大部分表,(influencer_id, date, followers) 上的索引 可以(很大)帮助进行仅索引扫描。

    • 如果您只查询表的一小部分,(date)(date, influencer_id, followers) 上的索引 会有所帮助(很多)。

    2。 DISTINCT & 窗口函数

    Gordon 还演示了 DISTINCT 的窗口函数。同样,可以明显更快

    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 窗口函数,使用与主查询相同的排序顺序 (!)。为此,我们需要使用ROWS BETWEEN ... 定义非默认窗口,请参阅:

    还有DISTINCT ON 而不是DISTINCT。见:

    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 $1[array_upper($1, 1)] - $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小提琴here

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

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

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


    密切相关:

    【讨论】:

    • @ddriver1:我会对提供的查询的相对性能感兴趣。您能否对每个运行EXPLAIN (ANALYZE, TIMING OFF)(最好的 5 级缓存工件)并添加答案? (加上表和索引定义和基数,如果这不是太多要求的话。)
    • 非常感谢您提供非常详细的答案。该表在列 ID 和影响者 ID 上有索引,这是一个单独的影响者表的 FK。其他列是关注者和日期。对于您提供的每个查询,我都做了最好的 10 次。他们都产生了正确的结果,并在某个时候对 66293 行进行了排序。
    • 结果如下: 1. array_agg(): 103.2ms 2. Distinct+window: 137.1ms 3. first() 和 last() 使用可移植 SQL(非 C):256.3ms 4.自定义聚合增长():166.8ms(5. Gordon's distinct+window:284.4ms)
    • @ddriver1:感谢您回来提供测试结果!和我预期的一样。
    【解决方案2】:

    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;
    

    【讨论】:

    • 这行得通!顺便说一句,您在第一个查询中在 DATE ASC 之后添加了一个意外的减号 (-)。我试图编辑它,但编辑太小了。
    猜你喜欢
    • 2019-07-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-23
    • 2017-12-08
    • 2021-11-25
    • 2017-12-17
    • 1970-01-01
    相关资源
    最近更新 更多