Я пишу для себя небольшое бюджетное приложение и немного зациклился на запросе.

Это запрос для расчета итоговых сумм месячного бюджета в месяц по категориям.

Каждый месяц вы можете установить правило для категории, которое либо влияет на основной буфер бюджета, либо ограничивается только этой категорией.

Как это работает:

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

    Примечание. Последняя отрицательная сумма должна быть засчитана при первом появлении Effectsbuffer, чтобы вы могли погасить любую отрицательную сумму за этот месяц. Таким образом, негатив перестает переноситься ПОСЛЕ первого воздействия буферного месяца.

Вот мои образцы данных и столбец Должен быть баланс , чтобы показать, как он должен работать.

CREATE TABLE [BGT].[BudgetTemp2] ( [Month] date, [CategoryID] int, [Budgeted] money, [Outflows] money, [BudgetedAndOutflows] money, [OverspendingHandling] nvarchar(50), [BalanceShouldBe] money )
INSERT INTO [BGT].[BudgetTemp2]
VALUES
( N'2016-01-01T00:00:00', 116, 0.0000, -500.0000, -500.0000, N'AffectsBuffer', -500.0000 ), 
( N'2016-02-01T00:00:00', 116, 50.0000, -200.0000, -150.0000, N'AffectsBuffer', -150.0000 ), 
( N'2016-03-01T00:00:00', 116, 50.0000, 0.0000, 50.0000, N'AffectsBuffer', 50.0000 ), 
( N'2016-04-01T00:00:00', 116, 0.0000, -350.0000, -350.0000, N'AffectsBuffer', -300.0000 ), 
( N'2016-05-01T00:00:00', 116, 100.0000, 0.0000, 100.0000, N'AffectsBuffer', 100.0000 ), 
( N'2016-06-01T00:00:00', 116, 0.0000, 10699.8900, 10699.8900, N'AffectsBuffer', 10799.8900 ), 
( N'2016-07-01T00:00:00', 116, 4147.8800, -16707.6900, -12559.8100, N'Confined', -1759.9200 ), 
( N'2016-08-01T00:00:00', 116, 0.0000, -4504.9600, -4504.9600, N'Confined', -6264.8800 ), 
( N'2016-09-01T00:00:00', 116, 0.0000, -5486.5400, -5486.5400, N'Confined', -11751.4200 ), 
( N'2016-10-01T00:00:00', 116, 0.0000, -3795.7700, -3795.7700, N'Confined', -15547.1900 ), 
( N'2016-11-01T00:00:00', 116, 0.0000, 407.3200, 407.3200, N'Confined', -15139.8700 ), 
( N'2016-12-01T00:00:00', 116, 298324.5900, -282434.7200, 15889.8700, N'Confined', 750.0000 ), 
( N'2017-01-01T00:00:00', 116, 4196.4400, -4196.4400, 0.0000, N'Confined', 750.0000 ), 
( N'2017-02-01T00:00:00', 116, 10999.9000, -15199.9000, -4200.0000, N'Confined', -3450.0000 ), 
( N'2017-03-01T00:00:00', 116, 4987.6600, -2875.1800, 2112.4800, N'Confined', -1337.5200 ), 
( N'2017-04-01T00:00:00', 116, 4899.1600, -65100.0000, -60200.8400, N'Confined', -61538.3600 ), 
( N'2017-05-01T00:00:00', 116, 504.3200, 0.0000, 504.3200, N'Confined', -61034.0400 ), 
( N'2017-06-01T00:00:00', 116, 0.0000, -104505.0300, -104505.0300, N'Confined', -165539.0700 ), 
( N'2017-07-01T00:00:00', 116, 0.0000, -72317.7100, -72317.7100, N'Confined', -237856.7800 ), 
( N'2017-08-01T00:00:00', 116, 0.0000, -82.2200, -82.2200, N'Confined', -237939.0000 ), 
( N'2017-09-01T00:00:00', 116, 237916.0900, -814.4600, 237101.6300, N'Confined', -837.3700 ), 
( N'2017-10-01T00:00:00', 116, 906.8300, -1523.5500, -616.7200, N'Confined', -1454.0900 ), 
( N'2017-11-01T00:00:00', 116, 175.6100, -3348.5500, -3172.9400, N'Confined', -4627.0300 ), 
( N'2017-12-01T00:00:00', 116, -14.4400, -1763.4400, -1777.8800, N'AffectsBuffer', -6404.9100 ), 
( N'2018-01-01T00:00:00', 116, 40.0000, -20.0000, 20.0000, N'AffectsBuffer', 20.0000 ), 
( N'2018-02-01T00:00:00', 116, 0.0000, -75.4300, -75.4300, N'AffectsBuffer', -55.4300 ), 
( N'2018-04-01T00:00:00', 116, 4899.7400, -4899.7400, 0.0000, N'AffectsBuffer', 0.0000 ), 
( N'2018-05-01T00:00:00', 116, 750.3900, -750.3900, 0.0000, N'AffectsBuffer', 0.0000 ), 
( N'2018-06-01T00:00:00', 116, 0.0000, -500.0000, -500.0000, N'Confined', -500.0000 ), 
( N'2018-07-01T00:00:00', 116, 100.0000, 0.0000, 0.0000, N'Confined', -400.0000 ), 
( N'2018-08-01T00:00:00', 116, 200.0000, -100.0000, 100.0000, N'Confined', -300.0000 ), 
( N'2018-09-01T00:00:00', 116, 0.0000, 0.0000, 0.0000, N'AffectsBuffer', -300.0000 ), 
( N'2018-10-01T00:00:00', 116, 100.0000, -50.0000, 50.0000, N'AffectsBuffer', 50.0000 ), 
( N'2018-11-01T00:00:00', 116, 0.0000, -500.0000, -500.0000, N'AffectsBuffer', -450.0000 ), 
( N'2018-12-01T00:00:00', 116, 100.0000, -50.0000, 50.0000, N'AffectsBuffer', 50.0000 ), 
( N'2019-01-01T00:00:00', 116, 0.0000, 0.0000, 0.0000, N'AffectsBuffer', 50.0000 ), 
( N'2019-02-01T00:00:00', 116, 100.0000, 0.0000, 100.0000, N'Confined', 150.0000 ), 
( N'2019-03-01T00:00:00', 116, 0.0000, -50.0000, -50.0000, N'Confined', 100.0000 ), 
( N'2019-04-01T00:00:00', 116, 0.0000, -200.0000, -200.0000, N'Confined', -100.0000 ), 
( N'2019-05-01T00:00:00', 116, 0.0000, -200.0000, -200.0000, N'Confined', -300.0000 ), 
( N'2019-06-01T00:00:00', 116, 0.0000, 0.0000, 0.0000, N'AffectsBuffer', -300.0000 ), 
( N'2019-07-01T00:00:00', 116, 0.0000, 0.0000, 0.0000, N'AffectsBuffer', 0.0000 )

