【问题标题】:PostgreSQL calculate with calculated value from previous rowsPostgreSQL 使用前行的计算值进行计算
【发布时间】:2019-03-02 11:25:53
【问题描述】:

我需要解决的问题:

为了计算每天用于(公共)假期或病假的小时数,使用前 3 个月的平均工作时间(起始值为每天 8 小时)。

棘手的部分是需要考虑上个月的计算值,这意味着如果上个月有一个公共假期,分配了 8.5 小时的计算值,这些计算的小时数会影响平均值上个月每天的工作时间,然后用于分配给当前月份假期的工作时间。

到目前为止,我只提出了以下内容,还没有考虑到逐行计算:

WITH
    const (h_target, h_extra) AS (VALUES (8.0, 20)),
    monthly_sums (c_month, d_work, d_off, h_work) AS (VALUES
        ('2018-12', 16, 5, 150.25),
        ('2019-01', 20, 3, 171.25),
        ('2019-02', 15, 5, 120.5)
    ),
    calc AS (
        SELECT
            ms.*,
            (ms.d_work + ms.d_off) AS d_total,
            (ms.h_work + ms.d_off * const.h_target) AS h_total,
            (avg((ms.h_work + ms.d_off * const.h_target) / (ms.d_work + ms.d_off))
                OVER (ORDER BY ms.c_month ROWS BETWEEN 2 PRECEDING AND CURRENT ROW))::numeric(10,2)
                AS h_off
        FROM monthly_sums AS ms
        CROSS JOIN const
    )
SELECT
    calc.c_month,
    calc.d_work,
    calc.d_off,
    calc.d_total,
    calc.h_work,
    calc.h_off,
    (d_off * lag(h_off, 1, const.h_target) OVER (ORDER BY c_month)) AS h_off_sum,
    (h_work + d_off * lag(h_off, 1, const.h_target) OVER (ORDER BY c_month)) AS h_sum
FROM calc CROSS JOIN const;

...给出以下结果:

 c_month | d_work | d_off | d_total | h_work | h_off | h_off_sum | h_sum  
---------+--------+-------+---------+--------+-------+-----------+--------
 2018-12 |     16 |     5 |      21 | 150.25 |  9.06 |      40.0 | 190.25
 2019-01 |     20 |     3 |      23 | 171.25 |  8.77 |     27.18 | 198.43
 2019-02 |     15 |     5 |      20 |  120.5 |  8.52 |     43.85 | 164.35
(3 rows)

对于依赖于前一行值 (lag) 的列的第一行和第二行计算正确,但每天的平均小时数计算显然是错误的,因为我无法弄清楚如何提供当前行值 (h_sum) 返回到新 h_off 的计算中。

想要的结果应该如下:

 c_month | d_work | d_off | d_total | h_work | h_off | h_off_sum | h_sum  
---------+--------+-------+---------+--------+-------+-----------+--------
 2018-12 |     16 |     5 |      21 | 150.25 |  9.06 |      40.0 | 190.25
 2019-01 |     20 |     3 |      23 | 171.25 |  8.84 |     27.18 | 198.43
 2019-02 |     15 |     5 |      20 |  120.5 |  8.64 |      44.2 |  164.7
(3 rows)

...意思是h_off 用于下个月的h_off_sum 和生成的h_sumh_sum 的可用月份(最多三个)反过来导致计算当前月份的@987654331 @(基本上是 avg(h_sum / d_total) 最多三个月)。

所以,实际计算是:

 c_month | calculation                                        | h_off
---------+----------------------------------------------------+-------
         |                                                    |  8.00 << initial
               .---------------------- uses ---------------------^
 2018-12 | ((190.25 / 21)) / 1                                |  9.06
                               .------------ uses ---------------^
 2019-01 | ((190.25 / 21) + (198.43 / 23)) / 2                |  8.84
                                               .--- uses --------^
 2019-02 | ((190.25 / 21) + (198.43 / 23) + (164.7 / 20)) / 3 |  8.64

P.S.:我使用的是 PostgreSQL 11,所以如果有什么不同的话,我手头有最新的功能。

