智能债务老化代码
Intelligent Debt Ageing Code
我有一些数据格式为;
Client Amt Date
ABC Co £250 20/09/16
ABC Co £250 20/10/16
CDE Co £200 20/11/16
CDE Co £200 20/10/16
CDE Co £-200 20/09/16
FGH Co £600 01/01/16
FGH Co £-500 20/09/16
FGH Co £-50 20/10/16
FGH Co £100 20/11/16
我可以像这样轻松旋转它;
Client Balance 0-29days 30-59days 60-89days 90days+
ABC Co £500 £0 £250 £250 £0
CDE Co £200 £200 £200 £-200 £0
FGH Co £100 £100 £-50 £-500 £600
IJK Co £-100 £100 £0 £0 £-200
但我需要它看起来像;
Client Balance 0-29days 30-59days 60-89days 90days+
ABC Co £500 £0 £250 £250 £0
CDE Co £200 £200 £0 £0 £0
FGH Co £100 £100 £0 £0 £50
IJK Co £-100 £0 £0 £0 £-100
列或 "aging buckets" 表示 debit/credit 的年龄。单个事务不会发生在多个桶中。如果有贷方和借方,则应将它们应用到彼此(从最早的开始)。因此,详细说明一些记录...
CDE 公司; 2009 年 20 月 20 日最早的一笔交易 £-200 贷方与下一笔 20 年 10 月 20 日借方 200 英镑的交易相平衡。这只留下 20/11 的 200 英镑借记(因此 0-29 天桶中的 200 英镑借记)。
FGH 公司; 01/01 的最早交易 600 英镑借记部分由 2 笔付款支付 -500 英镑(20/09)和 -50 英镑(20/10),在 90 天以上的桶中留下 50 英镑的借记和最近的借记2011 年 20 月 10 日在 0-29 天桶中 100 英镑。
有没有 query/formula 我可以用来评估这个?还是我必须使用游标?
谢谢
如果您只需要您提供的格式的数据以及您在评论中所说的关于拥有一个未旋转的基础 table 的数据,查询非常简单:
declare @t table(PaymentDate date
,Client nvarchar(50)
,Amount decimal(10,2)
);
insert into @t values
('20160920','ABC Co',250),('20161020','ABC Co',250 ),('20161020','CDE Co',200 ),('20161020','CDE Co',200 ),('20160920','CDE Co',-200 ),('20160101','FGH Co',600 ),('20160920','FGH Co',-500 ),('20161020','FGH Co',-100 ),('20161120','FGH Co',100 );
declare @ReportDate date = getdate();
select Client
-- Data aggregated by each period
,sum(Amount) as ClientBalance
,sum(case when PaymentDate between dateadd(d,-29,@ReportDate) and @ReportDate then Amount else 0 end) as [0-29 Days]
,sum(case when PaymentDate between dateadd(d,-59,@ReportDate) and dateadd(d,-30,@ReportDate) then Amount else 0 end) as [30-59 Days]
,sum(case when PaymentDate between dateadd(d,-89,@ReportDate) and dateadd(d,-60,@ReportDate) then Amount else 0 end) as [60-89 Days]
,sum(case when PaymentDate <= dateadd(d,-90,@ReportDate) then Amount else 0 end) as [90+ Days]
,'' as [ ]
-- Data aggregated as a rolling periodic balance
,sum(Amount) as ClientBalance
,sum(case when PaymentDate <= @ReportDate then Amount else 0 end) as [0-29 Days]
,sum(case when PaymentDate <= dateadd(d,-30,@ReportDate) then Amount else 0 end) as [30-59 Days]
,sum(case when PaymentDate <= dateadd(d,-60,@ReportDate) then Amount else 0 end) as [60-89 Days]
,sum(case when PaymentDate <= dateadd(d,-90,@ReportDate) then Amount else 0 end) as [90+ Days]
from @t
group by Client
order by Client;
Link 到工作示例:http://rextester.com/NAAUE88941
DECLARE @Table AS TABLE (Client CHAR(6), AMT INT, Date DATE)
INSERT INTO @Table VALUES
('ABC Co',250 ,'2016/09/20')
,('ABC Co',250 ,'2016/10/20')
,('CDE Co',200 ,'2016/11/20')
,('CDE Co',200 ,'2016/10/20')
,('CDE Co',-200,'2016/09/20')
,('FGH Co',600 ,'2016/01/01')
,('FGH Co',-500,'2016/09/20')
,('FGH Co',-50 ,'2016/10/20')
,('FGH Co',100 ,'2016/11/20')
,('IJK Co',-100 ,'2016/01/01')
,('IJK Co',-100 ,'2016/09/20')
;WITH cte AS (
SELECT
Client
,Date
,AMT
,CurrentBalance = SUM(AMT) OVER (PARTITION BY Client)
,BackwardsRunningTotal = SUM(AMT) OVER (PARTITION BY Client ORDER BY Date DESC)
,CurrentBalanceMinusBackwardsRunningTotal = SUM(AMT) OVER (PARTITION BY Client) - SUM(AMT) OVER (PARTITION BY Client ORDER BY Date DESC)
,DateGroup = CASE
WHEN DATEDIFF(day,Date,GETDATE()) BETWEEN 0 AND 29 THEN '0-29days'
WHEN DATEDIFF(day,Date,GETDATE()) BETWEEN 30 AND 59 THEN '30-59days'
WHEN DATEDIFF(day,Date,GETDATE()) BETWEEN 60 AND 89 THEN '60-89days'
WHEN DATEDIFF(day,Date,GETDATE()) >= 90 THEN '90days+'
ELSE 'Unknown Error'
END
,BalanceAtTime = SUM(AMT) OVER (PARTITION BY Client ORDER BY Date)
FROM
@Table
)
, cteWhenCurrentBalanceIsMet AS (
SELECT
Client
,MaxDate = MAX(DATE)
FROM
cte
WHERE
CurrentBalanceMinusBackwardsRunningTotal = 0
GROUP BY
Client
)
, cteAgedDebtPrepared AS (
SELECT
c.Client
,Balance = c.CurrentBalance
,c.DateGroup
,Amt = CASE
WHEN CurrentBalanceMinusBackwardsRunningTotal = 0
THEN ISNULL(LAG(CurrentBalanceMinusBackwardsRunningTotal) OVER (PARTITION BY c.Client ORDER BY Date DESC),AMT)
ELSE AMT
END
FROM
cteWhenCurrentBalanceIsMet m
INNER JOIN cte c
ON m.Client = c.Client
AND m.MaxDate <= c.Date
AND SIGN(c.AMT) = SIGN(c.CurrentBalance)
)
SELECT *
FROM
cteAgedDebtPrepared
PIVOT (
SUM(Amt)
FOR DateGroup IN ([0-29days],[30-59days],[60-89days],[90days+])
) pvt
ORDER BY
Client
这绝对是一个具有挑战性的问题,它更是如此,因为即使您说您正在查看老化的债务,您实际上在您的数据透视表中同时显示了老化的债务和老化的信用 Table。我认为在递归 CTE 中做起来会更容易,但我想要更多基于集合的操作,所以上面是我想出的,它适用于您的所有测试用例。请注意,我确实添加了一个,其中网络是信用。
一般步骤
- 确定当前余额
- 向后 运行 总计(例如 SUM(AMT) 从最新日期到最早日期)
- 从当前余额中向后减去 运行 总计以确定余额最后为 0 的点,然后得到
MAX(date)
发生的
- 执行自连接以获取
MAX(date)
与 SIGN()
相同的所有记录 >=
,例如当余额为正时,amt 必须为正或反转为负和负的学分。它必须是相同的 SIGN() 的原因是 inverse 实际上会影响我们正在寻找的相反方向的余额。
- 通过查看上一行或分配 AMT,找出当余额最后为 0 时需要归因于第一行的剩余债务或信贷
- 根据需要旋转
结果:
Client Balance 0-29days 30-59days 60-89days 90days+
ABC Co 500 NULL 250 250 NULL
CDE Co 200 200 NULL NULL NULL
FGH Co 150 100 NULL NULL 50
IJK Co -200 NULL NULL -100 -100
对于我的 IJK 示例,请注意我有两个 -100 的学分。
这是一个似乎符合您预期输出的解决方案。请注意,它有点混乱,您也许可以稍微简化逻辑,但至少它看起来有效。
Link 工作示例:http://rextester.com/OWH97326
请注意,此答案改编自 dba.stackexchange.com 上的 solution to a slightly similar problem。这个解决方案给我留下了深刻的印象。
Create Table Debt (
Client char(6),
Amount money,
[Date] date);
Insert Into Debt
Values
('ABC Co', 250, Convert(date, '20/09/2016', 103)),
('ABC Co', 250, Convert(date, '20/10/2016', 103)),
('CDE Co', 200, Convert(date, '20/11/2016', 103)),
('CDE Co', 200, Convert(date, '20/10/2016', 103)),
('CDE Co', -200, Convert(date, '20/09/2016', 103)),
('FGH Co', 600, Convert(date, '01/01/2016', 103)),
('FGH Co', -500, Convert(date, '20/09/2016', 103)),
('FGH Co', -50, Convert(date, '20/10/2016', 103)),
('FGH Co', 100, Convert(date, '20/11/2016', 103));
With Grouping_cte As (
Select Client, Sum(ABS(Amount)) As Amount,
Case When DateDiff(Day, GetDate(), [Date]) > -30 Then '0-29 days'
When DateDiff(Day, GetDate(), [Date]) > -60 Then '30-59 days'
When DateDiff(Day, GetDate(), [Date]) > -90 Then '60-89 days'
Else '90+ days' End As [Date],
Case When Amount < 0 Then 'In' Else 'Out' End As [Type]
From Debt
Group By Client,
Case When DateDiff(Day, GetDate(), [Date]) > -30 Then '0-29 days'
When DateDiff(Day, GetDate(), [Date]) > -60 Then '30-59 days'
When DateDiff(Day, GetDate(), [Date]) > -90 Then '60-89 days'
Else '90+ days' End,
Case When Amount < 0 Then 'In' Else 'Out' End),
RunningTotals_cte As (
Select Client, Amount, [Date], [Type],
Sum(Amount) Over (Partition By Client, [Type] Order By [Date] Desc) - Amount As RunningTotalFrom,
Sum(Amount) Over (Partition By Client, [Type] Order By [Date] Desc) As RunningTotalTo
From Grouping_cte),
Allocated_cte As (
Select Outs.Client, Outs.Date, Outs.Amount + IsNull(Sum(x.borrowed_qty),0) As AdjustedAmount
From (Select * From RunningTotals_cte Where [Type] = 'Out') As Outs
Left Join (Select * From RunningTotals_cte Where [Type] = 'In') As Ins
On Ins.RunningTotalFrom < Outs.RunningTotalTo
And Outs.RunningTotalFrom < Ins.RunningTotalTo
And Ins.Client = Outs.Client
Cross Apply (
Select Case When ins.RunningTotalTo < Outs.RunningTotalTo Then Case When ins.RunningTotalFrom > Outs.RunningTotalFrom Then -1 * Ins.Amount
Else -1 * (Ins.RunningTotalTo - Outs.RunningTotalFrom) End
Else Case When Outs.RunningTotalFrom > Ins.RunningTotalFrom Then Outs.Amount
Else -1 * (Outs.RunningTotalTo - Ins.RunningTotalFrom) End End) As x (borrowed_qty)
Group By Outs.Client, Outs.Date, Outs.Amount)
--Select * From Allocated_cte;
Select Client,
Sum(AdjustedAmount) As Balance,
Sum(iif([Date] = '0-29 days', AdjustedAmount, Null)) As [0-29 days],
Sum(iif([Date] = '30-59 days', AdjustedAmount, Null)) As [30-59 days],
Sum(iif([Date] = '60-89 days', AdjustedAmount, Null)) As [60-89 days],
Sum(iif([Date] = '90+ days', AdjustedAmount, Null)) As [90+ days]
From Allocated_cte
Group By Client;
Link 显示有效:http://rextester.com/MLFE98410
我很好奇从逻辑上讲哪个更容易递归 cte 更容易一些但仍然有一些相同的障碍。注意我在这里也添加了 1 个测试用例。
DECLARE @Table AS TABLE (Client CHAR(6), AMT INT, Date DATE)
INSERT INTO @Table VALUES
('ABC Co',250 ,'2016/09/20')
,('ABC Co',250 ,'2016/10/20')
,('CDE Co',200 ,'2016/11/20')
,('CDE Co',200 ,'2016/10/20')
,('CDE Co',-200,'2016/09/20')
,('FGH Co',600 ,'2016/01/01')
,('FGH Co',-500,'2016/09/20')
,('FGH Co',-50 ,'2016/10/20')
,('FGH Co',100 ,'2016/11/20')
,('IJK Co',-100 ,'2016/01/01')
,('IJK Co',-100 ,'2016/09/20')
,('LMN Co',-200 ,'2016/01/01')
,('LMN Co', 50 ,'2016/06/10')
,('LMN Co',-100 ,'2016/09/20')
;WITH cteRowNumbers AS (
SELECT *, RowNumber = ROW_NUMBER() OVER (PARTITION BY Client ORDER BY Date DESC)
FROM
@Table
)
, cteRecursive AS (
SELECT
Client
,CurrentBalance = SUM(AMT)
,Date = CAST(GETDATE() AS DATE)
,Amt = CAST(0 AS INT)
,RemainingBalance = SUM(Amt)
,AttributedAmt = 0
,RowNumber = CAST(0 AS BIGINT)
FROM
@Table
GROUP BY
Client
UNION ALL
SELECT
r.Client
,r.CurrentBalance
,c.Date
,c.AMT
,CASE WHEN SIGN(r.CurrentBalance) = SIGN(c.AMT) THEN r.CurrentBalance - c.AMT ELSE r.RemainingBalance END
,CASE
WHEN SIGN(r.CurrentBalance) <> SIGN(c.AMT) THEN 0
WHEN ABS(r.RemainingBalance) < ABS(c.AMT) THEN r.RemainingBalance
ELSE c.AMT END
,c.RowNumber
FROM
cteRecursive r
INNER JOIN cteRowNumbers c
ON r.Client = c.Client
AND r.RowNumber + 1 = c.RowNumber
WHERE
SIGN(r.RemainingBalance) = SIGN(r.CurrentBalance)
)
, ctePrepared AS (
SELECT
Client
,CurrentBalance
,DateGroup = CASE
WHEN DATEDIFF(day,Date,GETDATE()) BETWEEN 0 AND 29 THEN '0-29days'
WHEN DATEDIFF(day,Date,GETDATE()) BETWEEN 30 AND 59 THEN '30-59days'
WHEN DATEDIFF(day,Date,GETDATE()) BETWEEN 60 AND 89 THEN '60-89days'
WHEN DATEDIFF(day,Date,GETDATE()) >= 90 THEN '90days+'
ELSE 'Unknown Error'
END
,AttributedAmt
FROM
cteRecursive
WHERE
RowNumber > 0
AND AttributedAmt <> 0
)
SELECT *
FROM
ctePrepared c
PIVOT (
SUM(AttributedAmt)
FOR DateGroup IN ([0-29days],[30-59days],[60-89days],[90days+])
) pvt
ORDER BY
Client
结果
Client CurrentBalance 0-29days 30-59days 60-89days 90days+
ABC Co 500 NULL 250 250 NULL
CDE Co 200 200 NULL NULL NULL
FGH Co 150 100 NULL NULL 50
IJK Co -200 NULL NULL -100 -100
LMN Co -250 NULL NULL -100 -150
我已经回答过类似的问题here and and
您需要将借方和贷方分解为单个单位,然后按时间顺序将它们耦合,并过滤掉匹配的行,然后您可以按期间对它们进行帐龄处理。
只需旋转每个时期的总和。
DECLARE @Table AS TABLE (Client CHAR(6), AMT INT, Date DATE)
INSERT INTO @Table VALUES
('ABC Co',250 ,'2016/09/20')
,('ABC Co',250 ,'2016/10/20')
,('CDE Co',200 ,'2016/11/20')
,('CDE Co',200 ,'2016/10/20')
,('CDE Co',-200,'2016/09/20')
,('FGH Co',600 ,'2016/01/01')
,('FGH Co',-500,'2016/09/20')
,('FGH Co',-50 ,'2016/10/20')
,('FGH Co',100 ,'2016/11/20')
,('IJK Co',-200 ,'2016/01/01')
,('IJK Co',100 ,'2016/09/20')
对于 FN_NUMBERS(n),它是一个计数 table,查看我在上面链接的其他答案以获得示例或 google 它。
;with
m as (select * from @Table),
e as (select * from m where AMT>0),
r as (select * from m where AMT<0),
ex as (
select *, ROW_NUMBER() over (partition by Client order by [date] ) rn, 1 q
from e
join FN_NUMBERS(1000) on N<= e.AMT
),
rx as (
select *, ROW_NUMBER() over (partition by Client order by [date] ) rn, 1 q
from r
join FN_NUMBERS(1000) on N<= -r.AMT
),
j as (
select
isnull(ex.Client, rx.Client) Client,
(datediff(DAY, ISNULL(ex.[Date],rx.[Date]), GETDATE()) / 30) dd,
(isnull(ex.q,0) - isnull(rx.q,0)) q
from ex
full join rx on ex.Client = rx.Client and ex.rn = rx.rn
where ex.Client is null or rx.Client is null
),
mm as (
select j.Client, j.q, isnull(x.n,99) n
from j
left join (values (0),(1),(2)) x (n) on dd=n
),
b as (
select Client, SUM(AMT) balance
from m
group by Client
),
p as (
select b.*, p.[0] as [0-12days], p.[1] as [30-59days], p.[2] as [60-89days], p.[99] as [90days+]
from mm
pivot (sum(q) for n in ([0],[1],[2],[99])) p
left join b on p.Client = b.Client
)
select *
from p
order by 1
完美输出
Client balance 0-12days 30-59days 60-89days 90days+
ABC Co 500 NULL 250 250 NULL
CDE Co 200 200 NULL NULL NULL
FGH Co 150 100 NULL NULL 50
IJK Co -100 NULL NULL NULL -100
再见
我有一些数据格式为;
Client Amt Date
ABC Co £250 20/09/16
ABC Co £250 20/10/16
CDE Co £200 20/11/16
CDE Co £200 20/10/16
CDE Co £-200 20/09/16
FGH Co £600 01/01/16
FGH Co £-500 20/09/16
FGH Co £-50 20/10/16
FGH Co £100 20/11/16
我可以像这样轻松旋转它;
Client Balance 0-29days 30-59days 60-89days 90days+
ABC Co £500 £0 £250 £250 £0
CDE Co £200 £200 £200 £-200 £0
FGH Co £100 £100 £-50 £-500 £600
IJK Co £-100 £100 £0 £0 £-200
但我需要它看起来像;
Client Balance 0-29days 30-59days 60-89days 90days+
ABC Co £500 £0 £250 £250 £0
CDE Co £200 £200 £0 £0 £0
FGH Co £100 £100 £0 £0 £50
IJK Co £-100 £0 £0 £0 £-100
列或 "aging buckets" 表示 debit/credit 的年龄。单个事务不会发生在多个桶中。如果有贷方和借方,则应将它们应用到彼此(从最早的开始)。因此,详细说明一些记录...
CDE 公司; 2009 年 20 月 20 日最早的一笔交易 £-200 贷方与下一笔 20 年 10 月 20 日借方 200 英镑的交易相平衡。这只留下 20/11 的 200 英镑借记(因此 0-29 天桶中的 200 英镑借记)。
FGH 公司; 01/01 的最早交易 600 英镑借记部分由 2 笔付款支付 -500 英镑(20/09)和 -50 英镑(20/10),在 90 天以上的桶中留下 50 英镑的借记和最近的借记2011 年 20 月 10 日在 0-29 天桶中 100 英镑。
有没有 query/formula 我可以用来评估这个?还是我必须使用游标?
谢谢
如果您只需要您提供的格式的数据以及您在评论中所说的关于拥有一个未旋转的基础 table 的数据,查询非常简单:
declare @t table(PaymentDate date
,Client nvarchar(50)
,Amount decimal(10,2)
);
insert into @t values
('20160920','ABC Co',250),('20161020','ABC Co',250 ),('20161020','CDE Co',200 ),('20161020','CDE Co',200 ),('20160920','CDE Co',-200 ),('20160101','FGH Co',600 ),('20160920','FGH Co',-500 ),('20161020','FGH Co',-100 ),('20161120','FGH Co',100 );
declare @ReportDate date = getdate();
select Client
-- Data aggregated by each period
,sum(Amount) as ClientBalance
,sum(case when PaymentDate between dateadd(d,-29,@ReportDate) and @ReportDate then Amount else 0 end) as [0-29 Days]
,sum(case when PaymentDate between dateadd(d,-59,@ReportDate) and dateadd(d,-30,@ReportDate) then Amount else 0 end) as [30-59 Days]
,sum(case when PaymentDate between dateadd(d,-89,@ReportDate) and dateadd(d,-60,@ReportDate) then Amount else 0 end) as [60-89 Days]
,sum(case when PaymentDate <= dateadd(d,-90,@ReportDate) then Amount else 0 end) as [90+ Days]
,'' as [ ]
-- Data aggregated as a rolling periodic balance
,sum(Amount) as ClientBalance
,sum(case when PaymentDate <= @ReportDate then Amount else 0 end) as [0-29 Days]
,sum(case when PaymentDate <= dateadd(d,-30,@ReportDate) then Amount else 0 end) as [30-59 Days]
,sum(case when PaymentDate <= dateadd(d,-60,@ReportDate) then Amount else 0 end) as [60-89 Days]
,sum(case when PaymentDate <= dateadd(d,-90,@ReportDate) then Amount else 0 end) as [90+ Days]
from @t
group by Client
order by Client;
Link 到工作示例:http://rextester.com/NAAUE88941
DECLARE @Table AS TABLE (Client CHAR(6), AMT INT, Date DATE)
INSERT INTO @Table VALUES
('ABC Co',250 ,'2016/09/20')
,('ABC Co',250 ,'2016/10/20')
,('CDE Co',200 ,'2016/11/20')
,('CDE Co',200 ,'2016/10/20')
,('CDE Co',-200,'2016/09/20')
,('FGH Co',600 ,'2016/01/01')
,('FGH Co',-500,'2016/09/20')
,('FGH Co',-50 ,'2016/10/20')
,('FGH Co',100 ,'2016/11/20')
,('IJK Co',-100 ,'2016/01/01')
,('IJK Co',-100 ,'2016/09/20')
;WITH cte AS (
SELECT
Client
,Date
,AMT
,CurrentBalance = SUM(AMT) OVER (PARTITION BY Client)
,BackwardsRunningTotal = SUM(AMT) OVER (PARTITION BY Client ORDER BY Date DESC)
,CurrentBalanceMinusBackwardsRunningTotal = SUM(AMT) OVER (PARTITION BY Client) - SUM(AMT) OVER (PARTITION BY Client ORDER BY Date DESC)
,DateGroup = CASE
WHEN DATEDIFF(day,Date,GETDATE()) BETWEEN 0 AND 29 THEN '0-29days'
WHEN DATEDIFF(day,Date,GETDATE()) BETWEEN 30 AND 59 THEN '30-59days'
WHEN DATEDIFF(day,Date,GETDATE()) BETWEEN 60 AND 89 THEN '60-89days'
WHEN DATEDIFF(day,Date,GETDATE()) >= 90 THEN '90days+'
ELSE 'Unknown Error'
END
,BalanceAtTime = SUM(AMT) OVER (PARTITION BY Client ORDER BY Date)
FROM
@Table
)
, cteWhenCurrentBalanceIsMet AS (
SELECT
Client
,MaxDate = MAX(DATE)
FROM
cte
WHERE
CurrentBalanceMinusBackwardsRunningTotal = 0
GROUP BY
Client
)
, cteAgedDebtPrepared AS (
SELECT
c.Client
,Balance = c.CurrentBalance
,c.DateGroup
,Amt = CASE
WHEN CurrentBalanceMinusBackwardsRunningTotal = 0
THEN ISNULL(LAG(CurrentBalanceMinusBackwardsRunningTotal) OVER (PARTITION BY c.Client ORDER BY Date DESC),AMT)
ELSE AMT
END
FROM
cteWhenCurrentBalanceIsMet m
INNER JOIN cte c
ON m.Client = c.Client
AND m.MaxDate <= c.Date
AND SIGN(c.AMT) = SIGN(c.CurrentBalance)
)
SELECT *
FROM
cteAgedDebtPrepared
PIVOT (
SUM(Amt)
FOR DateGroup IN ([0-29days],[30-59days],[60-89days],[90days+])
) pvt
ORDER BY
Client
这绝对是一个具有挑战性的问题,它更是如此,因为即使您说您正在查看老化的债务,您实际上在您的数据透视表中同时显示了老化的债务和老化的信用 Table。我认为在递归 CTE 中做起来会更容易,但我想要更多基于集合的操作,所以上面是我想出的,它适用于您的所有测试用例。请注意,我确实添加了一个,其中网络是信用。
一般步骤
- 确定当前余额
- 向后 运行 总计(例如 SUM(AMT) 从最新日期到最早日期)
- 从当前余额中向后减去 运行 总计以确定余额最后为 0 的点,然后得到
MAX(date)
发生的 - 执行自连接以获取
MAX(date)
与SIGN()
相同的所有记录>=
,例如当余额为正时,amt 必须为正或反转为负和负的学分。它必须是相同的 SIGN() 的原因是 inverse 实际上会影响我们正在寻找的相反方向的余额。 - 通过查看上一行或分配 AMT,找出当余额最后为 0 时需要归因于第一行的剩余债务或信贷
- 根据需要旋转
结果:
Client Balance 0-29days 30-59days 60-89days 90days+
ABC Co 500 NULL 250 250 NULL
CDE Co 200 200 NULL NULL NULL
FGH Co 150 100 NULL NULL 50
IJK Co -200 NULL NULL -100 -100
对于我的 IJK 示例,请注意我有两个 -100 的学分。
这是一个似乎符合您预期输出的解决方案。请注意,它有点混乱,您也许可以稍微简化逻辑,但至少它看起来有效。
Link 工作示例:http://rextester.com/OWH97326
请注意,此答案改编自 dba.stackexchange.com 上的 solution to a slightly similar problem。这个解决方案给我留下了深刻的印象。
Create Table Debt (
Client char(6),
Amount money,
[Date] date);
Insert Into Debt
Values
('ABC Co', 250, Convert(date, '20/09/2016', 103)),
('ABC Co', 250, Convert(date, '20/10/2016', 103)),
('CDE Co', 200, Convert(date, '20/11/2016', 103)),
('CDE Co', 200, Convert(date, '20/10/2016', 103)),
('CDE Co', -200, Convert(date, '20/09/2016', 103)),
('FGH Co', 600, Convert(date, '01/01/2016', 103)),
('FGH Co', -500, Convert(date, '20/09/2016', 103)),
('FGH Co', -50, Convert(date, '20/10/2016', 103)),
('FGH Co', 100, Convert(date, '20/11/2016', 103));
With Grouping_cte As (
Select Client, Sum(ABS(Amount)) As Amount,
Case When DateDiff(Day, GetDate(), [Date]) > -30 Then '0-29 days'
When DateDiff(Day, GetDate(), [Date]) > -60 Then '30-59 days'
When DateDiff(Day, GetDate(), [Date]) > -90 Then '60-89 days'
Else '90+ days' End As [Date],
Case When Amount < 0 Then 'In' Else 'Out' End As [Type]
From Debt
Group By Client,
Case When DateDiff(Day, GetDate(), [Date]) > -30 Then '0-29 days'
When DateDiff(Day, GetDate(), [Date]) > -60 Then '30-59 days'
When DateDiff(Day, GetDate(), [Date]) > -90 Then '60-89 days'
Else '90+ days' End,
Case When Amount < 0 Then 'In' Else 'Out' End),
RunningTotals_cte As (
Select Client, Amount, [Date], [Type],
Sum(Amount) Over (Partition By Client, [Type] Order By [Date] Desc) - Amount As RunningTotalFrom,
Sum(Amount) Over (Partition By Client, [Type] Order By [Date] Desc) As RunningTotalTo
From Grouping_cte),
Allocated_cte As (
Select Outs.Client, Outs.Date, Outs.Amount + IsNull(Sum(x.borrowed_qty),0) As AdjustedAmount
From (Select * From RunningTotals_cte Where [Type] = 'Out') As Outs
Left Join (Select * From RunningTotals_cte Where [Type] = 'In') As Ins
On Ins.RunningTotalFrom < Outs.RunningTotalTo
And Outs.RunningTotalFrom < Ins.RunningTotalTo
And Ins.Client = Outs.Client
Cross Apply (
Select Case When ins.RunningTotalTo < Outs.RunningTotalTo Then Case When ins.RunningTotalFrom > Outs.RunningTotalFrom Then -1 * Ins.Amount
Else -1 * (Ins.RunningTotalTo - Outs.RunningTotalFrom) End
Else Case When Outs.RunningTotalFrom > Ins.RunningTotalFrom Then Outs.Amount
Else -1 * (Outs.RunningTotalTo - Ins.RunningTotalFrom) End End) As x (borrowed_qty)
Group By Outs.Client, Outs.Date, Outs.Amount)
--Select * From Allocated_cte;
Select Client,
Sum(AdjustedAmount) As Balance,
Sum(iif([Date] = '0-29 days', AdjustedAmount, Null)) As [0-29 days],
Sum(iif([Date] = '30-59 days', AdjustedAmount, Null)) As [30-59 days],
Sum(iif([Date] = '60-89 days', AdjustedAmount, Null)) As [60-89 days],
Sum(iif([Date] = '90+ days', AdjustedAmount, Null)) As [90+ days]
From Allocated_cte
Group By Client;
Link 显示有效:http://rextester.com/MLFE98410
我很好奇从逻辑上讲哪个更容易递归 cte 更容易一些但仍然有一些相同的障碍。注意我在这里也添加了 1 个测试用例。
DECLARE @Table AS TABLE (Client CHAR(6), AMT INT, Date DATE)
INSERT INTO @Table VALUES
('ABC Co',250 ,'2016/09/20')
,('ABC Co',250 ,'2016/10/20')
,('CDE Co',200 ,'2016/11/20')
,('CDE Co',200 ,'2016/10/20')
,('CDE Co',-200,'2016/09/20')
,('FGH Co',600 ,'2016/01/01')
,('FGH Co',-500,'2016/09/20')
,('FGH Co',-50 ,'2016/10/20')
,('FGH Co',100 ,'2016/11/20')
,('IJK Co',-100 ,'2016/01/01')
,('IJK Co',-100 ,'2016/09/20')
,('LMN Co',-200 ,'2016/01/01')
,('LMN Co', 50 ,'2016/06/10')
,('LMN Co',-100 ,'2016/09/20')
;WITH cteRowNumbers AS (
SELECT *, RowNumber = ROW_NUMBER() OVER (PARTITION BY Client ORDER BY Date DESC)
FROM
@Table
)
, cteRecursive AS (
SELECT
Client
,CurrentBalance = SUM(AMT)
,Date = CAST(GETDATE() AS DATE)
,Amt = CAST(0 AS INT)
,RemainingBalance = SUM(Amt)
,AttributedAmt = 0
,RowNumber = CAST(0 AS BIGINT)
FROM
@Table
GROUP BY
Client
UNION ALL
SELECT
r.Client
,r.CurrentBalance
,c.Date
,c.AMT
,CASE WHEN SIGN(r.CurrentBalance) = SIGN(c.AMT) THEN r.CurrentBalance - c.AMT ELSE r.RemainingBalance END
,CASE
WHEN SIGN(r.CurrentBalance) <> SIGN(c.AMT) THEN 0
WHEN ABS(r.RemainingBalance) < ABS(c.AMT) THEN r.RemainingBalance
ELSE c.AMT END
,c.RowNumber
FROM
cteRecursive r
INNER JOIN cteRowNumbers c
ON r.Client = c.Client
AND r.RowNumber + 1 = c.RowNumber
WHERE
SIGN(r.RemainingBalance) = SIGN(r.CurrentBalance)
)
, ctePrepared AS (
SELECT
Client
,CurrentBalance
,DateGroup = CASE
WHEN DATEDIFF(day,Date,GETDATE()) BETWEEN 0 AND 29 THEN '0-29days'
WHEN DATEDIFF(day,Date,GETDATE()) BETWEEN 30 AND 59 THEN '30-59days'
WHEN DATEDIFF(day,Date,GETDATE()) BETWEEN 60 AND 89 THEN '60-89days'
WHEN DATEDIFF(day,Date,GETDATE()) >= 90 THEN '90days+'
ELSE 'Unknown Error'
END
,AttributedAmt
FROM
cteRecursive
WHERE
RowNumber > 0
AND AttributedAmt <> 0
)
SELECT *
FROM
ctePrepared c
PIVOT (
SUM(AttributedAmt)
FOR DateGroup IN ([0-29days],[30-59days],[60-89days],[90days+])
) pvt
ORDER BY
Client
结果
Client CurrentBalance 0-29days 30-59days 60-89days 90days+
ABC Co 500 NULL 250 250 NULL
CDE Co 200 200 NULL NULL NULL
FGH Co 150 100 NULL NULL 50
IJK Co -200 NULL NULL -100 -100
LMN Co -250 NULL NULL -100 -150
我已经回答过类似的问题here and
您需要将借方和贷方分解为单个单位,然后按时间顺序将它们耦合,并过滤掉匹配的行,然后您可以按期间对它们进行帐龄处理。
只需旋转每个时期的总和。
DECLARE @Table AS TABLE (Client CHAR(6), AMT INT, Date DATE)
INSERT INTO @Table VALUES
('ABC Co',250 ,'2016/09/20')
,('ABC Co',250 ,'2016/10/20')
,('CDE Co',200 ,'2016/11/20')
,('CDE Co',200 ,'2016/10/20')
,('CDE Co',-200,'2016/09/20')
,('FGH Co',600 ,'2016/01/01')
,('FGH Co',-500,'2016/09/20')
,('FGH Co',-50 ,'2016/10/20')
,('FGH Co',100 ,'2016/11/20')
,('IJK Co',-200 ,'2016/01/01')
,('IJK Co',100 ,'2016/09/20')
对于 FN_NUMBERS(n),它是一个计数 table,查看我在上面链接的其他答案以获得示例或 google 它。
;with
m as (select * from @Table),
e as (select * from m where AMT>0),
r as (select * from m where AMT<0),
ex as (
select *, ROW_NUMBER() over (partition by Client order by [date] ) rn, 1 q
from e
join FN_NUMBERS(1000) on N<= e.AMT
),
rx as (
select *, ROW_NUMBER() over (partition by Client order by [date] ) rn, 1 q
from r
join FN_NUMBERS(1000) on N<= -r.AMT
),
j as (
select
isnull(ex.Client, rx.Client) Client,
(datediff(DAY, ISNULL(ex.[Date],rx.[Date]), GETDATE()) / 30) dd,
(isnull(ex.q,0) - isnull(rx.q,0)) q
from ex
full join rx on ex.Client = rx.Client and ex.rn = rx.rn
where ex.Client is null or rx.Client is null
),
mm as (
select j.Client, j.q, isnull(x.n,99) n
from j
left join (values (0),(1),(2)) x (n) on dd=n
),
b as (
select Client, SUM(AMT) balance
from m
group by Client
),
p as (
select b.*, p.[0] as [0-12days], p.[1] as [30-59days], p.[2] as [60-89days], p.[99] as [90days+]
from mm
pivot (sum(q) for n in ([0],[1],[2],[99])) p
left join b on p.Client = b.Client
)
select *
from p
order by 1
完美输出
Client balance 0-12days 30-59days 60-89days 90days+
ABC Co 500 NULL 250 250 NULL
CDE Co 200 200 NULL NULL NULL
FGH Co 150 100 NULL NULL 50
IJK Co -100 NULL NULL NULL -100
再见