具有运行总计的SQL Montlhy预算查询

时间:2018-10-04 19:46:04

标签: sql sql-server

我正在为自己编写一个小型预算应用程序,并且对查询有点困惑。

这是一个用于计算每个类别每月预算总额的查询。

每个月,您都可以针对该类别设置规则,以影响主预算缓冲区,或者仅将其限制在该类别中。

它是如何工作的:

  • 如果它影响缓冲,那么您超支的所有资金都会在该类别的每月重置。
  • 如果它影响缓冲,则您保存的所有资金将针对该类别存储,以备后用。
  • 如果它是受限的,那么它始终(即使您超支)也将在下个月继续对其计数,直到将其重新设置为影响缓冲区为止。

    注意:最后一个负数应计入第一次出现的ImpactsBuffer中,以便您可以还清该月的任何负数。因此,负数会在影响缓冲月份的第一个月后停止结转。

这是我的示例数据,以及余额应为列,以显示其工作原理。

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更新了BalanceShouldBe从-6384.91到20.00
  • 2018-02-01更新了BalanceShouldBe从-75.43到-55.43

我也在主插入脚本中对此进行了更新。

1 个答案:

答案 0 :(得分:1)

在某些情况下,您需要根据自上次重置以来的运行总计重置运行总计。

这不是为窗口函数设计的。

最佳性能的方法可能是使用CLR按顺序处理流,并根据需要使用重置逻辑输出运行总计(similar to this though you would need to add the logic to reset the totals)。如果数据较小,则可以使用递归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; 

以上使用行号查找每个CategoryID的下一行。如果保证所有日期都是该月的第一天并且没有丢失的月份,则可以改用R.CategoryID = T.CategoryID AND T.Month = DATEADD(1, MONTH, R.Month)的连接谓词。这样做会更有效率-尤其是如果您对此有支持的索引。