SQL Server:存储过程变得非常慢,原始 SQL 查询仍然非常快
Posted
技术标签:
【中文标题】SQL Server:存储过程变得非常慢,原始 SQL 查询仍然非常快【英文标题】:SQL Server: stored procedure become very slow, raw SQL query is still very fast 【发布时间】:2014-07-23 20:23:43 【问题描述】:我们正在努力解决一个奇怪的问题:当原始 SQL 执行得相当快时,存储过程会变得非常慢。
我们有
SQL Server 2008 R2 Express Edition SP1 10.50.2500.0,上面有多个数据库。 一个数据库(大小约为 747Mb) 一个存储过程,它采用不同的参数并在数据库的多个表中进行选择。代码:
ALTER Procedure [dbo].[spGetMovieShortDataList](
@MediaID int = null,
@Rfa nvarchar(8) = null,
@LicenseWindow nvarchar(8) = null,
@OwnerID uniqueidentifier = null,
@LicenseType nvarchar(max) = null,
@PriceGroupID uniqueidentifier = null,
@Format nvarchar(max) = null,
@GenreID uniqueidentifier = null,
@Title nvarchar(max) = null,
@Actor nvarchar(max) = null,
@ProductionCountryID uniqueidentifier = null,
@DontReturnMoviesWithNoLicense bit = 0,
@DontReturnNotReadyMovies bit = 0,
@take int = 10,
@skip int = 0,
@order nvarchar(max) = null,
@asc bit = 1)
as
begin
declare @SQLString nvarchar(max);
declare @ascending nvarchar(5);
declare @ParmDefinition nvarchar(max);
set @ParmDefinition = '@MediaID int,
declare @now DateTime;
declare @Rfa nvarchar(8),
@LicenseWindow nvarchar(8),
@OwnerID uniqueidentifier,
@LicenseType nvarchar(max),
@PriceGroupID uniqueidentifier,
@Format nvarchar(max),
@GenreID uniqueidentifier,
@Title nvarchar(max),
@Actor nvarchar(max),
@ProductionCountryID uniqueidentifier,
@DontReturnMoviesWithNoLicense bit = 0,
@DontReturnNotReadyMovies bit = 0,
@take int,
@skip int,
@now DateTime';
set @ascending = case when @asc = 1 then 'ASC' else 'DESC' end
set @now = GetDate();
set @SQLString = 'SELECT distinct m.ID, m.EpisodNo, m.MediaID, p.Dubbed, pf.Format, t.OriginalTitle into #temp
FROM Media m
inner join Asset a1 on m.ID=a1.ID
inner join Asset a2 on a1.ParentID=a2.ID
inner join Asset a3 on a2.ParentID=a3.ID
inner join Title t on t.ID = a3.ID
inner join Product p on a2.ID = p.ID
left join AssetReady ar on ar.AssetID = a1.ID
left join License l on l.ProductID=p.ID
left join ProductFormat pf on pf.ID = p.Format '
+ CASE WHEN @PriceGroupID IS NOT NULL THEN
'left join LicenseToPriceGroup lpg on lpg.LicenseID = l.ID ' ELSE '' END
+ CASE WHEN @Title IS NOT NULL THEN
'left join LanguageAsset la on la.AssetID = m.ID ' ELSE '' END
+ CASE WHEN @LicenseType IS NOT NULL THEN
'left join LicenseType lt on lt.ID=l.LicenseTypeID ' ELSE '' END
+ CASE WHEN @Actor IS NOT NULL THEN
'left join Cast c on c.AssetID = a1.ID ' ELSE '' END
+ CASE WHEN @GenreID IS NOT NULL THEN
'left join ListToCountryToAsset lca on lca.AssetID=a1.ID ' ELSE '' END
+ CASE WHEN @ProductionCountryID IS NOT NULL THEN
'left join ProductionCountryToAsset pca on pca.AssetID=t.ID ' ELSE '' END
+
'where (
1 = case
when @Rfa = ''All'' then 1
when @Rfa = ''Ready'' then ar.Rfa
when @Rfa = ''NotReady'' and (l.TbaWindowStart is null OR l.TbaWindowStart = 0) and ar.Rfa = 0 and ar.SkipRfa = 0 then 1
when @Rfa = ''Skipped'' and ar.SkipRfa = 1 then 1
end) '
+
CASE WHEN @LicenseWindow IS NOT NULL THEN
'AND
1 = (case
when (@LicenseWindow = 1 And (l.WindowEnd < @now and l.TbaWindowEnd = 0)) then 1
when (@LicenseWindow = 2 And (l.TbaWindowStart = 0 and l.WindowStart < @now and (l.TbaWindowEnd = 1 or l.WindowEnd > @now))) then 1
when (@LicenseWindow = 4 And ((l.TbaWindowStart = 1 or l.WindowStart > @now) and (l.TbaWindowEnd = 1 or l.WindowEnd > @now))) then 1
when (@LicenseWindow = 3 And ((l.WindowEnd < @now and l.TbaWindowEnd = 0) or (l.TbaWindowStart = 0 and l.WindowStart < @now and (l.TbaWindowEnd = 1 or l.WindowEnd > @now)))) then 1
when (@LicenseWindow = 5 And ((l.WindowEnd < @now and l.TbaWindowEnd = 0) or ((l.TbaWindowStart = 1 or l.WindowStart > @now) and (l.TbaWindowEnd = 1 or l.WindowEnd > @now)))) then 1
when (@LicenseWindow = 6 And ((l.TbaWindowStart = 0 and l.WindowStart < @now and (l.TbaWindowEnd = 1 or l.WindowEnd > @now)) or ((l.TbaWindowStart = 1 or l.WindowStart > @now) and (l.TbaWindowEnd = 1 or l.WindowEnd > @now)))) then 1
when ((@LicenseWindow = 7 Or @LicenseWindow = 0) And ((l.WindowEnd < @now and l.TbaWindowEnd = 0) or (l.TbaWindowStart = 0 and l.WindowStart < @now and (l.TbaWindowEnd = 1 or l.WindowEnd > @now)) or ((l.TbaWindowStart = 1 or l.WindowStart > @now) and (l.TbaWindowEnd = 1 or l.WindowEnd > @now)))) then 1
end) ' ELSE '' END
+ CASE WHEN @OwnerID IS NOT NULL THEN
'AND (l.OwnerID = @OwnerID) ' ELSE '' END
+ CASE WHEN @MediaID IS NOT NULL THEN
'AND (m.MediaID = @MediaID) ' ELSE '' END
+ CASE WHEN @LicenseType IS NOT NULL THEN
'AND (lt.Name = @LicenseType) ' ELSE '' END
+ CASE WHEN @PriceGroupID IS NOT NULL THEN
'AND (lpg.PriceGroupID = @PriceGroupID) ' ELSE '' END
+ CASE WHEN @Format IS NOT NULL THEN
'AND (pf.Format = @Format) ' ELSE '' END
+ CASE WHEN @GenreID IS NOT NULL THEN
'AND (lca.ListID = @GenreID) ' ELSE '' END
+ CASE WHEN @DontReturnMoviesWithNoLicense = 1 THEN
'AND (l.ID is not null) ' ELSE '' END
+ CASE WHEN @Title IS NOT NULL THEN
'AND (t.OriginalTitle like N''%' + @Title + '%'' OR la.LocalTitle like N''%' + @Title + '%'') ' ELSE '' END
+ CASE WHEN @Actor IS NOT NULL THEN
'AND (rtrim(ltrim(replace(c.FirstName + '' '' + c.MiddleName + '' '' + c.LastName, '' '', '' ''))) like ''%'' + rtrim(ltrim(replace(@Actor,'' '','' ''))) + ''%'') ' ELSE '' END
+ CASE WHEN @DontReturnNotReadyMovies = 1 THEN
'AND ((ar.ID is not null) AND (ar.Ready = 1) AND (ar.CountryID = l.CountryID))' ELSE '' END
+ CASE WHEN @ProductionCountryID IS NOT NULL THEN
'AND (pca.ProductionCountryID = @ProductionCountryID)' ELSE '' END
+
'
select #temp.* ,ROW_NUMBER() over (order by ';
if @order = 'Title'
begin
set @SQLString = @SQLString + 'OriginalTitle';
end
else if @order = 'MediaID'
begin
set @SQLString = @SQLString + 'MediaID';
end
else
begin
set @SQLString = @SQLString + 'ID';
end
set @SQLString = @SQLString + ' ' + @ascending + '
) rn
into #numbered
from #temp
declare @count int;
select @count = MAX(#numbered.rn) from #numbered
while (@skip >= @count )
begin
set @skip = @skip - @take;
end
select ID, MediaID, EpisodNo, Dubbed, Format, OriginalTitle, @count TotalCount from #numbered
where rn between @skip and @skip + @take
drop table #temp
drop table #numbered';
execute sp_executesql @SQLString,@ParmDefinition, @MediaID, @Rfa, @LicenseWindow, @OwnerID, @LicenseType, @PriceGroupID, @Format, @GenreID,
@Title, @Actor, @ProductionCountryID, @DontReturnMoviesWithNoLicense,@DontReturnNotReadyMovies, @take, @skip, @now
end
存储过程运行得非常好而且很快(它的执行通常需要 1-2 秒)。
调用示例
DBCC FREEPROCCACHE
EXEC value = [dbo].[spGetMovieShortDataList]
@LicenseWindow =N'1',
@Rfa = N'NotReady',
@DontReturnMoviesWithNoLicense = False,
@DontReturnNotReadyMovies = True,
@take = 20,
@skip = 0,
@asc = False,
@order = N'ID'
基本上在存储过程执行过程中执行了3个SQL查询,第一个Select Into
查询占用了99%的时间。
这个查询是
declare @now DateTime;
set @now = GetDate();
SELECT DISTINCT
m.ID, m.EpisodNo, m.MediaID, p.Dubbed, pf.Format, t.OriginalTitle
FROM Media m
INNER JOIN Asset a1 ON m.ID = a1.ID
INNER JOIN Asset a2 ON a1.ParentID = a2.ID
INNER JOIN Asset a3 ON a2.ParentID = a3.ID
INNER JOIN Title t ON t.ID = a3.ID
INNER JOIN Product p ON a2.ID = p.ID
LEFT JOIN AssetReady ar ON ar.AssetID = a1.ID
LEFT JOIN License l on l.ProductID = p.ID
LEFT JOIN ProductFormat pf on pf.ID = p.Format
WHERE
((l.TbaWindowStart is null OR l.TbaWindowStart = 0)
and ar.Rfa = 0 and ar.SkipRfa = 0)
And (l.WindowEnd < @now and l.TbaWindowEnd = 0 )
AND ((ar.ID is not null) AND (ar.Ready = 1) AND (ar.CountryID = l.CountryID))
这个存储过程,在对数据库进行大量数据更新后(很多表和行都受到了更新的影响,但是数据库大小几乎没有变化,现在是 752 )工作变得非常缓慢。现在需要 20 到 90 秒。
如果我从存储过程中获取原始 SQL 查询 - 它会在 1-2 秒内执行。
我们已经尝试过:
使用参数创建存储过程
设置 ANSI_NULLS ON 设置 QUOTED_IDENTIFIER ON
使用参数with recompile
重新创建存储过程
DBCC FREEPROCCACHE
将部分 where 子句移至连接部分
重新索引表
使用类似UPDATE STATISTICS Media WITH FULLSCAN
的语句从查询中更新表的统计信息
但是存储过程的执行仍然是>> 30 秒。
但如果我运行由 SP 生成的 SQL 查询 - 它的执行时间不到 2 秒。
我比较了 SP 和原始 SQL 的执行计划——它们完全不同。在执行 RAW SQL 期间 - 优化器正在使用 Merge Joins,但是当我们执行 SP 时 - 它使用 Hash Match (Inner Join),就像没有索引一样。
Execution Plan for RAW SQl - Fast Execution Plan for SP - Slow如果有人知道它可能是什么 - 请帮忙。提前致谢!
【问题讨论】:
执行计划的链接失效了。 阅读您的问题标题并查看过程定义后,最大的嫌疑人是Parameter Sniffing
。尝试执行过程WITH RECOMPLE
选项。
对我来说听起来像参数嗅探问题,但 OP 说他已经尝试过“重新编译”。只是好奇,当您执行原始 sql 时,您是使用 date 参数还是硬编码值?
大家好!我刚刚检查了链接——它们正在工作,至少对我来说是这样。当您单击链接时 - 您需要单击另一个链接。从页面: > 要下载文件,请单击下面的链接: > ExecutionPlan_RAW_SQL_FAST.sqlplan 是的,正如我所说,我已经尝试使用重新编译来存储过程 - 它没有帮助:(日期是在执行期间分配的:set @now = 获取日期();
【参考方案1】:
尝试使用提示OPTIMIZE FOR UNKNOWN
。如果可行,这可能比每次都强制重新编译要好。问题是,最有效的查询计划取决于提供的日期参数的实际值。在编译 SP 时,sql server 必须对将提供的实际值进行猜测,并且很可能在这里做出错误的猜测。 OPTIMIZE FOR UNKNOWN
专门针对这个问题。
在查询的末尾添加
OPTION (OPTIMIZE FOR (@now UNKNOWN))
http://blogs.msdn.com/b/sqlprogrammability/archive/2008/11/26/optimize-for-unknown-a-little-known-sql-server-2008-feature.aspx
【讨论】:
非常感谢!你救了我们:)。我在 SP 中的第一个查询之后添加了OPTION (OPTIMIZE FOR (@now UNKNOWN, @LicenseWindow UNKNOWN))
,它变得非常快。
但是我有一个问题:你知道为什么 SP 以前工作,现在我需要使用这个提示吗?
这取决于表中的基础数据和提供的日期参数的值。要弄清楚这一点,您需要分析数据更改的性质。这一切都围绕 l.WindowEnd
【参考方案2】:
由于您使用sp_executesql
重新编译过程,或者清除过程的缓存计划实际上并没有帮助,因此通过 sp_executesql 执行的查询的查询计划
单独缓存到存储过程中。
您要么需要将查询提示 WITH (RECOMPILE)
添加到执行的 sql 中,要么在执行之前清除该特定 sql 的缓存:
DECLARE @PlanHandle VARBINARY(64);
SELECT @PlanHandle = cp.Plan_Handle
FROM sys.dm_exec_cached_plans cp
CROSS APPLY sys.dm_exec_sql_text(plan_handle) AS st
WHERE st.text LIKE '%' + @SQLString;
DBCC FREEPROCCACHE (@PlanHandle); -- CLEAR THE CACHE FOR THIS QUERY
EXECUTE sp_executesql @SQLString,@ParmDefinition, @MediaID, @Rfa, @LicenseWindow, @OwnerID, @LicenseType, @PriceGroupID, @Format, @GenreID,
@Title, @Actor, @ProductionCountryID, @DontReturnMoviesWithNoLicense,@DontReturnNotReadyMovies, @take, @skip, @now;
如果你在执行DBCC FREEPROCCACHE
时没有传递任何参数并清除了整个缓存,这当然是无关紧要的。
【讨论】:
感谢您的帮助。我已经使用建议给优化器提示“优化未知”,它解决了我的问题。【参考方案3】:OPTIMIZE FOR (@parameter1 UNKNOWN, @parameter2 UNKNOWN,...)
为我创造了奇迹。我有完全相同的问题。我没有使用任何其他提示,甚至:WITH (RECOMPILE)
【讨论】:
以上是关于SQL Server:存储过程变得非常慢,原始 SQL 查询仍然非常快的主要内容,如果未能解决你的问题,请参考以下文章
非常慢的 ExecuteNonQuery(存储过程)与使用 SQL Server Management Studio 的快速执行