【问题标题】:Cumulative sum based on same column calculated result基于同列计算结果的累计
【发布时间】:2020-10-26 00:19:03
【问题描述】:

我有下表,我正在尝试为其计算运行余额和剩余值,但剩余值是先前计算的行的函数,如下所示:

date         PR    amount  total    balance  remaining_value
----------------------------------------------------------
'2020-1-1'   1     1.0     100.0    1.0      100    -- 100 (inital total)
'2020-1-2'   1     2.0     220.0    3.0      320   -- 100 (previous row) + 220 
'2020-1-3'   1    -1.5    -172.5    1.5      160   -- 320 - 160 (see explanation 1)
'2020-1-4'   1     3.0     270.0    4.5      430   -- 160 + 270
'2020-1-5'   1     1.0      85.0    5.5      515   -- 430 + 85 
'2020-1-6'   1     2.0     202.0    7.5      717   -- 575 + 202 
'2020-1-7'   1    -4.0    -463.0    3.5      334.6 -- 717 - 382.4 (see explanation 2)
'2020-1-8'   1    -0.5     -55.0    3.0      ...
'2020-1-9'   1     2.0     214.0    5.0
'2020-1-1'   2     1.0     100      1.0      100   -- different PR: start new running total

逻辑如下:

  • 对于正数行,剩余值只是remaining_value 列中前一行的值 + 该行total 列中的值。

  • 对于负数行,它会变得更复杂:

解释 1: 我们从320(前一行余额)开始,从中删除1.5/3.0(当前行金额的绝对值除以前一行余额),然后乘以上一行remaining_value,即320。计算得出:

320 - (1.5/3 * 320) = 160

解释2:逻辑同上。 717 - (4/7.5 * 717) = 717 - 382.4

4/7.5这里表示当前行的绝对金额除以上一行的余额。

我尝试了窗口函数sum(),但没有得到想要的结果。有没有办法在 PostgreSQL 中完成这项工作而不必求助于循环?

额外的复杂性:有多个产品由 PR(产品 ID)、1、2 等标识。每个产品都需要自己的运行总计和计算。

【问题讨论】:

  • 我使用了一个日期字段来对它们进行排序,但出于本示例的目的,我只显示了排序表。你知道了,当总数为正时,我们只需将它们相加,当总数为负时,我们使用前一个总数的公式(因此忽略负数)。我要补充一点,PR 字段代表产品 ID,并且可能有多个产品 ID。通常,我会用窗口函数和分区将它们分开,但在这种情况下不知道该怎么做..

标签: sql postgresql aggregate-functions window-functions cumulative-sum


【解决方案1】:

你可以create a custom aggregate function:

CREATE OR REPLACE FUNCTION f_special_running_sum (_state numeric, _total numeric, _amount numeric, _prev_balance numeric)
  RETURNS numeric
  LANGUAGE sql IMMUTABLE AS
'SELECT CASE WHEN _amount > 0 THEN _state + _total
             ELSE _state * (1 + _amount / _prev_balance) END';

CREATE OR REPLACE AGGREGATE special_running_sum (_total numeric, _amount numeric, _prev_balance numeric) (
  sfunc    = f_special_running_sum 
, stype    = numeric
, initcond = '0'
);

CASE 表达式进行拆分:如果金额为正,只需添加总计,否则应用您的(简化)公式:
320 * (1 + -1.5 / 3.0) 而不是320 - (1.5/3 * 320),即:

_state * (1 + _amount / _prev_balance) 

函数和聚合参数名称仅用于文档。

那么您的查询可能如下所示:

SELECT *
     , special_running_sum(total, amount, prev_balance) OVER (PARTITION BY pr ORDER BY date)
FROM  (
   SELECT pr, date, amount, total
        , lag(balance, 1, '1') OVER (PARTITION BY pr ORDER BY date) AS prev_balance
   FROM   tbl
   ) t;

db小提琴here

我们需要一个子查询来应用第一个窗口函数lag() 并将之前的余额提取到当前行 (prev_balance)。如果没有上一行来避免NULL 值,我默认为1

注意事项:

  • 如果第一行的总数为负数,则结果未定义。我的聚合函数默认为0

  • 您没有声明数据类型,也没有关于精度的要求。我假设numeric 并以最大精度为目标。 numeric 的计算是精确的。但是你的公式会产生小数。如果不进行四舍五入,则在几次除法之后会有很多小数位,并且计算会很快降低性能。您必须在精度和性能之间做出妥协。例如,对double precision 执行相同操作具有恒定的性能。

相关:

【讨论】:

  • 非常感谢!做了一些测试,它适用于所有情况。确实我用双精度代替了,这对我的目的有好处。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-02-26
  • 2020-12-17
  • 2021-10-29
  • 1970-01-01
  • 1970-01-01
  • 2010-12-23
  • 2021-07-15
相关资源
最近更新 更多