| 類型 | 語法特徵 | 優化器視角 | 效能特性 |
|---|---|---|---|
| 內聯表值函數 (Inline TVF) | RETURNS TABLE AS RETURN SELECT ...無 BEGIN...END、無變數宣告、單一 SELECT | 視為 **View / 衍生表**,**參與查詢最佳化**,可推導統計資訊、索引、Predicate Pushdown | ✅ 效能佳,等同直接寫 JOIN |
| 多陳述式表值函數 (Multi-statement TVF) | RETURNS @TableVar TABLE (...) BEGIN ... INSERT @TableVar ... RETURN END | 視為 **黑盒子**,**無統計資訊、無索引**,優化器假設回傳 **1 資料列** (極小) | ⚠️ 易產生嚴重效能問題 (Nested Loops + 大量 Key Lookup) |
-- 多陳述式 TVF (黑盒子)
CREATE FUNCTION dbo.tvf_Multi_Test()
RETURNS @SaleDetail TABLE (ProductID INT)
AS
BEGIN
INSERT INTO @SaleDetail
SELECT ProductID
FROM Sales.SalesOrderHeader soh
JOIN Sales.SalesOrderDetail sod ON soh.SalesOrderID = sod.SalesOrderID;
RETURN;
END
GO
-- 內聯 TVF (透明)
CREATE FUNCTION dbo.tvf_Inline_Test()
RETURNS TABLE
AS
RETURN
SELECT ProductID
FROM Sales.SalesOrderHeader soh
JOIN Sales.SalesOrderDetail sod ON soh.SalesOrderID = sod.SalesOrderID;
GO
-- 多陳述式 TVF JOIN
SELECT c.PersonID, p.Name, COUNT(*) AS Cnt
FROM Person.BusinessEntityContact c
JOIN dbo.tvf_Multi_Test() t ON c.PersonID = t.ProductID
JOIN Production.Product p ON t.ProductID = p.ProductID
GROUP BY c.PersonID, p.Name;
-- 內聯 TVF JOIN
SELECT c.PersonID, p.Name, COUNT(*) AS Cnt
FROM Person.BusinessEntityContact c
JOIN dbo.tvf_Inline_Test() t ON c.PersonID = t.ProductID
JOIN Production.Product p ON t.ProductID = p.ProductID
GROUP BY c.PersonID, p.Name;
成本對比 (圖) :

BusinessEntityContact 走 **121,317 次 Key Lookup** (成本 0.38)。SalesOrderHeader + SalesOrderDetail,直接 **Hash Match / Merge Join**,單次掃描 (成本 0.02)。查詢計畫快取,找出包含 Table-valued function 邏輯運算子的 JOIN:
WITH XMLNAMESPACES('https://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p)
SELECT
st.text AS QueryText,
qp.query_plan
FROM (
SELECT TOP 50 * FROM sys.dm_exec_query_stats ORDER BY total_worker_time DESC
) qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
WHERE qp.query_plan.exist('
//p:RelOp[contains(@LogicalOp, "Join")]/*/p:RelOp[@LogicalOp="Table-valued function"]
') = 1;
結果範例:

| 策略 | 適用情境 | 改動幅度 | 效果 |
|---|---|---|---|
| 改寫為內聯 TVF | 邏輯可單一 SELECT 表達 | 低 (僅改函數定義) | ✅ 徹底解決,最佳化器完整可見 |
| 結果存入暫存表再 JOIN | 邏輯複雜、需多步驟、無法內聯 | 中 (呼叫端改動) | ✅ 暫存表有統計資訊、可建索引 |
| 加入 OPTION (RECOMPILE) | 參數嗅嚐導致計畫不穩 | 低 | ⚠️ 緩解,不解決基數估算 |
| 使用 APPLY 而非 JOIN | TVF 回傳極少列 (如 < 100) | 低 | ⚠️ 適用「一對多展開」場景 |
| 建立同義詞/檢視表替代 | TVF 純封裝查詢、無參數 | 低 | ✅ 檢視表等同內聯 TVF |
-- 呼叫端
CREATE TABLE #TmpProducts (ProductID INT PRIMARY KEY);
INSERT INTO #TmpProducts
SELECT ProductID FROM dbo.tvf_Multi_Test(); -- 執行一次,結果物質化
-- 後續 JOIN 有統計資訊、可用索引
SELECT c.PersonID, p.Name, COUNT(*)
FROM Person.BusinessEntityContact c
JOIN #TmpProducts t ON c.PersonID = t.ProductID
JOIN Production.Product p ON t.ProductID = p.ProductID
GROUP BY c.PersonID, p.Name;
DROP TABLE #TmpProducts;
CROSS/OUTER APPLY 做逐列展開 (APPLY 語意本身就是逐列呼叫,基數估算較準)。sys.dm_exec_query_plan XQuery 掃描快取,主動發現隱性 TVF JOIN 問題。