--DROP TABLE [BGT].[BudgetTemp2]

Вот мой запрос, но, как вы можете видеть, он закончился немного беспорядком. Я чувствую, что у меня есть все, чтобы сделать это правильно, мне просто не хватает нескольких ключевых вещей. Он подсчитывает правильно, пока я не получу изменения между Affects Buffer и Confined.

-- FROM: https://stackoverflow.com/a/23020788
;WITH c1 AS (
    SELECT
        *,
        LAG([BudgetTemp].[OverspendingHandling], 1, [BudgetTemp].[OverspendingHandling]) 
            OVER (PARTITION BY [BudgetTemp].[CategoryID] ORDER BY [BudgetTemp].[Month]) AS PrevOverspendingHandling
    FROM [BGT].[BudgetTemp2] [BudgetTemp]
), c2 AS (
    SELECT
        *,
        SUM(CASE WHEN [c1].[OverspendingHandling] <> [c1].[PrevOverspendingHandling] THEN 1 ELSE 0 END)
            OVER (PARTITION BY [c1].[CategoryID] ORDER BY [c1].[Month]) AS Ranker
    FROM [c1]
), c3 AS (
    SELECT
        *,
        SUM([c2].[BudgetedAndOutflows]) OVER (PARTITION BY [c2].[CategoryID] ORDER BY [c2].[Month] ROWS UNBOUNDED PRECEDING) AS rt,
        SUM([c2].[BudgetedAndOutflows]) OVER (PARTITION BY [c2].[CategoryID], [c2].[Ranker] ORDER BY [c2].[Month] ROWS UNBOUNDED PRECEDING) AS rt2
    FROM [c2]
), c4 AS (
    SELECT
        *,
        MIN(rt) OVER (PARTITION BY [c3].[CategoryID] ORDER BY [c3].[Month] ROWS UNBOUNDED PRECEDING) AS rt_min,
        MIN(rt) OVER (PARTITION BY [c3].[CategoryID], [c3].[Ranker] ORDER BY [c3].[Month] ROWS UNBOUNDED PRECEDING) AS rt2_min
    FROM [c3]
), c5 AS (
    SELECT
        *,
        -- WE WANT TO LAG MIN_CUR BY 1 SO THAT WE STILL GET A SINGLE NEGATIVE FOR THAT MONTH, BUT IT RESETS THE NEXT MONTH
        LAG(rt_min, 1, 0) OVER (PARTITION BY [c4].[CategoryID] ORDER BY [c4].[Month]) AS rt_min_lag,
        LAG(rt2_min, 1, 0) OVER (PARTITION BY [c4].[CategoryID], [c4].[Ranker] ORDER BY [c4].[Month]) AS rt2_min_lag
    FROM [c4]
)
SELECT
    [Month],
    [CategoryID],
    [Budgeted],
    [Outflows],
    [BudgetedAndOutflows],
    [OverspendingHandling],
    [PrevOverspendingHandling],
    [Ranker],
    [rt],
    [rt_min],
    [rt_min_lag],
    [rt2],
    [rt2_min],
    [rt2_min_lag],
    [rt] + (CASE WHEN [rt_min_lag] < 0 THEN -[rt_min_lag] ELSE 0 END) AS Balance1,
    [rt2] + (CASE WHEN [rt2_min_lag] < 0 THEN -[rt2_min_lag] ELSE 0 END) AS Balance2,
    [BalanceShouldBe]
