【问题标题】:Count over rows in previous time range partitioned by a specific column计算由特定列分区的先前时间范围内的行数
【发布时间】:2014-03-31 00:17:19
【问题描述】:

我的数据集包含来自不同行业的不同公司的每日(实际上是工作日)时间序列,我使用 PostgreSQL。我的数据集中有一个指标变量,取值 1、-1,大多数情况下为 0。为了更好地理解问题,我将指标变量不等于零的日子称为指标事件。

因此对于前三个工作日内同一行业的另一个指标事件之前的所有指标事件,指标变量应更新为零。

我们可以想到以下示例数据集:

day              company    industry       indicator
2012-01-12       A          financial      1
2012-01-12       B          consumer       0 
2012-01-13       A          financial      1 
2012-01-13       B          consumer       -1
2012-01-16       A          financial      0 
2012-01-16       B          consumer       0 
2012-01-17       A          financial      0
2012-01-17       B          consumer       0
2012-01-17       C          consumer       0
2012-01-18       A          financial      0
2012-01-18       B          consumer       0
2012-01-18       C          consumer       1

因此,应更新为零的指标值在 2012-01-13 是公司 A 的条目,在 2012-01-18 是公司 C 的条目,因为它们之前是同一个中的另一个指标事件3 个工作日内的行业。

我尝试通过以下方式完成它:

UPDATE test SET indicator = 0 
WHERE (day, industry) IN (
SELECT day, industry
  FROM (
       SELECT industry, day,
       COUNT(CASE WHEN indicator <> 0 THEN 1 END) 
          OVER (PARTITION BY industry ORDER BY day 
                ROWS BETWEEN 3 PRECEDING AND CURRENT ROW) As cnt
       FROM test
       ) alias
  WHERE cnt >= 2) 

我的想法是按行业划分当天和前 3 天的指标事件数。如果计数大于 1,则将指标值更新为零。

弱点是,到目前为止,它计入了前三行(按行业划分),而不是前三个工作日。因此,在示例数据中,它无法在 2012 年 1 月 18 日更新公司 C,因为它计算了最后三个行业 = 消费者的行,而不是计算过去三个工作日行业 = 消费者的所有行。

我尝试了不同的方法,例如在代码的倒数第三行添加另一个子查询或在倒数第三行后添加WHERE EXISTS - 子句,以确保代码计数超过前三个日期。但没有任何效果。我真的不知道该怎么做(我只是学习使用 PostgreSQL)。

您有任何解决方法的想法吗?

或者也许我的想法完全错误,您知道如何解决我的问题的另一种方法?

【问题讨论】:

  • 如果同一行业每三个工作日就有一个指标会怎样?您是否重置除第一个指标之外的所有指标?你不应该有一个静态网格吗?比如,“只从周一到周三选择第一个事件,从周四到周五选择第一个事件”。
  • 是的,在这种情况下,我会重置除第一个以外的所有指标。因此,您的解决方案运行良好,非常感谢。 @ErwinBrandstetter

标签: sql postgresql sql-update time-series window-functions


【解决方案1】:

与此同时,我自己找到了一种可能的解决方案(我希望这不会违反论坛的礼仪)。

请注意,这只是一种可能的解决方案。非常欢迎您发表评论或开发 如果您愿意,可以进行改进。

对于第一部分,函数 addbusinessdays 可以增加(或减少)工作日到 给定日期,我指的是: http://osssmb.wordpress.com/2009/12/02/business-days-working-days-sql-for-postgres-2/ (我只是稍微修改了一下,因为我不在乎假期,只在周末)

    CREATE OR REPLACE FUNCTION addbusinessdays(date, integer)
      RETURNS date AS
    $BODY$ 
    with alldates as (
        SELECT i,
        $1 + (i * case when $2 < 0 then -1 else 1 end) AS date
        FROM generate_series(0,(abs($2) + 5)*2) i
    ),
    days as (
        select i, date, extract('dow' from date) as dow
        from alldates
    ),
    businessdays as (
        select i, date, d.dow from days d
        where d.dow between 1 and 5
        order by i
    )

    select date from businessdays where
            case when $2 > 0 then date >=$1 when $2 < 0 then date <=$1 else date =$1 end
        limit 1
        offset abs($2)
    $BODY$
      LANGUAGE 'sql' VOLATILE
      COST 100;
    ALTER FUNCTION addbusinessdays(date, integer) OWNER TO postgres;

