为什么查询选择可怕的执行计划?

Jas*_*son 4 performance sql-server sql-server-2016 query-performance

我正在尝试使我们的应用程序发送的查询更有效。我稍微修改了 SSMS 中的查询,它将在大约 1 秒内执行。

查询 A

SELECT  O.Code AS 'Code', O.[Action] AS 'Action',
    SUM(OpenResponseWithin) AS 'OpenResponseWithin',
    CONVERT(VARCHAR,convert(decimal(10,2),(((SUM(OpenResponseWithin))*100))/convert(decimal(10,2),(SUM(OpenResponseWithin)+SUM(OpenResponseAfter))))) + '%' AS 'OpenResponseWithinPercentage',
    SUM(OpenResponseAfter) AS 'OpenResponseAfter',
    CONVERT(VARCHAR,convert(decimal(10,2),(((SUM(OpenResponseAfter))*100))/convert(decimal(10,2),(SUM(OpenResponseWithin)+SUM(OpenResponseAfter))))) + '%' AS 'OpenResponseAfterPercentage',
    (SUM(OpenResponseWithin)+SUM(OpenResponseAfter)) AS 'OpenTotal',
    SUM(CloseResponseWithin) AS 'CloseResponseWithin',
    CONVERT(VARCHAR,convert(decimal(10,2),(((SUM(CloseResponseWithin))*100))/convert(decimal(10,2),(SUM(OpenResponseWithin)+SUM(OpenResponseAfter))))) + '%' AS 'CloseResponseWithinPercentage',
    SUM(CloseResponseAfter) AS 'CloseResponseAfter',
    CONVERT(VARCHAR,convert(decimal(10,2),(((SUM(CloseResponseAfter))*100))/convert(decimal(10,2),(SUM(OpenResponseWithin)+SUM(OpenResponseAfter))))) + '%' AS 'CloseResponseAfterPercentage',
    SUM(CloseNever) AS 'CloseNever',
    CONVERT(VARCHAR,convert(decimal(10,2),(((SUM(CloseNever))*100))/convert(decimal(10,2),(SUM(OpenResponseWithin)+SUM(OpenResponseAfter))))) + '%' AS 'CloseNeverPercentage'
FROM Custom_OpenCodeMRC O
WHERE O.ActionDate BETWEEN '10/5/2017' AND '12/4/2017'
      AND
      O.Code IN ('BERZ20','BERZ21','BERZ24','BERZ50','FTHZ63','YOR56','YOR57')
GROUP BY O.Code,O.[Action]
ORDER BY O.Code,O.[Action]
Run Code Online (Sandbox Code Playgroud)

如果我按照应用程序通过参数传入的方式保留查询,则执行至少需要 20 秒。

查询 B

DECLARE @parm1 NVARCHAR(10) = '10/05/2017';
DECLARE @parm2 NVARCHAR(10) = '12/04/2017';
DECLARE @parm9 NVARCHAR(6) = 'BERZ20';
DECLARE @parm8 NVARCHAR(6) = 'BERZ21';
DECLARE @parm7 NVARCHAR(6) = 'BERZ24';
DECLARE @parm6 NVARCHAR(6) = 'BERZ50';
DECLARE @parm5 NVARCHAR(6) = 'FTHZ63';
DECLARE @parm4 NVARCHAR(5) = 'YOR56';
DECLARE @parm3 NVARCHAR(5) = 'YOR57';

SELECT O.Code AS 'Code',O.[Action] AS 'Action',
    SUM(OpenResponseWithin) AS 'OpenResponseWithin',
    CONVERT(VARCHAR,convert(decimal(10,2),(((SUM(OpenResponseWithin))*100))/convert(decimal(10,2),(SUM(OpenResponseWithin)+SUM(OpenResponseAfter))))) + '%' AS 'OpenResponseWithinPercentage',
    SUM(OpenResponseAfter) AS 'OpenResponseAfter',
    CONVERT(VARCHAR,convert(decimal(10,2),(((SUM(OpenResponseAfter))*100))/convert(decimal(10,2),(SUM(OpenResponseWithin)+SUM(OpenResponseAfter))))) + '%' AS 'OpenResponseAfterPercentage',
    (SUM(OpenResponseWithin)+SUM(OpenResponseAfter)) AS 'OpenTotal',
    SUM(CloseResponseWithin) AS 'CloseResponseWithin',
    CONVERT(VARCHAR,convert(decimal(10,2),(((SUM(CloseResponseWithin))*100))/convert(decimal(10,2),(SUM(OpenResponseWithin)+SUM(OpenResponseAfter))))) + '%' AS 'CloseResponseWithinPercentage',
    SUM(CloseResponseAfter) AS 'CloseResponseAfter',
    CONVERT(VARCHAR,convert(decimal(10,2),(((SUM(CloseResponseAfter))*100))/convert(decimal(10,2),(SUM(OpenResponseWithin)+SUM(OpenResponseAfter))))) + '%' AS 'CloseResponseAfterPercentage',
    SUM(CloseNever) AS 'CloseNever',
    CONVERT(VARCHAR,convert(decimal(10,2),(((SUM(CloseNever))*100))/convert(decimal(10,2),(SUM(OpenResponseWithin)+SUM(OpenResponseAfter))))) + '%' AS 'CloseNeverPercentage'
