純量 UDF 內嵌

適用於:SQL Server 2019 (15.x)Azure SQL DatabaseAzure SQL 受控執行個體

本文介紹純量 UDF 內嵌,這是智慧型查詢處理功能套件下的一項功能。 此功能改善了在 SQL Server (從 SQL Server 2019 (15.x) 開始) 中叫用純量 UDF 的查詢效能。

T-SQL 純量使用者定義的函式

以 Transact-SQL 實作並傳回單一資料值的使用者定義函式 (UDF),就是所謂的 T-SQL 純量使用者定義函式。 T-SQL UDF 是一種在 Transact-SQL 查詢之間實現程式碼重複使用和模組化的完美方式。 某些計算 (例如複雜的商務規則) 更容易以命令式 UDF 格式表達。 UDF 有助於建置複雜的邏輯,而不需要撰寫複雜 SQL 查詢的專業知識。 如需 UDF 的詳細資訊,請參閱建立使用者定義函式 (資料庫引擎)

純量 UDF 的效能

純量 UDF 最終效能不佳通常是下列原因所致:

  • 反覆叫用。 UDF 會以反覆方式叫用,每個合格的元組一次。 這會因函式叫用而產生重複內容切換的額外成本。 特別是,在其定義中執行 Transact-SQL 查詢的 UDF 會嚴重受到影響。

  • 缺少成本估算。 在最佳化期間,只會估算關係運算子的成本,而不會估算純量運算子的成本。 在引入純量 UDF 之前,其他純量運算子通常很便宜,而不需要進行成本估算。 為純量運算子新增少量 CPU 成本便已足夠。 但還是有一些實際成本很高,卻仍然未充分表示的情況。

  • 解譯執行。 UDF 會評估為陳述式批次,逐一執行陳述式。 系統會編譯每個陳述式本身,並快取已編譯的計劃。 雖然此快取策略可因避免重新編譯而節省一些時間,但每個陳述式都會單獨執行。 不會執行跨陳述式的最佳化。

  • 序列執行。 SQL Server 不允許在叫用 UDF 的查詢中使用內部查詢平行處理原則。

自動內嵌純量 UDF

純量 UDF 內嵌功能的目標是改善叫用 T-SQL 純量 UDF 的查詢效能,其中的 UDF 執行是主要瓶頸。

使用這項新功能,純量 UDF 會自動轉換成純量運算式或純量子查詢,以在呼叫查詢中替代 UDF 運算子。 接著,系統就會將這些運算式和子查詢最佳化。 因此,查詢計劃不再具有使用者定義函式運算子,但在計劃中將觀察到其效果,例如檢視或內嵌 TVF。

範例 1 - 單一陳述式純量 UDF

請考慮下列查詢。

SELECT L_SHIPDATE, O_SHIPPRIORITY, SUM (L_EXTENDEDPRICE *(1 - L_DISCOUNT))
FROM LINEITEM
INNER JOIN ORDERS
  ON O_ORDERKEY = L_ORDERKEY
GROUP BY L_SHIPDATE, O_SHIPPRIORITY ORDER BY L_SHIPDATE;

此查詢會計算明細項目折扣價格的總和,並顯示依據送貨日期和送貨優先順序分組的結果。 運算式 L_EXTENDEDPRICE *(1 - L_DISCOUNT) 是指定明細項目折扣價格的公式。 這類公式可以擷取至函式,以方便模組化和重複使用。

CREATE FUNCTION dbo.discount_price(@price DECIMAL(12,2), @discount DECIMAL(12,2))
RETURNS DECIMAL (12,2) AS
BEGIN
  RETURN @price * (1 - @discount);
END

現在,您可以修改查詢來叫用此 UDF。

SELECT L_SHIPDATE, O_SHIPPRIORITY, SUM (dbo.discount_price(L_EXTENDEDPRICE, L_DISCOUNT))
FROM LINEITEM
INNER JOIN ORDERS
  ON O_ORDERKEY = L_ORDERKEY
GROUP BY L_SHIPDATE, O_SHIPPRIORITY ORDER BY L_SHIPDATE

根據先前所述的原因,具有 UDF 的查詢執行狀況不佳。 現在,透過純量 UDF 內嵌,您可以在查詢中直接替代 UDF 主體中的純量運算式。 執行此查詢的結果會顯示在下表中:

查詢: 沒有 UDF 的查詢 具有 UDF 的查詢 (不使用內嵌) 使用純量 UDF 內嵌的查詢
執行時間: 1.6 秒 29 分 11 秒 1.6 秒

