Craig Walker
Craig Walker

Reputation: 51727

Why would a Stored Procedure run slower than naked T-SQL?

I have a stored procedure in a MS-SQL 2005 database that:

The SP takes two parameters, which are then used in the first query.

When I run the SP for a given set of parameters, it takes 3 minutes to execute.

When I execute the contents of the SP as a regular T-SQL batch (declaring and setting the parameters beforehand), it takes 10 seconds. These numbers are consistent across multiple sequential runs.

This is a huge difference and there's no obvious functional changes. What could be causing this?

UPDATE

Reindexing my tables (DBCC REINDEX) sped up the SP version dramatically. The SP version now takes 1 second, while the raw SQL takes 6.

That's great as a solution to the immediate problem, but I'd still like to know the "why".

Upvotes: 4

Views: 9380

Answers (4)

Nilesh Mohite
Nilesh Mohite

Reputation: 75

this issue is resolved with Different approaches as show by Greg Larsen Visit https://www.simple-talk.com/sql/t-sql-programming/parameter-sniffing/

Upvotes: 0

Catch22
Catch22

Reputation: 3351

I've experienced exactly the same problem a couple of times recently (with MS-SQL 2008). Specific stored procedures would be extremely slow to run (minutes) but the same SQL pasted into SSMS took only seconds.

The problem is basically that the stored procedure is using a bad execution plan while the pasted SQL is using a different (and much better) execution plan.

Compare Execution Plans

To test this hypothesis, open a new query window in SSMS and turn on "Include Actual Execution Plan" (Ctrl-M is the keyboard shortcut for this).

Then paste the contents of the stored procedure into the window and follow that with a call to the actual stored procedure. For example:

SELECT FirstName, LastName FROM Users where ID = 10

EXEC dbo.spGetUserById 10

Run both queries together and then compare the execution plans for both. I have to say that in my case the "Query cost" estimate for each query did not help at all and pointed me in the wrong direction. Instead, look closely at the indexes being used, whether scans are being performed instead of seeks and the number of rows being processed.

There should be a difference in the plans and that should help identify the tables & indexes that need to be investigated further.

To help fix the issue, in one instance I was able to rewrite the stored procedure to avoid using an index scan and instead rely on index seeks. In another instance, I found that updating that rebuilding the indexes for a specific table used in the query made all the difference.

Find & Update Indexes

I've used this SQL to find and rebuild the appropriate indexes:

/* Originally created by Microsoft */
/* Error corrected by Pinal Dave (http://www.SQLAuthority.com) */
/* http://blog.sqlauthority.com/2008/03/04/sql-server-2005-a-simple-way-to-defragment-all-indexes-in-a-database-that-is-fragmented-above-a-declared-threshold/ */
/* Catch22: Added parameters to filter by table & view proposed changes */

-- Specify your Database Name
USE AdventureWorks

/* Parameters */
Declare @MatchingTableName nvarchar(100) = 'MyTablePrefix'  -- Specify Table name (can be prefix of table name) or blank for all tables
DECLARE @ViewOnly bit = 1                                   -- Set to 1 to view proposed actions, set to 0 to Execute proposed actions:


-- Declare variables
SET NOCOUNT ON
DECLARE @tablename VARCHAR(128)
DECLARE @execstr VARCHAR(255)
DECLARE @objectid INT
DECLARE @indexid INT
DECLARE @frag decimal
DECLARE @maxreorg decimal
DECLARE @maxrebuild decimal
DECLARE @IdxName varchar(128)
DECLARE @ReorgOptions varchar(255)
DECLARE @RebuildOptions varchar(255)

-- Decide on the maximum fragmentation to allow for a reorganize.
-- AVAILABLE OPTIONS: http://technet.microsoft.com/en-us/library/ms188388(SQL.90).aspx
SET @maxreorg = 20.0
SET @ReorgOptions = 'LOB_COMPACTION=ON'
-- Decide on the maximum fragmentation to allow for a rebuild.
SET @maxrebuild = 30.0
-- NOTE: only specifiy FILLFACTOR=x if x is something other than zero:
SET @RebuildOptions = 'PAD_INDEX=OFF, SORT_IN_TEMPDB=ON, STATISTICS_NORECOMPUTE=OFF, ALLOW_ROW_LOCKS=ON, ALLOW_PAGE_LOCKS=ON'

-- Declare a cursor.
DECLARE tables CURSOR FOR
SELECT CAST(TABLE_SCHEMA AS VARCHAR(100))
+'.'+CAST(TABLE_NAME AS VARCHAR(100))
AS Table_Name
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE = 'BASE TABLE'
AND TABLE_NAME like @MatchingTableName + '%'

-- Create the temporary table.
if exists (select name from tempdb.dbo.sysobjects where name like '#fraglist%')
drop table #fraglist

