将表 UDT 传递给表函数时强制执行正确的执行计划

GSe*_*erg 1 performance sql-server execution-plan sql-server-2012 table-valued-parameters query-performance

我有一个标量函数,它返回一个大的 XML,它是通过一堆发票创建的。

可以使用几种不同的方法计算要提供给函数的确切发票列表,但函数每次都是相同的。出于这个原因,我声明了一个用户定义的表类型来包含表中的主键eInvoice.Header并将其传递给函数。这样我就可以有几个不同的函数来决定要处理哪些发票,并且只有一个函数可以实际生成 XML:

create function eInvoice.GetRelevantLinesInOneWay()
returns table ...

create function eInvoice.GetRelevantLinesInAnotherWay()
returns table ...

create function eInvoice.GetXML(@lines eInvoice.InvoicePrimaryKeys readonly)
returns xml
as
begin
    declare @x xml;

    with xmlnamespaces(N'important namespace' as pro)
    select @x = (
        select
            ...
        from
            eInvoice.Header h
            inner join @lines l on h.ST_PRIMARY = l.invoice_row_id
        for xml path(N'pro:Import'), type
    );

    return @x;
end;
Run Code Online (Sandbox Code Playgroud)

不幸的是,这种设置已被证明是非常脆弱的。

通常@lines包含大约 150 行(大约1min eInvoce.Header)。正确的执行计划是在 上使用索引查找ST_PRIMARY,当我将 的主体eInvoice.GetXML作为临时查询执行时,总是会发生这种情况。

然而,当我将它存储为一个函数时,它会按预期工作一段时间,然后发生了一些事情(太多行@lines,比如大约300?),它决定将执行计划更改为完整扫描eInvoice.Header保持它方式

用seek plan功能立即执行,用scan plan不知道要花多少时间,等了30分钟就取消了。

我尝试了各种方法来强制执行该函数的搜索计划。

  • 当我单击“显示估计的执行计划”时,添加with (forceseek)aftereInvoce.Header似乎工作,但当我实际执行它时,提示被忽略并执行扫描。
  • 添加option (recompile)函数内部还是外部,在调用代码,似乎没有产生效果。
  • 捕获执行计划并将其硬编码在函数option (use plan N'<plan>')中不起作用,因为@lines在其唯一的列上包含一个主键,并且该索引的名称对于 的每个实例都不同@lines,但执行计划必须通过固定名称引用索引.
  • 我尝试删除缓存的执行计划并多次重新编译该函数。它有时在非常有限的时间内有帮助,有时没有效果。

有没有办法强制索引查找?

Pau*_*ite 6

概括

该问题不提供执行计划或完整的复制脚本,但根据所提供的信息,您应该使用FAST 1提示。如果可以,您还应该考虑将标量函数转换为内联表值类型。

基于 AdventureWorks 的示例

此处下载 Microsoft 示例数据库。

表型

CREATE TYPE dbo.PrimaryKeys AS TABLE
(
    PK integer PRIMARY KEY
);
Run Code Online (Sandbox Code Playgroud)

标量函数

您可以尝试各种组合INNER LOOP JOIN(将写入的连接顺序颠倒,并FORCE ORDER添加以抑制警告),FORCESEEK(index_name(columns))(没有模式绑定)甚至QUERYTRACEON(8690)防止性能假脱机;但根据我的经验,这FAST 1是获得正确计划的最可靠方法,而且也更简单。

CREATE FUNCTION dbo.GetXML
(
    @Lines dbo.PrimaryKeys READONLY
)
RETURNS xml
WITH SCHEMABINDING -- If possible
AS
BEGIN
    DECLARE @x xml;

    WITH XMLNAMESPACES (N'important namespace' AS pro)
    SELECT @x =
    (
        SELECT
            SOH.SalesOrderID,
            SOH.OrderDate,
            SOH.DueDate,
            SOH.ShipDate,
            SOH.[Status],
            SOH.PurchaseOrderNumber,
            SOH.AccountNumber,
            SOH.CustomerID,
            SOH.TotalDue
        FROM @Lines AS L
        JOIN Sales.SalesOrderHeader AS SOH
            ON SOH.SalesOrderID = L.PK
        ORDER BY
            SOH.SalesOrderID
        FOR XML PATH(N'pro:Import'), TYPE
    )
    OPTION (FAST 1);

    RETURN @x;
END;
Run Code Online (Sandbox Code Playgroud)

除了避免标量函数的所有常见原因外,它们确实使计划分析变得更加困难。首先,您需要使用 Profiler 或扩展事件捕获实际计划 - 它们不会出现在 SSMS 等查询工具中。其次,并非所有提示都如您所愿,例如RECOMPILE由于范围界定和单独的计划。

DECLARE @Lines dbo.PrimaryKeys;

INSERT @Lines (PK)
SELECT TOP (300) SOH.SalesOrderID
FROM Sales.SalesOrderHeader AS SOH;

SELECT dbo.GetXML(@Lines);
Run Code Online (Sandbox Code Playgroud)

无论表变量中的行数如何,上面的代码都能可靠地为我生成所需的计划。

没有FAST 1函数中的提示,优化器选择合并连接并扫描两个表。该计划被缓存并重用于稍后的函数执行,其中TOP (300)更改为TOP (1),重现问题的核心。

内联表值函数

除非有绝对令人信服的理由坚持使用标量函数,否则您应该将其重写为内联表值形式,如下所示:

CREATE FUNCTION dbo.GetXMLTable
(
    @Lines dbo.PrimaryKeys READONLY
)
RETURNS TABLE
AS
RETURN
    WITH XMLNAMESPACES (N'important namespace' AS pro)
    SELECT
        X.xml_result 
    FROM 
    (
        SELECT
            SOH.SalesOrderID,
            SOH.OrderDate,
            SOH.DueDate,
            SOH.ShipDate,
            SOH.[Status],
            SOH.PurchaseOrderNumber,
            SOH.AccountNumber,
            SOH.CustomerID,
            SOH.TotalDue
        FROM @Lines AS L
        CROSS APPLY
        (
            SELECT TOP (1) SOH.*
            FROM Sales.SalesOrderHeader AS SOH
                WITH (FORCESEEK(PK_SalesOrderHeader_SalesOrderID(SalesOrderID)))
            WHERE
                SOH.SalesOrderID = L.PK
            ORDER BY
                SOH.SalesOrderID
        ) AS SOH
        ORDER BY
            SOH.SalesOrderID
        FOR XML PATH(N'pro:Import'), TYPE
    ) AS X (xml_result);
Run Code Online (Sandbox Code Playgroud)

TOP (1)在交叉运用阻止了优化选择的任何其他比嵌套循环连接。该FORCESEEK提示是不是必需的; 我已经包含它以说明在必要时如何使用它。

DECLARE @Lines dbo.PrimaryKeys;

INSERT @Lines (PK)
SELECT TOP (300) SOH.SalesOrderID
FROM Sales.SalesOrderHeader AS SOH;

SELECT GXT.xml_result 
FROM dbo.GetXMLTable(@Lines) AS GXT;
Run Code Online (Sandbox Code Playgroud)

实际的执行计划(不使用 Profiler/XE)是:

iTVF 执行计划