首页 文章

SQL Server:查询速度快,但程序速度慢

提问于
浏览
221

查询运行速度很快:

DECLARE @SessionGUID uniqueidentifier
SET @SessionGUID = 'BCBA333C-B6A1-4155-9833-C495F22EA908'

SELECT *
FROM Report_Opener
WHERE SessionGUID = @SessionGUID
ORDER BY CurrencyTypeOrder, Rank

subtree cost: 0.502

但是将相同的SQL放在存储过程中运行速度很慢,执行计划完全不同

CREATE PROCEDURE dbo.ViewOpener @SessionGUID uniqueidentifier AS
SELECT *
FROM Report_Opener
WHERE SessionGUID = @SessionGUID
ORDER BY CurrencyTypeOrder, Rank

EXECUTE ViewOpener @SessionGUID

Subtree cost: 19.2

我跑了

sp_recompile ViewOpener

它仍然运行相同(严重),我也已将存储过程更改为

CREATE PROCEDURE dbo.ViewOpener @SessionGUID uniqueidentifier AS
SELECT *, 'recompile please'
FROM Report_Opener
WHERE SessionGUID = @SessionGUID
ORDER BY CurrencyTypeOrder, Rank

再回来,试图真正欺骗它重新编译 .

我已经删除并重新创建存储过程以使其生成新计划 .

我尝试使用一个诱饵变量强制重新编译, and prevent parameter sniffing

CREATE PROCEDURE dbo.ViewOpener @SessionGUID uniqueidentifier AS

DECLARE @SessionGUIDbitch uniqueidentifier
SET @SessionGUIDbitch = @SessionGUID

SELECT *
FROM Report_Opener
WHERE SessionGUID = @SessionGUIDbitch
ORDER BY CurrencyTypeOrder, Rank

我也尝试过定义存储过程 WITH RECOMPILE

CREATE PROCEDURE dbo.ViewOpener @SessionGUID uniqueidentifier 
WITH RECOMPILE
AS
SELECT *
FROM Report_Opener
WHERE SessionGUID = @SessionGUID
ORDER BY CurrencyTypeOrder, Rank

所以它的计划永远不会被缓存,我已经尝试在执行时强制重新编译:

EXECUTE ViewOpener @SessionGUID WITH RECOMPILE

这没有帮助 .

我已经尝试将过程转换为动态SQL:

CREATE PROCEDURE dbo.ViewOpener @SessionGUID uniqueidentifier 
WITH RECOMPILE AS
DECLARE @SQLString NVARCHAR(500)

SET @SQLString = N'SELECT *
   FROM Report_OpenerTest
   WHERE SessionGUID = @SessionGUID
   ORDER BY CurrencyTypeOrder, Rank'

EXECUTE sp_executesql @SQLString,
N'@SessionGUID uniqueidentifier',
@SessionGUID

这没有帮助 .

实体“ Report_Opener ”是一个未编入索引的视图 . 该视图仅引用基础表 . 没有表包含计算列,索引或其他 .

对于它的地狱,我尝试创建视图

SET ANSI_NULLS ON
SET QUOTED_IDENTIFER ON

这没有解决它 .

怎么样?

  • 查询速度很快

  • 将查询移动到视图,并从视图中选择是快速的
    从存储过程的视图中选择

  • 慢40倍?

我尝试将视图的定义直接移动到存储过程中(违反了3个业务规则,并打破了重要的封装),这使得它只减慢了大约6倍 .

为什么存储过程版本如此之慢?什么可能导致SQL Server运行ad-hoc SQL比不同类型的ad-hoc SQL更快?

我真的不愿意

  • 在代码中嵌入SQL

  • 完全改变了代码

Microsoft SQL Server  2000 - 8.00.2050 (Intel X86)
Mar  7 2008 21:29:56
Copyright (c) 1988-2003 Microsoft Corporation
Standard Edition on Windows NT 5.2 (Build 3790: Service Pack 2)

但是,如果没有参数嗅探,那么SQL Server无法像运行查询的SQL Sever那样快速运行 .


