和表值函数连接引发的性能问题

原文:和表值函数连接引发的性能问题

    最近调优过程中遇到一个问题,就是表值函数作为连接中的一部分时,可能会引起麻烦,本文会简单阐述表值函数是什么,以及为什么使用表值函数进行连接时会引发性能问题。

表值函数

    SQL Server中提供了类似其他编程语言的函数,而函数的本质通常是一段代码的封装,并返回值。在SQL Server中,函数除了可以返回简单的数据类型之外(Int、Varchar等),还可以返回一个集合,也就是返回一个表。

    而根据是否直接返回集合或是定义后再返回集合,表值函数又分为内联用户定义表值函数和用户定义表值函数(下文统称为表值函数,省去“用户定义”四个字)。

 

内联表值函数

    内联表值函数和普通函数并无不同,唯一的区别是返回结果为集合(表),而不是简单数据类型,一个简单的内联表值函数如代码清单1所示(摘自MSDN)。

CREATE FUNCTION Sales.ufn_CustomerNamesInRegion
                 ( @Region nvarchar(50) )
RETURNS table
AS
RETURN (
        SELECT DISTINCT s.Name AS Store, a.City
        FROM Sales.Store AS s
        INNER JOIN Person.BusinessEntityAddress AS bea 
            ON bea.BusinessEntityID = s.BusinessEntityID 
        INNER JOIN Person.Address AS a 
            ON a.AddressID = bea.AddressID
        INNER JOIN Person.StateProvince AS sp 
            ON sp.StateProvinceID = a.StateProvinceID
        WHERE sp.Name = @Region
       );
GO

代码清单1.一个简单的表值函数

 

用户定义表值函数

    而用户定义表值函数,需要在函数开始时定义返回的表结构,然后可以写任何代码进行数据操作,插入到定义的表结构之后进行返回,一个稍微负责的用户定义表值函数示例如代码清单2所示(摘自MSDN)。

CREATE FUNCTION dbo.ufnGetContactInformation(@ContactID int)
RETURNS @retContactInformation TABLE 
(
    -- Columns returned by the function
    ContactID int PRIMARY KEY NOT NULL, 
    FirstName nvarchar(50) NULL, 
    LastName nvarchar(50) NULL, 
    JobTitle nvarchar(50) NULL, 
    ContactType nvarchar(50) NULL
)
AS 
-- Returns the first name, last name, job title, and contact type for the specified contact.
BEGIN
    DECLARE 
        @FirstName nvarchar(50), 
        @LastName nvarchar(50), 
        @JobTitle nvarchar(50), 
        @ContactType nvarchar(50);
    -- Get common contact information
    SELECT 
        @ContactID = BusinessEntityID, 
        @FirstName = FirstName, 
        @LastName = LastName
    FROM Person.Person 
    WHERE BusinessEntityID = @ContactID;
    -- Get contact job title
    SELECT @JobTitle = 
        CASE 
            -- Check for employee
            WHEN EXISTS(SELECT * FROM Person.Person AS p 
                        WHERE p.BusinessEntityID = @ContactID AND p.PersonType = 'EM') 
                THEN (SELECT JobTitle 
                      FROM HumanResources.Employee AS e
                      WHERE e.BusinessEntityID = @ContactID)
            -- Check for vendor
            WHEN EXISTS(SELECT * FROM Person.Person AS p 
                        WHERE p.BusinessEntityID = @ContactID AND p.PersonType = 'VC') 
                THEN (SELECT ct.Name 
                      FROM Person.ContactType AS ct 
                      INNER JOIN Person.BusinessEntityContact AS bec 
                          ON bec.ContactTypeID = ct.ContactTypeID  
                      WHERE bec.PersonID = @ContactID)
 
            -- Check for store
            WHEN EXISTS(SELECT * FROM Person.Person AS p 
                        WHERE p.BusinessEntityID = @ContactID AND p.PersonType = 'SC') 
                THEN (SELECT ct.Name 
                      FROM Person.ContactType AS ct 
                      INNER JOIN Person.BusinessEntityContact AS bec 
                          ON bec.ContactTypeID = ct.ContactTypeID  
                      WHERE bec.PersonID = @ContactID)
            ELSE NULL 
        END;
    -- Get contact type
    SET @ContactType = 
        CASE 
            -- Check for employee
            WHEN EXISTS(SELECT * FROM Person.Person AS p 
                        WHERE p.BusinessEntityID = @ContactID AND p.PersonType = 'EM') 
            THEN 'Employee'
            -- Check for vendor
            WHEN EXISTS(SELECT * FROM Person.Person AS p 
                        WHERE p.BusinessEntityID = @ContactID AND p.PersonType = 'VC')
            THEN 'Vendor Contact'
            -- Check for store
            WHEN EXISTS(SELECT * FROM Person.Person AS p 
                        WHERE p.BusinessEntityID = @ContactID AND p.PersonType = 'SC')
            THEN 'Store Contact'
            -- Check for individual consumer
            WHEN EXISTS(SELECT * FROM Person.Person AS p 
                        WHERE p.BusinessEntityID = @ContactID AND p.PersonType = 'IN') 
            THEN 'Consumer'
             -- Check for general contact
            WHEN EXISTS(SELECT * FROM Person.Person AS p 
                        WHERE p.BusinessEntityID = @ContactID AND p.PersonType = 'GC') 
            THEN 'General Contact'
        END;
    -- Return the information to the caller
    IF @ContactID IS NOT NULL 
    BEGIN
        INSERT @retContactInformation
        SELECT @ContactID, @FirstName, @LastName, @JobTitle, @ContactType;
    END;
    RETURN;
