同一个查询不同的执行计划

Roh*_*hit 3 performance sql-server optimization parameter-sniffing query-performance

我正在尝试优化服务器的性能,这个特定的查询导致从数据库中读取大量数据,进而导致查询超时。此查询是从 Asp.Net MVC 中的 EF6 生成的。

这是有问题的查询:

exec sp_executesql N'SELECT 
[Project1].[C1] AS [C1], 
[Project1].[Date] AS [Date], 
[Project1].[AssetID] AS [AssetID], 
[Project1].[EventData] AS [EventData]
FROM ( SELECT 
    [Extent1].[AssetID] AS [AssetID], 
    [Extent1].[Date] AS [Date], 
    [Extent1].[EventData] AS [EventData], 
    1 AS [C1]
    FROM [dbo].[Alarm] AS [Extent1]
    WHERE ([Extent1].[AssetID] IN (cast(''c6e3142e-5b1f-4a91-90d2-03a504e86ece'' as uniqueidentifier), cast(''4de25e8a-7401-49ae-bd6d-0861d67f0d2f'' as uniqueidentifier), cast(''455e3a5f-1091-4784-9964-0a1a54eaa644'' as uniqueidentifier), cast(''04b46c21-c44f-4b67-b64b-12f2764c0448'' as uniqueidentifier), cast(''a350992b-8548-4bf1-bd22-131c114a5343'' as uniqueidentifier), cast(''98ec1f36-cc54-45d2-a0e3-22aa1b669373'' as uniqueidentifier), cast(''27abcf37-2093-43d5-ae62-2e7b10fe4692'' as uniqueidentifier), cast(''c9f43598-2b9c-47b0-9230-37440e6aea54'' as uniqueidentifier), cast(''c5964caa-5c73-4c0e-bb80-4c1dc7e11039'' as uniqueidentifier), cast(''6ac30678-3876-43c9-b708-61ef19b5ea17'' as uniqueidentifier), cast(''e69d870a-87de-4e3d-b4fc-62c962489a7b'' as uniqueidentifier), cast(''a7c2f407-c605-4491-85fe-66c16fc15586'' as uniqueidentifier), cast(''a38f452e-ee3a-4be7-94ad-99c1474a417f'' as uniqueidentifier), cast(''b0f65616-d5d1-4af9-bffd-9c4b2b7f52e7'' as uniqueidentifier), cast(''3940fed6-9c40-4db6-bdc2-9dc5ef7b49ea'' as uniqueidentifier), cast(''e09f7618-c7d7-414d-b5d7-9ec22b3e9b64'' as uniqueidentifier), cast(''62c91349-d33f-42ed-b16d-a63424acca4a'' as uniqueidentifier), cast(''46812e72-45af-426e-9d72-aafdbcc9c4a7'' as uniqueidentifier), cast(''2a1d2b2a-0471-4f57-adc5-b42a03eb5e01'' as uniqueidentifier), cast(''2797d370-b237-4d2c-bede-b7af67f2b0f4'' as uniqueidentifier), cast(''0c50bb44-133e-4434-b403-c172873564e9'' as uniqueidentifier), cast(''dda75f7a-d366-472e-81b5-c4f3119dd715'' as uniqueidentifier), cast(''cc469264-a706-49c6-961b-d6520437e796'' as uniqueidentifier), cast(''ee2d7ea0-1f94-4cc3-9f64-d8d56374cae5'' as uniqueidentifier), cast(''f7dc6b77-3735-479d-b420-e145d4e3d66f'' as uniqueidentifier), cast(''8a7dbe93-ed1a-49f3-a3d0-e19c6dd4e4ef'' as uniqueidentifier), cast(''b0524a60-f980-4b82-a799-e788a9a4d04b'' as uniqueidentifier), cast(''ceaf8a0b-f410-4f88-9062-e804b76b6e78'' as uniqueidentifier))) AND ([Extent1].[EventCode] = @p__linq__0) AND ([Extent1].[Date] >= @p__linq__1) AND ([Extent1].[Date] <= @p__linq__2) AND (([Extent1].[SiteID] = @p__linq__3) OR (([Extent1].[SiteID] IS NULL) AND (@p__linq__3 IS NULL)))
)  AS [Project1]
ORDER BY [Project1].[Date] DESC',N'@p__linq__0 int,@p__linq__1 datetime2(7),@p__linq__2 datetime2(7),@p__linq__3 uniqueidentifier',@p__linq__0=1799,@p__linq__1='2018-04-22 10:00:00',@p__linq__2='2018-04-23 10:00:00',@p__linq__3='B02A51FE-2248-E611-A64E-782BCB72ACED' 
go
Run Code Online (Sandbox Code Playgroud)