对于第二部分,我指的是这个相关问题,我在其中应用 Erwin Brandstetter 的相关子查询方法:Window Functions or Common Table Expressions: count previous rows within range

    UPDATE test SET indicator = 0 
    WHERE (day, industry) IN (
    SELECT day, industry
      FROM (
           SELECT industry, day,
                  (SELECT COUNT(CASE WHEN indicator <> 0 THEN 1 END) 
                   FROM test t1
                   WHERE t1.industry = t.industry
                   AND t1.day between addbusinessdays(t.day,-3) and t.day) As cnt
           FROM test t
           ) alias
      WHERE cnt >= 2) 

【讨论】:

    【解决方案2】:

    数据库设计

    拳头,你的桌子应该标准化。 industry 应该是引用industry 表的industry_id 的小外键列(通常为integer)。也许你已经有了,只是为了这个问题而简化了。您的实际表定义会有很长的路要走。

    由于带有指示符的行很少见但非常有趣,因此创建一个(可能“覆盖”)部分索引以使 any 解决方案更快:

    CREATE INDEX tbl_indicator_idx ON tbl (industry, day)
    WHERE  indicator <> 0;
    

    Equality first, range last.
    假设indicator 定义为NOT NULL。如果industryinteger,则此索引将非常有效。

    查询

    此查询标识要重置的行:

    WITH x AS (               -- only with indicator
       SELECT DISTINCT industry, day
       FROM   tbl t 
       WHERE  indicator <> 0
       )
    SELECT industry, day
    FROM  (
       SELECT i.industry, d.day, x.day IS NOT NULL AS incident
            , count(x.day) OVER (PARTITION BY industry ORDER BY day_nr
                                 ROWS BETWEEN 3 PRECEDING AND CURRENT ROW) AS ct
       FROM  (
          SELECT *, row_number() OVER (ORDER BY d.day) AS day_nr
          FROM  (
             SELECT generate_series(min(day), max(day), interval '1d')::date AS day
             FROM   x
             ) d
          WHERE  extract('ISODOW' FROM d.day) < 6
          ) d
       CROSS  JOIN (SELECT DISTINCT industry FROM x) i
       LEFT   JOIN x USING (industry, day)
       ) sub
    WHERE  incident
    AND    ct > 1
    ORDER  BY 1, 2;
    

    SQL Fiddle.

    ISODOW as extract() parameter 方便截断周末。

    将此集成到您的UPDATE

    WITH x AS (               -- only with indicator
       SELECT DISTINCT industry, day
       FROM   tbl t 
       WHERE  indicator <> 0
       )
    UPDATE tbl t
    SET    indicator = 0 
    FROM  (
       SELECT i.industry, d.day, x.day IS NOT NULL AS incident
            , count(x.day) OVER (PARTITION BY industry ORDER BY day_nr
                                 ROWS BETWEEN 3 PRECEDING AND CURRENT ROW) AS ct
       FROM  (
          SELECT *, row_number() OVER (ORDER BY d.day) AS day_nr
          FROM  (
             SELECT generate_series(min(day), max(day), interval '1d')::date AS day
             FROM   x
             ) d
          WHERE  extract('isodow' FROM d.day) < 6
          ) d
       CROSS  JOIN (SELECT DISTINCT industry FROM x) i
       LEFT   JOIN x USING (industry, day)
       ) u
    WHERE  u.incident
    AND    u.ct > 1
    AND    t.industry = u.industry
    AND    t.day = u.day;
    

    这应该比具有相关子查询和每行函数调用的解决方案快得多。即使这是基于我自己之前的回答,它对于 这种 的情况并不完美。

    【讨论】:

    • 非常感谢您的出色回答,以及您对我如何进一步改进工作的建议。对此,我真的非常感激!! @ErwinBrandstetter
    猜你喜欢
    • 2018-12-28
    • 2020-05-05
    • 2023-03-03
    • 2013-05-29
    • 2021-10-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多