May 5, 2011 at 8:50 pm
I have SP that runs rather slow. I am attaching the code to this post. The obvious comments are I should eliminate the cursors and change from dynamic SQL to static. These are both valid comments, but when I run the "generated" SQL it takes almost the same time as when the SP is directly invoked. I believe this is because the cursors return a small number of rows (say 15-20). Also, I need the dynamic SQL because all the databases are on the same server, so I don't want to have different versions of the SP for each environment. I am currently doing this for another app and it is a royal pain. I can't supply the DDL for some of the tables because they are vendor tables (CodCode, EmpComp, EmpPers, Company)
ALTER PROCEDURE [dbo].[TimeGridSelect]
-- Add the parameters for the stored procedure here
@CoID char(5),
@TspLocCode char(6),
@TspPayGroup char(6),
@DbName char(128) = 'Time',
@TspPeriodType char(1),
@TspPayDate datetime
AS
BEGIN
DECLARE @CommonQuerySelectFields varchar(7000)
DECLARE @CommonQueryRestOfQuery varchar(1000)
DECLARE @TspEarnCode char(5)
DECLARE @TecShiftCode char(2)
DECLARE @ErnLongDesc varchar(40)
DECLARE @CursorStatus int
DECLARE @CursorQuery varchar(8000)
DECLARE @QueryStringComp as nvarchar(4000)
DECLARE @CompParm as nvarchar(100)
DECLARE @CompName as char(50)
DECLARE @CompPrefix as char(50)
DECLARE @CompPrefixLen as int
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Get the company prefix
SELECT @QueryStringComp = N'SELECT @CompPrefix = CtsPrefix
FROM dbo.TimeCompanySheet
WHERE CtsCoID = ''' + @CoID + ''''
SELECT @CompParm = N'@CompPrefix char(50) OUTPUT'
--SELECT @CompParm
--SELECT @QueryStringComp
EXEC sp_executesql @QueryStringComp,@CompParm,@CompPrefix = @CompName OUTPUT
-- Length of prefix
SELECT @CompPrefixLen = LEN(RTRIM(@CompName)) + 1
-- GET Common Fields for every client
IF LEN(@CompPrefixLen) > 0
BEGIN
SELECT @CommonQuerySelectFields = 'SELECT DISTINCT '''' AS [Insert],'''' AS [Delete],EecEmpNo AS [EmployeeNo],
RTRIM(EepNameLast) + '', '' + RTRIM(EepNameFirst) + '' '' + SUBSTRING(ISNULL(EepNameMiddle,''''), 1, 1)
AS Name, CmpCompanyCode AS [Company Code], TspLocCode as [Location],
TspPayGroup as [PayGroup], TspPayDate as [PayDate],
CASE
WHEN RTRIM(TspCode) = ''0'' THEN ''0 - Select a Project''
ELSE SUBSTRING(RTRIM(TspCode),' + CAST(@CompPrefixLen as char(1)) +
',LEN(RTRIM(TspCode)) - ' + CAST(@CompPrefixLen - 1 as varchar(2)) +
') + '' - '' + RTRIM(CodDesc)
END as [Project],
TspCode as [OldProject] '
END
ELSE
BEGIN
SELECT @CommonQuerySelectFields = 'SELECT DISTINCT '''' AS [Insert],'''' AS [Delete],EecEmpNo AS [EmployeeNo],
RTRIM(EepNameLast) + '', '' + RTRIM(EepNameFirst) + '' '' + SUBSTRING(ISNULL(EepNameMiddle,''''), 1, 1)
AS Name, CmpCompanyCode AS [Company Code], TspLocCode as [Location],
TspPayGroup as [PayGroup], TspPayDate as [PayDate],
''0 - Select a Project'' as [Project],
TspCode as [OldProject] '
END
--SELECT @CommonQuerySelectFields
SELECT @CommonQueryRestofQuery = '
FROM dbo.TimeSheetPayPeriod TSP
INNER JOIN ' + RTRIM(@DbName) + '.dbo.Codes C
ON (RTRIM(TspCode) = RTRIM(CodCode))
INNER JOIN ' + RTRIM(@DbName) + '.dbo.EmpComp EC
ON (TspCoID = EecCoID)
AND (TspPayGroup = EecPayGroup)
AND (TspEEID = EecEEID)
AND (TspPayDate = ''' + cast(@TspPayDate as varchar(30)) + ''')
INNER JOIN ' + RTRIM(@DbName) + '.dbo.EmpPers
ON EecEEID = EepEEID
INNER JOIN ' + RTRIM(@DbName) + '.dbo.Company
ON EecCoID = CmpCoID
WHERE ((EecEmplStatus = ''A'')
OR (EecEmplStatus = ''T'' AND EecDateOfTermination >= ''' +
cast(@TspPayDate as varchar(30)) + '''))
AND (EecCoID = ''' + @CoID + ''')
AND (TspPeriodType = ''' + @TspPeriodType + ''')
AND CodTable = ''PROJECT'' '
IF @TspLocCode <> 'ALL '
SELECT @CommonQueryRestofQuery = @CommonQueryRestofQuery + '
AND (TspLocCode = ''' + @TspLocCode + ''') '
IF @TspPayGroup <> 'ALL '
SELECT @CommonQueryRestofQuery = @CommonQueryRestofQuery + '
AND (TspPayGroup = ''' + @TspPayGroup + ''') '
SELECT @CommonQueryRestofQuery = @CommonQueryRestofQuery + '
AND (TspPayDate = ''' + cast(@TspPayDate as varchar(50)) + ''')
AND (RTRIM(EepNameLast) + '', '' + RTRIM(EepNameFirst) IS NOT NULL)
ORDER BY NAME'
-- Cursor processing for each deduction
SELECT @CursorQuery = '
DECLARE Deductions_Cursor CURSOR GLOBAL SCROLL FOR
SELECT TecEarnCode,TecShiftCode,ErnLongDesc
FROM dbo.TimeEarnCodes
INNER JOIN ' + RTRIM(@DbName) + '.dbo.EarnCode
ON TecEarnCode = ErnEarnCode
WHERE TecCoID = ''' + @CoID + ''''
IF @TspLocCode <> 'ALL '
SELECT @CursorQuery = @CursorQuery + '
AND TecLocCode = ''' + @TspLocCode + ''''
IF @TspPayGroup <> 'ALL '
SELECT @CursorQuery = @CursorQuery + '
AND TecPayGroup = ''' + @TspPayGroup + ''''
SELECT @CursorQuery = @CursorQuery + '
AND TecIsActive = ''Y''
ORDER BY TecEarnSequence'
--SELECT @CursorQuery
EXECUTE (@CursorQuery)
OPEN Deductions_Cursor
FETCH NEXT FROM Deductions_Cursor INTO @TspEarnCode,@TecShiftCode,@ErnLongDesc
SELECT @CursorStatus = @@FETCH_STATUS
-- Check @CursorStatus to see if there are any more rows to fetch.
WHILE @CursorStatus = 0
BEGIN
-- Build Query Cursor (Z shift code -- No shift differential -- check)
IF @TecShiftCode = 'Z '
BEGIN
SELECT @CommonQuerySelectFields = @CommonQuerySelectFields + ',' +
'(SELECT TspEarnAmount FROM dbo.TimeSheetPayPeriod ' +
'INNER JOIN ' + RTRIM(@DbName) + '.dbo.Codes c
ON (RTRIM(TSP.TspCode) = RTRIM(CodCode))
WHERE TspCoID = ''' + @CoID + ''' '
IF @TspLocCode <> 'ALL '
BEGIN
SELECT @CommonQuerySelectFields = @CommonQuerySelectFields +
'AND TspLocCode = ''' + @TspLocCode + ''' '
END
IF @TspPayGroup <> 'ALL '
BEGIN
SELECT @CommonQuerySelectFields = @CommonQuerySelectFields +
'AND TspPayGroup = ''' + @TspPayGroup + ''' '
END
SELECT @CommonQuerySelectFields = @CommonQuerySelectFields +
'AND CodTable = ''PROJECT'' ' +
'AND TspCode = TSP.TspCode ' +
'AND TspEEID = EC.EecEEID ' +
'AND TspPeriodType = TSP.TspPeriodType ' +
'AND TspPayDate = ''' + cast(@TspPayDate as varchar(50)) + ''' ' +
'AND TspEarnCode = ''' + @TspEarnCode + ''') AS [' +
SUBSTRING(@ErnLongDesc,1,20) + ']'
END
ELSE
BEGIN
SELECT @CommonQuerySelectFields = @CommonQuerySelectFields + ',' +
'(SELECT TspEarnAmount FROM dbo.TimeSheetPayPeriod ' +
'INNER JOIN ' + RTRIM(@DbName) + '.dbo.Codes
ON (RTRIM(TSP.TspCode) = RTRIM(CodCode)) ' +
'WHERE TspCoID = ''' + @CoID + ''' ' +
'AND CodTable = ''PROJECT'' ' +
'AND TspCode = TSP.TspCode ' +
'AND TspLocCode = ''' + @TspLocCode + ''' ' +
'AND TspPayGroup = ''' + @TspPayGroup + ''' ' +
'AND TspPeriodType = ''' + @TspPeriodType + ''' ' +
'AND TspEEID = EC.EecEEID ' +
'AND TspPayDate = ''' + cast(@TspPayDate as varchar(50)) + ''' ' +
'AND TspEarnCode = ''' + @TspEarnCode + ''') AS [' +
SUBSTRING(@ErnLongDesc,1,17) + '_' + @TecShiftCode + ']'
END
-- This is executed as long as the previous fetch succeeds.
FETCH NEXT FROM Deductions_Cursor INTO @TspEarnCode,@TecShiftCode,@ErnLongDesc
SELECT @CursorStatus = @@FETCH_STATUS
--SELECT @CommonQuerySelectFields
END
CLOSE Deductions_Cursor
-- Add Total Hours column
SELECT @CommonQUerySelectFields = @CommonQuerySelectFields + ','''' as [Total Hours] '
--SELECT cast(@CommonQuerySelectFields + @CommonQueryRestofQuery as varchar(4000))
EXECUTE (@CommonQuerySelectFields + @CommonQueryRestofQuery)
-- Reference the named cursor with a cursor variable.
DEALLOCATE Deductions_Cursor
May 6, 2011 at 12:29 pm
There are a couple of things that might help:
1. You are using functions on columns in the where clause which means they are not SARGable so you are getting scans.
2. You are using an OR which tends to cause scans as well.
3. Your CURSOR is even less efficient than is has to be. You don't need a scrollable cursor because you are only going forward (FETCH NEXT) so if you use a FAST_FORWARD cursor you should get better performance
4. It also looks like that your cursor is added a correlated sub-query to the select list. This is essentially another cursor. If you can find another way to do that without the corrlated sub-query it will likely run faster as well. I've found that a CTE or derived table can often replace correlated sub-queries and perform better.
Without structures and query plans it's hard to give any more specific recommendations.
Jack Corbett
Consultant - Straight Path Solutions
Check out these links on how to get faster and more accurate answers:
Forum Etiquette: How to post data/code on a forum to get the best help
Need an Answer? Actually, No ... You Need a Question
May 6, 2011 at 8:08 pm
Jack,
Thanks for getting back to me. As for your comments/recommendations
1. You are using functions on columns in the where clause which means they are not SARGable so you are getting scans.
2. You are using an OR which tends to cause scans as well.
These comments make perfect sense, but do you have any recommendations on how I can get around this.
3. Your CURSOR is even less efficient than is has to be. You don't need a scrollable cursor because you are only going forward (FETCH NEXT) so if you use a FAST_FORWARD cursor you should get better performance
I will implement this.
4. It also looks like that your cursor is added a correlated sub-query to the select list. This is essentially another cursor. If you can find another way to do that without the correlated sub-query it will likely run faster as well. I've found that a CTE or derived table can often replace correlated subqueries and perform better.
Since I have an undetermined number of columns I don't think I can use either a CTE or derived table. Let me describe why I did it this way and see if we can come up with a better solution. For each client in the database there is variable number of earnings codes. Rather than have a de-normalized design, I have one row for each client's compound key for each earnings type. When I call the SP I need to return all the earnings types for each employee and load it into a FarPoint spread control.
Would a temp table predefined with the "correct" number of columns for the client that gets populated one earnings type at a time be faster than a correlated sub-query?
I am posting the "generated" code so you can see what the procedure produces.
SELECT DISTINCT '' AS [Insert],'' AS [Delete],EecEmpNo AS [EmployeeNo],
RTRIM(EepNameLast) + ', ' + RTRIM(EepNameFirst) + ' ' + SUBSTRING(ISNULL(EepNameMiddle,''), 1, 1)
AS Name, CmpCompanyCode AS [Company Code], TspLocCode as [Location],
TspPayGroup as [PayGroup], TspPayDate as [PayDate],
CASE
WHEN RTRIM(TspCode) = '0' THEN '0 - Select a Project'
ELSE SUBSTRING(RTRIM(TspCode),1,LEN(RTRIM(TspCode)) - 0) + ' - ' + RTRIM(CodDesc)
END as [Project],
TspCode as [OldProject] ,(SELECT TspEarnAmount FROM dbo.TimeSheetPayPeriod INNER JOIN dbo.Codes c
ON (RTRIM(TSP.TspCode) = RTRIM(CodCode))
WHERE TspCoID = 'XBXBX' AND TspPayGroup = 'JAMAI ' AND CodTable = 'PROJECT' AND TspCode = TSP.TspCode AND TspEEID = EC.EecEEID AND TspPeriodType = TSP.TspPeriodType AND TspPayDate = 'Apr 29 2011 12:00AM' AND TspEarnCode = 'REG ') AS [Regular],(SELECT TspEarnAmount FROM dbo.TimeSheetPayPeriod INNER JOIN dbo.Codes c
ON (RTRIM(TSP.TspCode) = RTRIM(CodCode))
WHERE TspCoID = 'XBXBX' AND TspPayGroup = 'JAMAI ' AND CodTable = 'PROJECT' AND TspCode = TSP.TspCode AND TspEEID = EC.EecEEID AND TspPeriodType = TSP.TspPeriodType AND TspPayDate = 'Apr 29 2011 12:00AM' AND TspEarnCode = 'OT ') AS [Overtime],(SELECT TspEarnAmount FROM dbo.TimeSheetPayPeriod INNER JOIN dbo.Codes c
ON (RTRIM(TSP.TspCode) = RTRIM(CodCode))
WHERE TspCoID = 'XBXBX' AND TspPayGroup = 'JAMAI ' AND CodTable = 'PROJECT' AND TspCode = TSP.TspCode AND TspEEID = EC.EecEEID AND TspPeriodType = TSP.TspPeriodType AND TspPayDate = 'Apr 29 2011 12:00AM' AND TspEarnCode = 'CLEAN') AS [Cleaning Service],(SELECT TspEarnAmount FROM dbo.TimeSheetPayPeriod INNER JOIN dbo.Codes c
ON (RTRIM(TSP.TspCode) = RTRIM(CodCode))
WHERE TspCoID = 'XBXBX' AND TspPayGroup = 'JAMAI ' AND CodTable = 'PROJECT' AND TspCode = TSP.TspCode AND TspEEID = EC.EecEEID AND TspPeriodType = TSP.TspPeriodType AND TspPayDate = 'Apr 29 2011 12:00AM' AND TspEarnCode = 'AOG ') AS [Aircraft on Ground S],(SELECT TspEarnAmount FROM dbo.TimeSheetPayPeriod INNER JOIN dbo.Codes c
ON (RTRIM(TSP.TspCode) = RTRIM(CodCode))
WHERE TspCoID = 'XBXBX' AND TspPayGroup = 'JAMAI ' AND CodTable = 'PROJECT' AND TspCode = TSP.TspCode AND TspEEID = EC.EecEEID AND TspPeriodType = TSP.TspPeriodType AND TspPayDate = 'Apr 29 2011 12:00AM' AND TspEarnCode = 'BONUS') AS [Bonus],'' as [Total Hours]
FROM dbo.TimeSheetPayPeriod TSP
INNER JOIN dbo.Codes C
ON (RTRIM(TspCode) = RTRIM(CodCode))
INNER JOIN dbo.EmpComp EC
ON (TspCoID = EecCoID)
AND (TspPayGroup = EecPayGroup)
AND (TspEEID = EecEEID)
AND (TspPayDate = 'Apr 29 2011 12:00AM')
INNER JOIN dbo.EmpPers
ON EecEEID = EepEEID
INNER JOIN dbo.Company
ON EecCoID = CmpCoID
WHERE ((EecEmplStatus = 'A')
OR (EecEmplStatus = 'T' AND EecDateOfTermination >= 'Apr 29 2011 12:00AM'))
AND (EecCoID = 'XBXBX')
AND (TspPeriodType = 'R')
AND CodTable = 'PROJECT'
AND (TspPayGroup = 'JAMAI ')
AND (TspPayDate = 'Apr 29 2011 12:00AM')
AND (RTRIM(EepNameLast) + ', ' + RTRIM(EepNameFirst) IS NOT NULL)
ORDER BY NAME
The DDL for the TimeSheetPeriod table is below. The other tables this SP access belong to a package and cannot be altered.
CREATE TABLE [dbo].[TimeSheetPayPeriod](
[TspCode] [char](15) NOT NULL,
[TspCoID] [char](5) NOT NULL,
[TspEarnAmount] [money] NULL,
[TspEarnCode] [char](5) NOT NULL,
[TspEEID] [char](12) NOT NULL,
[TspLastUpdateDate] [datetime] NOT NULL,
[TspLocCode] [char](6) NOT NULL,
[TspPayGroup] [char](6) NOT NULL,
[TspPayDate] [datetime] NOT NULL,
[TspPeriodType] [char](1) NOT NULL,
[TspShiftCode] [char](2) NOT NULL,
[TspUpdateUserID] [char](12) NOT NULL,
CONSTRAINT [PK_TimeSheetPayPeriod] PRIMARY KEY CLUSTERED
(
[TspCode] ASC,
[TspCoID] ASC,
[TspEarnCode] ASC,
[TspEEID] ASC,
[TspLocCode] ASC,
[TspPayGroup] ASC,
[TspPayDate] ASC,
[TspPeriodType] ASC,
[TspShiftCode] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY]
) ON [PRIMARY]
When I ran the SP it took 2:01 minutes. When I ran just the above select statement, it also took 2:01 minutes.
When I created the estimated execution plan the following costs were noted:
CLUSTER INDEX SCAN of TimeSheetPayPeriod - 26%
HASH MATCH JOIN (Inner Join) - 11%
INDEX SCAN of Codes - 9%
5 other INDEX SCANs of Codes - 7% each
Thanks for your help.
May 7, 2011 at 5:55 am
The subqueries to populate [Regular] and the like most probably will perform a index scan rather than a seek due to the functions involved (RTRIM(TSP.TspCode) = RTRIM(CodCode)).
Maybe a CROSS APPPLY solution can help to further improve performance.
But without table def for all tables involved as well as sample data and expected result together with the current (actual( execution plan it's hard to go into more details, as stated before.
May 7, 2011 at 10:10 am
I am going to try 2 things:
1) Extract the Codes values to a temp table indexed on the code value. For a given client there shouldn't be more than 100 rows.
2) Get rid of the rtrim
Thanks,
David
May 7, 2011 at 5:35 pm
I tried the following two items and the response went from 2+ minutes to less than a second:
1) Extract the Codes values to a temp table indexed on the code value. For a given client there shouldn't be more than 100 rows.
2) Get rid of the rtrim
Thanks to everybody who helped me out.
May 7, 2011 at 7:56 pm
dwg1011 (5/7/2011)
I tried the following two items and the response went from 2+ minutes to less than a second:1) Extract the Codes values to a temp table indexed on the code value. For a given client there shouldn't be more than 100 rows.
2) Get rid of the rtrim
Thanks to everybody who helped me out.
AWESOME!
Does your code still have those c.u.r.s.o.r.s in it?
Wayne
Microsoft Certified Master: SQL Server 2008
Author - SQL Server T-SQL Recipes
May 8, 2011 at 11:34 am
It does...
May 8, 2011 at 12:12 pm
Regarding indexes and ORs... http://sqlinthewild.co.za/index.php/2011/05/03/indexing-for-ors/
Gail Shaw
Microsoft Certified Master: SQL Server, MVP, M.Sc (Comp Sci)
SQL In The Wild: Discussions on DB performance with occasional diversions into recoverability
Viewing 9 posts - 1 through 9 (of 9 total)
You must be logged in to reply to this topic. Login to reply