FROM Custom_OpenCodeMRC O
WHERE O.ActionDate BETWEEN @parm1 AND @parm2
      AND
      O.Code IN (@parm3,@parm4,@parm5,@parm6,@parm7,@parm8,@parm9)
GROUP BY O.Code,O.[Action]
ORDER BY O.Code Asc,O.[Action] Asc
Run Code Online (Sandbox Code Playgroud)

我保存了执行计划,但它们很长。如果有人想看,我可以用 XML 发布计划。以下是一些查询统计信息。

 +-----------+---------------------+
 | Query A   | CPU time = 1439 ms  |
 | Query B   | CPU time = 23282 ms |
 +-----------+---------------------+
Run Code Online (Sandbox Code Playgroud)

+----------------+--------------+--------------+---------------+---------------+
|   Table        |  Query A     |   Query B    |  Query A      |  Query B      |
|                |  Scan Count  |  Scan Count  | Logical Reads | Logical Reads |
+----------------+--------------+--------------+---------------+---------------+
| ResponseAction |      1       |      1       |      2        |      2        |
|    Code        |      7       |      5       |      14       |      13       |
|   Workfile     |      0       |      0       |      0        |      0        |
|   Worktable    |      57      |    1,305     |    30,712     |    1,735,281  |
|   Response     |      7       |      7       |   12,1136     |    12,1137    |
| ResponseHistory|      24      |      23      |     3,507     |     3,507     |
|    Ticket      |      5       |      5       |     907       |      907      |
|  Organization  |      0       |      5       |    3,479      |     1,463     |
+----------------+--------------+--------------+---------------+---------------+
Run Code Online (Sandbox Code Playgroud)

它们都具有完全相同的结果,但执行时间却大不相同。有人可以向我解释为什么查询 A 使用比查询 B 更有效的执行计划吗?谢谢!

以下是执行计划的链接:

查询 A

查询 B

Joe*_*ish 19

想象一下,您必须计划前往某个神秘地点的旅行。它可以在世界任何地方。您可以选择步行、乘车或乘飞机出行。您需要在知道目的地之前进行选择。你会选什么?我会选择乘飞机旅行。如果您采用世界上所有可能选择的平均旅行时间,这是最好的选择。当然,如果你的目的地是在街上,你可能会倒霉。对于该目的地,乘坐飞机的效率相对较低。另一方面,这肯定是比需要步行数千英里更好的选择。

在查询中使用局部变量通常会使查询优化器处于相同类型的情况。查询优化器旨在创建一个缓存计划,该计划对所有可能的输入变量都表现良好。它通常会使用统计对象的密度,这是获得“平均”基数估计的一种方法。默认情况下,它不会将您的特定参数的值嵌入到查询中并使用这些值来创建有效的计划。

从另一种角度来看,您的数据和表结构可能意味着一个查询计划适用于处理相对少量的数据,但不同的查询计划适用于处理相对大量的数据。例如,如果值@parm1从“10/05/2017”变为“10/05/2000” ,您可能希望更改查询计划。使用局部变量,您只能获得一个缓存计划。对于这些日期值中的一个或两个,该缓存计划将导致低于最佳性能。

提高查询 B 性能的最简单方法是添加RECOMPILE提示。这将启用参数嵌入优化,它为您提供基于运行时变量值的自定义计划。缺点RECOMPILE是查询优化器每次都需要编译一个新的查询计划。如果您的良好查询需要一秒钟的时间来运行,您可能不必为此担心太多。