所以我所做的是从查询中删除参数并像这样作为一个单元运行它。

 select * from (  SELECT  1 as c1,  [Extent1].[Date] AS [Date], 
    [Extent1].[AssetID] AS [AssetID],         
    [Extent1].[EventData] AS [EventData]  
    FROM [dbo].[Alarm] AS [Extent1]
    WHERE ([Extent1].[AssetID] IN (cast('c6e3142e-5b1f-4a91-90d2-03a504e86ece' as uniqueidentifier), cast('4de25e8a-7401-49ae-bd6d-0861d67f0d2f' as uniqueidentifier), cast('455e3a5f-1091-4784-9964-0a1a54eaa644' as uniqueidentifier), cast('04b46c21-c44f-4b67-b64b-12f2764c0448' as uniqueidentifier), cast('a350992b-8548-4bf1-bd22-131c114a5343' as uniqueidentifier), cast('98ec1f36-cc54-45d2-a0e3-22aa1b669373' as uniqueidentifier), cast('27abcf37-2093-43d5-ae62-2e7b10fe4692' as uniqueidentifier), cast('c9f43598-2b9c-47b0-9230-37440e6aea54' as uniqueidentifier), cast('c5964caa-5c73-4c0e-bb80-4c1dc7e11039' as uniqueidentifier), cast('6ac30678-3876-43c9-b708-61ef19b5ea17' as uniqueidentifier), cast('e69d870a-87de-4e3d-b4fc-62c962489a7b' as uniqueidentifier), cast('a7c2f407-c605-4491-85fe-66c16fc15586' as uniqueidentifier), cast('a38f452e-ee3a-4be7-94ad-99c1474a417f' as uniqueidentifier), cast('b0f65616-d5d1-4af9-bffd-9c4b2b7f52e7' as uniqueidentifier), cast('3940fed6-9c40-4db6-bdc2-9dc5ef7b49ea' as uniqueidentifier), cast('e09f7618-c7d7-414d-b5d7-9ec22b3e9b64' as uniqueidentifier), cast('62c91349-d33f-42ed-b16d-a63424acca4a' as uniqueidentifier), cast('46812e72-45af-426e-9d72-aafdbcc9c4a7' as uniqueidentifier), cast('2a1d2b2a-0471-4f57-adc5-b42a03eb5e01' as uniqueidentifier), cast('2797d370-b237-4d2c-bede-b7af67f2b0f4' as uniqueidentifier), cast('0c50bb44-133e-4434-b403-c172873564e9' as uniqueidentifier), cast('dda75f7a-d366-472e-81b5-c4f3119dd715' as uniqueidentifier), cast('cc469264-a706-49c6-961b-d6520437e796' as uniqueidentifier), cast('ee2d7ea0-1f94-4cc3-9f64-d8d56374cae5' as uniqueidentifier), cast('f7dc6b77-3735-479d-b420-e145d4e3d66f' as uniqueidentifier), cast('8a7dbe93-ed1a-49f3-a3d0-e19c6dd4e4ef' as uniqueidentifier), cast('b0524a60-f980-4b82-a799-e788a9a4d04b' as uniqueidentifier), cast('ceaf8a0b-f410-4f88-9062-e804b76b6e78' as uniqueidentifier))) AND ([Extent1].[EventCode] = 1799) AND ([Extent1].[Date] >= '2018-04-22 10:00:00') AND ([Extent1].[Date] <= '2018-04-23 10:00:00') AND (([Extent1].[SiteID] = 'B02A51FE-2248-E611-A64E-782BCB72ACED') OR (([Extent1].[SiteID] IS NULL) AND ('B02A51FE-2248-E611-A64E-782BCB72ACED' IS NULL))) 
) as proeject1  order by proeject1.Date desc
Run Code Online (Sandbox Code Playgroud)

这些是查询的相应 IO 成本。

(3721 row(s) affected)
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Alarm'. Scan count 5, logical reads 69032, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Run Code Online (Sandbox Code Playgroud)

糟糕的计划

更好的计划

(3721 row(s) affected)
Table 'Alarm'. Scan count 28, logical reads 564, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Run Code Online (Sandbox Code Playgroud)

好计划

具有良好性能的实际执行计划

性能不佳的实际执行计划

索引:

CREATE NONCLUSTERED INDEX [IX_Alarm_Lat] 
ON [dbo].[Alarm] ( [lat] ASC ) 
INCLUDE ( [AssetID], [Date])