END;
GO

代码订单2.表值函数

为什么要用表值函数

    看起来表值函数所做的事情和存储过程并无不同,但实际上还是有所差别。是因为表值函数可以被用于写入其他查询,而存储过程不行。此外,表值函数和Apply操作符联合使用可以极大的简化连接操作。

    如果存储过程符合下述条件的其中一个,可以考虑重写为表值函数。

  • 存储过程逻辑非常简单,仅仅是一个Select语句,不用视图的原因仅仅是由于需要参数。
  • 存储过程中没有更新操作。
  • 存储过程中没有动态SQL。
  • 存储过程中只返回一个结果集。
  • 存储过程的主要目的是为了产生临时结果集,并将结果集存入临时表以供其他查询调用。

 

用户定义表值函数的问题

    表值函数与内联表值函数不同,内联表值函数在处理的过程中更像是一个视图,这意味着在查询优化阶段,内联表值函数可以参与查询优化器的优化,比如将筛选条件(Where)推到代数树的底部,这意味着可以先Where再Join,从而可以利用索引查找降低IO从而提升性能。

    让我们来看一个简单的例子。下面代码示例是一个简单的和表值函数做Join的例子:

    首先我们创建表值函数,分别为内联表值函数方式和表值函数方式,如代码清单3所示。

--创建表值行数
CREATE FUNCTION tvf_multi_Test ( )
RETURNS @SaleDetail TABLE ( ProductId INT )
AS
    BEGIN 
        INSERT  INTO @SaleDetail
                SELECT  ProductID
                FROM    Sales.SalesOrderHeader soh
                        INNER JOIN Sales.SalesOrderDetail sod ON soh.SalesOrderID = sod.SalesOrderID 
        RETURN 
    END
--创建内联表值函数
CREATE FUNCTION tvf_inline_Test ( )
RETURNS TABLE
AS
   RETURN
    SELECT  ProductID
    FROM    Sales.SalesOrderHeader soh
            INNER JOIN Sales.SalesOrderDetail sod ON soh.SalesOrderID = sod.SalesOrderID 

代码清单3.创建两种不同的函数

 

    现在,我们使用相同的查询,对这两个表值函数进行Join,代码如代码清单4所示。

--表值函数做Join
SELECT  c.personid ,
        Prod.Name ,
        COUNT(*) 'numer of unit'
FROM    Person.BusinessEntityContact c
        INNER JOIN dbo.tvf_multi_Test() tst ON c.personid = tst.ProductId
        INNER JOIN Production.Product prod ON tst.ProductId = prod.ProductID
