0

I have a stored procedure like this:

CREATE PROCEDURE [dbo].[GetInventoryData] 
    @supplierId UNIQUEIDENTIFIER,
    @numbers dbo.ListNumbers READONLY,
    @locations dbo.ListLocations READONLY
AS
BEGIN
    SET NOCOUNT ON; 
    
    SELECT DISTINCT
        i.Field1,
        i.Field2,
        i.Field3,
        i.Field4
    FROM 
        dbo.Inventory AS i WITH (index(idx_Inventory_Abc_Xyz))
    JOIN 
        @numbers o ON i.OemNumber = o.OemNumber
    JOIN 
        @locations AS l ON l.YardLocation = i.YardLocation
    WHERE 
        i.SupplierId = @supplierId
        AND i.PartType <> 'ABC'
        AND i.PartType <> 'XYZ'
END

This is how I call the stored procedure:

DECLARE @p2 dbo.Locations  

INSERT INTO @p2 VALUES (N'AA1')  
INSERT INTO @p2 VALUES (N'AA3')    

DECLARE @p3 dbo.ListNumbers  

INSERT INTO @p3 VALUES (N'631006CA0A')    


EXEC GetInventoryData 
           @supplierId = 'e418fac4-c89e-4f5d-ad7d-ee7fcba7f41f',
           @locations = @p2,
           @numbers = @p3 

The above stored procedure sometime got timeout while almost time it just took < 1s.

I check system and see that compilations/sec is high, and it suggested that ad-hoc queries can be reason.

Then I used this query to list ad-hoc query:

SELECT text, cp.objtype, cp.size_in_bytes
FROM sys.dm_exec_cached_plans AS cp 
CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) st
WHERE cp.cacheobjtype = N'Compiled Plan'
  AND cp.objtype IN (N'Adhoc', N'Prepared')
  AND cp.usecounts = 1
ORDER BY cp.size_in_bytes DESC 
OPTION (RECOMPILE);

!!! Here is sql plan: https://www.brentozar.com/pastetheplan/?id=BkP5cAOW9

My question is why my stored procedure an ad-hoc query? I guess table value parameter cause it. Can someone explain, also give me some idea to fix issue please

Update: added .NET code to call stored procedure

enter image description here

Thank you

minhhungit
  • 180
  • 4
  • 25
  • 2
    Why did you need to use index hint for `idx_Inventory_Abc_Xyz` index? – D-Shih Mar 10 '22 at 02:19
  • @D-Shid just want to make sure that it used correct index – minhhungit Mar 10 '22 at 02:20
  • 1
    Its pretty rare that you can pick a better index than SQL Server. – Dale K Mar 10 '22 at 02:23
  • @DaleK yeah maybe, but almost times the stored proceduce work good ( <1 sec) while sometime it took 7-10 seconds even more. So I guess the index hint is still fine – minhhungit Mar 10 '22 at 02:29
  • 1
    The thing about investigating performance issues is to simplify the factors as much as possible... – Dale K Mar 10 '22 at 02:31
  • @minhhungit Did you want to as why it sometimes be slow or SP be `ad-hoc`? if you want to ask why sometimes be slow I might answer it, but another one I don't know – D-Shih Mar 10 '22 at 02:43
  • @D-Shid it's okay to understand more why it's sometime slow so please – minhhungit Mar 10 '22 at 02:51
  • 1
    For heavy use of differently sized Table Valued Parameters it might be worth switching on Trace Flag 2453, see https://www.brentozar.com/archive/2017/02/using-trace-flag-2453-improve-table-variable-performance/. If you are calling the procedure from a client app then do not use the batch at the top (with the `DECLARE... INSERT`) instead use the client driver's tools to pass a Table Valued Parameter directly – Charlieface Mar 10 '22 at 12:22
  • thanks @Charlieface I will try the flag, btw, I call stored procedure from client and it auto generates ```DECLARE ... INSERT``` – minhhungit Mar 10 '22 at 12:41
  • 1
    You will find that it doesn't actually generate that code, it is merely a represntation generated by the SQL Profiler of what the inserts might look like if they were T-SQL code. It's actually a high-performance `BULK INSERT` style codepath sent directly over the TDS protocol, and the recompilation count is actually a artifact that can be safely ignored. See also https://www.sqlskills.com/blogs/bobb/tvps-and-plan-compilation-the-reprise/ and https://stackoverflow.com/a/56744520/14868997 – Charlieface Mar 10 '22 at 13:48
  • Please share the query plans via https://brentozar.com/pastetheplan. Without seeing query plans I couldn't say for sure, but a performance boost could be had by making the two Table Types have primary keys. You can declare them inline like this `CREATE TYPE dbo.ListNumbers (Value int PRIMARY KEY);` – Charlieface Mar 10 '22 at 16:33
  • @Charlieface here is the plan https://www.brentozar.com/pastetheplan/?id=BkP5cAOW9 – minhhungit Mar 11 '22 at 14:21
  • @Charlieface yeah, I did created user defined type as you can see in parameter zone of above stored procedure – minhhungit Mar 11 '22 at 14:24
  • Looks like `DISTINCT` is your main issue here, as it is causing a `Sort`. Why is it necessary? If the two table types have primary keys then they should not be causing duplicates. I suggest you remove it. Which client driver are you using? ADO.Net should be making ad-hoc queries just because of table types, but other drivers may be different – Charlieface Mar 11 '22 at 15:34
  • @Charlieface I'm using ADO.NET – minhhungit Mar 11 '22 at 15:38
  • @Charlieface I updated this post and add image for the code, please check – minhhungit Mar 11 '22 at 15:50
  • Please don't add images of code, just paste them in as text. Yeah that looks mostly OK, except for that you should specify the table type name `AsTableValuedParameter("dbo.ListLocation")`, and if there are any parameters that are `varchar` or `nvarchar` then you should use `DbString` and set `IsAnsi` and `Length` explicitly – Charlieface Mar 11 '22 at 15:54
  • @Charlieface yep, I have table name, it's inside PriorityLocationCollection class, it's also define column type, something like: ``` var sqlRow = new SqlDataRecord( new SqlMetaData("YardLocation", SqlDbType.NVarChar, 50), new SqlMetaData("Priority", SqlDbType.Int)); ``` – minhhungit Mar 11 '22 at 16:00
  • You should specify it directly on the table parameter as I mentioned `locationData.AsTableValuedParameter("dbo.ListLocation")` – Charlieface Mar 11 '22 at 16:03