CREATE NONCLUSTERED INDEX [IX_AlarmEventDate] 
ON [dbo].[Alarm] ( [AssetID] ASC, [EventCode] ASC, [Date] ASC, [Ended] ASC ) 
INCLUDE ( [ID], [EventData], [No], [AlarmTS], [ResetID], [lat], [lon], 
[StartTime], [SiteID], [AlarmStatus]) 
Run Code Online (Sandbox Code Playgroud)

我试过了:

  1. 选项重新编译
  2. DBCC FREEPROCCACHE
  3. sp_updatestats
  4. 重建对应索引

但是查询总是选择这个计划,导致服务器磁盘上的巨大负载。

我读过应用程序中的慢,SSMS 中的快?- 了解Erland Sommarskog 的表演之谜,但它没有回答我的问题:

我有两个查询,都在 SSMS 中运行,它们具有不同的性能。主要区别在于一个是参数化的,另一个是普通查询。我完全理解查询之间的差异,但我的问题是关于选择错误的索引。

Joe*_*ish 5

第一个查询调用参数化动态 SQL,这使得它有资格进行参数嗅探:

<ParameterList>
  <ColumnReference Column="@p__linq__3" ParameterCompiledValue="{guid'B02A51FE-2248-E611-A64E-782BCB72ACED'}" ParameterRuntimeValue="{guid'B02A51FE-2248-E611-A64E-782BCB72ACED'}" />
  <ColumnReference Column="@p__linq__2" ParameterCompiledValue="'2018-04-23 10:00:00.0000000'" ParameterRuntimeValue="'2018-04-23 10:00:00.0000000'" />
  <ColumnReference Column="@p__linq__1" ParameterCompiledValue="'2018-04-22 10:00:00.0000000'" ParameterRuntimeValue="'2018-04-22 10:00:00.0000000'" />
  <ColumnReference Column="@p__linq__0" ParameterCompiledValue="(1799)" ParameterRuntimeValue="(1799)" />
</ParameterList>
Run Code Online (Sandbox Code Playgroud)

第二个查询不使用动态 SQL。它具有硬编码值而不是参数。据我了解,问题是,即使嗅探参数值与其他查询相匹配,为什么 SQL Server 仍会使用动态 SQL 选择效率较低的计划?这是一个合理的问题。

首先让我说服您,有时 SQL Server 必须给出不同的计划。即使使用参数嗅探,SQL Server 仍然必须缓存一个对所有可能的参数值都是安全的计划。对于一个简单的例子,请考虑以下查询:

CREATE TABLE #DEMO (
    ID INT NOT NULL,
    PRIMARY KEY (ID)
);

exec sp_executesql N'
SELECT *
FROM #DEMO
WHERE (@id IS NULL OR id = @id)'
, N'@id int'
, @id=1;
Run Code Online (Sandbox Code Playgroud)

我们得到参数嗅探:

<ParameterList>
  <ColumnReference Column="@id" ParameterDataType="int" ParameterCompiledValue="(1)" ParameterRuntimeValue="(1)" />
</ParameterList>
Run Code Online (Sandbox Code Playgroud)

但是查询计划使用了扫描:

在此处输入图片说明

查找会更有效(如果表有任何数据)。如果我尝试强制搜索:

exec sp_executesql N'
SELECT *
FROM #DEMO WITH (FORCESEEK)
WHERE (@id IS NULL OR id = @id)'
,N'@id int'
,@id=1;
Run Code Online (Sandbox Code Playgroud)

我收到以下错误:

消息 8622,级别 16,状态 1,第 15 行 由于此查询中定义的提示,查询处理器无法生成查询计划。在不指定任何提示且不使用 SET FORCEPLAN 的情况下重新提交查询。

缓存计划需要对所有可能的参数值都有效。如果参数值为 NULL,则无法执行查找。因此,扫描是唯一安全的选择。如果我使用文字值:

SELECT *
FROM #DEMO
WHERE (1 IS NULL OR id = 1);
Run Code Online (Sandbox Code Playgroud)

查询优化器可以简化谓词 ( 1 IS NULL)的不可能部分,我得到了一个搜索:

在此处输入图片说明

据我所知,您的查询并没有遇到这种情况。此示例的重点是表明您不能总是期望获得与应用于原始查询的转换类型相同的查询计划。

对于您的特定查询,我认为问题与参数的数据类型有关。当您使用硬编码参数值编写 SQL 时,您可能会无意中使用与硬编码不同的数据类型。在查询的动态 SQL 版本中,您拥有@p__linq__1@p__linq__2定义为datetime2(7). 这与 的列定义匹配Alarm.[Date]吗?如果没有,那么您可能没有资格进行索引搜索。您使用文字值的查询不会将日期转换为datetime2(7),因此那里肯定存在差异。我怀疑这是导致您出现问题的原因。