August 25, 2017 at 7:34 am
Good morning,
I am trying to migrate transactions (charges/payments) from a old application to a new one.
The new application works as follows:
A table named transaction that holds
-the charges for a contract.
- the amount of a payment applied to a charge
A table named payment that holds the full payment amount.
Now since we need to preserve historical data I can't just go and apply the payments to the charges in a fifo order. If someone for example didn't pay a late fee back in 2010 I can't all of a sudden have my query pay this late fee.
Therefore I created my own table named 'PreviouslyPaid' that tells me what amount on what charge is paid, but the payments in the old application are not strongly linked to a specific charge (Terrible, which is one of the reasons we're switching applications).
I am working on a query that will pay off the charges that I tell it to pay off, however if a payment does not cover a charge than the charge will be paid by 2 different payments.
Here is a example of some charges
Here are some payments, note that they each are for a specific contract.
This is how much I want to pay for each transaction, note that I don't have a paymentid here (that information is unknown)
This is the final result.
See how charge 1 is paid by payment 1 and 2 (since payment 1 was only $40 and it did not cover the $100)
Then when I go and pay $20 towards charge 2 it uses payment 2 since there is still money left
When I go and pay charge 3 it uses payment 4 because that is the first one that is available for that contract.
Below is the query that I wrote to get this result:
CREATE TABLE #Transaction
(TransactionID INT IDENTITY(1,1),
BatchID INT,
TransactionNo INT,
OwnerID INT,
ContractID INT,
TransactionType VARCHAR(3), -- CHG = Charge, PMT = Payment
TransactionDate DATE,
ApplyID INT,
PaymentID INT,
Amount DECIMAL(16,2)
)
CREATE TABLE #Payment
(PaymentID INT IDENTITY(1,1),
PaymentAmount DECIMAL(16,2),
ContractID INT
)
CREATE TABLE #PreviouslyPaid
(
TransactionID INT,
AmountPaid DECIMAL(16,2),
ContractID INT
)
INSERT INTO #Transaction
(BatchID,
TransactionNo,
OwnerID,
ContractID,
TransactionType,
TransactionDate,
ApplyID,
PaymentID,
Amount)
VALUES
(1,1,344557,854123,'CHG','2017-01-01',1,NULL,100), -- add charge of $100 for contract 854123
(1,2,344557,854123,'CHG','2017-01-02',2,NULL,200), -- add charge of $200 for contract 854123
(1,3,443243,534227,'CHG','2017-01-01',3,NULL,700), -- add charge of $700 for contract 534227
(1,4,443243,534227,'CHG','2017-01-02',4,NULL,300), -- add charge of $300 for contract 534227
(1,1,344557,854123,'CHG','2017-01-04',5,NULL,10) -- add charge of $10 for contract 854123
INSERT INTO #Payment
(PaymentAmount, ContractID)
VALUES
(-40,854123),
(-90,854123),
(-30,854123), -- total payments for contract 854123 = $160
(-750,534227),
(-200,534227),
(-50,534227) -- total payments for contract 534227 = 900
--Determine that there are 2 charges already paid, at this point we don't know which payments apply to those but we do know
-- that these are the charges that we have to satisfy.
INSERT INTO #PreviouslyPaid
(TransactionID,AmountPaid,ContractID)
VALUES
(1,100,854123), -- charge of $100 fully paid
(2,20,854123), -- charge of $200 not fully paid, only $20 paid
(3,700,534227) -- charge of $700 fully paid
-- this is the cursor I would like to get rid of
DECLARE @CursorTransactionID INT
DECLARE @CursorAmountToPay DECIMAL(16,2)
DECLARE @CursorContractID INT
DECLARE @CursorRemainingBalance DECIMAL(16,2)
DECLARE ApplyAlreadyPaidChargesCursor CURSOR FOR
SELECT
TransactionID,
AmountPaid,
ContractID
FROM #PreviouslyPaid
OPEN ApplyAlreadyPaidChargesCursor
FETCH NEXT FROM ApplyAlreadyPaidChargesCursor INTO
@CursorTransactionID,
@CursorAmountToPay,
@CursorContractID
WHILE @@FETCH_STATUS = 0 BEGIN
--Cusor logic
SET @CursorRemainingBalance = @CursorAmountToPay
WHILE
-- The charge has not yet been fully paid
(SELECT ISNULL(SUM(ABS(Amount)),0) FROM #Transaction WHERE ApplyID = @CursorTransactionID
AND TransactionType = 'PMT') < ABS(@CursorAmountToPay)
AND
-- and the amount of money we have availalbe for this contract is lager than the amount we already spend for this contract.
(SELECT SUM(ABS(PaymentAmount)) FROM #Payment WHERE ContractID = @CursorContractID)
>
(SELECT ISNULL(SUM(ABS(Amount)),0) FROM #Transaction
WHERE ContractID = @CursorContractID
AND TransactionType = 'PMT')
BEGIN
-- Insert payment transaction
INSERT INTO #Transaction
(BatchID,
TransactionNo,
OwnerID,
ContractID,
TransactionType,
TransactionDate,
ApplyID,
PaymentID,
Amount)
SELECT
1,
(SELECT ISNULL(MAX(TransactionNo),0)+1 FROM #Transaction), -- transaction no increases by 1 for every row.
(SELECT TOP 1 OwnerID FROM #Transaction WHERE ContractID = @CursorContractID),
@CursorContractID,
'PMT', -- payment
GETDATE(),
@CursorTransactionID, -- Apply this payment to the charge with id @CursorTransactionID
PaymentToTakeFrom.PaymentID, -- We are taking money from this payment
CASE
WHEN ABS(PaymentToTakeFrom.RemainingBalance) > ABS(@CursorRemainingBalance) THEN ABS(@CursorRemainingBalance) * -1
ELSE PaymentToTakeFrom.RemainingBalance * -1
END AS PayAmount
FROM -- Find out which payment still has money left that we can distribute
(
SELECT TOP 1 * FROM
(SELECT
TPayment.PaymentID,
(SELECT ABS(TPayment.PaymentAmount)
-
(SELECT ISNULL(SUM(ABS(Amount)),0) FROM
#Transaction WHERE PaymentID = TPayment.PaymentID)) AS RemainingBalance
FROM #Payment TPayment
WHERE TPayment.ContractID = @CursorContractID
) AvailablePayment
WHERE AvailablePayment.RemainingBalance > 0
) AS PaymentToTakeFrom
-- Calculate remaining balance for this charge
SET @CursorRemainingBalance = ABS(@CursorAmountToPay) -
(SELECT ISNULL(SUM(ABS(Amount)),0) FROM #Transaction WHERE ApplyID = @CursorTransactionID AND TransactionType = 'PMT')
END
FETCH NEXT FROM ApplyAlreadyPaidChargesCursor INTO
@CursorTransactionID,
@CursorAmountToPay,
@CursorContractID
END
CLOSE ApplyAlreadyPaidChargesCursor
DEALLOCATE ApplyAlreadyPaidChargesCursor
SELECT * FROM #Transaction
SELECT * FROM #Payment
SELECT * FROM #PreviouslyPaid
DROP TABLE #Transaction
DROP TABLE #Payment
DROP TABLE #PreviouslyPaid
I would like to get rid of the cursor in there since it is not efficient when dealing with large datasets but I can't figure out how to calculate a running total of money remaining when I have multiple payments, but I can't sum them up because the amount for a 'PMT' in the transaction table can't exceed the PaymentAmount in Payment table for the record it applies to.
Any help is appreciated. 🙂
August 25, 2017 at 7:46 am
I'm not entirely sure there's a set-based way to accomplish the UPDATE portion of your task, as there's always going to be a need to know exactly how much of each incoming payment has been "allocated" to a given charge. Cursors are NOT always bad. Sometimes, however, they're the only way to go, especially when UPDATE is involved, and the results of a previous record update can affect the methodology for updating a subsequent record, which is what this appears to be. Are there any performance problems with the cursor as is? You may be able to assist the queries in it via indexing.
Steve (aka sgmunson) 🙂 🙂 🙂
Rent Servers for Income (picks and shovels strategy)
August 25, 2017 at 8:05 am
One of the issues I am having is that while I was testing I wrote my stored procedure for 1 contract specific.
So taking all the charges/payments from the old system, insert them into the new system and using a output clause to get the new keys in the new system.
When testing it actually performed really well, it took either 1 or 2 seconds for a contract to migrate, that included applying what was left over, handling adjustments and chargebacks / NSF checks etc.
The problem with that is that when I started executing that in a loop the first couple of hundred contracts go in like a breeze, but after that it slows down. The SQL server process starts to consume over 90% CPU.
This took 16 hours to complete, I would expect it to be done in 2. Since every time I execute the LoadManual stored procedure for any given contract id it never takes more than a few seconds. Is there any reason this process slows down after a few 100 iterations ?
CREATE TABLE #ContractsToMigrate
(TSWContractID INT)
CREATE TABLE #ErrorMessage
(EMessage VARCHAR(MAX))
INSERT INTO #ContractsToMigrate(TSWContractID)
SELECT DISTINCT
ARLineItem.[ContractID]
FROM [TswData].[dbo].[t_ARLineItem] ARLineItem
INNER JOIN [TswData].[dbo].[t_Contract] TSWContract
ON TSWContract.ContractID = ARLineItem.ContractID
INNER JOIN [SPIConversionSales].[dbo].[ts_contract]
ON ts_contract.[cont_legacy_key] = TSWContract.ContractNumber
INNER JOIN [SPIConversionPMS].[dbo].[SalesContract]
ON SalesContract.[Sales_Cont_ID] = ts_contract.Cont_ID
WHERE SalesContract.ResortID = 'IFK '
AND TSWContract.SiteID = 16
SELECT * FROM #ContractsToMigrate
DECLARE @ContractID INT
DECLARE cur CURSOR LOCAL FOR
SELECT TSWContractID FROM #ContractsToMigrate
OPEN cur
FETCH NEXT FROM cur INTO @ContractID
while @@FETCH_STATUS = 0 BEGIN
BEGIN TRY
EXEC SPIMigration.dbo.LoadManual @ContractID
PRINT 'NOW DOING CONTRACT'
PRINT @ContractID
END TRY
BEGIN CATCH
INSERT INTO #ErrorMessage(EMessage)
SELECT 'contract ' + CAST(@ContractID AS VARCHAR) + ' cannot be processed due to error: ' + error_message() + '; at line ' + cast(error_line() as varchar)
END CATCH
FETCH NEXT FROM cur INTO @ContractID
END
close cur
deallocate cur
SELECT * FROM #ErrorMessage
DROP TABLE #ContractsToMigrate
DROP TABLE #ErrorMessage
August 25, 2017 at 8:16 am
I believe that the following article addresses this topic, or it should at least get you started in the right direction. Solving FIFO Queues Using Windowed Functions
Drew
J. Drew Allen
Business Intelligence Analyst
Philadelphia, PA
August 25, 2017 at 8:19 am
Without your code for the stored procedure, it's going to be hard to see what's going on. However, if you use temp tables, you probably ought to give them PRIMARY KEYs, and even indexes if they're going to hold large quantities of data, and it will possibly make the query plans better. Even temp tables with just one field can benefit from that. Please post your sproc code and then we'll see what's up.
Steve (aka sgmunson) 🙂 🙂 🙂
Rent Servers for Income (picks and shovels strategy)
August 25, 2017 at 8:31 am
See attached, this would be to do everything in 1 big bulk. (eliminating the code I had in the previous post)
I had to zip the file it didn't allow me to upload a .sql
August 25, 2017 at 11:45 am
That's a TON of temp tables, and not one of them has any primary key or any indexing. At least get a useful primary key (use field or fields that are getting joined on) for every one of those, and then post back the new query and it's execution plan. Some record count information would also be helpful, as that will help with figuring out indexing.
Steve (aka sgmunson) 🙂 🙂 🙂
Rent Servers for Income (picks and shovels strategy)
August 28, 2017 at 12:19 pm
It sure is, the tables most accessed I guess are the 'chargesinserted' , 'paymentsinserted' and 'ar', I will create a composite primary key for the first two. And a PK for 'ar'.
. Do you have any suggestions on indexing ?
August 29, 2017 at 6:58 am
drooijackers - Monday, August 28, 2017 12:19 PMIt sure is, the tables most accessed I guess are the 'chargesinserted' , 'paymentsinserted' and 'ar', I will create a composite primary key for the first two. And a PK for 'ar'.
. Do you have any suggestions on indexing ?
Indexing is generally the most useful when the additional cost for the insert to update the index is not large enough to offset the benefit from improved read performance for queries against that table. Typically, what you look to do with an index is make it "covering", which means that the fields in the index or those included in it, include all the fields from that table that are being selected in the queries against that table. However, that's a rather simple view of indexing, as it's not always obvious as to what the best choices are.
Steve (aka sgmunson) 🙂 🙂 🙂
Rent Servers for Income (picks and shovels strategy)
August 30, 2017 at 8:00 am
I'll let you know what the execution time is after indexing. We're looking at roughly 2,500,000 transactions. (payments/charges combined)
September 12, 2017 at 8:44 am
I have a execution plan do you think that will help ? It shows every cursor loop as a individual query with a cost of 0% though.
September 13, 2017 at 12:27 pm
drooijackers - Tuesday, September 12, 2017 8:44 AMI have a execution plan do you think that will help ? It shows every cursor loop as a individual query with a cost of 0% though.
Go ahead and post it as a .SQLPlan file, and I'll let you know.
Steve (aka sgmunson) 🙂 🙂 🙂
Rent Servers for Income (picks and shovels strategy)
Viewing 12 posts - 1 through 11 (of 11 total)
You must be logged in to reply to this topic. Login to reply