上述範例有使用到 IX_LastName、IX_FirstName、IX_MiddleName 這三個 Index 來搜尋資料
- 參考資料
- Microsoft SQL Server Performance Tuning 效能調校 P450 8-2-4 單一查詢使用多個索引
USE AdventrueWorks2016 GO SELECT * FROM Person.Person GO SELECT cap.plan_handle , cap.usecounts , cap.cacheobjtype , cap.objtype , st.text FROM sys.dm_exec_cached_plans AS cap CROSS APPLY sys.dm_exec_sql_text(cap.plan_handle) AS st WHERE st.text NOT LIKE '%sys%' AND st.text LIKE N'SELECT * FROM Person.Person%' GO
DBCC FREEPROCCACHE(0x06000900FB17592A9082C9015102000001000000000000000000000000000000000000000000000000000000)
ALTER TABLE 物流申請 ADD 已運輸數量 int NOT NULL DEFAULT(0)
GO
CREATE INDEX IX_物流申請_已運輸數量 ON 物流申請 (已運輸數量)
GO
要在已運送數量欄位形式,去找出待運送數量資訊,就只有下面兩種不符合 SARG 寫法-- 欄位資料 運算子 欄位資料
SELECT * FROM 物流申請 WHERE 申請運輸數量 > 已運輸數量
-- 對欄位進行運算
SELECT * FROM 物流申請 WHERE 申請運輸數量 - 已運輸數量 > 0
觀察執行計劃就會發現,上述兩種寫法,因為違反 SARG,所以根本就不會使用 IX_物流申請_已運輸數量,通通跑 Clustered Index ScanSELECT * FROM 物流申請 WHERE 申請運輸數量 > 已運輸數量
SELECT * FROM 物流申請 WITH (INDEX(IX_物流申請_已運輸數量)) WHERE 申請運輸數量 > 已運輸數量
結果當然是更慘囉ALTER TABLE 物流申請 ADD ComputeCol AS (申請運輸數量 - 已運輸數量) PERSISTED
CREATE INDEX IX_物流單_ComputeCol ON 物流申請 (ComputeCol)
原語法和計算欄位較SELECT * FROM 物流申請 WHERE 申請運輸數量 > 已運輸數量
SELECT * FROM 物流申請 WHERE ComputeCol > 0
公司生管派工系統,彼此之間會已完工數量來溝通,基本上不會有人跟下製程說,還剩下多少數量沒有完成,而是告知已完成數量,也因此在 Table 設計時,各製程是儲存已完工數量資訊,在判斷製程完工時也會遇上該問題,剛好整理一下,以後這類系統發生問題時,至少有個明確方向可以來改善
ALTER TABLE 物流申請 ADD 待運輸數量 int NOT NULL DEFAULT(0)
因為商業邏輯上是待運送數量,運送完後數量會減少,所以預期會有一堆 0 的資料,因此建立篩選索引來過濾掉 0 的資料SELECT
待運輸數量 ,
COUNT(待運輸數量) AS 待運輸總數量
FROM 物流申請
GROUP BY 待運輸數量
ORDER BY 待運輸總數量 DESC
-- 建立非叢集索引
CREATE INDEX IX_物流申請_待運輸數量 ON 物流申請 (待運輸數量)
-- 建立篩選索引
CREATE INDEX IF_物流申請_待運輸數量 ON 物流申請 (待運輸數量)
WHERE 待運輸數量 > 0
指定索引來測試看看是否有差異SET STATISTICS IO , TIME ON
-- 強制使用非叢集索引
SELECT * FROM 物流申請 WITH (INDEX(IX_物流申請_待運輸數量)) WHERE 待運輸數量 > 0
-- 強制使用篩選索引
SELECT * FROM 物流申請 WITH (INDEX(IF_物流申請_待運輸數量)) WHERE 待運輸數量 > 0
測試相關資料非叢集 | 篩選 | |
---|---|---|
Logical Read | 92 | 89 |
執行計劃成本 | 0.0855 | 0.0855 |
Index 使用方式 | Index Seek | Index Scan |
SELECT
T2.index_id,
T2.name,
T2.type_desc,
T1.reserved_page_count,
T1.used_page_count,
T1.row_count,
T2.filter_definition
FROM sys.dm_db_partition_stats AS T1
INNER JOIN sys.indexes T2 ON T1.[Object_ID] = T2.[Object_ID]
AND T1.index_id = T2.index_id
WHERE T1.[Object_ID] = OBJECT_ID('物流申請')
AND T2.[Name] IN ('IX_物流申請_待運輸數量','IF_物流申請_待運輸數量')
最後一步就是 AP 端也改一改,利用一個欄位來儲存資料,就可以避免欄位比較情況SELECT
T.*
, D.物流單編號
, ISNULL(D.已運輸數量, 0) AS 已運輸數量
FROM 物流申請 AS T
LEFT JOIN
(
SELECT
物流單編號
, SUM(已運輸數量) AS 已運輸數量
FROM 派車檔身
GROUP BY 物流單編號
) AS D ON T.物流單編號 = D.物流單編號
WHERE (D.物流單編號 IS NULL -- 物流申請還未派車
OR T.物流申請數量 > D.已運輸數量) -- 物流申請已經派車,但還未完全派車完
改完之後發現更慘,Statistics 為原本兩倍左右,執行計畫也肥了不少-- 物流申請還未派車 SELECT T.* , 0 AS 已運輸數量 FROM 物流申請 AS T WHERE NOT EXISTS ( SELECT 1 FROM 派車檔身 AS D WHERE T.物流單編號 = D.物流單編號 ) UNION ALL -- 物流申請已經派車,但還未完全派車完 SELECT T.* , D.已運輸數量 FROM 物流申請 AS T JOIN ( SELECT 物流單編號 , SUM(已運輸數量) AS 已運輸數量 FROM 派車檔身 GROUP BY 物流單編號 ) AS D ON T.物流單編號 = D.物流單編號 WHERE T.物流申請數量 > D.已運輸數量
SELECT
..................
FROM
..................
JOIN [派工工序 Table] AS F1 ON D.派工單號 = F1.派工單號
AND D.派工工序 = F1.派工工序
AND F1.工序結案數量 <> S1.派工數量 -- 關鍵條件
條件修正---- 原語法
F1.工序結案數量 <> S1.派工數量
----- 了解商業邏輯情況下改為
F1.工序結案數量 < S1.派工數量
----- 完全不懂商業邏輯下會改為
F1.工序結案數量 < S1.派工數量 OR F1.工序結案數量 > S1.派工數量
上述兩種改法都是可以得到相同結果,Statistics 雖然沒有甚麼變化,執行計劃成本降低不少;
WITH CTE
AS
(
SELECT
CAST(@P1 AS DATETIME) AS CTEDate UNION ALL SELECT
DATEADD(DD, 1, CTEDate)
FROM CTE
WHERE DATEADD(DD, 1, CTEDate) <= CAST(@P2 AS DATETIME)
)
SELECT
..................
FROM
(
SELECT
..................
FROM
(
SELECT
..................
FROM CTE AS T
JOIN [NC資料表] AS N ON N.結案時間 >= CTEDate
AND N.結案時間 < DATEADD(DD, 1, CTEDate)
..................
) AS TF
GROUP BY ..................
) AS F
OPTION (MAXRECURSION 0)
Statistics 很糟糕外,執行計畫也跑出平行處理,下圖為部分截圖SELECT
..................
FROM
(
SELECT
..................
FROM
(
SELECT
..................
FROM DateTable AS T
JOIN [NC資料表] AS N ON N.結案時間 >= T.Date
AND N.結案時間 < DATEADD(DD, 1, T.Date)
..................
WHERE T.Date BETWEEN @P1 AND @P2
) AS TF
GROUP BY ..................
) AS F
修正前 | 修正後 | |
---|---|---|
CPU Time | 1,562 ms | 31 ms |
WorkTable Logical Read | 815,419 | 0 |
SQL Server 提供在索引子頁含入與鍵值無關的資料表其他欄位,但這個欄位值不做排序等額外維護動作文字說明應該是來至 [SQL Server Performance Tuning 效能調校] 書籍,當初筆記時忘記紀錄出處,Orz
-- 複合索引語法 CREATE INDEX IX_Person_MultiColumnIndex ON Person.Person (LastName,FirstName,MiddleName) -- 覆蓋索引語法 CREATE INDEX IX_Person_CoveringIndex ON Person.Person (LastName) INCLUDE(FirstName , MiddleName)
SELECT
..................
FROM
(
SELECT
..................
FROM 出貨檔頭
WHERE
..................
UNION ALL
SELECT
..................
FROM 出貨檔頭 AS R
JOIN
(
SELECT
..................
FROM 客戶
) AS w1 ON r.客戶編號 = w1.客戶編號
AND (r.date BETWEEN w1.起始日期 AND w1.結束日期)
WHERE
..................
) AS T
JOIN vw客戶 AS c1 ON T.CustNO = c1.CustNO
JOIN
(
SELECT
..................
FROM 出貨 AS t
UNION ALL
SELECT
..................
FROM 退貨 AS t
UNION ALL
SELECT
..................
FROM 庫存出貨 AS t
) AS p ON T.out_id = p.out_id
JOIN 訂單 AS F ON P.訂單編號 = F.訂單編號
從下面執行計畫部分截圖,就可以發現啦,是利用 Nested Loops 一筆一筆進 UNION ALL 內的每一個 Table 內把資料找出來,最後再 UNION ALL 後傳出SELECT
........................
FROM [物料設計BOM表] AS w
JOIN [物料View] AS m
WHERE
........................
AND m.物料隱藏 = 0
SELECT
........................
FROM [物料設計BOM表] AS w
JOIN [物料View] AS m
WHERE
........................
AND m.物料隱藏 = CAST(0 AS bit)
最後發現是 View 內老早就隱含轉換了,OrzSELECT
.................. ,
m.物料隱藏 -- Table 內真實存在欄位,資料型態為 bit
FROM 物料表 AS m
UNION ALL
SELECT
.................. ,
0 -- 配合 UNION ALL 的虛擬欄位
FROM 客戶物料表
UNION ALL
SELECT
.................. ,
0 -- 配合 UNION ALL 的虛擬欄位
FROM 客戶規格表
UNION ALL
SELECT
.................. ,
0 -- 配合 UNION ALL 的虛擬欄位
FROM 設計變更表
UNION ALL
SELECT
.................. ,
0 -- 配合 UNION ALL 的虛擬欄位
FROM 派工單檔頭
WHERE
..................
從 資料類型優先順序 文章內就可以查到,int 優先權是高於 bit,所以 View 內的 [物料隱藏] 欄位就被轉為 int 了,把 View 內的 0 通通改為 CAST(0 as bit),執行計畫中隱含轉換字樣終於消失了SELECT
S.派工單號 ,
I.訂單單號
FROM 派工單Table AS S
LEFT JOIN 訂單明細Table AS I ON S.派工單號 = I.派工單號
WHERE S.派工單號 = @P1
OR I.訂單單號 = @P2
SELECT
S.ORDER_NO ,
I.ODNO
FROM PMSETORDER AS S
LEFT JOIN POITEM AS I ON S.ORDER_NO = I.ORDER_NO
WHERE S.ORDER_NO = @P1
OR i.odno = @P2
SELECT
S.ORDER_NO ,
I.ODNO
FROM PMSETORDER AS S
LEFT JOIN POITEM AS I ON S.ORDER_NO = I.ORDER_NO
WHERE S.ORDER_NO = @P1
執行計畫比較