PostgreSQL вычисляет с вычисленным значением из предыдущих строк

#postgresql

#postgresql

Вопрос:

Проблема, которую мне нужно решить:

Для того, чтобы вычислить количество часов в день, которые используются для (государственных) праздников или дней болезни, используются средние рабочие часы за предыдущие 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_sum и h_sum значения доступных месяцев (максимум трех), в свою очередь, приводят к вычислению текущих месяцев h_off (по существу 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, поэтому у меня под рукой самые последние функции, если это имеет какое-либо значение.

Ответ №1:

Я вообще не смог решить эту проблему вычисления между столбцами и между строками с использованием оконных функций, не прибегая к специальному использованию рекурсивного CTE, а также к введению специальных столбцов для дней ( d_total_1 ) и часов ( h_sum_1 ) 3-го исторического месяца (поскольку вы не можете присоединиться к рекурсивной временной таблице более одного раза).

Кроме того, я добавил 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 по умолчанию заключается в округлении числовых значений, поэтому результат немного отличается от первоначально ожидаемого, но на самом деле правильный)

Пожалуйста, обратите внимание, что PostgreSQL обычно довольно придирчив к типам данных и отказывается обрабатывать подобные запросы всякий раз, когда возникает несоответствие, которое потенциально может привести к потере точности (например, numeric против integer ), вот почему я использовал явные типы для столбцов в обоих местах.

Одна из последних частей головоломки была решена с помощью БОКОВЫХ подзапросов, что позволяет мне ссылаться на результат предыдущего вычисления и даже перемещаться по столбцам в конечном выводе независимо от иерархии вычислений.

Если кто-нибудь может предложить более простой вариант, я был бы рад узнать об этом.