這些數字是根據 10-GB CCI 資料庫 (使用 TPC-H 結構描述),其在具有雙處理器 (12 個核心)、96 GB RAM 且 SSD 支援的機器上執行。 這些數字包含冷程序快取和緩衝集區的編譯及執行時間。 使用了預設設定,但未建立任何其他的索引。

範例 2 - 多重陳述式純量 UDF

使用多個 T-SQL 陳述式 (例如變數指派和條件式分支) 實作的純量 UDF 也可以進行內嵌。 請考慮下列純量 UDF,其可根據客戶索引鍵來判斷該客戶的服務類別。 它會先使用 SQL 查詢計算客戶所下全部訂單的總價,以到達類別。 然後,使用 IF (...) ELSE 邏輯根據總價決定類別。

CREATE OR ALTER FUNCTION dbo.customer_category(@ckey INT)
RETURNS CHAR(10) AS
BEGIN
  DECLARE @total_price DECIMAL(18,2);
  DECLARE @category CHAR(10);

  SELECT @total_price = SUM(O_TOTALPRICE) FROM ORDERS WHERE O_CUSTKEY = @ckey;

  IF @total_price < 500000
    SET @category = 'REGULAR';
  ELSE IF @total_price < 1000000
    SET @category = 'GOLD';
  ELSE
    SET @category = 'PLATINUM';

  RETURN @category;
END

現在,請考慮叫用此 UDF 的查詢。

SELECT C_NAME, dbo.customer_category(C_CUSTKEY) FROM CUSTOMER;

SQL Server 2017 (14.x)(相容性層級 140 及更早版本) 中此查詢的執行計劃如下:

Query Plan without inlining.

如計劃所示,SQL Server 在此採用一個簡單的策略:針對 CUSTOMER 資料表中的每個元組,叫用 UDF 並輸出結果。 此策略過於簡易且效率不彰。 透過內嵌,這類 UDF 可轉換成相等的純量子查詢,這些子查詢會在呼叫查詢中替代 UDF。

針對相同查詢,內嵌 UDF 的計劃如下所示。

Query Plan with inlining.

如先前所述,查詢計劃不再具有使用者定義函式運算子,但目前在計劃中可觀察到其效果,例如檢視或內嵌 TVF。 以下是上述計劃的一些重要觀察結果:

  • SQL Server 已推斷 CUSTOMERORDERS 之間的隱含聯結,並透過聯結運算子使其變成明確聯結。
  • SQL Server 還會推斷隱含的 GROUP BY O_CUSTKEY on ORDERS,並使用 IndexSpool + StreamAggregate 來進行實作。
  • SQL Server 現在正在所有的運算子之間使用平行處理原則。

視 UDF 的邏輯複雜度而定,產生的查詢計劃也可能會變得更大且更複雜。 如我們所見,UDF 內的作業現在不再是不透明的,因此查詢最佳化工具能夠估算那些作業的成本,並將其最佳化。 此外,因為計劃中不再有 UDF,所以反覆執行 UDF 叫用會取代為完全避免函式呼叫額外負荷的計劃。

可內嵌的純量 UDF 需求

如果符合下列所有條件,則可內嵌純量 T-SQL UDF:

  • UDF 使用下列建構函式撰寫:
    • DECLARESET:變數宣告和指派。
    • SELECT:包含單一/多個變數指派的 SQL 查詢 1
    • IF/ELSE:使用任意巢狀層級的分支。
    • RETURN:單一或多個傳回陳述式。 從 SQL Server 2019 (15.x) CU5 開始,UDF 只能包含要考慮用於內嵌 6 的單一 RETURN 陳述式。
    • UDF:巢狀/遞迴函式呼叫 2
    • 其他:EXISTSIS NULL 等關聯式作業。
  • UDF 不會叫用任何與時間相依 (例如 GETDATE()) 或有副作用 3 (例如 NEWSEQUENTIALID()) 的內建函式。
  • UDF 會使用 EXECUTE AS CALLER 子句 (如果未指定 EXECUTE AS 子句,則為預設行為)。
  • UDF 不會參考資料表變數或資料表值參數。
  • 叫用純量 UDF 的查詢不會在其 GROUP BY 子句中參考純量 UDF 呼叫。
  • 在其選取清單中搭配 DISTINCT 子句叫用純量 UDF 的查詢不會有 ORDER BY 子句。
  • UDF 不會在 ORDER BY 子句中使用。
  • 不會以原生方式編譯 UDF (支援 Interop)。
  • UDF 不會用於計算資料行或檢查條件約束定義。
  • UDF 不會參考使用者定義型別。
  • UDF 中沒有新增任何簽章。
  • UDF 不是資料分割函式。
  • UDF 不包含對通用資料表運算式 (CTE) 的參考。
  • UDF 不包含內建函式的參考,這些函式可能會在內嵌時改變結果 (例如 @@ROWCOUNT)4
  • UDF 不包含當作參數傳遞至純量 UDF 的彙總函式 4
  • UDF 不會參考內建檢視 (例如 OBJECT_ID)4
  • UDF 不會參考 XML 方法 5
  • UDF 不包含使用 ORDER BY 的 SELECT 而沒有 TOP 1 子句 5
  • UDF 不包含具有 ORDER BY 子句執行指派的 SELECT 查詢 (例如 SELECT @x = @x + 1 FROM table1 ORDER BY col1)5
  • UDF 不包含多個 RETURN 陳述式 6
  • 不會從 RETURN 陳述式呼叫 UDF 6
  • UDF 不會參考 STRING_AGG 函式 6
  • UDF 不會參考遠端資料表 7
  • UDF 呼叫查詢不會使用 GROUPING SETSCUBEROLLUP7
  • UDF 呼叫查詢不包含作為指派之 UDF 參數使用的變數 (例如,SELECT @y = 2@x = UDF(@y))7
  • UDF 不會參考加密的資料行 8
  • UDF 不包含 WITH XMLNAMESPACES 的參考 8
  • 叫用 UDF 的查詢沒有通用資料表運算式 (CTE)8

