用户定义函数的优化问题


26

我有一个问题,为什么SQL Server决定为表中的每个值调用用户定义的函数,即使应该只提取一行。实际的SQL复杂得多,但是我能够将问题减少到这个程度:

select  
    S.GROUPCODE,
    H.ORDERCATEGORY
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT    
    cross apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

对于此查询,SQL Server决定为PRODUCT表中存在的每个单个值调用GetGroupCode函数,即使从ORDERLINE返回的估计行数和实际行数为1(这是主键):

查询计划

计划浏览器中的同一计划显示行数:

计划浏览器 表格:

ORDERLINE: 1.5M rows, primary key: ORDERNUMBER + ORDERLINE + RMPHASE (clustered)
ORDERHDR:  900k rows, primary key: ORDERID (clustered)
PRODUCT:   6655 rows, primary key: PRODUCT (clustered)

用于扫描的索引是:

create unique nonclustered index PRODUCT_FACTORY on PRODUCT (PRODUCT, FACTORY)

该函数实际上稍微复杂一些,但是使用虚拟多语句函数时也会发生以下情况:

create function GetGroupCode (@FACTORY varchar(4))
returns @t table(
    TYPE        varchar(8),
    GROUPCODE   varchar(30)
)
as begin
    insert into @t (TYPE, GROUPCODE) values ('XX', 'YY')
    return
end

我可以通过强制SQL Server获取前1种产品来“修复”性能,尽管可以找到最大的1种产品:

select  
    S.GROUPCODE,
    H.ORDERCAT
from    
    ORDERLINE L
    join ORDERHDR H
        on H.ORDERID = M.ORDERID
    cross apply (select top 1 P.FACTORY from PRODUCT P where P.PRODUCT = L.PRODUCT) P
    cross apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

然后,计划形状也将更改为我原本希望的样子:

顶部查询计划

尽管索引PRODUCT_FACTORY小于聚集索引PRODUCT_PK也会产生影响,但是即使强制查询使用PRODUCT_PK,该计划仍与原始计划相同,对函数的调用为6655。

如果我完全忽略了ORDERHDR,则该计划首先从ORDERLINE和PRODUCT之间的嵌套循环开始,并且该函数仅被调用一次。

我想了解这可能是什么原因,因为所有操作都是使用主键完成的,如果它发生在无法轻松解决的更复杂的查询中,该如何解决。

编辑:创建表语句:

CREATE TABLE dbo.ORDERHDR(
    ORDERID varchar(8) NOT NULL,
    ORDERCATEGORY varchar(2) NULL,
    CONSTRAINT ORDERHDR_PK PRIMARY KEY CLUSTERED (ORDERID)
)

CREATE TABLE dbo.ORDERLINE(
    ORDERNUMBER varchar(16) NOT NULL,
    RMPHASE char(1) NOT NULL,
    ORDERLINE char(2) NOT NULL,
    ORDERID varchar(8) NOT NULL,
    PRODUCT varchar(8) NOT NULL,
    CONSTRAINT ORDERLINE_PK PRIMARY KEY CLUSTERED (ORDERNUMBER,ORDERLINE,RMPHASE)
)

CREATE TABLE dbo.PRODUCT(
    PRODUCT varchar(8) NOT NULL,
    FACTORY varchar(4) NULL,
    CONSTRAINT PRODUCT_PK PRIMARY KEY CLUSTERED (PRODUCT)
)

Answers:


30

制定计划的三个主要技术原因是:

  1. 优化器的成本核算框架不真正支持非内联函数。它不会尝试查看函数定义以了解它可能有多昂贵,它只是分配了非常小的固定成本,并估计函数每次被调用时都会产生一行输出。这两种建模假设通常都是完全不安全的。2014年,启用了新的基数估计器后,情况有了一点改善,因为固定的1行猜测被固定的100行猜测所代替。但是,仍然不支持对非内联函数的内容进行成本计算。
  2. SQL Server最初会折叠联接并应用于单个内部n元逻辑联接。这有助于优化程序稍后确定加入订单的原因。随后将单个n元连接扩展为候选连接顺序,这在很大程度上是基于启发式方法。例如,内部联接先于外部联接,小表和选择性联接先于大表和选择性下降的联接,依此类推。
  3. 当SQL Server执行基于成本的优化时,它将工作分成多个可选阶段,以最大程度地减少花费太长时间优化低成本查询的机会。有三个主要阶段,搜索0,搜索1和搜索2。每个阶段都有进入条件,与新阶段相比,新阶段可以进行更多的优化程序探索。您的查询恰好符合能力最差的搜索阶段(阶段0)的要求。在该阶段发现的费用足够低,因此无需输入以后的阶段。

鉴于分配给UDF的基数估计值很小,很不幸,n元联接扩展试探法将其在树中的位置早于您希望的位置。

该查询还由于具有至少三个联接(包括适用)而符合搜索0优化的条件。通过看起来很奇怪的扫描,您获得的最终物理计划基于启发式推导的加入顺序。成本很低,以致优化器认为计划“足够好”。UDF的低成本估算和基数有助于此尽早完成。