GROUP BY c.personid ,
        Prod.Name 
 
--内联表值函数做Join
SELECT  c.personid ,
        Prod.Name ,
        COUNT(*) 'numer of unit'
FROM    Person.BusinessEntityContact c
        INNER JOIN dbo.tvf_inline_Test() tst ON c.personid = tst.ProductId
        INNER JOIN Production.Product prod ON tst.ProductId = prod.ProductID
GROUP BY c.personid ,
        Prod.Name 

代码清单4.表值函数和内联表值函数做Join

 

    执行的成本如图1所示。

图1.两种方式的成本

 

    从IO来看,很明显是选择了次优的执行计划,BusinessEntityContact选择了121317次查找,而不是一次扫描。而内联表函数能够正确知道扫描一次的成本远低于一次查找。

    那问题的根源是内联表值函数,对于SQL Server来说,和视图是一样的,这意味着内联表值函数可以参与到逻辑执行计划的代数运算(或者是代数树优化)中,这意味着内敛表可以进一步拆分(如图1所示,第二个内联表的查询,执行计划具体知道内敛表中是SalesOrderHeader表和SalesOrderDetail表,由于查询只选择了一列,所以执行计划优化直到可以无需扫描SalesOrderHeader表),对于内联表值函数来说,执行计划可以完整知道所涉及的表上的索引以及相关统计信息等元数据。

    另一方面,表值函数,如图1的第一部分所示,表值函数对整个执行计划来说是一个黑箱子,既不知道统计信息,也没有索引。执行计划中不知道表值函数所涉及的表(图1中为#AE4E5168这个临时表,而不是具体的表明),因此对整个执行计划来说该结果集SQL Server会假设返回的结果非常小,当表值函数返回的结果较多时(如本例所示),则会产生比较差的执行计划。

    因此综上所述,在表值函数返回结果极小时,对性能可能没有影响,但返回结果如果略多,则一定会影响执行计划的质量。

 

如何处理

    首先,在SQL Server中,我们要找出现存的和表值函数做Join的语句,通过挖掘执行计划,我们可以找出该类语句,使用的代码如代码清单5所示。

WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p)
SELECT  st.text,
        qp.query_plan
FROM    (
    SELECT  TOP 50 *
    FROM    sys.dm_exec_query_stats
    ORDER BY total_worker_time DESC
) AS qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) AS qp
WHERE qp.query_plan.exist('//p:RelOp[contains(@LogicalOp, "Join")]/*/p:RelOp[(@LogicalOp[.="Table-valued function"])]') = 1

代码清单5.从执行计划缓存中找出和表值函数做Join的查询

 

   结果如图2所示。

图2.执行计划缓存中已经存在的和表值函数做Join的查询

 

小结

    本文阐述了表值函数的概念,表值函数为何会影响性能以及在执行计划缓存中找出和表值函数做Join的查询。对于和表值函数做Apply或表值函数返回的行数非常小的查询,或许并不影响。但对于返回结果较多的表值函数做Join,则可能产生性能问题,因此如果有可能,把表值函数重写为内联表值函数或将表值函数的结果存入临时表再进行Join可提升性能。

 

参考资料:

http://www.brentozar.com/blitzcache/tvf-join/

http://blogs.msdn.com/b/psssql/archive/2010/10/28/query-performance-and-multi-statement-table-valued-functions.aspx?CommentPosted=true#commentmessage

时间: 2024-09-17 04:50:34

和表值函数连接引发的性能问题的相关文章

和表值函数连接引发的性能问题分析

 表值函数     SQL Server中提供了类似其他编程语言的函数,而函数的本质通常是一段代码的封装,并返回值.在SQL Server中,函数除了可以返回简单的数据类型之外(Int.Varchar等),还可以返回一个集合,也就是返回一个表.     而根据是否直接返回集合或是定义后再返回集合,表值函数又分为内联用户定义表值函数和用户定义表值函数(下文统称为表值函数,省去"用户定义"四个字). 内联表值函数     内联表值函数和普通函数并无不同,唯一的区别是返回结果为集合(表),而

在 SQL Server 2005 中使用表值函数来实现空间数据库

