web-dev-qa-db-ja.com

データセットに対して「SpreadNegitive」を実行する

未払いの残高とクレジットを表すテーブルが両方とも1つのテーブルに含まれている状況があります。私がする必要があるのは、すべての未払いのクレジット(できれば最も古いものから順に)をすべての未払いの残高(最も古いものから順に)に適用することです。

たとえば(負の残高はクレジットを表します)

Account_ID  DateOfEntry  Balance  
----------  -----------  -------  
1           1/1/2012     10.00    
1           1/2/2012     -15.00
2           1/1/2012     -15.00
2           1/2/2012     10.00
3           1/1/2012     10.00
3           1/2/2012     1.00
3           1/3/2012     -5.00
4           1/1/2012     5.00
4           1/2/2012     5.00
4           1/3/2012     -7.00
5           1/1/2012     10.00
5           1/2/2012     -5.00
5           1/3/2012     -5.00

となります:

Account_ID  DateOfEntry  Balance  
----------  -----------  -------    
1           1/2/2012     -5.00
2           1/1/2012     -5.00
3           1/1/2012     5.00
3           1/2/2012     1.00
4           1/2/2012     2.00

これが起こったことの内訳です

  • アカウント1と2にはクレジットが残っています(支払いの順序とクレジットの相対的な順序は重要ではないことを示しています)
  • アカウント3には2つの残高が残っています(クレジットが最初に最も古い残高に適用されることを示しています)
  • アカウント4には、2012年1月2日に残っている残高が1つあります(最初の残高がクレジットを満たさない場合、次に古い残高にクレジットが適用されることを示します)
  • クレジットが残高と完全に一致するため、アカウント5はなくなりました。

これが私の実際のテーブルの関連する列のスキーマです

CREATE TABLE [dbo].[IDAT_AR_BALANCES](
    [cvtGUID] [uniqueidentifier] ROWGUIDCOL  NOT NULL,
    [CLIENT_ID] [varchar](11) NOT NULL,
    [AGING_DATE] [datetime] NOT NULL,
    [AMOUNT] [money] NOT NULL,
 CONSTRAINT [PK_IDAT_ARBALANCES] PRIMARY KEY CLUSTERED ([cvtGUID] ASC)
)

現在、利用可能なすべてのクレジットをカーソルでループしてこれを行っています。

--Remove AR that totals to 0.
DELETE FROM IDAT_AR_BALANCES 
WHERE client_id IN ( 
SELECT client_id 
FROM IDAT_AR_BALANCES 
GROUP BY client_id 
HAVING SUM(amount) = 0)

--Spred the credits on to existing balances.
select * into #balances from [IDAT_AR_BALANCES] where amount > 0
select * into #credits from [IDAT_AR_BALANCES] where amount < 0

declare credit_cursor cursor for select [CLIENT_ID], amount, cvtGUID from #credits 

open credit_cursor 

declare @client_id varchar(11) 
declare @credit money 
declare @balance money 
declare @cvtGuidBalance uniqueidentifier 
declare @cvtGuidCredit uniqueidentifier 

