I'm running a procedure which takes around 1 minute for the first time execution but for the next time it reduces to around 9-10 seconds. And after some time again it takes around 1 minute.
My procedure is working with single table which is having 6 non clustered and 1 clustered indexes and unique id column is uniqueidentifier data type with 1,218,833 rows.
Can you guide me where is the problem/possible performance improvement is?
Thanks in advance.
Here is the procedure.
PROCEDURE [dbo].[Proc] (
#HLevel NVARCHAR(100),
#HLevelValue INT,
#Date DATE,
#Numbers NVARCHAR(MAX)=NULL
)
AS
declare #LoopCount INT ,#DateLastYear DATE
DECLARE #Table1 TABLE ( list of columns )
DECLARE #Table2 TABLE ( list of columns )
-- LOOP FOR 12 MONTH DATA
SET #LoopCount=12
WHILE(#LoopCount>0)
BEGIN
SET #LoopCount= #LoopCount -1
-- LAST YEAR DATA
DECLARE #LastDate DATE;
SET #LastDate=DATEADD(D,-1, DATEADD(yy,-1, DATEADD(D,1,#Date)))
INSERT INTO #Table1
SELECT list of columns
FROM Table3 WHERE Date = #Date
AND
CASE
WHEN #HLevel='crieteria1' THEN col1
WHEN #HLevel='crieteria2' THEN col2
WHEN #HLevel='crieteria3' THEN col3
END =#HLevelValue
INSERT INTO #Table2
SELECT list of columns
FROM table4
WHERE Date= #LastDate
AND ( #Numbers IS NULL OR columnNumber IN ( SELECT * FROM dbo.ConvertNumbersToTable(#Numbers)))
INSERT INTO #Table1
SELECT list of columns
FROM #Table2 Prf2 WHERE Prf2.col1 IN (SELECT col2 FROM #Table1) AND Year(Date) = Year(#Date)
SET #Date = DATEADD(D,-1,DATEADD(m,-1, DATEADD(D,1,#Date)));
END
SELECT list of columns FROM #Table1
The first time the query runs, the data is not in the data cache and so has to be retrieved from disk. Also, it has to prepare an execution plan. Subsequent times you run the query, the data will be in the cache and so it will not have to go to disk to read it. It can also reuse the execution plan generated originally. This means execution time can be much quicker and why an ideal situation is to have large amounts of RAM in order to be able to cache as much data in memory as possible (it's the data cache that offers the biggest performance improvements).
If execution times subsequently increase again, it's possible that the data is being removed from the cache (and execution plans can be removed from the cache too) - depends on how much pressure there is for RAM. If SQL Server needs to free some up, it will remove stuff from the cache. Data/execution plans that are used most often/have the highest value will remain cached for longer.
There are of course other things that could be a factor such as what load is on the server at the time, whether your query is being blocked by other processes etc
It seems that stored procedure is recompiling repeatedly after some time. To reduce the recompilation please check this article:
http://blog.sqlauthority.com/2010/02/18/sql-server-plan-recompilation-and-reduce-recompilation-performance-tuning/
Related
I'm trying to figure out why a query against a single table is taking far longer than I think it should. I am sure this question has a simple answer, but I've been scratching my head now for a while and may just not be seeing the forest for the trees.
I have a table, roughly 35 columns wide, with a standard assortment of columns (few int's, bunch of varchar()'s of sizes ranging from 10 to 255, pretty basic), on which I have placed a Clustered Index on the column let's call "PackageID" for the sake of explanation. There are a little north of a Million records in this table so there's a fair amount of data to comb through, and there may one or more records with the same PackageID due to the nature of the records, but it's just a single 'flat' table.
Hitting the table I have a Stored Procedure that takes in a varchar(max) argument that could be a single PackageID or it could be a comma delimited list of 10, 50, 500, or more. The SProc calls a fairly standard simple Split() function (found here and on other sites) that splits the list returning the values as a table, which I then attempt to filter against my table for results. The ID's are int values currently up to 5 digits in length, in the future it will grow but only 5 right now.
I have tried a couple variations on the query inside the SProc (just the query here for brevity):
SELECT PackageID, Column01, Column02, Column03, ... , ColumnN
FROM MyTable
WHERE PackageID IN (SELECT SplitValue FROM dbo.Split(#ListOfIDs, ','))
and
;WITH cteIDs AS (
SELECT SplitValue
FROM dbo.Split(#ListOfIDs, ',')
)
SELECT PackageID, Column01, Column02, Column03, ... , ColumnN
FROM MyTable m
INNER JOIN cteIDs c ON m.PackageID = c.SplitValue
Running from SSMS, on both the Estimated Execution Plan shows as being identical, and take roughly the same amount of time. When the #ListOfIDs is short, the records return quickly, but as the IDs list grows (and it can get to hundreds or more) the execution time can go to minutes or longer. There are no triggers, nothing else is using it, the query isn't being blocked or deadlocked by anything I can tell... it just runs slow.
I feel like I am missing something crazy simple here, but I am just not seeing it.
Appreciate any help, thanks!
UPDATE
This is the Split() function I am using, it's something I pulled from here I don't know how long ago, and have been using ever since. If there is a better one I am happy to switch, this one just worked so I never gave it another thought...
CREATE FUNCTION [dbo].[Split]
(
#String VARCHAR(max),
#Delimiter VARCHAR(5)
)
RETURNS #SplittedValues TABLE
(
OccurenceId SMALLINT IDENTITY(1,1),
SplitValue VARCHAR(max)
)
AS
BEGIN
DECLARE #SplitLength INT
WHILE LEN(#String) > 0
BEGIN
SELECT #SplitLength = (CASE CHARINDEX(#Delimiter, #String)
WHEN 0 THEN LEN(#String)
ELSE CHARINDEX(#Delimiter, #String) -1
END)
INSERT INTO #SplittedValues
SELECT SUBSTRING(#String, 1, #SplitLength)
SELECT #String = (CASE (LEN(#String) - #SplitLength)
WHEN 0 THEN ''
ELSE RIGHT(#String, LEN(#String) - #SplitLength - 1)
END)
END
RETURN
END
GO
UPDATE - Testing Comment Suggestions
I have attempted to try out the suggestions in the comments, and here is what I have found out...
Table size: 1,081,154 records
Unique "PackageID" count: 16008 ID
List test size: 500 random IDs (comma delimited string arg input)
When I run (in SSMS) the query using just the Split() function it takes on average 309 seconds to return 373,761 records.
When I run the query but first dump the Split() results into a #TempTable (with Primary Key Index) and join that against the table, it takes on average 111 seconds to return the same 373,761 records.
I understand this is a lot of records, but this is a flat table with a Clustered Index on the PackageID. The query is a very basic select just asking for the records matching on the ID. There is no calculations, no processing, no other JOINS to other tables, CASE statements, groupings, havings, etc. I am failing to understand why it is taking so long to execute the query. I've seen other queries with massive logic involved return thousands of records sub-second, why does this "simple" looking thing get bogged down?
UPDATE - Adding Exec Plan
As requested, here is the Execution Plan for the query I am running. After dumping split values of the incoming delimited list of ID's into a #TempTable, the query is simply asking for all records out of Table A ("MyTable") with matching ID's found in Table B (the #TempTable). That's it.
Update - Order By
In the attached Execution Plan, noted in the comments, there was an ORDER BY that appeared to be consuming a fair amount of overhead. I removed this from my query and re-ran my tests, which resulted in a minimal improvement in execution time. On a test run that previously took 7 minutes, without the ORDER BY would complete in 6:30 to 6:45 minutes.
At this stage of the game, I am about to chalk this up to a case of Data Volume versus anything to do with the query itself. It could be something on our network, the amount of hops the data has to flow through between the SQL Server and the destination, connection speed of the end user, and/or any number of other factors outside my control or ability to do anything about.
Thank you to all who have responded and provided suggestions. Many of which I will use going forward, and keep in mind as I work with the database.
Assume you are not falling into the trap of using different datatype for index seek of your main table (i.e. your PackageID is varchar but not nvarchar or numeric), then your table join itself is very fast.
To confirm this, you can split the process into 2 steps, insert into temp table, then use temp table to join. If the first step is super slow and the second step is super fast, then that affirms my assumption above.
If step 1 is slow, it means the main cause of the slow performance is on the Split which uses a lot of substring calls.
Assume your list has 10000 items of 20 bytes for each ID. It means you have a variable of 200KB.
By your current SUBSTRING call, it will always copy the 200KB into a new string on each iteration. The string will gradually decrease from 200KB to 0KB but you already copy the 100+KB string for 5000 times. This is 1000MB of data flow in total.
Below are 3 functions.
[Split$60769735$0] is your original function
[Split$60769735$1] is using XML
[Split$60769735$2] is using binary split, but also make use of your original function
[Split$60769735$1] is fast because it utilizes the specialized parser for XML that already can handle split very well.
[Split$60769735$2] is fast because it changes your O(n^2) complexity to O(n log n)
Run time is:
[Split$60769735$0] = 3 to 4 min
[Split$60769735$1] = 2 seconds
[Split$60769735$2] = 7 seconds
NOTE: as this is for demo purpose, some edge cases are not yet handled.
1. For [Split$60769735$1], if the values may contain < > &, some escape is required
2. For [Split$60769735$2], if the delimiter can be not found in the second half of the string (i.e. one child item can be as long as 5000 char), you need to handle the case when the charindex function does not return a hit.
CREATE SCHEMA [TRY]
GO
CREATE FUNCTION [TRY].[Split$60769735$0]
(
#String VARCHAR(max),
#Delimiter VARCHAR(5)
)
RETURNS #SplittedValues TABLE
(
OccurenceId INT IDENTITY(1,1),
SplitValue VARCHAR(max)
)
AS
BEGIN
DECLARE #SplitLength INT
WHILE LEN(#String) > 0
BEGIN
SELECT #SplitLength = (CASE CHARINDEX(#Delimiter, #String)
WHEN 0 THEN LEN(#String)
ELSE CHARINDEX(#Delimiter, #String) -1
END)
INSERT INTO #SplittedValues
SELECT SUBSTRING(#String, 1, #SplitLength)
SELECT #String = (CASE (LEN(#String) - #SplitLength)
WHEN 0 THEN ''
ELSE RIGHT(#String, LEN(#String) - #SplitLength - 1)
END)
END
RETURN
END
GO
CREATE FUNCTION [TRY].[Split$60769735$1]
(
#String VARCHAR(max),
#Delimiter VARCHAR(5)
)
RETURNS #SplittedValues TABLE
(
OccurenceId INT IDENTITY(1,1),
SplitValue VARCHAR(max)
)
AS
BEGIN
DECLARE #x XML = cast('<i>'+replace(#String,#Delimiter,'</i><i>')+'</i>' AS XML)
INSERT INTO #SplittedValues
SELECT v.value('.','varchar(100)') FROM #x.nodes('i') AS x(v)
RETURN
END
GO
CREATE FUNCTION [TRY].[Split$60769735$2]
(
#String VARCHAR(max),
#Delimiter VARCHAR(5)
)
RETURNS #SplittedValues TABLE
(
OccurenceId INT IDENTITY(1,1),
SplitValue VARCHAR(max)
)
AS
BEGIN
DECLARE #len int = len(#String);
IF #len > 10000 BEGIN
DECLARE #mid int = charindex(#Delimiter,#String,#len/2);
INSERT INTO #SplittedValues
SELECT SplitValue FROM TRY.[Split$60769735$2](substring(#String, 1, #mid-1), #Delimiter);
INSERT INTO #SplittedValues
SELECT SplitValue FROM TRY.[Split$60769735$2](substring(#String, #mid+len(#Delimiter), #len-#mid-len(#Delimiter)+1), #Delimiter);
END ELSE BEGIN
INSERT INTO #SplittedValues
SELECT SplitValue FROM TRY.[Split$60769735$0](#String, #Delimiter);
END
RETURN
END
GO
NOTE:
- starting from SQL Server 2016, there will be a built-in split function. But unfortunately you are in 2012
If step 1 is fast but step 2 is slow, possible issues are datatype mismatch or missing index. For such case, posting what your execution plan looks like will help most.
This is not much of an answer, but rather a bucket list. I see no obvious reason why this query performs poorly. The following are some unlikely, really unlikely, and ridiculous possibilities.
+1 on “make sure datatypes on either side of the join are identical”
+1 on loading the “split” data into its own temporary table.
I recommend a #temp table built with a primary key (as opposed to #temp), for obscure reasons having to do with statistics that I believe stopped being relevant in later versions of SQL Server (I started with 7.0, and easily lose track of when the myriad newfangleds got added).
What does the query plan show?
Try running it with “set statistics io on”, to see how many page reads are actually involved.
During your testing, are you certain this is the ONLY query running against that database?
“MyTable” is a table, right? Not a view, synonym, linked server monstrosity, or other bizarre construct?
Any third-party tools installed that might be logging your every action on the DB and/or server?
Give that PackageId is not unique in MyTable, how much data is actually being returned? It may well be that it just takes that long for the data to be read and passed back to the calling system—though this really seems unlikely, unless the server’s flooded with other work to do as well.
While working on query performance optimisation, I noticed that the pattern below outperforms by a wide margin other, more obvious, ways of writing the same query. After looking at the execution plans, it appears this is due to parallelism.
The table MyTable, has a clustered primary key on (Identifier, MyId, date). The #xml variable usually contains tens of entries and data returned is a few hundred thousand rows.
Is there a way to achieve parallelism without using the XML or is this a standard pattern/trick?
SET QUOTED_IDENTIFIER ON;
DECLARE #xml xml;
SET #xml = '<recipe MyId="3654969" Identifier="foo1" StartDate="12-Dec-2017 00:00:00" EndDate="09-Jan-2018 23:59:59"/>
<recipe MyId="3670306" Identifier="foo2" StartDate="10-Jan-2018 00:00:00" EndDate="07-Feb-2018 23:59:59"/>
';
exec sp_executesql N'
SELECT date, val
FROM MyTable tbl
inner join (
SELECT t.data.value(''#MyId'', ''int'') AS xmlMyId,
t.data.value(''#StartDate'', ''datetime'') AS xmlStartDate,
t.data.value(''#EndDate'', ''datetime'') AS xmlEndDate,
t.data.value(''#Identifier'', ''varchar(32)'') as xmlIdentifier
FROM #queryXML.nodes(''/recipe'') t(data) ) cont
ON tbl.MyId = cont.xmlMyId
AND tbl.date >= cont.xmlStartDate
AND tbl.date <= cont.xmlEndDate
WHERE Identifier = cont.xmlIdentifier
ORDER BY date', N'#queryXML xml',#xml;
For example, the stored procedure below which returns the same data severely underperforms the query above (parameters for stored proc are passed in and the whole thing is executed using sp_executesql).
SELECT tbl.date, val
FROM marketdb.dbo.MyTable tbl
INNER JOIN #MyIds ids ON tbl.MyId = ids.MyId
AND (ids.StartDate IS NULL or (ids.StartDate IS NOT NULL AND ids.StartDate <= tbl.date))
AND (ids.EndDate IS NULL or (ids.EndDate IS NOT NULL AND tbl.date <= ids.EndDate))
WHERE tbl.Identifier in (SELECT Identifier FROM #identifier_list) AND date >= #start_date AND date <= #end_date
The actual execution plan of the XML query is shown below.
See also:
sp_executesql is slow with parameters
SQL Server doesn't have the statistics for the table variable?
As Jeroen Mostert said, table variables do not have statistics and the actual execution plan is not optimal. In my case, the xml version of the query was parallelised whereas the stored proc was not (this is what I mean by the execution plan not being optimal).
A way to help the optimiser is to add an appropriate primary key or an index on the table variables. One can also create statistics on the table columns in question but in the SQL server that I am using, table variables do not support statistics.
Having added an index on all columns in the table variables, the optimiser started parallelising the query and the execution speed was greatly improved.
In SQL Server 2012 I have the following user defined function:
CREATE FUNCTION [dbo].[udfMaxDateTime]()
RETURNS datetime
AS
BEGIN
RETURN '99991231';
END;
This is then being used in a stored procedure like so:
DECLARE #MaxDate datetime = dbo.udfMaxDateTime();
DELETE FROM TABLE_NAME
WHERE
ValidTo = #MaxDate
AND
Id NOT IN
(
SELECT
MAX(Id)
FROM
TABLE_NAME
WHERE
ValidTo = #MaxDate
GROUP
BY
COL1
);
Now, if I run the stored procedure with the above code, it takes around 12 seconds to execute. (1,2 million rows)
If I change the WHERE clauses to ValidTo = '99991231' then, the stored procedure runs in under 1 second and it runs in Parallel.
Could anyone try and explain why this is happening ?
It is not because of the user-defined function, it is because of the variable.
When you use a variable #MaxDate in the DELETE query optimizer doesn't know the value of this variable when generating the execution plan. So, it generates a plan based on available statistics on the ValidTo column and some built-in heuristics rules for cardinality estimates when you have an equality comparison in a query.
When you use a literal constant in the query the optimizer knows its value and can generate a more efficient plan.
If you add OPTION(RECOMPILE) the execution plan would not be cached and would be always regenerated and all parameter values would be known to the optimizer. It is quite likely that query will run fast with this option. This option does add a certain overhead, but it is noticeable only when you run a query very often.
DECLARE #MaxDate datetime = dbo.udfMaxDateTime();
DELETE FROM TABLE_NAME
WHERE
ValidTo = #MaxDate
AND
Id NOT IN
(
SELECT
MAX(Id)
FROM
TABLE_NAME
WHERE
ValidTo = #MaxDate
GROUP BY
COL1
)
OPTION(RECOMPILE);
I highly recommend to read Slow in the Application, Fast in SSMS by Erland Sommarskog.
In SQL Server, I'm trying to do a comparative analysis between two different table structures with regard to insert performance given different keys. Does it matter if I use a table variable to do this testing, or should I use a temporary table? Or do I need to go to the trouble of actually creating the tables and indexes?
Specifically, I'm currently using the following script:
DECLARE #uniqueidentifierTest TABLE
(
--yes, this is terrible, but I am looking for numbers on how bad this is :)
tblIndex UNIQUEIDENTIFIER PRIMARY KEY CLUSTERED,
foo INT,
blah VARCHAR(100)
)
DECLARE #intTest TABLE
(
tblindex INT IDENTITY(1,1) PRIMARY KEY CLUSTERED,
foo INT,
blah VARCHAR(100)
)
DECLARE #iterations INT = 250000
DECLARE #ctrl INT = 1
DECLARE #guidKey UNIQUEIDENTIFIER
DECLARE #intKey INT
DECLARE #foo INT = 1234
DECLARE #blah VARCHAR(100) = 'asdfjifsdj fds89fsdio23r'
SET NOCOUNT ON
--test uniqueidentifier pk inserts
PRINT 'begin uniqueidentifier insert test at ' + CONVERT(VARCHAR(50), GETDATE(), 109)
WHILE #ctrl < #iterations
BEGIN
SET #guidKey = NEWID()
INSERT INTO #uniqueidentifierTest (tblIndex, foo, blah)
VALUES (#guidKey, #foo, #blah)
SET #ctrl = #ctrl + 1
END
PRINT 'end uniqueidentifier insert test at ' + CONVERT(VARCHAR(50), GETDATE(), 109)
SET #CTRL = 1
--test int pk inserts
PRINT 'begin int insert test at ' + CONVERT(VARCHAR(50), GETDATE(), 109)
WHILE #ctrl < #iterations
BEGIN
INSERT INTO #intTest (foo, blah)
VALUES (#foo, #blah)
SET #ctrl = #ctrl + 1
END
PRINT 'end int insert test at ' + CONVERT(VARCHAR(50), GETDATE(), 109)
SET NOCOUNT OFF
If you want to compare actual performance, you need to create the tables and indexes (and everything else involved). While a temp table will be a much better analog than a table variable, neither is a substitute for an actual permanent table structure if you're seeking performance metrics.
All of that being said, however, you should avoid using uniqueidentifier as a primary key, or, at the very least, use newsequentialid() rather than newid(). Having a clustered index means that the rows will actually be stored in physical order. If an inserted value is out of sequence, SQL Server will have to rearrange the rows in order to insert it into its proper place.
First of all never ever cluster on a uniqueidentifier when using newid(), it will cause fragmentation and thus page splits, if you have to use a GUID then do it like this
create table #test (id uniqueidentifier primary key defualt newsequentialid())
newsequentialid() won't cause page splits
Still an int is still better as the PK since now all your non clustered indexes and foreign keys will be smaller and now you need less IO to get the same numbers of rows back
I dunno why but I'd like to cite Remus Rusanu [1]:
First of all, you need to run the query repeatedly under each [censored] and average the result, discarding the one with the maximum time. This will eliminate the buffer warm up impact: you want all runs to be on a warm cache, not have one query warm the cache and pay the penalty in comparison.
Next, you need to make sure you measure under realistic concurrency scenario. IF you will have updates/inserts/deletes occur under real life, then you must add them to your test, since they will impact tremendously the reads under various isolation level. The last thing you want is to conclude 'serializable reads are fastest, lets use them everywhere' and then watch the system melt down in production because everything is serialized.
1) Running the query on a cold cache is not accurate. Your production queries will not run on a cold cache, you'll be optimizing an unrealistic scenario and you don't measure the query, you are really measuring the disk read throughput. You need to measure the performance on a warm cache as well, and keep track of both (cold run time, warm run times).
How relevant is the cache for a large query (millions of rows) that under normal circumstances runs only once for particular data?
Still very relevant. Even if the data is so large that it never fits in memory and each run has to re-read every page of the table, there is still the caching of non-leaf pages (ie. hot pages in the table, root or near root), cache of narrower non-clustered indexes, cache of table metadata. Don't think at your table as an ISAM file
[1] Why better isolation level means better performance in SQL Server
Why better isolation level means better performance in SQL Server
I have an SP that takes 10 seconds to run about 10 times (about a second every time it is ran). The platform is asp .net, and the server is SQL Server 2005. I have indexed the table (not on the PK also), and that is not the issue. Some caveats:
usp_SaveKeyword is not the issue. I commented out that entire SP and it made not difference.
I set #SearchID to 1 and the time was significantly reduced, only taking about 15ms on average for the transaction.
I commented out the entire stored procedure except the insert into tblSearches and strangely it took more time to execute.
Any ideas of what could be going on?
set ANSI_NULLS ON
go
ALTER PROCEDURE [dbo].[usp_NewSearch]
#Keyword VARCHAR(50),
#SessionID UNIQUEIDENTIFIER,
#time SMALLDATETIME = NULL,
#CityID INT = NULL
AS
BEGIN
SET NOCOUNT ON;
IF #time IS NULL SET #time = GETDATE();
DECLARE #KeywordID INT;
EXEC #KeywordID = usp_SaveKeyword #Keyword;
PRINT 'KeywordID : '
PRINT #KeywordID
DECLARE #SearchID BIGINT;
SELECT TOP 1 #SearchID = SearchID
FROM tblSearches
WHERE SessionID = #SessionID
AND KeywordID = #KeywordID;
IF #SearchID IS NULL BEGIN
INSERT INTO tblSearches
(KeywordID, [time], SessionID, CityID)
VALUES
(#KeywordID, #time, #SessionID, #CityID)
SELECT Scope_Identity();
END
ELSE BEGIN
SELECT #SearchID
END
END
Why are you using top 1 #SearchID instead of max (SearchID) or where exists in this query? top requires you to run the query and retrieve the first row from the result set. If the result set is large this could consume quite a lot of resources before you get out the final result set.
SELECT TOP 1 #SearchID = SearchID
FROM tblSearches
WHERE SessionID = #SessionID
AND KeywordID = #KeywordID;
I don't see any obvious reason for this - either of aforementioned constructs should get you something semantically equivalent to this with a very cheap index lookup. Unless I'm missing something you should be able to do something like
select #SearchID = isnull (max (SearchID), -1)
from tblSearches
where SessionID = #SessionID
and KeywordID = #KeywordID
This ought to be fairly efficient and (unless I'm missing something) semantically equivalent.
Enable "Display Estimated Execution Plan" in SQL Management Studio - where does the execution plan show you spending the time? It'll guide you on the heuristics being used to optimize the query (or not in this case). Generally the "fatter" lines are the ones to focus on - they're ones generating large amounts of I/O.
Unfortunately even if you tell us the table schema, only you will be able to see actually how SQL chose to optimize the query. One last thing - have you got a clustered index on tblSearches?
Triggers!
They are insidious indeed.
What is the clustered index on tblSearches? If the clustered index is not on primary key, the database may be spending a lot of time reordering.
How many other indexes do you have?
Do you have any triggers?
Where does the execution plan indicate the time is being spent?