server|函数|数据|数据库 Gyorgy Fekete 和 Alex Szalay约翰霍普金丝大学 Jim GrayMicrosoft(联系作者) 适用于Microsoft SQL Server 2005 摘要:本文说明了如何使用 C# 和表值函数将空间搜索函数("邻近点的点"和"多边形内的点")添加到 Microsoft SQL Server 2005.使用此库可以在不编写任何特殊代码的情况下向应用程序中添加空间搜索.此库实现了来自约翰霍普金丝大学的公共域

RDS for MySQL 字符序(collation)引发的性能问题

在帮客户排查问题的时候,经常会遇到的 RDS 实例性能问题(比如 RDS 实例 CPU 使用率高),而其中有一类是由于字符集的字符排序规则不一致导致的.从处理的过程中可以看出来,这类问题比较容易出现但不容易定位排查,所以今天通过两个实战案例来分析的下"RDS for MySQL 字符序(collation)引发的性能问题". 首先介绍下背景知识: 字符集 和 字符序. 1. 字符集(character set)和字符序(collation) 字符集是一组符号和编码,用来保存和解释 My

sql server 2005中使用apply对每行调用表值函数

server|函数 使用 APPLY 运算符可以为实现查询操作的外部表表达式返回的每个行调用表值函数.表值函数作为右输入,外部表表达式作为左输入.通过对右输入求值来获得左输入每一行的计算结果,生成的行被组合起来作为最终输出.APPLY 运算符生成的列的列表是左输入中的列集,后跟右输入返回的列的列表. <?XML:NAMESPACE PREFIX = O /> APPLY 有两种形式: CROSS APPLY 和 OUTER APPLY.CROSS APPLY 仅返回外部表中通过表值函数生成结果

如何使用SQL CLR表值函数进行扩展

Microsoft SQL Server 2005 的一项新增功能是其与 Microsoft .NET Framework 公共语言运行库 (CLR) 的集成.这使得人们能够将 .NET Framework 类和函数纳入 Transact-SQL 语句和查询. CLR 集成的机制有多种: • CLR 用户定义函数(包括表值函数). • CLR 用户定义类型. • CLR 存储过程. • CLR 触发器. 本白皮书说明如何使用 CLR 表值函数根据包括数据库在内的各种源创建报表数据,从而创建可靠的

JavaScript通过join函数连接数组里所有元素的方法

 这篇文章主要介绍了JavaScript通过join函数连接数组里所有元素的方法,实例分析了javascript中join函数的使用技巧,具有一定参考借鉴价值,需要的朋友可以参考下     本文实例讲述了JavaScript通过join函数连接数组里所有元素的方法.分享给大家供大家参考.具体实现方法如下: ? 1 2 3 4 5 <script type="text/javascript"> var days = ["Sunday","Mond

拆分字符串的表值函数

 1--拆分字符串的表值函数 2 3alter Function f_Split 4( 5    @Str Nvarchar(max) 6)Returns @Re Table 7( 8    Id int Identity, Val Nvarchar(max) 9)10As11Begin12    Declare @Pos Int13    Set @Pos = CharIndex(N',', @Str)14    While @Pos > 015    Begin16        Inser

c-在ubuntu下如何通过C/C++/QT函数连接至指定的无线网络?

问题描述 在ubuntu下如何通过C/C++/QT函数连接至指定的无线网络? 在ubuntu的qt编程环境下,如何连接至已知名字与密码的无线网络?

表值函数与JS中split()的联系

在公司用云平台做开发就是麻烦 ,做了很多功能或者有些收获,都没办法写博客,结果回家了自己要把大脑里面记住的写出来. split()这个函数我们并不陌生,但是当前台有许多字段然后随意勾选后的这些参数传递到后台做处理的时候却麻烦了,我们这个时候需要把这些当字符串传递到存储过程,在存储过程里面将这些字符串分割成一个个单独的个体,我这里不说数组,是因为存储过程没有数组这一说. 这时候我们就会想到表值函数.表值函数返回的是一个Table类型的表.说到这里我想很多人都想到了,这不就是一个数组形式么?一个表就