【问题标题】:Rolling sum till a certain value is reached, plus calculated duration滚动总和直到达到某个值,加上计算的持续时间
【发布时间】:2019-07-21 20:30:26
【问题描述】:

我有一个要求,我需要知道sum(value) 何时达到某个点并计算持续时间。下面是示例表。

create table sample (dt timestamp, value real);

insert into sample values
     ('2019-01-20 00:29:43 ',0.29)
    ,('2019-01-20 00:35:06 ',0.31)
    ,('2019-01-20 00:35:50 ',0.41)
    ,('2019-01-20 00:36:32 ',0.26)
    ,('2019-01-20 00:37:20 ',0.33)
    ,('2019-01-20 00:41:30 ',0.42)
    ,('2019-01-20 00:42:28 ',0.35)
    ,('2019-01-20 00:43:14 ',0.52)
    ,('2019-01-20 00:44:18 ',0.25);

现在我的要求是计算以下行的累积总和,以查看 sum(value) 何时达到 1.0 以上。这可能只需要 1 行或 n 行。到达该行后,我需要计算当前行与sum(value) 达到1.0 以上的行之间的时间差。

基本上我想要的输出格式如下。
对于第 1 行,累积 sum(value) 在第 3 行达到。
对于第 2 行,累积 sum(value) 在第 4 行达到等。

         dt         | value | sum(value)| time_at_sum(value)_1| Duration
---------------------+--------+------------------------------------------
 2019-01-20 00:29:43| 0.29  |   1.01    | 2019-01-20 00:35:50 | 00:06:07
 2019-01-20 00:35:06| 0.31  |   1.31    | 2019-01-20 00:37:20 | 00:02:14 
 2019-01-20 00:35:50| 0.41  |   1.00    | 2019-01-20 00:37:20 | 00:01:30 
 2019-01-20 00:36:32| 0.26  |   1.01    | 2019-01-20 00:41:30 | 00:04:58 
 2019-01-20 00:37:20| 0.33  |   1.10    | 2019-01-20 00:42:28 | 00:05:08 
 2019-01-20 00:41:30| 0.42  |   1.29    | 2019-01-20 00:43:14 | 00:01:44 
 2019-01-20 00:42:28| 0.35  |   1.12    | 2019-01-20 00:44:18 | 00:01:50 
 2019-01-20 00:43:14| 0.52  |   NULL    |  -                  | -
 2019-01-20 00:44:18| 0.25  |   NULL    |  -                  | -

任何人对如何处理上述要求有想法或指示?

【问题讨论】:

  • 那么sum(value)和数据有什么关系?
  • 它的值列的累积总和,直到它达到 1.0.. 只是显示预期的输出..
  • 您为value 显示的数据显然不能与sum(value) 示例相加。您能否编辑描述以显示示例中从valuesum(value) 的实际计算?
  • 嗨@bignose,我已经编辑了我的请求。我手动填充了所需的输出列。基本上 Sum(value) 是下 n 个记录的累积总和,直到它达到值 1
  • 1.00 for sum(value) 在你的结果中违反了你的条件when the sum(value) reaches above 1.0,它转换为> 1.0,而不是>= 1.0

标签: sql postgresql sqlcommand cumulative-sum rolling-computation


【解决方案1】:

有效地解决这个问题的一种方法是使用两个游标的程序解决方案: 一个explicit cursor 和另一个implicit cursor of the FOR loop

CREATE OR REPLACE FUNCTION foo()
  RETURNS TABLE (dt timestamp
               , val real
               , sum_value real
               , time_at_sum timestamp
               , duration interval) AS
$func$
DECLARE
   _bound real := 1.0;          -- your bound here
   cur CURSOR FOR SELECT * FROM sample s ORDER BY s.dt; -- in chronological order
   s sample;                    -- cursor row 
