为什么执行计划包含用于持久保存的计算列的用户定义函数调用?

Kev*_*ock 18 performance calculated-columns sql-server-2008-r2 sql-execution-plan

我有一个包含2个计算列的表,两个都将"Is Persisted"设置为true.但是,在查询中使用它们时,执行计划会显示用于计算列的UDF作为计划的一部分.由于列数据是由UDF在添加/更新行时计算的,为什么计划会包含它?

当这些列包含在查询中时,查询速度非常慢(> 30s),当排除它们时,查询速度非常快(<1s).这使我得出结论,查询实际上是在运行时计算列值,这不应该是这种情况,因为它们被设置为持久化.

我在这里错过了什么吗?

更新:这里有关于我们使用计算列的推理的更多信息.

我们是一家体育公司,有一个客户将完整的球员名称存储在一个列中.他们要求我们允许他们分别按名字和/或姓氏搜索玩家数据.值得庆幸的是,他们使用一致的播放器名称格式 - LastName,FirstName(NickName) - 因此解析它们相对容易.我创建了一个UDF,调用CLR函数来使用正则表达式解析名称部分.因此,显然调用UDF(后者又调用CLR函数)非常昂贵.但由于它仅用于持久列,我认为它只会在我们将数据导入数据库的一天中使用几次.

小智 24

原因是查询优化器在计算用户定义的函数方面做得不好.在某些情况下,它决定完全重新评估每一行的功能会更便宜,而不是招致可能必要的磁盘读取.

SQL Server的成本计算模型不会检查函数的结构,看它实际上有多贵,因此优化器在这方面没有准确的信息.您的功能可能是任意复杂的,因此可能可以理解,成本计算受限于此方式.标量和多语句表值函数的效果最差,因为每行调用这些函数非常昂贵.

您可以通过检查查询计划来判断优化器是否已决定重新评估函数(而不是使用持久值).如果Compute Scalar迭代器在其Defined Values列表中显式引用了函数名,则每行调用一次该函数.如果"定义的值"列表引用了列名,则不会调用该函数.

我的建议通常是根本不使用计算列定义中的函数.

下面的复制脚本演示了该问题.请注意,为表定义的PRIMARY KEY是非聚簇的,因此获取持久值将需要从索引或表扫描中进行书签查找.优化器决定从索引读取函数的源列并重新计算每行的函数更便宜,而不是招致书签查找或表扫描的成本.

在这种情况下,索引持久列可以加快查询速度.通常,优化器倾向于支持避免重新计算函数的访问路径,但是该决策是基于成本的,因此即使在索引时仍然可以看到为每一行重新计算的函数.然而,为优化器提供"明显"且高效的访问路径确实有助于避免这种情况.

请注意,列不,必须坚持以被索引.这是一种非常常见的误解; 只有在不精确的情况下才需要持久化列(它使用浮点算术或值).在当前情况下保留列不会增加任何值并扩展基表的存储要求.

保罗怀特

-- An expensive scalar function
CREATE FUNCTION dbo.fn_Expensive(@n INTEGER)
RETURNS BIGINT 
WITH SCHEMABINDING
AS
BEGIN
    DECLARE @sum_n BIGINT;
    SET @sum_n = 0;

    WHILE @n > 0
    BEGIN
        SET @sum_n = @sum_n + @n;
        SET @n = @n - 1
    END;

    RETURN @sum_n;
END;
GO
-- A table that references the expensive
-- function in a PERSISTED computed column
CREATE TABLE dbo.Demo
(
    n       INTEGER PRIMARY KEY NONCLUSTERED,
    sum_n   AS dbo.fn_Expensive(n) PERSISTED
);
GO
-- Add 8000 rows to the table
-- with n from 1 to 8000 inclusive
WITH Numbers AS
(
    SELECT TOP (8000)
        n = ROW_NUMBER() OVER (ORDER BY (SELECT 0))
    FROM master.sys.columns AS C1
    CROSS JOIN master.sys.columns AS C2
    CROSS JOIN master.sys.columns AS C3
)
INSERT dbo.Demo (N.n)
SELECT
    N.n
FROM Numbers AS N
WHERE
    N.n >= 1
    AND N.n <= 5000
GO
-- This is slow
-- Plan includes a Compute Scalar with:
-- [dbo].[Demo].sum_n = Scalar Operator([[dbo].[fn_Expensive]([dbo].[Demo].[n]))
-- QO estimates calling the function is cheaper than the bookmark lookup
SELECT
    MAX(sum_n)
FROM dbo.Demo;
GO
-- Index the computed column
-- Notice the actual plan also calls the function for every row, and includes:
-- [dbo].[Demo].sum_n = Scalar Operator([[dbo].[fn_Expensive]([dbo].[Demo].[n]))
CREATE UNIQUE INDEX uq1 ON dbo.Demo (sum_n);
GO
-- Query now uses the index, and is fast
SELECT
    MAX(sum_n)
FROM dbo.Demo;
GO
-- Drop the index
DROP INDEX uq1 ON dbo.Demo;
GO
-- Don't persist the column
ALTER TABLE dbo.Demo
ALTER COLUMN sum_n DROP PERSISTED;
GO
-- Show again, as you would expect
-- QO has no option but to call the function for each row
SELECT
    MAX(sum_n)
FROM dbo.Demo;
GO
-- Index the non-persisted column
CREATE UNIQUE INDEX uq1 ON dbo.Demo (sum_n);
GO
-- Fast again
-- Persisting the column bought us nothing
-- and used extra space in the table
SELECT
    MAX(sum_n)
FROM dbo.Demo;
GO
-- Clean up
DROP TABLE dbo.Demo;
DROP FUNCTION dbo.fn_Expensive;
GO
Run Code Online (Sandbox Code Playgroud)