Please start any new threads on our new site at https://forums.sqlteam.com. We've got lots of great SQL Server experts to answer whatever question you can come up with.

 All Forums
 SQL Server 2005 Forums
 Transact-SQL (2005)
 Nested Cursor... is there another way to do it?

Author  Topic 

pipco
Starting Member

2 Posts

Posted - 2007-04-27 : 10:27:34
This is my first post, so please bear with me. I'm trying to tackle a problem regarding invoicing in an application I'm developing.

Big picture view: an administrator can create custom rules that will trigger certain fees on a set of users that will be invoiced. The rules will be stored in the database as T-SQL fragments that will then be applied to a select statement using dynamic SQL. When the admin generates the invoice it can contain multiple fees and therefore multiple rules. The end result is user information and the final bill amount which is the sum of all the fees for the user (depending on if they meet rule criteria). There is also a pro-rated calculation that comes into play.

Problem: I'm using nested cursors to build a dynamic sql statement that is executed, and the results are put into a temp table. I know cursors in general can hamper performance, and nested cursors hurt even more. In my tests with real production data of 70 users and 2 rule combinations, the query can take up to 40 seconds and that is after using the tuning wizard (for what it's worth). I'd like to see if there is a more efficient way to do this, possibly without cursors.

I've attached the schema creation scripts and the stored procedure, as well as the way to execute. I've simplified everything down to a few fields, but in the production version I have more data that is included in the final output as well as different billing calculations.

Thanks!

Rick