【问题讨论】:

    标签: postgresql


    【解决方案1】:

    我根本无法通过使用window functions 来解决列间+行间计算问题,而不是退回到recursive CTE 的特殊用途以及引入特殊用途第三个历史月份的天数 (d_total_1) 和小时数 (h_sum_1) 列(因为您不能多次加入递归临时表)。

    此外,我在输入数据中添加了第 4 行,并使用了一个额外的索引列,我可以在加入时引用它,通常由这样的子查询组成:

    SELECT ROW_NUMBER() OVER (ORDER BY c_month) AS row_num, * FROM monthly_sums
    

    所以,这是我的看法:

    WITH RECURSIVE calc AS (
            SELECT 
                monthly_sums.row_num,
                monthly_sums.c_month,
                monthly_sums.d_work,
                monthly_sums.d_off,
                monthly_sums.h_work,
                (monthly_sums.d_off * 8)::numeric(10,2) AS h_off_sum,
                monthly_sums.d_work + monthly_sums.d_off AS d_total,
                0.0 AS d_total_1,
                (monthly_sums.h_work + monthly_sums.d_off * 8)::numeric(10,2) AS h_sum,
                0.0 AS h_sum_1,
                (
                    (monthly_sums.h_work + monthly_sums.d_off * 8)
                    /
                    (monthly_sums.d_work + monthly_sums.d_off)
                )::numeric(10,2) AS h_off
            FROM
                (
                    SELECT * FROM (VALUES
                        (1, '2018-12', 16, 5, 150.25),
                        (2, '2019-01', 20, 3, 171.25),
                        (3, '2019-02', 15, 5, 120.5),
                        (4, '2019-03', 19, 2, 131.75)
                    ) AS tmp (row_num, c_month, d_work, d_off, h_work)
                ) AS monthly_sums
            WHERE
                monthly_sums.row_num = 1
        UNION ALL
            SELECT
                monthly_sums.row_num,
                monthly_sums.c_month,
                monthly_sums.d_work,
                monthly_sums.d_off,
                monthly_sums.h_work,
                lat_off.h_off_sum::numeric(10,2),
                lat_days.d_total,
                calc.d_total AS d_total_1,
                lat_sum.h_sum::numeric(10,2),
                calc.h_sum AS h_sum_1,
                lat_calc.h_off::numeric(10,2)
            FROM
                (
                    SELECT * FROM (VALUES
                        (1, '2018-12', 16, 5, 150.25),
                        (2, '2019-01', 20, 3, 171.25),
                        (3, '2019-02', 15, 5, 120.5),
                        (4, '2019-03', 19, 2, 131.75)
                    ) AS tmp (row_num, c_month, d_work, d_off, h_work)
                ) AS monthly_sums
                INNER JOIN calc ON (calc.row_num = monthly_sums.row_num - 1),
                LATERAL (SELECT monthly_sums.d_work + monthly_sums.d_off AS d_total) AS lat_days,
                LATERAL (SELECT monthly_sums.d_off * calc.h_off AS h_off_sum) AS lat_off,
                LATERAL (SELECT monthly_sums.h_work + lat_off.h_off_sum AS h_sum) AS lat_sum,
                LATERAL (SELECT
                    (calc.h_sum_1 + calc.h_sum + lat_sum.h_sum)
                    /
                    (calc.d_total_1 + calc.d_total + lat_days.d_total)
                    AS h_off
                ) AS lat_calc
            WHERE
                monthly_sums.row_num > 1
        )
    SELECT c_month, d_work, d_off, d_total, h_work, h_off, h_off_sum, h_sum FROM calc
    ;
    

    ...给出:

     c_month | d_work | d_off | d_total | h_work | h_off | h_off_sum | h_sum  
    ---------+--------+-------+---------+--------+-------+-----------+--------
     2018-12 |     16 |     5 |      21 | 150.25 |  9.06 |     40.00 | 190.25
     2019-01 |     20 |     3 |      23 | 171.25 |  8.83 |     27.18 | 198.43
     2019-02 |     15 |     5 |      20 |  120.5 |  8.65 |     44.15 | 164.65
     2019-03 |     19 |     2 |      21 | 131.75 |  8.00 |     17.30 | 149.05
    (4 rows)
    

    (PostgreSQL 的默认类型转换行为是round numeric values,因此结果与最初预期的略有不同,但实际上是正确的)

    请注意,PostgreSQL 通常对数据类型非常挑剔,并且在存在可能导致精度损失的差异时拒绝处理这样的查询(例如 numericinteger),这就是为什么我在这两个地方的列都使用了显式类型。

    使用LATERAL 子查询解决了最后一个难题,这使我能够让一个计算引用前一个计算的结果,甚至可以在最终输出中的列之间移动,而与计算层次结构无关。

    如果有人能想出一个更简单的变体,我很乐意了解它。

    【讨论】:

      猜你喜欢
      • 2020-05-27
      • 1970-01-01
      • 1970-01-01
      • 2022-06-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多