FROM [c5]
ORDER BY
    [CategoryID],
    [Month]

Любая помощь будет принята с благодарностью!

Ура ,

РЕДАКТИРОВАТЬ: @dfundako обнаружил пару неверных данных, и я обновил следующее:

  • 2018-01-01 Обновленный баланс ДОЛЖЕН БЫТЬ С -6384,91 ДО 20,00
  • 2018-02-01 Обновленный баланс ДОЛЖЕН БЫТЬ С -75,43 ДО -55,43

Я также обновил это в основном скрипте вставки.

2
Albert 4 Окт 2018 в 22:46

1 ответ

Лучший ответ

В некоторых случаях вам необходимо сбросить промежуточную сумму на основе текущей суммы с момента последнего сброса.

Это не то, для чего предназначены оконные функции.

Наиболее эффективным методом, вероятно, будет использование среды CLR для обработки потока по порядку и вывода промежуточной суммы с логикой сброса по мере необходимости (аналогично этому, хотя вам нужно будет добавить логику для сброса итогов). Если данных мало, вы можете использовать рекурсивный CTE.

WITH T
     AS (SELECT *,
                ROW_NUMBER()
                  OVER (
                    PARTITION BY CategoryID
                    ORDER BY Month) AS RN
         FROM   BGT.BudgetTemp2),
     R
     AS (SELECT *,
                Budgeted + Outflows AS BalanceCalculated
         FROM   T
         WHERE  RN = 1
         UNION ALL
         SELECT T.Month,
                T.CategoryID,
                T.Budgeted,
                T.Outflows,
                T.BudgetedAndOutflows,
                T.OverspendingHandling,
                T.BalanceShouldBe,
                T.RN,
                T.Budgeted + T.Outflows
                + IIF(R.OverspendingHandling = 'AffectsBuffer' AND R.BalanceCalculated < 0, 0, R.BalanceCalculated)
         FROM   T
                JOIN R
                  ON R.CategoryID = T.CategoryID
                     AND T.RN = R.RN + 1)
SELECT *
FROM   R; 

В приведенном выше коде номер строки используется для поиска следующей строки по идентификатору категории. Если все даты гарантированно приходятся на первое число месяца и нет пропущенных месяцев, вы можете вместо этого использовать предикат соединения R.CategoryID = T.CategoryID AND T.Month = DATEADD(1, MONTH, R.Month). Что будет более эффективным, особенно если у вас есть поддерживающий индекс для этого.

1
Martin Smith 10 Окт 2018 в 10:52