BEGIN
   OPEN cur;
   FETCH cur INTO time_at_sum, sum_value; -- fetch first row into target

   FOR dt, val IN  -- primary pass over table
      SELECT x.dt, x.value FROM sample x ORDER BY s.dt
   LOOP
      WHILE sum_value <= _bound LOOP
         FETCH cur INTO s;
         IF NOT FOUND THEN  -- end of table
            sum_value := NULL; time_at_sum := NULL;
            EXIT;           -- exits inner loop
         END IF;
         sum_value := sum_value + s.value; 
      END LOOP;
      IF sum_value > _bound THEN  -- to catch end-of-table
         time_at_sum := s.dt;
      END IF;   
      duration := time_at_sum - dt;
      RETURN NEXT;
      sum_value := sum_value - val;  -- subtract previous row before moving on
   END LOOP;
END
$func$  LANGUAGE plpgsql;

呼叫:

SELECT * FROM foo();

db小提琴here

应该执行得很好,因为它只需要对表进行 2 次扫描。

请注意,我按照您的描述要求实现了 &gt; _bound,而不是您的结果表明的 &gt;= _bound。无论哪种方式都易于更改。

假设值列是NOT NULL

相关:

【讨论】:

  • @acul:与 unutbu 的查询相比,我会对函数的实际时间非常感兴趣(以及表中的行数和每个 sum_value 要聚合的粗略平均行数) )。
  • 嗨@Erwin Brandstetter:我会在两种解决方案上都收到错误提示“错误:值超出范围:溢出”..
  • 您的专栏value 是否实际上定义为real?在您的错误情况下_bound 的真实值是多少(示例中为 1.0),最小值和最大值是多少? value 必须具有 巨大 值,real 的范围通常至少为 1E-37 到 1E+37。 (或者你以某种方式引入了一个独立的问题。)也许开始一个包含所有相关细节的新问题。
  • 嗨@Erwin Brandstetter:是的,你是对的。我的错,从父表复制时没有看值范围。我解决了这个问题。当我在具有 ~ 55K 记录的表上运行时,您的解决方案运行 ~ 4 秒,而 SQL 运行 ~ 6 分钟。这是一个很大的差异,并且随着表格大小的增加,时间差异会急剧增加。谢谢欧文。走哪条路很清楚
【解决方案2】:
WITH tmp AS (
    SELECT *
        , sum(value) OVER (ORDER BY dt rows between current row and unbounded following) as forward_sum
    FROM sample
    ORDER BY dt)
SELECT t1.dt, t1.value
    , (t2.value + t1.forward_sum - t2.forward_sum) as "sum(value)"
    , t2.dt as "time_at_sum(value)_1" 
    , t2.dt - t1.dt as "Duration"
FROM tmp t1
LEFT JOIN LATERAL (
    SELECT * 
    FROM tmp t
    WHERE t1.forward_sum - t.forward_sum < 1
        AND (t.value + t1.forward_sum - t.forward_sum) >= 0.999
    ORDER BY dt DESC 
    LIMIT 1
    ) t2
ON TRUE

产量

| dt                  | value | sum(value) | time_at_sum(value)_1 | Duration |
|---------------------+-------+------------+----------------------+----------|
| 2019-01-20 00:29:43 |  0.29 |       1.01 | 2019-01-20 00:35:50  | 00:06:07 |
| 2019-01-20 00:35:06 |  0.31 |       1.31 | 2019-01-20 00:37:20  | 00:02:14 |
| 2019-01-20 00:35:50 |  0.41 |          1 | 2019-01-20 00:37:20  | 00:01:30 |
| 2019-01-20 00:36:32 |  0.26 |       1.01 | 2019-01-20 00:41:30  | 00:04:58 |
| 2019-01-20 00:37:20 |  0.33 |        1.1 | 2019-01-20 00:42:28  | 00:05:08 |
| 2019-01-20 00:41:30 |  0.42 |       1.29 | 2019-01-20 00:43:14  | 00:01:44 |
| 2019-01-20 00:42:28 |  0.35 |       1.12 | 2019-01-20 00:44:18  | 00:01:50 |
| 2019-01-20 00:43:14 |  0.52 |            |                      |          |
| 2019-01-20 00:44:18 |  0.25 |            |                      |          |

