【问题标题】:Balance for each month by type按类型划分的每月余额
【发布时间】:2018-07-03 21:42:07
【问题描述】:

我在 SQL-Server 中有一个表,其中包含几条输入和输出值记录,其中包含类型和日期列。

类似的东西:

DATE       |INPUT  |OUTPUT |TYPE
2018-01-10 | 256.35|       |A
2018-02-05 |       |  35.00|B
2018-02-15 |  65.30|       |A
2018-03-20 | 158.00|       |B
2018-04-02 |       |  63.32|B
2018-05-12 |       | 128.12|A
2018-06-20 |       |   7.35|B

我需要帮助来进行查询以返回每种类型的输入和输出的总和(作为余额),但它应该在每个月底返回该总和,即:

YEAR|MONTH|TYPE|BALANCE
2018|    1|A   | 256.35
2018|    1|B   |   0.00
2018|    2|A   | 321.65
2018|    2|B   | -35.00
2018|    3|A   | 321.65
2018|    3|B   | 123.00
2018|    4|A   | 321.65
2018|    4|B   |  59.68
2018|    5|A   | 193.53
2018|    5|B   |  59.68
2018|    6|A   | 193.53
2018|    6|B   |  52.33
2018|    7|A   | 193.53
2018|    7|B   |  52.33

不要忘记,每个月的余额都会受到上月余额的影响,或者说,每个月的余额不仅是当月的变动,也是之前所有月份的变动。

还应该注意的是,它应该包括一年/类型的每个月的记录(直到当前日期),即使给定的月份/类型没有变动,从第一个月/年开始最旧的运动并在实际日期结束(在本例中为 2018 年 7 月)。

【问题讨论】:

  • 样本数据和期望的结果真的很有帮助。

标签: sql sql-server tsql


【解决方案1】:

结果实现了,你去吧:

declare @min_month datetime=(select dateadd(month,datediff(month,0,min([DATE])),0) from _yourtable)
declare @max_month datetime=(select dateadd(month,datediff(month,0,max([DATE])),0) from _yourtable)

;WITH months(d) AS (
  select @min_month
  UNION ALL
  SELECT dateadd(month,1,d) -- Recursion
  FROM months
  where dateadd(month,1,d)<=getdate()
)
select distinct
    year(m.d) as YEAR,
    month(m.d) as MONTH,
    types.v as [TYPE]
    ,sum(isnull(t.[INPUT],0)-isnull(t.[OUTPUT],0)) over (partition by types.v order by m.d)
from months m
cross join (select distinct type from _yourtable)types(v)
left join _yourtable t on dateadd(month,datediff(month,0,t.[DATE]),0)=m.d and types.v=t.TYPE
order by m.d,type
option(maxrecursion 0)

【讨论】:

  • 感谢@George Menoutis,这几乎是完美的!这些值是正确的,但在某些月份它会返回相同类型的多个记录。
  • 啊,对。尝试在选择后添加 distinct - 这是惰性编码,可能会导致严重的性能问题
  • 完美搭配 select distinct 语句。
【解决方案2】:

您可以使用 Lag 功能,下面的代码可能会有所帮助:

select year(date), month(date), type
, sum(input-output) + isnull(lag(sum(input-output),1,0) over(order by year(date), month(date), type), 0)
from test group by year(date), month(date), type

【讨论】:

  • 差不多了,但是有两个问题:一,大问题,数值不正确。另一个,几个月没有结果,没有动静……
  • 尝试使用下面的查询,它会给你正确的结果。但是,它仍然会返回表中有数据的年/月/类型的数据。 select year(date), month(date), type , sum(isnull(input,0)-isnull(output,0)) + isnull(lag(sum(isnull(input,0)-isnull(output,0)),1,0) over(partition by type order by year(date), month(date), type), 0) from test group by year(date), month(date), type
  • 值仍然错误...其他问题没有问题,但数学需要完美。
  • 感谢@Alankrit 向我介绍LAG 功能。仔细观察后,我注意到在没有某种类型的运动的月份之后,LAG 函数返回零而不是获取最后一个值。所以数学是不正确的,正如我所说的......有什么想法来克服这个吗?
【解决方案3】:

