执行计划显示昂贵的 CONVERT_IMPLICIT 操作。我可以通过索引解决这个问题还是需要更改表?

War*_* P 25 sql-server view

我有一个非常重要、非常缓慢的视图,其中在 where 子句中包含了一些非常丑陋的条件,例如这样。我也知道连接是粗略和慢速连接varchar(13)而不是整数标识字段,但想改进下面使用此视图的简单查询:

CREATE VIEW [dbo].[vwReallySlowView]  AS  
AS  
SELECT     
  I.booking_no_v32 AS bkno, 
  I.trans_type_v41 AS trantype, 
  B.Assigned_to_v61 AS Assignbk, 
  B.order_date AS dateo, B.HourBooked AS HBooked,   
  B.MinBooked AS MBooked, B.SecBooked AS SBooked, 
  I.prep_on AS Pon, I.From_locn AS Flocn, 
  I.Trans_to_locn AS TTlocn,   
                      (CASE I.prep_on WHEN 'Y' THEN I.PDate ELSE I.FirstDate END) AS PrDate, I.PTimeH AS PrTimeH, I.PTimeM AS PrTimeM,   
                      (CASE WHEN I.RetnDate < I.FirstDate THEN I.FirstDate ELSE I.RetnDate END) AS RDatev, I.bit_field_v41 AS bitField, I.FirstDate AS FDatev, I.BookDate AS DBooked,   
                      I.TimeBookedH AS TBookH, I.TimeBookedM AS TBookM, I.TimeBookedS AS TBookS, I.del_time_hour AS dth, I.del_time_min AS dtm, I.return_to_locn AS rtlocn,   
                      I.return_time_hour AS rth, I.return_time_min AS rtm, (CASE WHEN I.Trans_type_v41 IN (6, 7) AND (I.Trans_qty < I.QtyCheckedOut)   
                      THEN 0 WHEN I.Trans_type_v41 IN (6, 7) AND (I.Trans_qty >= I.QtyCheckedOut) THEN I.Trans_Qty - I.QtyCheckedOut ELSE I.trans_qty END) AS trqty,   
                      (CASE WHEN I.Trans_type_v41 IN (6, 7) THEN 0 ELSE I.QtyCheckedOut END) AS MyQtycheckedout, (CASE WHEN I.Trans_type_v41 IN (6, 7)   
                      THEN 0 ELSE I.QtyReturned END) AS retqty, I.ID, B.BookingProgressStatus AS bkProg, I.product_code_v42, I.return_to_locn, I.AssignTo, I.AssignType,   
                      I.QtyReserved, B.DeprepOn,  
        (CASE  B.DeprepOn       
        WHEN 1 THEN  B.DeprepDateTime     
        ELSE   I.RetnDate  
           END)  AS DeprepDateTime, I.InRack 
FROM         dbo.tblItemtran AS I 

INNER JOIN  -- booking_no = varchar(13)
         dbo.tblbookings AS B ON B.booking_no = I.booking_no_v32  --  string inner-join

INNER JOIN  -- product_code = varchar(13) 
        dbo.tblInvmas AS M ON I.product_code_v42 = M.product_code  --  string inner-join

WHERE     (I.trans_type_v41 NOT IN (2, 3, 7, 18, 19, 20, 21, 12, 13, 22)) AND (I.trans_type_v41 NOT IN (6, 7)) AND (I.bit_field_v41 & 4 = 0) OR  
                      (I.trans_type_v41 NOT IN (6, 7)) AND (I.bit_field_v41 & 4 = 0) AND (B.BookingProgressStatus = 1) OR  
                      (I.trans_type_v41 IN (6, 7)) AND (I.bit_field_v41 & 4 = 0) AND (I.QtyCheckedOut = 0) OR  
                      (I.trans_type_v41 IN (6, 7)) AND (I.bit_field_v41 & 4 = 0) AND (I.QtyCheckedOut > 0) AND (I.trans_qty - (I.QtyCheckedOut - I.QtyReturned) > 0)  
Run Code Online (Sandbox Code Playgroud)

这个视图通常是这样使用的:

select * from vwReallySlowView
where product_code_v42  = 'LIGHTBULB100W'  -- find "100 watt lightbulb" rows
Run Code Online (Sandbox Code Playgroud)

当我运行它时,我得到这个执行计划项目的成本占批处理总成本的 20% 到 80%,谓词CONVERT_IMPLICIT( .... &(4))表明它在执行这些操作时似乎很慢,bitwise boolean tests例如(I.ibitfield & 4 = 0).