首先计算value 列的累积和:

SELECT *
    , sum(value) OVER (ORDER BY dt rows between current row and unbounded following) as forward_sum
FROM sample
ORDER BY dt

产生

| dt                  | value | forward_sum |
|---------------------+-------+-------------|
| 2019-01-20 00:29:43 |  0.29 |        3.14 |
| 2019-01-20 00:35:06 |  0.31 |        2.85 |
| 2019-01-20 00:35:50 |  0.41 |        2.54 |
| 2019-01-20 00:36:32 |  0.26 |        2.13 |
| 2019-01-20 00:37:20 |  0.33 |        1.87 |
| 2019-01-20 00:41:30 |  0.42 |        1.54 |
| 2019-01-20 00:42:28 |  0.35 |        1.12 |
| 2019-01-20 00:43:14 |  0.52 |        0.77 |
| 2019-01-20 00:44:18 |  0.25 |        0.25 |

请注意,从forward_sum 中减去两个值对应于values 的部分和。 例如,

0.29 + 0.31 + 0.41 = 3.14 - 2.13

所以forward_sums 的差异将发挥重要作用,我们希望将这些差异与 1 进行比较。我们将希望使用以下连接条件将这个表与其自身连接起来:

t1.forward_sum - t.forward_sum < 1

让我们看看如果我们使用 LEFT JOIN LATERAL 会发生什么。关于 LEFT JOIN LATERAL 要了解的关键是 LATERAL 连接右侧的子查询has to be evaluated once for each row in the table on the left

WITH tmp AS (
    SELECT *
        , sum(value) OVER (ORDER BY dt rows between current row and unbounded following) as forward_sum
    FROM sample
    ORDER BY dt)
SELECT t1.*, t2.*
FROM tmp t1
LEFT JOIN LATERAL (
    SELECT * 
    FROM tmp t
    WHERE t1.forward_sum - t.forward_sum < 1
    ORDER BY dt DESC 
    LIMIT 1
    ) t2
ON TRUE

产量

| dt                  | value | forward_sum | dt                  | value | forward_sum |
|---------------------+-------+-------------+---------------------+-------+-------------|
| 2019-01-20 00:29:43 |  0.29 |        3.14 | 2019-01-20 00:35:50 |  0.41 |        2.54 |
| 2019-01-20 00:35:06 |  0.31 |        2.85 | 2019-01-20 00:37:20 |  0.33 |        1.87 |
| 2019-01-20 00:35:50 |  0.41 |        2.54 | 2019-01-20 00:37:20 |  0.33 |        1.87 |
| 2019-01-20 00:36:32 |  0.26 |        2.13 | 2019-01-20 00:41:30 |  0.42 |        1.54 |
| 2019-01-20 00:37:20 |  0.33 |        1.87 | 2019-01-20 00:42:28 |  0.35 |        1.12 |
| 2019-01-20 00:41:30 |  0.42 |        1.54 | 2019-01-20 00:43:14 |  0.52 |        0.77 |
| 2019-01-20 00:42:28 |  0.35 |        1.12 | 2019-01-20 00:44:18 |  0.25 |        0.25 |
| 2019-01-20 00:43:14 |  0.52 |        0.77 | 2019-01-20 00:44:18 |  0.25 |        0.25 |
| 2019-01-20 00:44:18 |  0.25 |        0.25 | 2019-01-20 00:44:18 |  0.25 |        0.25 |

请注意,我们已经猜到了匹配连接条件的方式 想要的日期。现在只需编写正确的值表达式 获取所需的列,sum(value)time_at_sum(value)_1

【讨论】:

  • 将您的智能查询添加到我的小提琴中 - 为了测试方便:dbfiddle.uk/…
  • 这很棒。非常感谢您的帮助 Unutbu。我为这个问题头疼了几天。
猜你喜欢
  • 2020-12-24
  • 1970-01-01
  • 1970-01-01
  • 2018-07-21
  • 1970-01-01
  • 1970-01-01
  • 2014-01-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多