我的下一次尝试将是 StoredProcedureA 调用 StoredProcedureB 调用 StoredProcedureC 调用 StoredProcedureD 来查询视图 .

如果失败,请让存储过程调用存储过程,调用UDF,调用UDF,调用存储过程,调用UDF查询视图 .


总而言之,以下命令从QA快速运行,但在放入存储过程时速度很慢:

原本的:

--Runs fine outside of a stored procedure
SELECT *
FROM Report_OpenerTest
WHERE SessionGUID = @SessionGUID
ORDER BY CurrencyTypeOrder, Rank

sp_executesql

--Runs fine outside of a stored procedure
DECLARE @SQLString NVARCHAR(500)
SET @SQLString = N'SELECT *
FROM Report_OpenerTest
WHERE SessionGUID = @SessionGUID
ORDER BY CurrencyTypeOrder, Rank'

EXECUTE sp_executesql @SQLString,
        N'@SessionGUID uniqueidentifier',
        @SessionGUID

EXEC(@sql)

--Runs fine outside of a stored procedure
DECLARE @sql NVARCHAR(500)
SET @sql = N'SELECT *
FROM Report_OpenerTest
WHERE SessionGUID = '''+CAST(@SessionGUID AS varchar(50))+'''
ORDER BY CurrencyTypeOrder, Rank'

EXEC(@sql)

执行计划

good 计划:

|--Sort(ORDER BY:([Expr1020] ASC, [Currencies].[Rank] ASC))
           |--Compute Scalar(DEFINE:([Expr1020]=If ([Currencies].[CurrencyType]='ctCanadianCash') then 1 else If ([Currencies].[CurrencyType]='ctMiscellaneous') then 2 else If ([Currencies].[CurrencyType]='ctTokens') then 3 else If ([Currencies].[CurrencyType]
                |--Nested Loops(Left Outer Join, OUTER REFERENCES:([Openers].[OpenerGUID]))
                     |--Filter(WHERE:((([Currencies].[IsActive]<>0 AND [Currencies].[OnOpener]<>0) AND ((((((([Currencies].[CurrencyType]='ctUSCoin' OR [Currencies].[CurrencyType]='ctMiscellaneousUS') OR [Currencies].[CurrencyType]='ctUSCash') OR [Currencies].
                     |    |--Nested Loops(Left Outer Join, OUTER REFERENCES:([Currencies].[CurrencyGUID], [Openers].[OpenerGUID]) WITH PREFETCH)
                     |         |--Nested Loops(Left Outer Join)
                     |         |    |--Bookmark Lookup(BOOKMARK:([Bmk1016]), OBJECT:([GrobManagementSystemLive].[dbo].[Windows]))
                     |         |    |    |--Nested Loops(Inner Join, OUTER REFERENCES:([Openers].[WindowGUID]))
                     |         |    |         |--Bookmark Lookup(BOOKMARK:([Bmk1014]), OBJECT:([GrobManagementSystemLive].[dbo].[Openers]))
                     |         |    |         |    |--Index Seek(OBJECT:([GrobManagementSystemLive].[dbo].[Openers].[IX_Openers_SessionGUID]), SEEK:([Openers].[SessionGUID]=[@SessionGUID]) ORDERED FORWARD)
                     |         |    |         |--Index Seek(OBJECT:([GrobManagementSystemLive].[dbo].[Windows].[IX_Windows]), SEEK:([Windows].[WindowGUID]=[Openers].[WindowGUID]) ORDERED FORWARD)
                     |         |    |--Clustered Index Scan(OBJECT:([GrobManagementSystemLive].[dbo].[Currencies].[IX_Currencies_CurrencyType]))
                     |         |--Clustered Index Seek(OBJECT:([GrobManagementSystemLive].[dbo].[OpenerDetails].[IX_OpenerDetails_OpenerGUIDCurrencyGUID]), SEEK:([OpenerDetails].[OpenerGUID]=[Openers].[OpenerGUID] AND [OpenerDetails].[CurrencyGUID]=[Currenc
                     |--Hash Match(Cache, HASH:([Openers].[OpenerGUID]), RESIDUAL:([Openers].[OpenerGUID]=[Openers].[OpenerGUID]))
                          |--Stream Aggregate(DEFINE:([Expr1006]=SUM(If (((([Currencies].[CurrencyType]='ctMiscellaneous' OR [Currencies].[CurrencyType]='ctTokens') OR [Currencies].[CurrencyType]='ctChips') OR [Currencies].[CurrencyType]='ctCanadianCoin') OR [
                               |--Nested Loops(Inner Join, OUTER REFERENCES:([OpenerDetails].[CurrencyGUID]) WITH PREFETCH)
                                    |--Nested Loops(Inner Join)
                                    |    |--Index Seek(OBJECT:([GrobManagementSystemLive].[dbo].[Openers].[IX_Openers_OneOpenerPerSession]), SEEK:([Openers].[OpenerGUID]=[Openers].[OpenerGUID]) ORDERED FORWARD)
                                    |    |--Clustered Index Seek(OBJECT:([GrobManagementSystemLive].[dbo].[OpenerDetails].[IX_OpenerDetails_OpenerGUIDCurrencyGUID]), SEEK:([OpenerDetails].[OpenerGUID]=[Openers].[OpenerGUID]) ORDERED FORWARD)
                                    |--Index Seek(OBJECT:([GrobManagementSystemLive].[dbo].[Currencies].[PK_Currencies_CurrencyGUID]), SEEK:([Currencies].[CurrencyGUID]=[OpenerDetails].[CurrencyGUID]) ORDERED FORWARD)

bad 计划

|--Sort(ORDER BY:([Expr1020] ASC, [Currencies].[Rank] ASC))
            |--Compute Scalar(DEFINE:([Expr1020]=If ([Currencies].[CurrencyType]='ctCanadianCash') then 1 else If ([Currencies].[CurrencyType]='ctMiscellaneous') then 2 else If ([Currencies].[CurrencyType]='ctTokens') then 3 else If ([Currencies].[Currency
                 |--Nested Loops(Left Outer Join, OUTER REFERENCES:([Openers].[OpenerGUID]))
                      |--Filter(WHERE:((([Currencies].[IsActive]<>0 AND [Currencies].[OnOpener]<>0) AND ((((((([Currencies].[CurrencyType]='ctUSCoin' OR [Currencies].[CurrencyType]='ctMiscellaneousUS') OR [Currencies].[CurrencyType]='ctUSCash') OR [Currenc
                      |    |--Nested Loops(Left Outer Join, OUTER REFERENCES:([Currencies].[CurrencyGUID], [Openers].[OpenerGUID]) WITH PREFETCH)
                      |         |--Filter(WHERE:([Openers].[SessionGUID]=[@SessionGUID]))
                      |         |    |--Concatenation
                      |         |         |--Nested Loops(Left Outer Join)
                      |         |         |    |--Table Spool
                      |         |         |    |    |--Hash Match(Inner Join, HASH:([Windows].[WindowGUID])=([Openers].[WindowGUID]), RESIDUAL:([Windows].[WindowGUID]=[Openers].[WindowGUID]))
                      |         |         |    |         |--Clustered Index Scan(OBJECT:([GrobManagementSystemLive].[dbo].[Windows].[IX_Windows_CageGUID]))
                      |         |         |    |         |--Table Scan(OBJECT:([GrobManagementSystemLive].[dbo].[Openers]))
                      |         |         |    |--Table Spool
                      |         |         |         |--Clustered Index Scan(OBJECT:([GrobManagementSystemLive].[dbo].[Currencies].[IX_Currencies_CurrencyType]))
                      |         |         |--Compute Scalar(DEFINE:([Openers].[OpenerGUID]=NULL, [Openers].[SessionGUID]=NULL, [Windows].[UseChipDenominations]=NULL))
                      |         |              |--Nested Loops(Left Anti Semi Join)
                      |         |                   |--Clustered Index Scan(OBJECT:([GrobManagementSystemLive].[dbo].[Currencies].[IX_Currencies_CurrencyType]))
                      |         |                   |--Row Count Spool
                      |         |                        |--Table Spool
                      |         |--Clustered Index Seek(OBJECT:([GrobManagementSystemLive].[dbo].[OpenerDetails].[IX_OpenerDetails_OpenerGUIDCurrencyGUID]), SEEK:([OpenerDetails].[OpenerGUID]=[Openers].[OpenerGUID] AND [OpenerDetails].[CurrencyGUID]=[Cu
                      |--Hash Match(Cache, HASH:([Openers].[OpenerGUID]), RESIDUAL:([Openers].[OpenerGUID]=[Openers].[OpenerGUID]))
                           |--Stream Aggregate(DEFINE:([Expr1006]=SUM([partialagg1034]), [Expr1007]=SUM([partialagg1035]), [Expr1008]=SUM([partialagg1036]), [Expr1009]=SUM([partialagg1037]), [Expr1010]=SUM([partialagg1038]), [Expr1011]=SUM([partialagg1039]
                                |--Nested Loops(Inner Join)
                                     |--Stream Aggregate(DEFINE:([partialagg1034]=SUM(If (((([Currencies].[CurrencyType]='ctMiscellaneous' OR [Currencies].[CurrencyType]='ctTokens') OR [Currencies].[CurrencyType]='ctChips') OR [Currencies].[CurrencyType]='
                                     |    |--Nested Loops(Inner Join, OUTER REFERENCES:([OpenerDetails].[CurrencyGUID]) WITH PREFETCH)
                                     |         |--Clustered Index Seek(OBJECT:([GrobManagementSystemLive].[dbo].[OpenerDetails].[IX_OpenerDetails_OpenerGUIDCurrencyGUID]), SEEK:([OpenerDetails].[OpenerGUID]=[Openers].[OpenerGUID]) ORDERED FORWARD)
                                     |         |--Index Seek(OBJECT:([GrobManagementSystemLive].[dbo].[Currencies].[PK_Currencies_CurrencyGUID]), SEEK:([Currencies].[CurrencyGUID]=[OpenerDetails].[CurrencyGUID]) ORDERED FORWARD)
                                     |--Index Seek(OBJECT:([GrobManagementSystemLive].[dbo].[Openers].[IX_Openers_OneOpenerPerSession]), SEEK:([Openers].[OpenerGUID]=[Openers].[OpenerGUID]) ORDERED FORWARD)

这个坏人急切地想要拨打600万行;另一个不是 .

注意:这不是关于调优查询的问题 . 我有一个快速运行的查询 . 我只是希望SQL Server从存储过程快速运行 .

12 回答

  • 0

    虽然我经常反对它(虽然在这种情况下似乎你有一个真正的原因),你试过提供查询的SP版本的任何查询提示吗?如果SQL Server在这两个实例中准备了不同的执行计划,您是否可以使用提示告诉它使用哪个索引,以便计划与第一个匹配?

    对于某些示例,you can go here .

    编辑:如果你可以在这里发布您的查询计划,也许我们可以确定正在讲述的计划之间的一些差异 .

    SECOND:更新了特定于SQL-2000的链接 . 你将不得不向下滚动一下,但是还有第二个 Headers 为“表提示”,这就是你要找的东西 .

    第三:“坏”查询似乎忽略了“开启者”表上的[IX_Openers_SessionGUID] - 添加INDEX提示以强制它使用该索引的任何机会都会改变一些事情吗?

  • 4
    • 这是解决方案:
    create procedure GetOrderForCustomers(@CustID varchar(20))
    
    as
    
    begin
    
    select * from orders
    
    where customerid = ISNULL(@CustID, '')
    
    end
    
    • 而已
  • 3

    您是否尝试过重建Report_Opener表上的统计信息和/或索引 . 如果统计数据仍显示数据库首次启动时的数据,那么SP的所有重复性将不值得 .

    初始查询本身很快就能运行,因为优化器可以看到参数永远不会为null . 对于SP,优化器无法确定参数永远不会为空 .

  • 1

    我有另一个想法 . 如果您创建此基于表的函数该怎么办:

    CREATE FUNCTION tbfSelectFromView
    (   
        -- Add the parameters for the function here
        @SessionGUID UNIQUEIDENTIFIER
    )
    RETURNS TABLE 
    AS
    RETURN 
    (
        SELECT *
        FROM Report_Opener
        WHERE SessionGUID = @SessionGUID
        ORDER BY CurrencyTypeOrder, Rank
    )
    GO
    

    然后使用以下语句从中选择(甚至将其放入SP中):

    SELECT *
    FROM tbfSelectFromView(@SessionGUID)
    

    看起来正在发生的事情(每个人都已经评论过)是SQL Server只是在某个地方做出错误的假设,也许这会迫使它纠正这个假设 . 我讨厌添加额外的步骤,但我不确定还有什么可能导致它 .

  • 1

    我和原版海报有同样的问题,但引用的答案并没有解决我的问题 . 查询仍然从存储过程运行得很慢 .

    我找到了另一个答案here "Parameter Sniffing",谢谢Omnibuzz . 归结为在存储过程查询中使用"local Variables",但阅读原文以获得更多理解,这是一个很好的写作 . 例如

    Slow way:

    CREATE PROCEDURE GetOrderForCustomers(@CustID varchar(20))
    AS
    BEGIN
        SELECT * 
        FROM orders
        WHERE customerid = @CustID
    END
    

    Fast way:

    CREATE PROCEDURE GetOrderForCustomersWithoutPS(@CustID varchar(20))
    AS
    BEGIN
        DECLARE @LocCustID varchar(20)
        SET @LocCustID = @CustID
    
        SELECT * 
        FROM orders
        WHERE customerid = @LocCustID
    END
    

    希望这有助于其他人,这样做可以将执行时间从5分钟减少到大约6-7秒 .

  • 342

    这可能听起来很愚蠢,从名称SessionGUID看起来很明显,但该列是Report_Opener上的唯一标识符吗?如果不,您可能想尝试将其转换为正确的类型并给它一个镜头或将您的变量声明为正确的类型 .

    作为sproc的一部分创建的计划可能不直观地工作,并在大 table 上进行内部演员 .

  • 123

    为您的数据库执行此操作 . 我有同样的问题 - 它在一个数据库中工作正常,但当我使用SSIS导入(而不是通常的恢复)将此数据库复制到另一个数据库时,这个问题发生在我的大多数存储过程中 . 所以经过谷歌搜索后,我找到了blog of Pinal Dave (which btw, I encountered most of his post and did help me a lot so thanks Pinal Dave) .

    我在我的数据库上执行以下查询,它纠正了我的问题:

    EXEC sp_MSforeachtable @command1="print '?' DBCC DBREINDEX ('?', ' ', 80)"
    GO
    EXEC sp_updatestats
    GO
    

    希望这可以帮助 . 只是通过帮助我的其他人的帮助 .

  • 16

    这次你发现了你的问题 . 如果下次你不那么幸运而无法弄清楚,你可以使用plan freezing并不再担心错误的执行计划 .

  • 1

    这可能不太可能,但鉴于您观察到的行为异常,需要进行检查,其他人没有提及 .

    您是否确保所有对象都归dbo所有,并且您没有自己或其他用户拥有的流氓副本?

    偶尔当我看到奇怪的行为时,这是因为实际上有两个对象的副本,你得到的是哪个取决于指定的内容以及你登录的对象 . 例如,完全可以拥有一个具有相同名称但由不同所有者拥有的视图或过程的两个副本 - 这种情况可能会出现在您没有以dbo身份登录数据库而忘记将dbo指定为对象所有者时你创建了对象 .

    请注意,在文本中,您运行的是一些事情而不指定所有者,例如

    sp_recompile ViewOpener
    

    例如,如果你没有指定,那么dbo和[其他用户]拥有的viewOpener的两个副本然后你实际重新编译哪个副本取决于环境 . 与Report_Opener视图同上 - 如果有两个副本(并且它们可能在规范或执行计划上有所不同),那么使用的内容取决于具体情况 - 并且由于您未指定所有者,因此您的adhoc查询完全可能使用一个并且编译过程可能会使用另一个 .

    正如我所说,它可能不太可能,但它是可能的,应该检查,因为你的问题可能是你只是在错误的地方寻找错误 .

  • 1

    我发现了问题,这是存储过程的慢速和快速版本的脚本:

    dbo.ViewOpener__RenamedForCruachan__Slow.PRC

    SET QUOTED_IDENTIFIER OFF 
    GO
    SET ANSI_NULLS OFF 
    GO
    
    CREATE PROCEDURE dbo.ViewOpener_RenamedForCruachan_Slow
        @SessionGUID uniqueidentifier
    AS
    
    SELECT *
    FROM Report_Opener_RenamedForCruachan
    WHERE SessionGUID = @SessionGUID
    ORDER BY CurrencyTypeOrder, Rank
    GO
    
    SET QUOTED_IDENTIFIER OFF 
    GO
    SET ANSI_NULLS ON 
    GO
    

    dbo.ViewOpener__RenamedForCruachan__Fast.PRC

    SET QUOTED_IDENTIFIER OFF 
    GO
    SET ANSI_NULLS ON 
    GO
    
    CREATE PROCEDURE dbo.ViewOpener_RenamedForCruachan_Fast
        @SessionGUID uniqueidentifier 
    AS
    
    SELECT *
    FROM Report_Opener_RenamedForCruachan
    WHERE SessionGUID = @SessionGUID
    ORDER BY CurrencyTypeOrder, Rank
    GO
    
    SET QUOTED_IDENTIFIER OFF 
    GO
    SET ANSI_NULLS ON 
    GO
    

    如果你没有发现差异,我不会责怪你 . 根本不在存储过程中 . 将快速0.5成本查询转换为执行600万行的急切假脱机的差异:

    Slow: SET ANSI_NULLS OFF

    Fast: SET ANSI_NULLS ON


    这个答案也可以有意义,因为视图确实有一个连接子句,它说:

    (table.column IS NOT NULL)
    

    所以有一些 NULL 涉及 .


    返回Query Analizer并运行,进一步证明了这一解释

    SET ANSI_NULLS OFF
    

    .

    DECLARE @SessionGUID uniqueidentifier
    SET @SessionGUID = 'BCBA333C-B6A1-4155-9833-C495F22EA908'
    

    .

    SELECT *
    FROM Report_Opener_RenamedForCruachan
    WHERE SessionGUID = @SessionGUID
    ORDER BY CurrencyTypeOrder, Rank
    

    而且查询很慢 .


    所以问题 isn't 因为查询是从存储过程运行的 . 问题是企业管理器的连接默认选项是 ANSI_NULLS off ,而不是 ANSI_NULLS on ,这是QA的默认选项 .

    Microsoft在KB296769中确认了这一事实(BUG:无法使用SQL企业管理器创建包含链接服务器对象的存储过程) . 解决方法是在存储过程对话框中包含 ANSI_NULLS 选项:

    Set ANSI_NULLS ON
    Go
    Create Proc spXXXX as
    ....
    
  • 0

    我遇到了这个问题 . 我的查询看起来像:

    select a, b, c from sometable where date > '20140101'
    

    我的存储过程定义如下:

    create procedure my_procedure (@dtFrom date)
    as
    select a, b, c from sometable where date > @dtFrom
    

    我将数据类型更改为datetime并瞧!从30分钟到1分钟!

    create procedure my_procedure (@dtFrom datetime)
    as
    select a, b, c from sometable where date > @dtFrom
    
  • 8

    我面临同样的问题,这篇文章对我很有帮助,但没有一个已发布的答案解决了我的具体问题 . 我想发布对我有用的解决方案,希望它可以帮助别人 .

    https://stackoverflow.com/a/24016676/814299

    在查询结束时,添加OPTION(OPTIMIZE FOR(@now UNKNOWN))

相关问题