智能债务老化代码

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

再见