1 內嵌不支援具有變數累積/彙總的 SELECT(例如 SELECT @val += col1 FROM table1)。

2 遞迴 UDF 只能內嵌到特定深度。

3 其結果取決於目前系統時間的內建函式具有時間相依性。 可能會更新某個內部全域狀態之內建函式為具有副作用的函式範例。 這類函式會在每次呼叫時,根據內部狀態傳回不同的結果。

4 SQL Server 2019 (15.x) CU2 中新增的限制

5 SQL Server 2019 (15.x) CU4 中新增的限制

6 SQL Server 2019 (15.x) CU5 中新增的限制

7 SQL Server 2019 (15.x) CU6 中新增的限制

8 SQL Server 2019 (15.x) CU11 中新增的限制

如需最新的 T-SQL 純量 UDF 內嵌修正和內嵌資格案例變更的資訊,請參閱知識庫文章:修正:SQL Server 2019 中的純量 UDF 內嵌問題 \(機器翻譯\)。

檢查是否可以內嵌 UDF

針對每個 T-SQL 純量 UDF,sys.sql_modules 目錄檢視包含一個稱為 is_inlineable 的屬性,其表示是否可內嵌 UDF。

is_inlineable 屬性衍生自 UDF 定義內找到的建構。 它不會在編譯時間檢查 UDF 是否可實際內嵌。 如需詳細資訊,請參閱內嵌條件

值 1 表示可內嵌,而 0 表示不可以。 對於所有內嵌 TVF,此屬性的值均為 1。 對於其他所有模組,值會是 0。

如果純量 UDF 可內嵌,並不表示它一律都會內嵌。 SQL Server 將決定 (在每個查詢上,根據每個 UDF) 是否要內嵌 UDF。 在 UDF 可能無法內嵌時的一些範例包括:

  • 如果 UDF 定義達到數千行程式碼時,SQL Server 可能會選擇不要加以內嵌。

  • 不會內嵌 GROUP BY 子句中的 UDF 叫用。 這項決定會在編譯參考純量 UDF 的查詢時進行。

  • 如果 UDF 是以憑證簽署。 因為可以在建立 UDF 之後新增及捨棄簽章,所以會在編譯參考純量 UDF 的查詢時決定是否要進行內嵌。 例如,系統函式通常會以憑證簽署。 您可以使用 sys. crypt_properties 尋找已簽署的物件。

    SELECT *
    FROM sys.crypt_properties AS cp
    INNER JOIN sys.objects AS o ON cp.major_id = o.object_id;
    

檢查是否已進行內嵌

如果符合所有先決條件,且 SQL Server 決定執行內嵌,則它會將 UDF 轉換成關聯運算式。 從查詢計劃中,很容易了解是否已進行內嵌:

  • 對於已成功內嵌的 UDF,計劃 XML 不會有 <UserDefinedFunction> XML 節點。
  • 會發出特定 XEvent。

啟用純量 UDF 內嵌

您可以啟用資料庫的相容性層級 150,讓工作負載自動符合純量 UDF 內嵌的資格。 您可以使用 Transact-SQL 設定此項目。 例如:

ALTER DATABASE [WideWorldImportersDW] SET COMPATIBILITY_LEVEL = 150;