我不是 MS SQL 或 DBA 类型工作的专家,因为我大部分时间都是非 SQL 软件开发人员。但我怀疑这种按位组合是一个坏主意,并且拥有离散的布尔字段会更好。

我能否以某种方式改进我拥有的这个索引,以便在不更改架构(已经在数千个位置生产)的情况下更好地处理这个视图,或者我必须更改将几个布尔值打包成一个整数的基础表bit_field_v41,以解决这个问题?

这是我tblItemtran在此执行计划中扫描的聚集索引:

-- goal:  speed up  select * from vwReallySlowView where productcode  = 'X'
CREATE CLUSTERED INDEX [idxtblItemTranProductCodeAndTransType] ON [dbo].[tblItemtran] 
(
    [product_code_v42] ASC,  -- varchar(13)
    [trans_type_v41] ASC     -- int
)WITH ( PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, 
        IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, 
        ALLOW_PAGE_LOCKS  = ON) 
ON [PRIMARY]
Run Code Online (Sandbox Code Playgroud)

这是执行计划,针对导致此CONVERT_IMPLICIT谓词27% 成本的其他产品之一。更新请注意,在这种情况下,我的最差节点现在是 上的“散列匹配” inner join,其成本为 34% 我相信这是我无法避免的成本,除非我可以避免对当前无法连接的字符串进行连接摆脱。INNER JOIN上面视图中的两个操作都在varchar(13)字段上。

放大右下角:

在此处输入图片说明

整个执行计划作为 .sqlplan 在 skydrive 上可用。此图像只是一个视觉概览。单击此处查看图像本身。

在此处输入图片说明

更新发布的整个执行计划。我似乎找不到什么product_code价值是病态的,但一种方法是不做select count(*) from view单一产品。但是只在底层表中 5% 或更少的记录中使用的产品似乎在CONVERT_IMPLICIT 操作中显示出更低的成本。如果我要在这里修复 SQL,我想我会WHERE在视图中使用毛细子句,并在基础表中计算并存储那个巨大的 where-clause-condition 作为“IncludeMeInTheView”位字段的结果. Presto,问题解决了,对吧?

Pau*_*ite 50

您不应过分依赖执行计划中的成本百分比。这些始终是估计成本,即使在具有“实际”数字的执行后计划中也是如此。估计成本基于一个模型,该模型恰好适用于其预期目的:使优化器能够为同一查询在不同的候选执行计划之间进行选择。成本信息很有趣,也是一个需要考虑的因素,但它很少是查询优化的主要指标。解释执行计划信息需要对呈现的数据有更广泛的了解。

ItemTran 聚集索引查找运算符

ItemTran 聚集索引查找

这个操作符实际上是两个操作合二为一。首先,索引查找操作会找到与 predicate 匹配的所有行product_code_v42 = 'M10BOLT',然后对每一行bit_field_v41 & 4 = 0应用残差谓词。存在bit_field_v41从其基类型 (tinyintsmallint) 到的隐式转换integer

发生转换是因为按位与运算符(&) 要求两个操作数的类型相同。常量值“4”的隐式类型是整数,数据类型优先规则意味着bit_field_v41转换优先级较低的字段值。

通过将谓词编写为bit_field_v41 & CONVERT(tinyint, 4) = 0- 意味着常量值具有较低的优先级并被转换(在常量折叠期间)而不是列值,可以轻松纠正问题(例如)。如果bit_field_v41根本tinyint没有转换发生。同样,CONVERT(smallint, 4)如果bit_field_v41is可以使用smallint。也就是说,在这种情况下,转换不是性能问题,但匹配类型并尽可能避免隐式转换仍然是一种很好的做法。

此查找的估计成本的主要部分取决于基表的大小。虽然聚集索引键本身相当窄,但每一行的大小都很大。没有给出表格的定义,但只是视图中使用的列加起来很重要。由于聚集索引包括所有列,聚集索引键之间的距离是的宽度,而不是索引键的宽度。在某些列上使用版本后缀表明实际表具有更多用于先前版本的列。

查看查找、残差谓词和输出列,可以通过构建等效查询来隔离检查该运算符的性能(这1 <> 2是防止自动参数化的技巧,优化器删除了矛盾,并且不会出现在查询计划):

SELECT
    it.booking_no_v32,
    it.QtyCheckedOut,
    it.QtyReturned,
    it.Trans_qty,
    it.trans_type_v41
FROM dbo.tblItemTran AS it
WHERE
    1 <> 2
    AND it.product_code_v42 = 'M10BOLT'
    AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0;
Run Code Online (Sandbox Code Playgroud)