-- CREATE THE TABLES
-- stores the report name
CREATE TABLE [dbo].[ReportHeader](
[ReportID] [int] NOT NULL,
[ReportName] [varchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL
) ON [PRIMARY]
GO

-- stores the fee name
CREATE TABLE [dbo].[FeeHeader](
[FeeID] [int] NOT NULL,
[FeeName] [varchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL
) ON [PRIMARY]
GO

-- links the reports to the fees. a report can have multiple fees
CREATE TABLE [dbo].[FeeLink](
[ReportID] [int] NULL,
[FeeID] [int] NULL
) ON [PRIMARY]
GO

-- the fee details including the rule
CREATE TABLE [dbo].[FeeDetails](
[FeeID] [int] NULL,
[FeeDollar] [money] NULL CONSTRAINT [DF_a-Billing_GlobalFee_BG_FeeDollar] DEFAULT ((0)),
[FeeRule] [nvarchar](max) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
) ON [PRIMARY]
GO

-- user information
CREATE TABLE [dbo].[UserTable](
[UserID] [int] NULL,
[Handed] [varchar](5),
[ProRate] [decimal](5,2)
) ON [PRIMARY]
GO

-- LOAD THE TABLES
INSERT INTO [dbo].[ReportHeader] (ReportID, ReportName)
VALUES (1, 'Management Fee Report')
INSERT INTO [dbo].[ReportHeader] (ReportID, ReportName)
VALUES (2, 'Misc Fee Report')

INSERT INTO [dbo].[FeeHeader] (FeeID, FeeName)
VALUES (1, 'Right-Handed Fee')
INSERT INTO [dbo].[FeeHeader] (FeeID, FeeName)
VALUES (2, 'Left-Handed Fee')
INSERT INTO [dbo].[FeeHeader] (FeeID, FeeName)
VALUES (3, 'Misc Fee')

INSERT INTO [dbo].[FeeLink] (ReportID, FeeID)
VALUES (1,1)
INSERT INTO [dbo].[FeeLink] (ReportID, FeeID)
VALUES (1,2)
INSERT INTO [dbo].[FeeLink] (ReportID, FeeID)
VALUES (2,3)

-- this fee is only applied to right-handed users
INSERT INTO [dbo].[FeeDetails] (FeeID, FeeDollar, FeeRule)
VALUES (1, 20.00, '(UserTable.Handed = ''LEFT'')')
-- this fee is only applied to left-handed users
INSERT INTO [dbo].[FeeDetails] (FeeID, FeeDollar, FeeRule)
VALUES (2, 10.00, '(UserTable.Handed = ''RIGHT'')')
-- this fee is applied to everyone
INSERT INTO [dbo].[FeeDetails] (FeeID, FeeDollar, FeeRule)
VALUES (3, 5.00, null)

INSERT INTO [dbo].[UserTable] (UserID, Handed, ProRate)
VALUES (1, 'LEFT', 1.00)
INSERT INTO [dbo].[UserTable] (UserID, Handed, ProRate)
VALUES (2, 'RIGHT', 1.50)
INSERT INTO [dbo].[UserTable] (UserID, Handed, ProRate)
VALUES (3, 'LEFT', 0.50)
INSERT INTO [dbo].[UserTable] (UserID, Handed, ProRate)
VALUES (4, 'RIGHT', 1.00)
INSERT INTO [dbo].[UserTable] (UserID, Handed, ProRate)
VALUES (5, 'LEFT', 1.00)

-- STORED PROCEDURE
CREATE PROCEDURE [dbo].[BillGenerate]
-- Add the parameters for the stored procedure here
@ReportId int
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;

declare @userid int
declare @prorate decimal(5,2)

declare @feedollar decimal(16,2)
declare @feerule varchar(max)
declare @sql varchar(max)

-- temp table to hold the results
CREATE TABLE #FeeReport (
UserID int,
BillAmount money
)

-- select all the users to bill
DECLARE c CURSOR FOR
select UserID, ProRate from UserTable
OPEN c
FETCH NEXT FROM c
INTO @userid, @prorate

WHILE @@FETCH_STATUS = 0
BEGIN

-- select all the fees for the report we are generating
DECLARE d CURSOR FOR
SELECT FD.FeeDollar, FD.FeeRule FROM FeeDetails FD inner join
FeeLink FL on FD.FeeID = FL.FeeID inner join
ReportHeader RH on FL.ReportID = RH.ReportID
WHERE RH.ReportID = @ReportId
open d
FETCH NEXT FROM d
INTO @feedollar, @feerule

WHILE @@FETCH_STATUS = 0
BEGIN

-- build the dynamic sql. if the rule is null (meaning it applies to everyone)
-- I use '1=1' to always make it a true condition
set @sql = 'INSERT INTO #FeeReport(UserID, BillAmount) Select UserId, '
set @sql = @sql + '(ProRate * ' + convert(varchar, @feedollar)+ ') as ToBill '
set @sql = @sql + ' from UserTable'
set @sql = @sql + ' where UserID = ' + convert(varchar,@userid) +' AND ('
set @sql = @sql + isnull(@feerule, '1=1')
set @sql = @sql + ')'

-- print for testing
print (@sql)
exec (@sql)

FETCH NEXT FROM d
INTO @feedollar, @feerule

END
CLOSE d
DEALLOCATE d

FETCH NEXT FROM c
INTO @userid, @prorate
END

CLOSE c
DEALLOCATE c

-- create the output. I need to sum the results and only have one record per user
SELECT UserId, Sum(BillAmount) as BillAmount
FROM (SELECT DISTINCT * from #FeeReport) As FeeTable
GROUP BY UserID


END
GO

-- TEST the process by executing these:
exec BillGenerate 1
exec BillGenerate 2

dinakar
Master Smack Fu Yak Hacker

2507 Posts

Posted - 2007-04-27 : 12:14:52
you can easily get rid of the outer cursor as follows:


Declare @table Table (RowId Int identity(1,1), Userid int, Prorate [decimal](5,2), Processed bit)
Insert into @table select UserID, ProRate, 0 from UserTable

Declare @rowid int
WHILE EXISTS (SELECT * FROM @table Where Processed = 0)
BEGIN
Select @rowid = MIN(rowid)
FROM @table
WHERE Processed = 0

Select @userid = Userid
,@prorate = Prorate
FROM @table
WHERE rowid = @rowid

-- Do the processing

update @table
set Processed = 1
where rowid = @rowid
END



Dinakar Nethi
SQL Server MVP
************************
Life is short. Enjoy it.
************************
http://weblogs.sqlteam.com/dinakar/
Go to Top of Page

pipco
Starting Member

2 Posts

Posted - 2007-04-27 : 14:48:43
dinakar,

Thanks for the response. I did test out your method and it does work correctly. When I implemented it with our production data I didn't see any improvement at all.

Does a WHILE loop act the same way as a cursor in some ways? I guess I could look under the hood and see if it's working similarly. It does seem as if the code is a bit cleaner though.

Anyone else have ideas? I guess in some way I'm looking for advice on the problem in general. This might not really be the best forum for general architecture type questions though.

Thanks again.

Rick
Go to Top of Page

dinakar
Master Smack Fu Yak Hacker

2507 Posts

Posted - 2007-04-27 : 14:59:46
HEre's a couple of links that might help:
http://www.sql-server-performance.com/cursors.asp
http://www.sqlteam.com/item.asp?ItemID=5761


Dinakar Nethi
SQL Server MVP
************************
Life is short. Enjoy it.
************************
http://weblogs.sqlteam.com/dinakar/
Go to Top of Page
   

- Advertisement -