搜索0(也称为事务处理阶段)针对低基数OLTP类型的查询,其最终计划通常具有嵌套循环联接。更重要的是,搜索0仅运行优化程序探索能力的一个相对较小的子集。此子集不包括将查询树上的应用拉到联接上(规则PullApplyOverJoin)。这恰恰是在测试用例中将UDF应用程序重新定位在联接上方,在操作序列中排在最后(按原样)时所需要的。

还有一个问题是,优化器可以在朴素的嵌套循环联接(联接本身上的联接谓词)和相关索引联接(应用)之间做出决定,在关联索引联接中,使用索引查找将相关谓词应用于联接的内侧。后者通常是所需的计划形状,但是优化程序可以同时探索两者。对于错误的成本核算和基数估计,它可以像提交的计划中那样选择不适用的NL联接(解释扫描)。

因此,存在涉及多个通用优化器功能的多种相互作用的原因,这些功能通常可以很好地在短时间内找到良好的计划而无需使用过多的资源。避免任何一种原因就足以为示例查询生成“预期的”计划形状,即使是空表也是如此:

在禁用搜索0的空表上计划

没有支持的方法来避免搜索0计划的选择,早期的优化程序终止或提高UDF的成本(除了为此而在SQL Server 2014 CE模型中的有限增强)。这就留下了诸如计划指南,手动查询重写(包括TOP (1)想法或使用中间临时表)之类的东西,并避免了非内联函数之类的成本低廉的“黑匣子”(从QO的角度来看)。

重写CROSS APPLYOUTER APPLY也可以工作,因为它目前阻止一些早期加入溃散的工作,但你必须要小心保留原始查询语义(如拒绝任何NULL可能被引入-Extended行,但不优化折叠回交叉申请)。但是,您需要注意,这种行为不能保证保持稳定,因此您需要记住,每次修补或升级SQL Server时都要重新测试任何此类观察到的行为。

总体而言,适合您的解决方案取决于我们无法为您判断的多种因素。但是,我鼓励您考虑可以在将来始终使用的解决方案,并尽可能与(而不是与)优化器一起使用。


24

看来这是优化程序基于成本的决定,但是这是一个非常糟糕的决定。

如果您向PRODUCT添加50000行,则优化器会认为扫描工作量太大,并为您提供了一个包含三个搜寻和一个UDF调用的计划。

我在PRODUCT中获得6655行的计划

在此处输入图片说明

在PRODUCT中有50000行,我得到了这个计划。

在此处输入图片说明

我认为调用UDF的成本被大大低估了。

在这种情况下,一种可行的解决方法是更改​​查询以对UDF使用外部应用。无论表PRODUCT中有多少行,我都能得到好的计划。

select  
    S.GROUPCODE,
    H.ORDERCATEGORY
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT    
    outer apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01' and
    S.GROUPCODE is not null

在此处输入图片说明

在这种情况下,最好的解决方法可能是将所需的值放入临时表中,然后使用对UDF的交叉应用查询临时表。这样,您可以确保不会执行不必​​要的UDF。

select  
    P.FACTORY,
    H.ORDERCATEGORY
into #T
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

select  
    S.GROUPCODE,
    T.ORDERCATEGORY
from #T as T
  cross apply dbo.GetGroupCode (T.FACTORY) S

drop table #T

可以使用top()派生表来强制SQL Server在调用UDF之前评估联接的结果,而不必坚持使用临时表。只需在顶部使用一个非常高的数字,SQL Server就必须继续计算该部分查询的行数,然后才能继续使用UDF。

select S.GROUPCODE,
       T.ORDERCATEGORY
from (
     select top(2147483647)
         P.FACTORY,
         H.ORDERCATEGORY
     from    
         ORDERLINE L
         join ORDERHDR H on H.ORDERID = L.ORDERID
         join PRODUCT P  on P.PRODUCT = L.PRODUCT    
     where   
         L.ORDERNUMBER = 'XXX/YYY-123456' and
         L.RMPHASE = '0' and
         L.ORDERLINE = '01'
     ) as T
  cross apply dbo.GetGroupCode (T.FACTORY) S

在此处输入图片说明

我想了解这可能是什么原因,因为所有操作都是使用主键完成的,如果它发生在无法轻松解决的更复杂的查询中,该如何解决。

我真的不能回答这个问题,但我认为我应该分享我所知道的。我完全不知道为什么要考虑扫描PRODUCT表。在某些情况下,这是最好的事情,而且还有一些关于优化器如何处理我不知道的UDF的东西。

另一个观察结果是,使用新的基数估计器,您的查询在SQL Server 2014中得到了很好的计划。这是因为对UDF的每次调用的估计行数是100,而不是SQL Server 2012及更高版本中的1。但是它仍将在计划的扫描版本和搜寻版本之间做出相同的基于成本的决策。如果PRODUCT中的行数少于500(在我的情况下为497),则即使在SQL Server 2014中,您也可以获得计划的扫描版本。


2
不知怎的让我想起了亚当Machanic的SQL在会话位的:sqlbits.com/Sessions/Event14/...
詹姆斯ž
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.