和表值函數(shù)連接引發(fā)的性能問題分析
表值函數(shù)
SQL Server中提供了類似其他編程語言的函數(shù),而函數(shù)的本質(zhì)通常是一段代碼的封裝,并返回值。在SQL Server中,函數(shù)除了可以返回簡單的數(shù)據(jù)類型之外(Int、Varchar等),還可以返回一個(gè)集合,也就是返回一個(gè)表。
而根據(jù)是否直接返回集合或是定義后再返回集合,表值函數(shù)又分為內(nèi)聯(lián)用戶定義表值函數(shù)和用戶定義表值函數(shù)(下文統(tǒng)稱為表值函數(shù),省去“用戶定義”四個(gè)字)。
內(nèi)聯(lián)表值函數(shù)
內(nèi)聯(lián)表值函數(shù)和普通函數(shù)并無不同,唯一的區(qū)別是返回結(jié)果為集合(表),而不是簡單數(shù)據(jù)類型,一個(gè)簡單的內(nèi)聯(lián)表值函數(shù)如代碼清單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.一個(gè)簡單的表值函數(shù)
用戶定義表值函數(shù)
而用戶定義表值函數(shù),需要在函數(shù)開始時(shí)定義返回的表結(jié)構(gòu),然后可以寫任何代碼進(jìn)行數(shù)據(jù)操作,插入到定義的表結(jié)構(gòu)之后進(jìn)行返回,一個(gè)稍微負(fù)責(zé)的用戶定義表值函數(shù)示例如代碼清單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.表值函數(shù)
為什么要用表值函數(shù)
看起來表值函數(shù)所做的事情和存儲(chǔ)過程并無不同,但實(shí)際上還是有所差別。是因?yàn)楸碇岛瘮?shù)可以被用于寫入其他查詢,而存儲(chǔ)過程不行。此外,表值函數(shù)和Apply操作符聯(lián)合使用可以極大的簡化連接操作。
如果存儲(chǔ)過程符合下述條件的其中一個(gè),可以考慮重寫為表值函數(shù)。
•存儲(chǔ)過程邏輯非常簡單,僅僅是一個(gè)Select語句,不用視圖的原因僅僅是由于需要參數(shù)。
•存儲(chǔ)過程中沒有更新操作。
•存儲(chǔ)過程中沒有動(dòng)態(tài)SQL。
•存儲(chǔ)過程中只返回一個(gè)結(jié)果集。
•存儲(chǔ)過程的主要目的是為了產(chǎn)生臨時(shí)結(jié)果集,并將結(jié)果集存入臨時(shí)表以供其他查詢調(diào)用。
用戶定義表值函數(shù)的問題
表值函數(shù)與內(nèi)聯(lián)表值函數(shù)不同,內(nèi)聯(lián)表值函數(shù)在處理的過程中更像是一個(gè)視圖,這意味著在查詢優(yōu)化階段,內(nèi)聯(lián)表值函數(shù)可以參與查詢優(yōu)化器的優(yōu)化,比如將篩選條件(Where)推到代數(shù)樹的底部,這意味著可以先Where再Join,從而可以利用索引查找降低IO從而提升性能。
讓我們來看一個(gè)簡單的例子。下面代碼示例是一個(gè)簡單的和表值函數(shù)做Join的例子:
首先我們創(chuàng)建表值函數(shù),分別為內(nèi)聯(lián)表值函數(shù)方式和表值函數(shù)方式,如代碼清單3所示。
--創(chuàng)建表值行數(shù) 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 --創(chuàng)建內(nèi)聯(lián)表值函數(shù) 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.創(chuàng)建兩種不同的函數(shù)
現(xiàn)在,我們使用相同的查詢,對(duì)這兩個(gè)表值函數(shù)進(jìn)行Join,代碼如代碼清單4所示。
--表值函數(shù)做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 --內(nèi)聯(lián)表值函數(shù)做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.表值函數(shù)和內(nèi)聯(lián)表值函數(shù)做Join
執(zhí)行的成本如圖1所示。
圖1.兩種方式的成本
從IO來看,很明顯是選擇了次優(yōu)的執(zhí)行計(jì)劃,BusinessEntityContact選擇了121317次查找,而不是一次掃描。而內(nèi)聯(lián)表函數(shù)能夠正確知道掃描一次的成本遠(yuǎn)低于一次查找。
那問題的根源是內(nèi)聯(lián)表值函數(shù),對(duì)于SQL Server來說,和視圖是一樣的,這意味著內(nèi)聯(lián)表值函數(shù)可以參與到邏輯執(zhí)行計(jì)劃的代數(shù)運(yùn)算(或者是代數(shù)樹優(yōu)化)中,這意味著內(nèi)斂表可以進(jìn)一步拆分(如圖1所示,第二個(gè)內(nèi)聯(lián)表的查詢,執(zhí)行計(jì)劃具體知道內(nèi)斂表中是SalesOrderHeader表和SalesOrderDetail表,由于查詢只選擇了一列,所以執(zhí)行計(jì)劃優(yōu)化直到可以無需掃描SalesOrderHeader表),對(duì)于內(nèi)聯(lián)表值函數(shù)來說,執(zhí)行計(jì)劃可以完整知道所涉及的表上的索引以及相關(guān)統(tǒng)計(jì)信息等元數(shù)據(jù)。
另一方面,表值函數(shù),如圖1的第一部分所示,表值函數(shù)對(duì)整個(gè)執(zhí)行計(jì)劃來說是一個(gè)黑箱子,既不知道統(tǒng)計(jì)信息,也沒有索引。執(zhí)行計(jì)劃中不知道表值函數(shù)所涉及的表(圖1中為#AE4E5168這個(gè)臨時(shí)表,而不是具體的表明),因此對(duì)整個(gè)執(zhí)行計(jì)劃來說該結(jié)果集SQL Server會(huì)假設(shè)返回的結(jié)果非常小,當(dāng)表值函數(shù)返回的結(jié)果較多時(shí)(如本例所示),則會(huì)產(chǎn)生比較差的執(zhí)行計(jì)劃。
因此綜上所述,在表值函數(shù)返回結(jié)果極小時(shí),對(duì)性能可能沒有影響,但返回結(jié)果如果略多,則一定會(huì)影響執(zhí)行計(jì)劃的質(zhì)量。
如何處理
首先,在SQL Server中,我們要找出現(xiàn)存的和表值函數(shù)做Join的語句,通過挖掘執(zhí)行計(jì)劃,我們可以找出該類語句,使用的代碼如代碼清單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.從執(zhí)行計(jì)劃緩存中找出和表值函數(shù)做Join的查詢
結(jié)果如圖2所示。
圖2.執(zhí)行計(jì)劃緩存中已經(jīng)存在的和表值函數(shù)做Join的查詢
小結(jié)
本文闡述了表值函數(shù)的概念,表值函數(shù)為何會(huì)影響性能以及在執(zhí)行計(jì)劃緩存中找出和表值函數(shù)做Join的查詢。對(duì)于和表值函數(shù)做Apply或表值函數(shù)返回的行數(shù)非常小的查詢,或許并不影響。但對(duì)于返回結(jié)果較多的表值函數(shù)做Join,則可能產(chǎn)生性能問題,因此如果有可能,把表值函數(shù)重寫為內(nèi)聯(lián)表值函數(shù)或?qū)⒈碇岛瘮?shù)的結(jié)果存入臨時(shí)表再進(jìn)行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
相關(guān)文章
MSSQL中進(jìn)行SQL除法運(yùn)算結(jié)果為小數(shù)卻顯示0的解決方法
這篇文章主要介紹了MSSQL中進(jìn)行SQL除法運(yùn)算結(jié)果為小數(shù)卻顯示0的解決方法,需要的朋友可以參考下2015-10-10sqlserver清空service broker中的隊(duì)列的語句分享
在我們開發(fā)service broker應(yīng)用時(shí)候,可能用于測試或者客戶端沒有配置正確等導(dǎo)致服務(wù)端隊(duì)列存在很多垃圾隊(duì)列,不便于我們排查錯(cuò)誤,我們可以使用SQL腳本來清空服務(wù)端這些垃圾數(shù)據(jù)2011-08-08SQL Server誤區(qū)30日談 第23天 有關(guān)鎖升級(jí)的誤區(qū)
在SQL Server 2005和之前的版本,頁鎖會(huì)直接升級(jí)到表鎖。在SQL Server 2005或SQL Server 2008,你可以通過如下跟蹤標(biāo)志改變鎖升級(jí)的行為2013-01-01MSSQL 多字段根據(jù)范圍求最大值實(shí)現(xiàn)方法
MSSQL 多字段根據(jù)范圍求最大值實(shí)現(xiàn)語句,大家可以參考下2009-09-09sql server把退款總金額拆分到盡量少的多個(gè)訂單中詳解
這篇文章主要給大家介紹了關(guān)于sql server把退款總金額拆分到盡量少的多個(gè)訂單中的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12