Using Microsoft SQL Server 2019;
I was investigating a Stored Procedure that has suddenly started taking 4 hours to complete for certain parameters passed.
Same SP for these parameters was taking only few minutes before and there is no change in the SP recently.
When I checked, NOT just the SP, the SELECT query inside the SP itself is running long.
When I did some research , I read about parameter sniffing issue.
So I first added OPTION(RECOMPILE) to the last SELECT statement and it started running within 1min for first 2 runs.
But then it started to take 4 hours from 3rd run.
So I changed again I used a variable to hold a value and used it in WHERE condition and it gives me result in 1 min.
Plan 1 of Final SELECT statement - Taking 4 hours to complete - uses a Key Lookup which seems to me as trouble maker
Note : WHERE condition is WHERE PR.PRSId = 1
https://www.brentozar.com/pastetheplan/?id=H1dAPJ5kj
Plan 2 of Final SELECT statement - Taking 1 min to complete
Note : WHERE condition is WHERE PR.PRSId = #PRSId
https://www.brentozar.com/pastetheplan/?id=H1VoI15Ji
From my understanding Parameter sniffing happens on SPs. But this case the SELECT query itself is having issue.
Can you explain on this behavior? Thanks
Full query set below
DECLARE #SDVId INT = 3
IF OBJECT_ID('tempdb..#tmp_DSCF') IS NOT NULL
DROP TABLE #tmp_DSCF;
SELECT
SDSDDS.DataSourceId
,C.CategoryId
,F.[Code]
,PF.PFId
INTO
#tmp_DSCF
FROM
dbo.tbl_SDV AS SDV WITH (NOLOCK)
INNER JOIN dbo.tbl_SDSDDataSource AS SDSDDS WITH (NOLOCK) ON SDSDDS.SDVId = SDV.SDVId
INNER JOIN dbo.tbl_SDC AS SDC WITH (NOLOCK) ON SDC.SDVId = SDV.SDVId
INNER JOIN dbo.tbl_Category AS C WITH (NOLOCK) ON C.CategoryId = SDC.CategoryId
INNER JOIN [dbo].[fnt_SPAF](#SDVId) AS F ON (F.Category = C.Description OR F.Category = '*')
INNER JOIN dbo.tbl_PF AS PF WITH (NOLOCK) ON PF.Description = F.Filter AND PF.CategoryId = C.CategoryId
WHERE
SDV.SDVId = #SDVId;
CREATE CLUSTERED INDEX IX_tmp_DSCF ON #tmp_DSCF(PFId, CategoryId, DataSourceId);
SELECT
PR.PRId
,DSCF.Code AS AttributeName
,PFV.PFVId AS AttributeKey
,PFV.Value AS AttributeValue
FROM
#tmp_DSCF AS DSCF
INNER JOIN [dbo].[tbl_PR] AS PR WITH (NOLOCK) ON PR.DataSourceId = DSCF.DataSourceId AND PR.CategoryId = DSCF.CategoryId
INNER JOIN [dbo].[fnt_PPAtDate](GETDATE()) AS F ON F.ProductId = PR.ProductId AND F.PFId = DSCF.PFId
INNER JOIN [dbo].[tbl_PFV] AS PFV WITH (NOLOCK) ON PFV.PFId = F.PFId AND PFV.PFVId = F.PFVId
WHERE
PR.PRSId = 1
AND PR.ProductId IS NOT NULL
As some comments have mentioned, the server hangs on to the execution plan for a SQL statement in a SP and reuses it. Working out an execution plan for a query takes time. Some SPs are used heavily, rapid-fire, in transactional workloads so saving that time is good.
In your case, when the parameters changed the old execution plan became not-so-useful. You found you can fix this by marking the query for "recompilation" (making a new execution plan based on the current parameters). You can also do it by marking the SP for recompilation.
For a data-extraction query like yours the recompilation time is, duh, trivial compared to the query execution time. So there's no harm to be had for marking either the query or the SP for recompilation.
Related
I am struggling with figuring out what is happening with the T-SQL query shown below.
You will see two inner joins to the same table, although with different join criteria. The first join by itself runs in approximately 21 seconds and if I run the second join by itself it completes in approximately 27 seconds.
If I leave both joins in place, the query runs and runs and runs, until I finally stop the query. The appropriate indices appear to be in place and I know this query runs in a different environment with less horsepower, the only difference being the other server is running SQL Server 2012 and I am running SQL Server 2016, although the database is in 2012 compatibility mode:
This join runs in ~21 seconds.
SELECT
COUNT(*)
FROM
dbo.SPONSORSHIP as s
INNER JOIN
dbo.SPONSORSHIPTRANSACTION AS st
ON st.SPONSORSHIPCOMMITMENTID = s.SPONSORSHIPCOMMITMENTID
AND st.TRANSACTIONSEQUENCE = (SELECT MIN(TRANSACTIONSEQUENCE)
FROM dbo.SPONSORSHIPTRANSACTION AS ms
WHERE ms.SPONSORSHIPCOMMITMENTID = s.SPONSORSHIPCOMMITMENTID
AND ms.TARGETSPONSORSHIPID = s.ID)
This join runs in ~27 seconds.
SELECT
COUNT(*)
FROM
dbo.SPONSORSHIP AS s
INNER JOIN
dbo.SPONSORSHIPTRANSACTION AS lt ON lt.SPONSORSHIPCOMMITMENTID = s.SPONSORSHIPCOMMITMENTID
AND lt.TRANSACTIONSEQUENCE = (SELECT MAX(TRANSACTIONSEQUENCE)
FROM dbo.SPONSORSHIPTRANSACTION AS ms
WHERE ms.SPONSORSHIPCOMMITMENTID = s.SPONSORSHIPCOMMITMENTID
AND s.ID IN (ms.CONTEXTSPONSORSHIPID,
ms.TARGETSPONSORSHIPID,
ms.DECLINEDSPONSORSHIPID)
AND ms.ACTIONCODE <> 9)
These are both considered correlated subqueries. You should typically avoid this pattern, as it causes what is known as "RBAR"... which is "Row by Agonizing Row". Before you focus on troubleshooting this particular query, I'd suggest revisiting the query itself and see if you can solve this in a more set based approach. You'll find that in most cases you have other ways to accomplish this and cut cost down dramatically.
As one example:
select
total_count
,row_sequence
from
(
SELECT
total_count = COUNT(*)
,row_sequence = row_number() over(order by st.TRANSACTIONSEQUENCE asc)
FROM
dbo.SPONSORSHIP as s
INNER JOIN dbo.SPONSORSHIPTRANSACTION AS st
ON st.SPONSORSHIPCOMMITMENTID = s.SPONSORSHIPCOMMITMENTID
) as x
where
x.row_sequence = 1
This was a quick example that is not tested. For future reference, if you want the best answer, it's a great idea to generate a temp table or test data set that's able to be used so someone can provide a full working example.
The example I gave shows what is called a windowing function. Take a look more into them for helping with selecting results when you see the word sequence, need the the first/last in a group and more.
Hope this gives you some ideas! Welcome to Stack Overflow! 👋
The idea of the below query is to use the CTE to get the primary key of all rows in [Archive].[tia_tia_object] that meet the filter.
The execution time for the query within the CTE is 0 seconds.
The second part is supposed to do joins on other tables, to filter the data some more, but only if there are any rows returned in the CTE. This was the only way I could get the SQL server to use the correct indexes.
Why does it spend time (see execution plan) looking in TIA_TIA_AGREEMENT_LINE and TIA_TIA_OBJECT, when CTE returns 0 rows?
WITH cte_vehicle
AS (SELECT O.[Seq_no],
O.Object_No
FROM [Archive].[tia_tia_object] O
WHERE O.RECORD_TIMESTAMP >
(SELECT LastLoadTimeStamp FROM staging.Ufngetlastloadtimestamp('Staging.CoveredObject'))
AND O.[Meta_iscurrent] = 1
AND O.OBJECT_TYPE IN ( 'BIO01', 'CAO01', 'DKV', 'GFO01',
'KMA', 'KNO01', 'MCO01', 'VEO01',
'SVO01', 'AUO01' ))
SELECT O.[Seq_no] AS [Bkey_CoveredObject],
Cast(O.[Agr_Line_No] AS BIGINT) AS [Agr_Line_No],
O.[Cover_Start_Date] AS [CoverageFrom],
O.[Cover_End_Date] AS [CoverageTo],
O.[Timestamp] AS [TIMESTAMP],
O.[Record_Timestamp] AS [RECORD_TIMESTAMP],
O.[Newest] AS [Newest],
O.LOCATION_ID AS LocationNo,
O.[Cust_no],
O.[N01]
FROM cte_vehicle AS T
INNER JOIN [Archive].[tia_tia_object] O
ON t.Object_No = O.Object_No
AND t.Seq_No = O.Seq_No
INNER JOIN [Archive].[tia_tia_agreement_line] AL
ON O.Agr_line_no = AL.Agr_line_no
INNER JOIN [Archive].[tia_tia_policy] P
ON AL.Policy_no = P.Policy_no
WHERE P.[Transaction_type] <> 'D'
Execution plan:
Because it still needs to check and look for records. Even if there are no records in that table, it doesn't know that until it actually checks.
Much like if someone gives you a sealed box, you don't know it's empty or not till you open it.
I have the following query which takes about 20s to complete.
declare #shoppingBasketID int
select #shoppingBasketID = [uid]
from shoppingBasket sb
where sb.requestID = 21918154 and sb.[status] > 0
select
ingredientGroup.shoppingBasketItemID as itemID,
ingredientGroup.[uid] as groupUID
from shoppingBasketItem item
left outer join shoppingBasketItemBundle itemBundle on itemBundle.primeMenuItemID = item.[uid]
left outer join shoppingBasketItem bundleItem on bundleItem.[uid] = isnull(itemBundle.linkMenuItemID, item.[uid])
left outer join shoppingBasketItemIngredientGroup ingredientGroup on ingredientGroup.shoppingBasketItemID = isnull(itemBundle.linkMenuItemID, item.[uid])
left outer join shoppingBasketItemIngredient ingredient on ingredient.shoppingBasketItemIngredientGroupID = ingredientGroup.[uid]
where item.shoppingBasketID = #shoppingBasketID
The 'shoppingBasketItemIngredient' table has 40 millions rows.
When I change the last line to the following the query returns the results almost instantly. (I moved the first select into the second select query).
where item.shoppingBasketID = (select [uid] from shoppingBasket sb where sb.requestID = 21918154 and sb.[status] > 0)
Do you know why?
This is too long for a comment.
Queries in stored procedures are compiled the first time they are run and the query plan is cached. So, if you test the stored procedure on an empty table, then it might generate a bad query plan -- and that doesn't get updated automatically.
You can force a recompile at either the stored procedure or query level, using the option WITH (RECOMPILE). Here is some documentation.
You could add a query hint.
When using a variable the query optimizer could generate a slow execution plan.
It's easier for a query optimizer to calculate the optimal plan when a fixed value is used.
But by adding the right hint(s) it could go for a faster execution plan.
For example:
select
...
where item.shoppingBasketID = #shoppingBasketID
OPTION ( OPTIMIZE FOR (#shoppingBasketID UNKNOWN) );
In the example UNKNOWN was used, but you can give a value instead.
The following chart shows the performance of a process over time.
The process is calling a stored procedure with the following form:
CREATE PROCEDURE [dbo].[GetResultSetsAndResultsWhereStatusIsValidatedByPatientId]
#PatientId uniqueidentifier
AS
BEGIN
SELECT DISTINCT resultSetTable.ResultSetId,
resultSetTable.OrderId,
resultSetTable.ReceivedDateTime,
resultSetTable.ProfileId,
resultSetTable.Status,
profileTable.Code,
testResultTable.AbnormalFlag,
testResultTable.Result,
orderTable.ReceptionDateTime,
testTable.TestCode,
orderedProfileTable.[Status] as opStatus
FROM dbo.ResultSet resultSetTable
INNER JOIN dbo.[Profile] profileTable on (profileTable.ProfileId = resultSetTable.ProfileId)
INNER JOIN dbo.[TestResult] testResultTable on (testResultTable.ResultSetId = resultSetTable.ResultSetId)
INNER JOIN dbo.[Order] orderTable on (resultSetTable.OrderId = orderTable.OrderId)
INNER JOIN dbo.[Test] testTable on (testResultTable.TestId = testTable.TestId)
INNER JOIN dbo.OrderedProfile orderedProfileTable on (orderedProfileTable.ProfileId = resultSetTable.ProfileId)
WHERE orderTable.PatientId = #PatientId
AND orderedProfileTable.[Status] in ('V', 'REP')
END
The problem seems to be the IN-clause. If I remove the IN-clause and only check for one of the values then I get consistent performance as seen in the second part of the graph.
AND orderedProfileTable.[Status] = 'V'
The issue also seems to be related to the amount of data in the tables. Only two tables grow, [ResultSet] and [TestResult], and both these tables are empty at the start of performance runs.
I have tried the following:
Move the IN-clause to a an outer select - no effect
Replace the IN-clause with a join - severe performance degradation
Create an index for the "Status" field used in the IN-clause - no effect
Is there a way to always get the low performance even when there is no data in the two relevant tables?
Have you tried throwing the IN query into an EXIST clause?
WHERE
orderTable.PatientId = #PatientId
AND
EXISTS
(SELECT *
FROM dbo.OrderedProfile as p
WHERE
p.profileid = orderedprofiletable.profileid
AND
[Status] IN ('v','rep'))
Since you're only searching for static results ('v' and 'rep') I would think that the IN clause by itself would be your best bet, but EXIST can sometimes speed up performance so it's worth a shot.
The problem was not related to the IN-clause in the end but a logic error. We started questioning why the query needed DISTINCT and when we removed it we discovered a logic error in the last join (needed some more criteria in what it matches against).
The error has been partially resolved and the performance issue seems to be resolved.
The stored procedure now completes in less than 10 ms on average and no performance degradation.
This query takes 16 seconds to run
SELECT
WO.orderid
FROM
WebOrder as WO
INNER JOIN Addresses AS A ON WO.AddressID = A.AddressID
LEFT JOIN SalesOrders as SO on SO.SO_Number = WO.SalesOrderID
If I comment out either of the joins, it runs in a small fraction of a second. Example:
SELECT
WO.orderid
FROM
WebOrder as WO
INNER JOIN Addresses AS A ON WO.AddressID = A.AddressID
-- LEFT JOIN SalesOrders as SO on SO.SO_Number = WO.SalesOrderID
or
SELECT
WO.orderid
FROM
WebOrder as WO
-- INNER JOIN Addresses AS A ON WO.AddressID = A.AddressID
LEFT JOIN SalesOrders as SO on SO.SO_Number = WO.SalesOrderID
Notes
There exists about 40,000 records each in tables SalesOrders and Adddresses.
I have indexes or PKeys on all fields used in the ON clauses.
Execution Plan for the slow version (SalesOrders Join commented out)
Execution Plan for fast version
Why do these joins when used in conjunction with one another cause this to go from ~0.01 seconds to 16 seconds?
Your execution plan doesn't show any expensive operations, I would try to following to troubleshoot bad performance:
Rebuild Indexes
Update Stats
DBCC FREEPROCCACHE
Personally I wouldn't expect the latter to do anything -- it looks like you have a sensible query plan as it is.