除此之外,不需要對 UDF 或查詢進行任何其他變更,就能夠利用這項功能。

停用純量 UDF 內嵌而不變更相容性層級

您可以在資料庫、陳述式或 UDF 範圍內停用純量 UDF 內嵌,同時仍將資料庫相容性層級維持在 150 以上。 若要在資料庫範圍內停用純量 UDF 內嵌,請在適用資料庫的內容中執行下列陳述式:

ALTER DATABASE SCOPED CONFIGURATION SET TSQL_SCALAR_UDF_INLINING = OFF;

若要為資料庫重新啟用純量 UDF 內嵌,請在適用資料庫的內容中執行下列陳述式:

ALTER DATABASE SCOPED CONFIGURATION SET TSQL_SCALAR_UDF_INLINING = ON;

當設為 ON 時,此設定會在 sys.database_scoped_configurations 中顯示為已啟用。 您也可以將 DISABLE_TSQL_SCALAR_UDF_INLINING 指定為 USE HINT 查詢提示,以停用特定查詢的純量 UDF 內嵌。

USE HINT 查詢提示的優先順序高於資料庫範圍設定或相容性層級設定。

例如:

SELECT L_SHIPDATE, O_SHIPPRIORITY, SUM (dbo.discount_price(L_EXTENDEDPRICE, L_DISCOUNT))
FROM LINEITEM
INNER JOIN ORDERS
  ON O_ORDERKEY = L_ORDERKEY
GROUP BY L_SHIPDATE, O_SHIPPRIORITY ORDER BY L_SHIPDATE
OPTION (USE HINT('DISABLE_TSQL_SCALAR_UDF_INLINING'));

您也可以使用 CREATE FUNCTIONALTER FUNCTION 陳述式中的 INLINE 子句來停用特定 UDF 的純量 UDF 內嵌。 例如:

CREATE OR ALTER FUNCTION dbo.discount_price(@price DECIMAL(12,2), @discount DECIMAL(12,2))
RETURNS DECIMAL (12,2)
WITH INLINE = OFF
AS
BEGIN
    RETURN @price * (1 - @discount);
END;

一旦執行上述陳述式,絕對不會將此 UDF 內嵌至叫用它的任何查詢中。 若要重新啟用內嵌此 UDF 的功能,請執行下列陳述式:

CREATE OR ALTER FUNCTION dbo.discount_price(@price DECIMAL(12,2), @discount DECIMAL(12,2))
RETURNS DECIMAL (12,2)
WITH INLINE = ON
AS
BEGIN
    RETURN @price * (1 - @discount);
END

INLINE 子句不是必要的。 如果未指定 INLINE 子句,則它會根據是否可以內嵌 UDF 來自動設為 ON/OFF。 如果指定了 INLINE = ON,但發現 UDF 不適合進行內嵌,則會擲回錯誤。

重要事項

如本文中所述,純量 UDF 內嵌會將具有純量 UDF 的查詢轉換成具有對等純量子查詢的查詢。 由於此轉換之故,使用者可能會注意到下列案例中的一些行為差異:

  1. 內嵌會導致相同的查詢文字產生不同查詢雜湊。

  2. UDF 內先前可能已隱藏的陳述式特定警告 (例如除以零等等),可能會因內嵌而顯示出來。

  3. 查詢層級的聯結提示可能不再有效,因為內嵌可能會引進新的聯結。 必須改為使用本機聯結提示。

  4. 參考內嵌純量 UDF 的檢視無法編製索引。 如果您需要在這類檢視上建立索引,請停用所參考 UDF 的內嵌功能。

  5. 動態資料遮罩與內嵌 UDF 的行為可能有一些差異。

    在某些情況下 (視 UDF 中的邏輯而定),就遮罩輸出資料行而言,內嵌可能更為保守。 在 UDF 中所參考之資料行不是輸出資料行的情況下,它們不會被遮罩。

  6. 如果 UDF 參考內建函式 (例如 SCOPE_IDENTITY()@@ROWCOUNT@@ERROR),則內建函式所傳回的值會隨著內嵌而變更。 此行為變更是因為內嵌變更了陳述式在 UDF 內的範圍。 從 SQL Server 2019 (15.x) CU2 開始,如果 UDF 參考特定內建函式 (例如 @@ROWCOUNT),則會封鎖內嵌。

  7. 如果使用內嵌 UDF 的結果指派變數,而且它也用作 FORCESEEK 查詢提示中的 index_column_name,則會產生錯誤訊息 8622,指出查詢處理器因為查詢中所定義的提示而無法產生查詢計劃。

另請參閱