使用冷数据缓存的这个查询的性能很有趣,因为预读会受到表(聚集索引)碎片的影响。此表的集群键会导致碎片化,因此定期维护(重组或重建)此索引并使用适当的FILLFACTOR值为索引维护窗口之间的新行留出空间可能很重要。

我使用SQL Data Generator生成的示例数据对碎片化对预读的影响进行了测试。使用问题的查询计划中显示的相同表行计数,高度碎片化的聚集索引导致SELECT * FROM view15 秒后DBCC DROPCLEANBUFFERS。在相同条件下使用 ItemTrans 表上新近重建的聚集索引的相同测试在 3 秒内完成。

如果表数据通常完全在缓存中,那么碎片问题就不那么重要了。但是,即使碎片较少,宽表行也可能意味着逻辑和物理读取的数量远高于预期。您还可以尝试添加和删除显式CONVERT来验证我的期望,即隐式转换问题在这里并不重要,除非违反最佳实践。

更重要的是估计离开搜索运算符的行数。优化时间估计为 165 行,但在执行时生成了 4,226 行。稍后我将回到这一点,但造成差异的主要原因是优化器很难预测残差谓词(涉及按位与)的选择性——实际上它求助于猜测。

过滤器运算符

过滤器运算符

我在这里展示过滤谓词主要是为了说明两个NOT IN列表是如何组合、简化和扩展的,同时也为下面的哈希匹配讨论提供参考。可以扩展来自 seek 的测试查询以合并其影响并确定 Filter 运算符对性能的影响:

SELECT
    it.booking_no_v32,
    it.trans_type_v41,
    it.Trans_qty,
    it.QtyReturned,
    it.QtyCheckedOut
FROM dbo.tblItemTran AS it
WHERE
    it.product_code_v42 = 'M10BOLT'
    AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
    AND
    (
        (
            it.trans_type_v41 NOT IN (2, 3, 6, 7, 18, 19, 20, 21, 12, 13, 22)
            AND it.trans_type_v41 NOT IN (6, 7)
        )
        OR
        (
            it.trans_type_v41 NOT IN (6, 7)
        )
        OR 
        (
            it.trans_type_v41 IN (6, 7)
            AND it.QtyCheckedOut = 0
        )
        OR 
        (
            it.trans_type_v41 IN (6, 7)
            AND it.QtyCheckedOut > 0
            AND it.trans_qty - (it.QtyCheckedOut - it.QtyReturned) > 0
        )
    );
Run Code Online (Sandbox Code Playgroud)

计划中的 Compute Scalar 运算符定义了以下表达式(计算本身被推迟,直到后面的运算符需要结果):

[Expr1016] = (trans_qty - (QtyCheckedOut - QtyReturned))
Run Code Online (Sandbox Code Playgroud)

哈希匹配运算符

对字符数据类型执行连接并不是该运算符估计成本高的原因。SSMS 工具提示仅显示哈希键探测条目,但重要的详细信息在 SSMS 属性窗口中。

Hash Match 运算符使用booking_no_v32ItemTran 表中的列 (Hash Keys Build)的值构建一个哈希表,然后使用booking_noBookings 表中的列 (Hash Keys Probe)探测匹配。SSMS 工具提示通常也会显示探针残差,但文本对于工具提示来说太长了,因此被简单地省略了。

Probe Residual 类似于之前索引查找后看到的 Residual;残差谓词对所有哈希匹配的行进行评估,以确定该行是否应传递给父运算符。在平衡良好的哈希表中查找哈希匹配非常快,但相比之下,将复杂的残差谓词应用于匹配的每一行则相当慢。Plan Explorer 中的 Hash Match 工具提示显示了详细信息,包括 Probe Residual 表达式:

哈希匹配运算符

残差谓词很复杂,包括预订进度状态检查,因为预订表中的列可用。工具提示还显示了之前在索引查找中看到的估计行数和实际行数之间的相同差异。大部分过滤执行两次似乎很奇怪,但这只是优化器的乐观态度。它不期望过滤器的部分可以从探测残差中向下推计划以消除任何行(过滤器前后的行计数估计值相同),但优化器知道这可能是错误的。早期过滤行的机会(降低哈希连接的成本)值得额外过滤器的小成本。整个过滤器无法下推,因为它包括对预订表中的列的测试,但大部分都可以。