1 Answers1

1

The above stored procedure sometime got timeout while almost time it just took <1s.

Because you might encounter parameter sniffing,

values passed into the parameter are evaluated and used to create an execution plan for stored procedure that stored execution plan in the plan cache.

because compiling queries is expensive, SQL Server will re-use them as much as possible.

That will cause the execution plan might not be the best for every query parameters.

There are some ways you can avoid parameter sniffing

  1. Add OPTION(RECOMPILE) in the queries or procedure

that way will tell sql-server don't try to keep the execution plan, so you will get a suitable plan from QO every execution time.

CREATE PROCEDURE [dbo].[GetInventoryData] 
    @supplierId UNIQUEIDENTIFIER,
    @numbers dbo.ListNumbers READONLY,
    @locations dbo.ListLocations READONLY
AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON; 
    
    SELECT DISTINCT
        i.Field1,
        i.Field2,
        i.Field3,
        i.Field4
    FROM dbo.Inventory AS i with(index(idx_Inventory_Abc_Xyz))
    JOIN @numbers o ON i.OemNumber = o.OemNumber
    JOIN @locations AS l ON l.YardLocation = i.YardLocation
    WHERE i.SupplierId = @supplierId
    AND i.PartType <> 'ABC'
    AND i.PartType <> 'XYZ'
    OPTION(RECOMPILE)
END

But this way might cause your CPU high, if you execute stored procedure many times in short time

  1. Add OPTION (OPTIMIZE FOR UNKNOWN) or DECLARE local var to carry parameter.

OPTIMIZE FOR UNKNOWN That will create a plan that it expects to work well for all values of all parameters.

CREATE PROCEDURE [dbo].[GetInventoryData] 
    @supplierId UNIQUEIDENTIFIER,
    @numbers dbo.ListNumbers READONLY,
    @locations dbo.ListLocations READONLY
AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON; 
    
    SELECT DISTINCT
        i.Field1,
        i.Field2,
        i.Field3,
        i.Field4
    FROM dbo.Inventory AS i with(index(idx_Inventory_Abc_Xyz))
    JOIN @numbers o ON i.OemNumber = o.OemNumber
    JOIN @locations AS l ON l.YardLocation = i.YardLocation
    WHERE i.SupplierId = @supplierId
    AND i.PartType <> 'ABC'
    AND i.PartType <> 'XYZ'
    OPTION (OPTIMIZE FOR UNKNOWN)
END

parameter-sniffing-in-sql-server

D-Shih
  • 44,943
  • 6
  • 31
  • 51
  • I actually tried OPTION(RECOMPILE) long time ago, I also split stored proc to small stored proceduces to avoid parameter sniffing. My stored proc is called many many times all of day. Base on posts: https://www.brentozar.com/askbrent/compilations/ and https://www.sqlskills.com/blogs/bobb/tvps-and-plan-compilation-the-reprise/ ..... I think it's mechanism of sql server. I'm trying to turn on "optimize for ad hoc workloads" btw. – minhhungit Mar 10 '22 at 05:16
  • one more note, I also tried to using brentozar scripts https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit but don't see it warning "parameter sniffing" – minhhungit Mar 10 '22 at 05:23
  • If your SQL server support QueyStore I would recommend you use that to analytics. execute plan from your SP https://learn.microsoft.com/en-us/sql/relational-databases/performance/monitoring-performance-by-using-the-query-store?view=sql-server-ver15 – D-Shih Mar 10 '22 at 05:49
  • 1
    If not you can try to use who is active to collect what's different between slow query and normal query plan, we can discuss it by those information http://whoisactive.com/ – D-Shih Mar 10 '22 at 05:50