未払いの残高とクレジットを表すテーブルが両方とも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
これが起こったことの内訳です
これが私の実際のテーブルの関連する列のスキーマです
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
カーソルなしでこれを実行してパフォーマンスを向上させるには、これを行うためのより良い方法があると確信していますが、を使用せずに「最も古い日付を最初に使用する」要件を満たす方法を理解するのは困難です。カーソル。
多くの場合、現在の合計などの操作では、他のいくつかの方法よりもカーソルを使用する方が実際には効率的です。顕著なパフォーマンスの問題が発生しない限り、カーソルの使用を恐れないでください。
SQL Server 2012には、これを改善する新しいウィンドウ関数がありますが、明らかにそれらを使用することはできません。 「機能する」という風変わりな更新アプローチがありますが、構文は公式にはサポートされておらず、SQL Server 2012以降では機能しない可能性があります。いつか違法な構文になる可能性があり、順序がどのように機能するかについての保証はありません。一般的なセットベースのアプローチは適切にスケーリングされません。カーソルを使用すると、各行を1回スキャンするだけでよいので、多くの場合、より適切ですが、セットベースのソリューションでは、スキャンは非線形に増加します。私がこれまで行ってきた仕事をお見せしたいのですが、それをすべて明らかにするブログ投稿の公開日はまだ不明です。
少なくとも、カーソルの使用を宣言するときは、次のようにします。
DECLARE ... CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
これも読むのに役立つかもしれません:
アーロンの提案のおかげで、すべてをセットベースにするのではなく、セットが最もよく適用されるセットを作成し、残りはカーソルを使用するようにしました。
また、最も古いクレジットからプルするだけでよいという要件を変更することもできました。これで、クライアントごとに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