行计数低估是散列匹配运算符的一个问题,因为为散列表保留的内存量基于估计的行数。如果内存对于运行时所需的哈希表大小来说太小(由于行数较多),哈希表会递归地溢出到物理tempdb存储,通常会导致性能非常差。在最坏的情况下,执行引擎停止递归地溢出哈希桶并求助于一个非常慢的救助算法。散列溢出(递归或救助)是问题中概述的性能问题(不是字符类型连接列或隐式转换)的最可能原因。根本原因是服务器基于不正确的行计数(基数)估计为查询保留了太少的内存。

可悲的是,在 SQL Server 2012 之前,执行计划中没有任何迹象表明散列操作超出了其内存分配(在执行开始之前被保留后无法动态增长,即使服务器有大量空闲内存)并且不得不溢出到临时数据库。可以使用 Profiler监视哈希警告事件类,但很难将警告与特定查询相关联。

纠正问题

这三个问题是碎片化、散列匹配运算符中的复杂探测残差以及由于索引查找中的猜测而导致的不正确基数估计。

推荐方案

检查碎片并在必要时纠正它,安排维护以确保索引保持可接受的组织。纠正基数估计的常用方法是提供统计数据。在这种情况下,优化器需要组合 ( product_code_v42, bitfield_v41 & 4 = 0) 的统计信息。我们不能直接在表达式上创建统计信息,所以我们必须首先为位域表达式创建一个计算列,然后创建手动多列统计信息:

ALTER TABLE dbo.tblItemTran
ADD Bit3 AS bit_field_v41 & CONVERT(tinyint, 4);

CREATE STATISTICS [stats dbo.ItemTran (product_code_v42, Bit3)]
ON dbo.tblItemTran (product_code_v42, Bit3);
Run Code Online (Sandbox Code Playgroud)

对于要使用的统计信息,计算列文本定义必须与视图定义中的文本几乎完全匹配,因此应同时纠正视图以消除隐式转换,并注意确保文本匹配。

多列统计应该会产生更好的估计,大大降低哈希匹配运算符使用递归溢出或救助算法的机会。添加计算列(这是一个仅限元数据的操作,由于它没有标记,所以在表中不占用空间PERSISTED)和多列统计信息是我对第一个解决方案的最佳猜测。

在解决查询性能问题时,重要的是要衡量经过的时间、CPU 使用率、逻辑读取、物理读取、等待类型和持续时间等。如上所示,单独运行部分查询以验证可疑原因也很有用。

在某些环境中,数据的最新视图并不重要,运行后台进程将整个视图物化到快照表中时常会很有用。该表只是一个普通的基表,可以为读取查询建立索引,而不必担心影响更新性能。

查看索引

不要试图直接索引原始视图。读取性能将非常快(在视图索引上进行单次查找),但(在这种情况下)现有查询计划中的所有性能问题都将转移到修改视图中引用的任何表列的查询中。更改基表行的查询确实会受到非常严重的影响。

具有部分索引视图的高级解决方案

此特定查询有一个部分索引视图解决方案,可以纠正基数估计并删除过滤器和探测残差,但它基于对数据的一些假设(主要是我对模式的猜测)并且需要专家实施,特别是关于合适的索引以支持索引视图维护计划。我出于兴趣分享下面的代码,我不建议您在没有非常仔细的分析和测试的情况下实施它。

-- Indexed view to optimize the main view
CREATE VIEW dbo.V1
WITH SCHEMABINDING
AS
SELECT
    it.ID,
    it.product_code_v42,
    it.trans_type_v41,
    it.booking_no_v32,
    it.Trans_qty,
    it.QtyReturned,
    it.QtyCheckedOut,
    it.QtyReserved,
    it.bit_field_v41,
    it.prep_on,
    it.From_locn,
    it.Trans_to_locn,
    it.PDate,
    it.FirstDate,
    it.PTimeH,
    it.PTimeM,
    it.RetnDate,
    it.BookDate,
    it.TimeBookedH,
    it.TimeBookedM,
    it.TimeBookedS,
    it.del_time_hour,
    it.del_time_min,
    it.return_to_locn,
    it.return_time_hour,
    it.return_time_min,
    it.AssignTo,
    it.AssignType,
    it.InRack
FROM dbo.tblItemTran AS it
JOIN dbo.tblBookings AS tb ON
    tb.booking_no = it.booking_no_v32
WHERE
    (
        it.trans_type_v41 NOT IN (2, 3, 7, 18, 19, 20, 21, 12, 13, 22)
        AND it.trans_type_v41 NOT IN (6, 7)
        AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
    )
    OR
    (
        it.trans_type_v41 NOT IN (6, 7)
        AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
        AND tb.BookingProgressStatus = 1
    )
    OR 
    (
        it.trans_type_v41 IN (6, 7)
        AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
        AND it.QtyCheckedOut = 0
    )
    OR 
    (
        it.trans_type_v41 IN (6, 7)
        AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
        AND it.QtyCheckedOut > 0
        AND it.trans_qty - (it.QtyCheckedOut - it.QtyReturned) > 0
    );