fetch next from credit_cursor into @client_id, @credit, @cvtGuidCredit 
while @@fetch_status = 0 
begin 
  --While balances exist for the current client_ID and there are still credits to be applied, loop.
  while(@credit < 0 and (select count(*) from #balances where @client_id = CLIENT_ID and amount <> 0) > 0) 
  begin 
    --Find the oldest oustanding balance.
    select top 1  @balance = amount, @cvtGuidBalance = cvtGuid 
    from #balances 
    where @client_id = CLIENT_ID and amount <> 0 
    order by AGING_DATE 

    -- merge the balance and the credit
    set @credit = @balance + @credit 

    --If the credit is now postive save the leftover in the currently selected balance and set the credit to 0
    if(@credit > 0) 
    begin 
      update #balances set amount = @credit where cvtGuid = @cvtGuidBalance 
      set @credit = 0 
    end
    else -- Credit is larger than the balance, 0 out the balance and continue processesing
      update #balances set amount = 0 where cvtGuid = @cvtGuidBalance 

  end  -- end of while loop

  --There are no more balances to apply the credit to, save it back to the list.
  update #credits set amount = @credit where cvtGuid = @cvtGuidCredit 

  --Get the next credit.
  fetch next from credit_cursor into @client_id, @credit, @cvtGuidCredit 
end 
close credit_cursor 
deallocate credit_cursor

--Delete any balances and credits that where 0'ed out durning the spred negitive.
delete #balances where AMOUNT = 0
delete #credits where AMOUNT = 0

truncate table [IDAT_AR_BALANCES]
insert [IDAT_AR_BALANCES] select * from #balances
insert [IDAT_AR_BALANCES] select * from #credits
drop table #balances
drop table #credits

カーソルなしでこれを実行してパフォーマンスを向上させるには、これを行うためのより良い方法があると確信していますが、を使用せずに「最も古い日付を最初に使用する」要件を満たす方法を理解するのは困難です。カーソル。

2

多くの場合、現在の合計などの操作では、他のいくつかの方法よりもカーソルを使用する方が実際には効率的です。顕著なパフォーマンスの問題が発生しない限り、カーソルの使用を恐れないでください。

SQL Server 2012には、これを改善する新しいウィンドウ関数がありますが、明らかにそれらを使用することはできません。 「機能する」という風変わりな更新アプローチがありますが、構文は公式にはサポートされておらず、SQL Server 2012以降では機能しない可能性があります。いつか違法な構文になる可能性があり、順序がどのように機能するかについての保証はありません。一般的なセットベースのアプローチは適切にスケーリングされません。カーソルを使用すると、各行を1回スキャンするだけでよいので、多くの場合、より適切ですが、セットベースのソリューションでは、スキャンは非線形に増加します。私がこれまで行ってきた仕事をお見せしたいのですが、それをすべて明らかにするブログ投稿の公開日はまだ不明です。

少なくとも、カーソルの使用を宣言するときは、次のようにします。

DECLARE ... CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY

これも読むのに役立つかもしれません:

1
Aaron Bertrand

アーロンの提案のおかげで、すべてをセットベースにするのではなく、セットが最もよく適用されるセットを作成し、残りはカーソルを使用するようにしました。

また、最も古いクレジットからプルするだけでよいという要件を変更することもできました。これで、クライアントごとに1つのバケットにクレジットを合計して、それに対処することができます。

今でははるかに高速です、これが更新されたバージョンです

--indexes to speed up first two queires
IF not EXISTS (SELECT * FROM sys.indexes WHERE object_id = OBJECT_ID(N'[dbo].[IDAT_AR_BALANCES]') AND name = N'IX_IDATARBALANCES_CLIENTID_GUID')
    CREATE NONCLUSTERED INDEX IX_IDATARBALANCES_CLIENTID_GUID
    ON [dbo].[IDAT_AR_BALANCES] ([CLIENT_ID])
    INCLUDE ([cvtGUID])

IF not EXISTS (SELECT * FROM sys.indexes WHERE object_id = OBJECT_ID(N'[dbo].[IDAT_AR_BALANCES]') AND name = N'IX_IDATARBALANCES_AMOUNT_ID_DATE')   
    CREATE NONCLUSTERED INDEX [IX_IDATARBALANCES_AMOUNT_ID_DATE]
    ON [dbo].[IDAT_AR_BALANCES] ([AMOUNT])
    INCLUDE ([CLIENT_ID],[AGING_DATE])

--Remove AR that totals to 0.
DELETE FROM IDAT_AR_BALANCES 
WHERE client_id IN ( 
SELECT client_id 
FROM IDAT_AR_BALANCES 
GROUP BY client_id 
HAVING SUM(amount) = 0)

--find all instances that credit > balance
SELECT newid() as cvtGUID, client_id, max(AGING_DATE) as AGING_DATE, sum(AMOUNT) as amount
into #creditLarger
FROM IDAT_AR_BALANCES 
GROUP BY client_id 
HAVING SUM(amount) < 0

--remove all of the creditLargerEntries
delete IDAT_AR_BALANCES where client_id in (select client_id from #creditLarger)

--Build a list of remaining balances and summed credits
select * into #balances from [IDAT_AR_BALANCES] where amount > 0

SELECT newid() as cvtGUID, client_id, max(AGING_DATE) as AGING_DATE, sum(AMOUNT) as amount
into #credits 
FROM [IDAT_AR_BALANCES] 
where amount < 0 
GROUP BY client_id

--Index to make the update faster
CREATE NONCLUSTERED INDEX BALANCE_INDEX ON #balances ([CLIENT_ID],[AMOUNT])

--Begin loop of processing credits
set nocount on
declare credit_cursor cursor LOCAL STATIC READ_ONLY FORWARD_ONLY for select top 10 [CLIENT_ID], amount, cvtGUID from #credits 
open credit_cursor 

declare @client_id varchar(11) 
declare @credit money 
declare @balance money 
declare @cvtGuidBalance uniqueidentifier 
declare @cvtGuidCredit uniqueidentifier 

fetch next from credit_cursor into @client_id, @credit, @cvtGuidCredit 
while @@fetch_status = 0 
begin 
  --While balances exist for the current client_ID and there are still credits to be applied, loop.
  while(@credit < 0 and exists(select * from #balances where @client_id = CLIENT_ID and amount > 0)) 
  begin 
    --Find the oldest oustanding balance.
    select top 1  @balance = amount, @cvtGuidBalance = cvtGuid 
    from #balances 
    where @client_id = CLIENT_ID and amount <> 0 
    order by AGING_DATE 

    -- merge the balance and the credit
    set @credit = @balance + @credit 

    if(@credit > 0) 
    begin 
      --If the credit is now postive save the leftover in the currently selected balance and set the credit to 0
      update #balances set amount = @credit where cvtGuid = @cvtGuidBalance  
      set @credit = 0 
    end
    else
      -- Credit is larger than the balance, 0 out the balance and continue processesing
      update #balances set amount = 0 where cvtGuid = @cvtGuidBalance  

  end -- end of while loop

  --There are no more balances to apply the credit to, save it back to the list.
  update #credits set amount = @credit where cvtGuid = @cvtGuidCredit 

  --Get the next credit.
  fetch next from credit_cursor into @client_id, @credit, @cvtGuidCredit 
end 
close credit_cursor 
deallocate credit_cursor
set nocount off

truncate table [IDAT_AR_BALANCES]

insert into [IDAT_AR_BALANCES] 
    select * from #balances
    union select * from #credits
    union select * from #creditLarger

--Delete any balances and credits that where 0'ed out durning the spred negitive.
delete [IDAT_AR_BALANCES] where amount = 0

drop table #balances
drop table #credits
drop table #creditLarger
0