假设您的源数据是您最初提供的结构(即:这不是另一个查询的结果),这是一个相当简单的转换,它使用日期表和通过有序sum 的运行总计。

如果您已经有日期表,则可以删除此脚本中的前 2 个 ctes:

declare @t table(DateValue date,InputAmount decimal(8,2),OutputAmount decimal(8,2),ProdType nvarchar(1));
insert into @t values
 ('2018-01-10',256.35,null,'A')
,('2018-02-05',null, 35.00,'B')
,('2018-02-15', 65.30,null,'A')
,('2018-03-20',158.00,null,'B')
,('2018-04-02',null, 63.32,'B')
,('2018-05-12',null,128.12,'A')
,('2018-06-20',null,  7.35,'B')
;

-- Min date can just be min date in the source table, but the max date should be the month end of the max date in the source table0
declare @MinDate date = (select min(DateValue) from @t);
declare @MaxDate date = (select max(dateadd(day,-1,dateadd(month,datediff(month,0,DateValue)+1,0))) from @t);

with n(n) as (select * from (values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) t(t))   -- Using a tally table, built a table of dates
    ,d(d) as (select top(select datediff(day,@MinDate,@MaxDate)+1) dateadd(day,row_number() over (order by (select null))-1,@MinDate) from n n1,n n2,n n3, n n4)
    ,m as (select p.ProdType                                                        -- Then join to the source data to create a date value for each posible day for each product type
                    ,d.d
                    ,dateadd(day,-1,dateadd(month,datediff(month,0,d)+1,0)) as m    -- And calculate a running total using a windowed aggregate
                    ,sum(isnull(t.InputAmount,0) - isnull(t.OutputAmount,0)) over (partition by p.ProdType order by d.d) as RunningTotal
            from d
                cross join (select distinct ProdType
                            from @t
                            ) as p
                left join @t as t
                    on d.d = t.DateValue
                        and p.ProdType = t.ProdType
            )
select m
        ,ProdType
        ,RunningTotal as Balance
from m
where m = d
order by m.d
        ,m.ProdType;

输出:

+-------------------------+----------+---------+
|            m            | ProdType | Balance |
+-------------------------+----------+---------+
| 2018-01-31 00:00:00.000 | A        |  256.35 |
| 2018-01-31 00:00:00.000 | B        |    0.00 |
| 2018-02-28 00:00:00.000 | A        |  321.65 |
| 2018-02-28 00:00:00.000 | B        |  -35.00 |
| 2018-03-31 00:00:00.000 | A        |  321.65 |
| 2018-03-31 00:00:00.000 | B        |  123.00 |
| 2018-04-30 00:00:00.000 | A        |  321.65 |
| 2018-04-30 00:00:00.000 | B        |   59.68 |
| 2018-05-31 00:00:00.000 | A        |  193.53 |
| 2018-05-31 00:00:00.000 | B        |   59.68 |
| 2018-06-30 00:00:00.000 | A        |  193.53 |
| 2018-06-30 00:00:00.000 | B        |   52.33 |
+-------------------------+----------+---------+

【讨论】:

  • 有趣的是,您对 distinct 发表评论很有趣,但您提供的代码也是如此。在 OP 的示例中,没有共享相同类型和月份(日期)的行。但是,OP 显然在他们的数据库中有这样的情况,这就是多行的来源。您是如何防止这种情况发生的?
  • 我试过这个查询,它也可以工作。但是,我不认为它是最佳答案,因为它不返回年/月,而是返回该月最后一天的日期,此外,在我看来,执行(在我的数据库中)比@George 的查询。
  • @GeorgeMenoutis 跟随代码,你会看到每个type 是为每个date 计算的,然后合计到月底。如果您看不到这是如何工作的,请添加一些额外的虚拟数据并自己测试。尽管看起来很相似,但我的代码实际上与您的代码有很大不同。
  • @PJLG 如果您需要yearmonth,您可以使用year()month() 函数。然而,实践中,当您想要表示日期值时,您很少不想实际返回 date 值。
猜你喜欢
  • 1970-01-01
  • 2016-10-11
  • 2021-11-25
  • 1970-01-01
  • 1970-01-01
  • 2022-01-09
  • 1970-01-01
  • 1970-01-01
  • 2020-02-21
相关资源
最近更新 更多