GO
CREATE UNIQUE CLUSTERED INDEX cuq ON dbo.V1 (product_code_v42, ID);
GO
Run Code Online (Sandbox Code Playgroud)

调整现有视图以使用上面的索引视图:

CREATE VIEW [dbo].[vwReallySlowView2]
AS
SELECT
    I.booking_no_v32 AS bkno,
    I.trans_type_v41 AS trantype,
    B.Assigned_to_v61 AS Assignbk,
    B.order_date AS dateo,
    B.HourBooked AS HBooked,
    B.MinBooked AS MBooked,
    B.SecBooked AS SBooked,
    I.prep_on AS Pon,
    I.From_locn AS Flocn,
    I.Trans_to_locn AS TTlocn,
    CASE I.prep_on 
        WHEN 'Y' THEN I.PDate
        ELSE I.FirstDate
    END AS PrDate,
    I.PTimeH AS PrTimeH,
    I.PTimeM AS PrTimeM,
    CASE
        WHEN I.RetnDate < I.FirstDate 
        THEN I.FirstDate 
        ELSE I.RetnDate
    END AS RDatev,
    I.bit_field_v41 AS bitField,
    I.FirstDate AS FDatev,
    I.BookDate AS DBooked,
    I.TimeBookedH AS TBookH,
    I.TimeBookedM AS TBookM,
    I.TimeBookedS AS TBookS,
    I.del_time_hour AS dth,
    I.del_time_min AS dtm,
    I.return_to_locn AS rtlocn,
    I.return_time_hour AS rth,
    I.return_time_min AS rtm,
    CASE
        WHEN
            I.Trans_type_v41 IN (6, 7) 
            AND I.Trans_qty < I.QtyCheckedOut
            THEN 0 
        WHEN
            I.Trans_type_v41 IN (6, 7)
            AND I.Trans_qty >= I.QtyCheckedOut
            THEN I.Trans_Qty - I.QtyCheckedOut
        ELSE
            I.trans_qty
    END AS trqty,
    CASE
        WHEN I.Trans_type_v41 IN (6, 7)
        THEN 0
        ELSE I.QtyCheckedOut
    END AS MyQtycheckedout,
    CASE
        WHEN I.Trans_type_v41 IN (6, 7)
        THEN 0
        ELSE I.QtyReturned
    END AS retqty,
    I.ID,
    B.BookingProgressStatus AS bkProg,
    I.product_code_v42,
    I.return_to_locn,
    I.AssignTo,
    I.AssignType,
    I.QtyReserved,
    B.DeprepOn,
    CASE B.DeprepOn
        WHEN 1 THEN B.DeprepDateTime
        ELSE I.RetnDate
    END AS DeprepDateTime,
    I.InRack
FROM dbo.V1 AS I WITH (NOEXPAND)
JOIN dbo.tblbookings AS B ON
    B.booking_no = I.booking_no_v32
JOIN dbo.tblInvmas AS M ON
    I.product_code_v42 = M.product_code;
Run Code Online (Sandbox Code Playgroud)

示例查询和执行计划:

SELECT
    vrsv.*
FROM dbo.vwReallySlowView2 AS vrsv
WHERE vrsv.product_code_v42 = 'M10BOLT';
Run Code Online (Sandbox Code Playgroud)

新的执行计划

在新的计划中,哈希匹配没有残差谓词没有复杂的过滤器,索引视图搜索上没有残差谓词,并且基数估计是完全正确的。

作为插入/更新/删除计划将如何受到影响的示例,这是插入到 ItemTrans 表的计划:

插入计划

突出显示的部分是索引视图维护所需的新部分。表假脱机重放插入的基表行以进行索引视图维护。每行都使用聚集索引查找连接到预订表,然后过滤器应用复杂的WHERE子句谓词来查看是否需要将行添加到视图中。如果是,则对视图的聚集索引执行插入操作。

SELECT * FROM view之前执行的相同测试在 150 毫秒内完成,索引视图就位。

最后一件事:我注意到您的 2008 R2 服务器仍处于 RTM 状态。它不会解决您的性能问题,但2008 R2 的 Service Pack 2自 2012 年 7 月起已可用,并且有许多充分的理由使 Service Pack 尽可能保持最新。