CREATE TABLE #fraglist (
ObjectName CHAR(255),
ObjectId INT,
IndexName CHAR(255),
IndexId INT,
Lvl INT,
CountPages INT,
CountRows INT,
MinRecSize INT,
MaxRecSize INT,
AvgRecSize INT,
ForRecCount INT,
Extents INT,
ExtentSwitches INT,
AvgFreeBytes INT,
AvgPageDensity INT,
ScanDensity decimal,
BestCount INT,
ActualCount INT,
LogicalFrag decimal,
ExtentFrag decimal)

-- Open the cursor.
OPEN tables

-- Loop through all the tables in the database.
FETCH NEXT
FROM tables
INTO @tablename

WHILE @@FETCH_STATUS = 0
BEGIN
-- Do the showcontig of all indexes of the table
INSERT INTO #fraglist
EXEC ('DBCC SHOWCONTIG (''' + @tablename + ''')
WITH FAST, TABLERESULTS, ALL_INDEXES, NO_INFOMSGS')
FETCH NEXT
FROM tables
INTO @tablename
END

-- Close and deallocate the cursor.
CLOSE tables
DEALLOCATE tables

-- Declare the cursor for the list of indexes to be defragged.
DECLARE indexes CURSOR FOR
SELECT ObjectName, ObjectId, IndexId, LogicalFrag, IndexName
FROM #fraglist
WHERE ((LogicalFrag >= @maxreorg) OR (LogicalFrag >= @maxrebuild))
AND INDEXPROPERTY (ObjectId, IndexName, 'IndexDepth') > 0

-- Open the cursor.
OPEN indexes

-- Loop through the indexes.
FETCH NEXT
FROM indexes
INTO @tablename, @objectid, @indexid, @frag, @IdxName

WHILE @@FETCH_STATUS = 0
BEGIN
IF (@frag >= @maxrebuild)
BEGIN
IF (@ViewOnly=1)
BEGIN
PRINT 'WOULD be executing ALTER INDEX ' + RTRIM(@IdxName) + ' ON ' + RTRIM(@tablename) + ' REBUILD WITH ( ' + @RebuildOptions + ' ) -- Fragmentation currently ' + RTRIM(CONVERT(VARCHAR(15),@frag)) + '%'
END
ELSE
BEGIN
PRINT 'Now executing ALTER INDEX ' + RTRIM(@IdxName) + ' ON ' + RTRIM(@tablename) + ' REBUILD WITH ( ' + @RebuildOptions + ' ) -- Fragmentation currently ' + RTRIM(CONVERT(VARCHAR(15),@frag)) + '%'
SELECT @execstr = 'ALTER INDEX ' + RTRIM(@IdxName) + ' ON ' + RTRIM(@tablename) + ' REBUILD WITH ( ' + @RebuildOptions + ' )'
EXEC (@execstr)
END
END
ELSE IF (@frag >= @maxreorg)
BEGIN
IF (@ViewOnly=1)
BEGIN
PRINT 'WOULD be executing ALTER INDEX ' + RTRIM(@IdxName) + ' ON ' + RTRIM(@tablename) + ' REORGANIZE WITH ( ' + @ReorgOptions + ' ) -- Fragmentation currently ' + RTRIM(CONVERT(VARCHAR(15),@frag)) + '%'
END
ELSE
BEGIN
PRINT 'Now executing ALTER INDEX ' + RTRIM(@IdxName) + ' ON ' + RTRIM(@tablename) + ' REORGANIZE WITH ( ' + @ReorgOptions + ' ) -- Fragmentation currently ' + RTRIM(CONVERT(VARCHAR(15),@frag)) + '%'
SELECT @execstr = 'ALTER INDEX ' + RTRIM(@IdxName) + ' ON ' + RTRIM(@tablename) + ' REORGANIZE WITH ( ' + @ReorgOptions + ' )'
EXEC (@execstr)
END
END

FETCH NEXT
FROM indexes
INTO @tablename, @objectid, @indexid, @frag, @IdxName
END

-- Close and deallocate the cursor.
CLOSE indexes
DEALLOCATE indexes

-- Delete the temporary table.
DROP TABLE #fraglist

Upvotes: 2

vaso
vaso

Reputation: 213

It might have been exactly due to the fact that in SP the execution plan was cached and it was not optimal for the data set. When data set depends greatly on the parameters or changes considerably between invocations it's better to specify 'with recompile' in 'create proc'. You lose a fraction of a second on recompilation, but may win minutes on execution.

PS Why cannot I comment? Only "Your Answer" is available.

Upvotes: 11

Basic
Basic

Reputation: 26766

Does your SP use dynamic T-SQL at all? If so, you' lose the benefits of cached execution plans...

Failing that, are the connections used to run the SP vs T-SQL configured in the same way? Is the speed differential consistent or is the SP as slow the fist time it's run after moification?

Upvotes: 0

Related Questions