【问题标题】:Count distinct customers, active within a year, for every week of the year计算一年内每周活跃的不同客户
【发布时间】:2020-10-06 14:08:36
【问题描述】:

我正在使用现有的电子商务数据库。其实这个过程一般都是在Excel中完成的,但是我们想直接用PostgreSQL(10.6版)的查询试试。

我们将在 1 年内至少购买过一次的人定义为活跃客户。这意味着,如果我分析 2020 年的第 22 周,活跃客户将是自 2019 年 22 周以来至少购买过一次的客户。

我想要一年中每周(2020 年)的输出。基本上我需要的是...

select
    email,
    orderdate,
    id
from
    orders_table
where
    paid = true;

|---------------------|-------------------|-----------------|
|      email          |     orderdate     |        id       |
|---------------------|-------------------|-----------------|
|  email1@email.com   |2020-06-02 05:04:32|     Order-2736  |
|---------------------|-------------------|-----------------|

我无法创建新表。我希望看到这样的输出:

Year| Week | Active customers
2020| 25   | 6978
2020| 24   | 3948

【问题讨论】:

  • 请提供您的 Postgres 版本(SELECT version() 帮助)、表定义(CREATE TABLE 语句)和一些示例行。
  • 还有“一年中的每个星期”?这个日历年?最后的?年初至今?

标签: sql database postgresql distinct window-functions


【解决方案1】:

根据是否有year 和week 列,您可以使用OVER (PARTITION BY ...) 和extract:

SELECT
    extract(year from orderdate),
    extract(week from orderdate),
    sum(1) as customer_count_in_week,
    OVER (PARTITION BY extract(YEAR FROM TIMESTAMP orderdate), 
                       extract(WEEK FROM TIMESTAMP orderdate))
FROM ordertable
WHERE paid=true;

这应该按年和周存储所有订单,从而显示一年中每周的总计数,其中支付是真实的。

参考:

【讨论】:

    【解决方案2】:

    如果我分析 2020 年的第 22 周,活跃客户将是自 2019 年 22 周以来至少购买过一次的客户。

    你身边的问题

    这种方法有一些极端情况的歧义/问题:

    • 您是否包括或排除“2020 年第 22 周”? (我在下面排除它以更接近“一年”。)

    • 一年可以有 52 或 53 个整周。根据当前日期,计算基于 52 周或 53 周,导致可能存在近 2% 的偏差!

    如果您从“去年的同一日期”开始时间范围,则由于闰年,误差范围仅为 1 / 365 或 ~ 0.3 %。

    固定的“365 天”(或 366 天)将完全消除偏差。

    SQL 端的问题

    很遗憾,window functions 目前不允许使用 DISTINCT 关键字(有充分的理由)。所以某种形式:

    SELECT count(DISTINCT email) OVER (ORDER BY year, week
                                       GROUPS BETWEEN 52 PRECEDING AND 1 PRECEDING)
    FROM   ...
    

    .. 触发器:

    ERROR:  DISTINCT is not implemented for window functions
    

    GROUPS keyword 仅在 Postgres 10 中添加,否则正是我们所需要的。

    更重要的是,您的奇数帧定义甚至不会完全起作用,因为要考虑的周数并不总是 52,如上所述。

    所以我们必须自己动手。

    解决方案

    以下仅生成所有感兴趣的周数,并计算每个周的不同客户数。很简单,只是日期数学从来都不是完全简单的。但是,根据您的设置细节,可能会有更快的解决方案。 (我还有其他几个想法。)

    报告的时间范围可能会发生变化。这是一个辅助函数,用于生成给定年份的周数:

    CREATE OR REPLACE FUNCTION f_weeks_of_year(_year int)
      RETURNS TABLE(year int, week int, week_start timestamp)
      LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE
      ROWS 52 COST 10 AS
    $func$
    SELECT _year, d.week::int, d.week_start
    FROM   generate_series(date_trunc('week', make_date(_year, 01, 04)::timestamp)  -- first day of first week
                         , LEAST(date_trunc('week', localtimestamp), make_date(_year, 12, 28)::timestamp) -- latest possible start of week
                         , interval '1 week')  WITH ORDINALITY d(week_start, week)
    $func$;
    

    呼叫:

    SELECT * FROM f_weeks_of_year(2020);
    

    它每周返回 1 行,但在当前年份的当前周停止。 (未来年份的空集。)

    计算基于these facts:

    • 一年中的第一个 ISO 周始终包含 1 月 4 日。
    • 最后一周的 ISO 不能在 12 月 28 日之后开始。

    使用WITH ORDINALITY 即时计算实际周数。见:

    除此之外,我坚持使用timestamp 并避免使用timestamptz。见:

    该函数还返回一周开始的时间戳 (week_start),对于手头的问题,我们不需要它。但我把它留了下来,以使该功能在一般情况下更有用。

    使主查询更简单:

    WITH weekly_customer AS (
       SELECT DISTINCT
              EXTRACT(YEAR FROM orderdate)::int AS year
            , EXTRACT(WEEK FROM orderdate)::int AS week
            , email
       FROM   orders_table
       WHERE  paid
       AND    orderdate >= date_trunc('week', timestamp '2019-01-04')  -- max range for 2020!
       ORDER  BY 1, 2, 3  -- optional, might improve performance
       )
    SELECT d.year, d.week
         , (SELECT count(DISTINCT email)
            FROM   weekly_customer w
            WHERE  (w.year, w.week) >= (d.year - 1, d.week)  -- row values, see below
            AND    (w.year, w.week) <  (d.year    , d.week)  -- exclude current week
           ) AS active_customers
    FROM   f_weeks_of_year(2020) d;  -- (year int, week int, week_start timestamp)
    

    db小提琴here

    CTE weekly_customer 每个日历周对唯一客户折叠一次,因为重复条目只是我们计算的噪音。它在主查询中多次使用。截止条件再次基于 1 月 4 日。根据您的实际报告期进行调整。

    实际计数是通过低相关性子查询完成的。可能是LEFT JOIN LATERAL ... ON true。见:

    使用行值比较来简化范围定义。见:

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-11-28
      • 2020-09-13
      • 2017-11-27
      • 1970-01-01
      • 2021-06-17
      • 1970-01-01
      • 1970-01-01
      • 2019-04-29
      相关资源
      最近更新 更多