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


24

我有一个非常重要,非常缓慢的观点,它的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)  

该视图通常如下使用:

select * from vwReallySlowView
where product_code_v42  = 'LIGHTBULB100W'  -- find "100 watt lightbulb" rows

当我运行它,我得到这个执行计划项目成本核算批次的总成本的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]

这是其他产品之一的执行计划,该执行结果使该CONVERT_IMPLICIT谓词的成本为27%。更新请注意,在这种情况下,我的最差节点现在是“”上的“哈希匹配” inner join,花费34%。摆脱。INNER JOIN上面视图中的两个操作都在varchar(13)字段上。

右下角放大:

在此处输入图片说明

整个执行计划如.sqlplan在skydrive上可用。此图像只是视觉概述。单击此处单独查看图像。

在此处输入图片说明

更新发布的整个执行计划。我似乎找不到product_code病理上不好的价值,但是做到这一点的一种方法是,select count(*) from view而不是制造单一产品。但是仅在基础表中的记录中使用5%或更少的产品似乎显示出较低的CONVERT_IMPLICIT 运营成本。如果要在此处修复SQL,我想我会WHERE在视图中使用Gross 子句,并将该巨大的where-clause-condition的结果作为“ IncludeMeInTheView”位字段存储并存储在基础表中。Presto,问题解决了,对吧?



我发布的.SQLPLAN与原始图像中的432k / 17k相比,病态少很多。抱歉,我无法再product_code使用87%我的数据来生成该病理案例的值。现在显示图像27%。再次,为我的编辑造成的混乱表示歉意。
沃伦·P

Answers:


49

您不应在执行计划中过分依赖成本百分比。即使在执行后计划中,诸如行数之类的“实际”数字也总是这些成本的估算值。估计成本基于一个模型,该模型恰好可以达到预期的目的:使优化器可以为同一查询在不同的候选执行计划之间进行选择。成本信息很有趣,并且是要考虑的因素,但是它很少应该成为查询调整的主要指标。解释执行计划信息需要更广泛地查看所呈现的数据。

ItemTran聚集索引查找运算符

ItemTran聚集索引寻求

该运算符实际上是两个操作合而为一。首先,索引查找操作会找到与谓词匹配的所有行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_v41tinyint根本不会发生转换。同样,CONVERT(smallint, 4)如果bit_field_v41是,则可以使用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;

具有冷数据高速缓存的此查询的性能非常重要,因为预读会受到表(聚集索引)碎片的影响。该表的聚簇键会引起碎片,因此定期维护(重组或重建)该索引并使用适当的FILLFACTOR索引空间在索引维护窗口之间留出新的行可能很重要。

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

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

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

过滤运算符

过滤运算符

我在这里显示过滤谓词主要是为了说明如何NOT IN组合,简化和扩展两个列表,并为后面的哈希匹配讨论提供参考。可以扩展搜索的测试查询以合并其影响,并确定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
        )
    );

计划中的“计算标量”运算符定义以下表达式(计算本身将推迟到以后的运算符需要结果之前):

[Expr1016] = (trans_qty - (QtyCheckedOut - QtyReturned))

哈希匹配运算符

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

哈希匹配运算符使用booking_no_v32ItemTran表中的列值(哈希键构建)来构建哈希表,然后使用booking_no预订表中的列(哈希键探针)来探测匹配项。SSMS工具提示通常还会显示“探测残差”,但是文本对于工具提示而言太长,因此将其省略。

探测残差类似于在索引较早查找之后看到的残差。在所有哈希匹配的行上评估剩余谓词,以确定是否应将该行传递给父运算符。在平衡良好的哈希表中查找哈希匹配非常快,但是与之相比,将复杂的残差谓词应用于匹配的每一行则非常慢。计划资源管理器中的哈希匹配工具提示显示了详细信息,包括探针残差表达式:

哈希匹配运算符

剩余谓词很复杂,并且由于预订表中有此列,因此包含预订进度状态检查。工具提示还显示了在索引查找中较早看到的估计行数与实际行数之间的相同差异。许多过滤执行两次,这似乎很奇怪,但这只是优化程序是乐观的。它并不期望可以从探针残差向下推到计划中的过滤器部分消除任何行(过滤器前后的行数估计是相同的),但是优化器知道这可能是错误的。尽早过滤行(减少哈希连接的成本)的机会值得额外过滤器的少量成本。不能向下推整个过滤器,因为它在预订表中的列上包含一个测试,但大多数可以。

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

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

纠正问题

这三个问题是碎片化,散列匹配运算符中的复杂探针残差以及索引搜寻中的猜测导致的错误基数估计。

推荐方案

检查碎片并在必要时进行更正,并安排维护以确保索引组织得当。校正基数估计值的通常方法是提供统计信息。在这种情况下,优化需求组合统计(product_code_v42bitfield_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);

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

多列统计信息应该产生更好的估计,从而大大减少了哈希匹配运算符将使用递归溢出或救援算法的机会。添加计算列(这是纯元数据操作,并且由于未标记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

现有视图进行了调整,以使用上面的索引视图:

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;

查询和执行计划示例:

SELECT
    vrsv.*
FROM dbo.vwReallySlowView2 AS vrsv
WHERE vrsv.product_code_v42 = 'M10BOLT';

新的执行计划

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

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

插入方案

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

SELECT * FROM view先前执行的相同测试在150ms内完成并建立了索引视图。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.