值班期間研發同事打來電話,說應用有超時,上服務器上檢查發現有SQL大批量地執行,該SQL消耗IO資源較多,導致服務器存在IO瓶頸,細看SQL,發現自己都被整蒙了,不知道這SQL是要干啥,處理完問題趕緊研究下。
SQL類似於:
WITH T1 AS ( SELECT TOP ( 100 ) ID , ROW_NUMBER() OVER ( ORDER BY C1 ) AS RID FROM [dbo].[TB002] ) SELECT * FROM T1 WHERE T1.RID > (1-1)*2147483647 AND T1.RID < 1*2147483647
第一趕腳是寫這代碼的研發同事想分頁,但是這每頁的數據量有點嚇人啊(是我太膽小么?)
再仔細看下,趕腳又不是分頁,上面還有TOP(100)呢?
如果把TOP(100) 放到CTE外面,很容易理解,根據RID列過濾完后再取前100行數據。
對於上面的TOP(100) 在CTE內部SQL執行步驟如下
1>對表TB002中C1列排序計算每行的RID值,得到臨時結果集T1
2>對臨時結果集T1中數據“隨機”取100條(注意:因為CTE中TOP(100) 沒有對應ORDER BY 子句,因此無法保證返回的100條數據是有序的,即使在不少場景下返回的數據是按RID排序的) 得到臨時結果集T2
3>將臨時結果集T2的數據按照T1.RID > (1-1)*2147483647 AND T1.RID < 1*2147483647 的條件過濾,得到最終結果集T3
4>強最終結果集T3返回給客戶端
--=========================華麗分割線=======================================--
在SQL SERVER 世界里,ROW_NUMBER函數已經有些泛濫成災,很多不明真相的群眾磕着瓜子就把ROW_NUMBER函數寫到應用查詢中,甚至不少研發同事(抱歉有些人躺槍了)把ROW_NUMBER函數用到登峰造極的程度,當看到一條SQL里使用到N多ROW_NUMBER函數和子查詢再加N多大表關聯查詢,我都對自己DBA的身份表示懷疑,完全看不懂啊!!!
--=========================華麗分割線=======================================--
回歸正題,ROW_NUMBER函數的引入是為了更簡單地實現分頁,SQL SERVER 查詢引擎會將CTE外部的條件引入到CET內部,以避免CTE內部語句執行時訪問“無用”數據,如對下面的語句
;WITH T1 AS ( SELECT ID , ROW_NUMBER() OVER ( ORDER BY ID ) AS RID FROM [dbo].[TB002] ) SELECT * FROM T1 WHERE T1.RID > 10 AND T1.RID < 30
由於表TB002上ID有索引,因此查詢會利用索引訪問前30條記錄,丟棄不滿足RID>10的第1到10條數據。
由於這種優化的存在,使得查詢無需先執行
SELECT ID ,ROW_NUMBER() OVER ( ORDER BY ID ) AS RID FROM [dbo].[TB002]
然后再執行WHERE T1.RID > 10 AND T1.RID < 30 的過濾操作。
但如果CTE內部加入TOP子句,就使得CTE外部的T1.RID > 10 AND T1.RID < 30條件不能引入到CET內部(查詢優化器首先得保障返回結果集的正確性,然后才考慮執行的高效性)。對於研發同事也一樣,他們首先關注查詢結果是否正確,然后才考慮查詢效率是否高效,那么引入TOP是否能保證數據正確呢?
為了掩飾,我們將查詢做輕微調整如下:
;WITH T1 AS ( SELECT TOP(10) ID , ROW_NUMBER() OVER ( ORDER BY ID ) AS RID FROM [dbo].[TB002] ) SELECT * FROM T1 WHERE T1.RID > 10 AND T1.RID < 30
我們會悲哀地發現,查詢返回結果為空,這顯然不是一個好兆頭,為什么會返回空呢?
輕輕推敲一下,我們就會發現,CTE內部的執行結果總是“巧合”地返回RID為1到10的數據,而外部條件RID>10又將這10條數據過濾掉,SO返回為空。
PS: 查詢優化器真的是“順手”返回前10條數據,因為恰好這10條數據“在手邊”,不能保證其他場景下也是返回RID為1到10的數據,當然也不是查詢優化器故意“坑人”哈
--=========================華麗分割線=======================================--
至此,我總算明白為啥要將寫SQL的那位兄弟要傳入入2147483647 這么大一個頁數量,估計是傳小了查不出數據,所以一勞永逸傳個最大值,想想也是醉了!
--=========================華麗分割線=======================================
總結:
編寫SQL的目的在於實現業務需求,而不是顯示個人SQL能力,也沒有“一招鮮吃遍天”可以秒殺所有問題的寫法,在尊重業務需求的前提下,依據業務場景,考慮數據分布和當前以及未來的數據量,用盡可能簡單的SQL地實現業務需求才是王道。
其實寫博客的目的是發圖,你們懂的!
圖片來源網絡,勿求粽子!