使用row_number來分頁幾乎是家喻戶曉的東東了,而且這東西簡單易用,簡直就是程式設計師居家必備之殺器,然而row_number也不是一招吃遍天下鮮的無敵bug般存在,最近就遇到幾個小問題,拿出來供大家娛樂下。
問題1:為什麼加where條件就慢,不加反而快?
查詢sql:
with開發大哥很激動地問我,對上面類似的的查詢,如果沒有where rid between 0 and 10的話,查詢在1秒內完成,如果有where條件,執行超過30秒未結束,不帶where條件返回300行左右資料,where條件過濾後返回10行資料,返回的資料行長度較小,可以忽略由於返回資料大小對網路和顯示的影響,那問題出在那呢?稍微有點dba經驗的人都會很快找到問題根源--執行計畫不對。tempas(
select*,
row_number()
over(order
by t2.c6 desc) as
ridfrom tb001 as
t1inner
join tb002 as
t2on t1.c1=
t2.c1
where t1.c2>
1000
and t2.c3<
99999
and t1.c4=5)
select
*from
temp
where rid between
0and
10
讓我們換個簡單的sql來分析下
with讓我們揣測下上面查詢如何實現,假設在t1.c1有索引ix_c1,在t1.c2上有索引ix_c2。tempas(
select*,
row_number()
over(order
by t1.c1 desc) as
ridfrom tb001 as
t1where t1.c2>
1000
)select
*from
temp
where rid between
0and
10
實現方式1:
a=>針對cte內部的查詢,先利用索引ix_c2找出滿足條件t1.c2>1000的資料,得到結果集u1
b=>對結果集u1按t1.c1排序,計算出u1中每行rid列的值,得到結果集u2
c=>對結果集u2查詢滿足rid between 0 and 10過濾的行,得到結果集u3
d=>將結果集u3返回
實現方式2:
a=>利用索引ix_c1按order by t1.c1 desc來依次訪問t1資料
b=>檢查步驟a得到的行是否滿足t1.c2>1000條件,將滿足條件的結果放入結果集u1中,然後一次遞增rid
c=>檢查步驟b得到的結果集ui,當得到足夠資料行(rid between 0 and 10)後停止步驟a和b
d=>將結果集u1返回
以上兩種方式都能得到正確的返回結果,但是那種更好呢?
對於實現方式1,假設表t1有100w資料,如果滿足t1.c2>1000的行只有20行,那麼使用索引ix_c2快速找出滿足條件的20行資料,然後對這20行資料排序也只會消耗很輕微的cpu資源;但如果滿足t1.c2>1000的行只有99w行,那麼排序就消耗大量cpu資源,從而導致查詢慢。
對於實現方式2,假設表t1有100w資料,按照索引ix_c1 倒序遍歷c1的值,如果遍歷前50行便能查詢到滿足t1.c2>1000的10行資料,那麼查詢可以很快結束,只消耗少量的邏輯讀;但如果需要遍歷前99w資料才能找到滿足t1.c2>1000的10行資料,那麼就會消耗大量的邏輯讀,從而導致查詢慢。
由此,我們不難得出乙個結論:沒有絕對正確的執行計畫,只有相對高質量的執行計畫。
我們知道,在sql server生成執行計畫時,會根據輸入的引數和統計資訊去預估一些步驟的影響行數和開銷,尋找開銷較小的執行計畫,對於本篇開頭提到的查詢,sql server很容易受到rid between 0 and 10的**,選擇類似於實現方式2的的執行計畫,而資料分布情況又恰好是針對該方式最壞的情況,就出現了我們遇到的結果,查詢死慢死慢的。
類似的案例還有:
1. 查詢返回資料20行,然後在此查詢的基礎上增加order by 和top(10), 結果執行效率慢了很多,於是就產生了為什麼對20行資料排序取top會這麼慢的疑惑?
2. 查詢返回資料20行,在查詢中分別增加select top(20)和select top(10000),結果select top(10000)的比select top(20)快很多倍,我遇到的案例有select top(10000)在5ms內完成,然後select top(1)的十分鐘都沒有結果
以上案例都有相同的操作order by+top,row_number本質上也是order by+top,我們知道cpu資源是伺服器資源中最寶貴的資源,而對結果集排序又是乙個很耗cpu資源的過程,sql server為節省cpu資源選擇了乙個「它」認為比較合適的執行計畫,結果悲劇了。
針對哪位開發大哥的問題,我嘗試了各種寫法,在不動用臨時表和索引提示的情況下,我還真搞不定這sql,於是我來了個**小招數:
with學術派們要開始叫囂了,這種rid+0 between 0 and 10寫法不科學啊,效率低下,初級程式設計師不懂sql寫的爛sql啊。。。tempas(
select*,
row_number()
over(order
by t2.c6 desc) as
ridfrom tb001 as
t1inner
join tb002 as
t2on t1.c1=
t2.c1
where t1.c2>
1000
and t2.c3<
99999
and t1.c4=5)
select
*from
temp
where rid+
0between
0and
10
使用rid+0來騙過查詢優化器,讓「它」無法估算出between 0 and 10需要返回的行數,這樣「它」只能老老實實地「先」做cet內部的查詢.
ps: 我騙得過查詢優化器,騙不過開發大哥,他一直認為這個寫法太bt,問了其他的dba好幾次,就是不採納我的建議,悲催啊。
乙個小建議:
不要見到類似wheere c1+10>20這種的就叫囂不好,就喊著不能走索引的口號,看看場景再說麼,萬一c1上就壓根沒有索引呢?
row_number在實現分頁行的確很好用,但是也不是所有場景都適用,這是乙個真實的例子
乙個查詢只有兩個引數@p1和@p2,代表取第@p1行到第@p2行之間的數
當@p1=0 and @p2=1000時,消耗是這樣的:
表 '當@p1=7241284 and @p2=7240285時,消耗是這樣的:***detail
'。掃瞄計數 186,邏輯讀取 4922 次,物理讀取 0 次,預讀 0 次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0
次。表
'***
'。掃瞄計數 1,邏輯讀取 809 次,物理讀取 0 次,預讀 0 次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0
次。 cpu 時間
=0 毫秒,占用時間 =
73 毫秒。
表 '真有份這麼多頁的,無語吧!!!***detail
'。掃瞄計數 1468817,邏輯讀取 35838994 次,物理讀取 1 次,預讀 0 次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0
次。表
'***
'。掃瞄計數 1,邏輯讀取 5983509 次,物理讀取 0 次,預讀 0 次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0
次。 cpu 時間
=45926 毫秒,占用時間 =
56816 毫秒。
既然無語,我就不多做解釋,說多就是眼淚,看看就好。
打完收工,妹子附上
曲演雜壇 頁拆分
以下測試基於版本 sql server 2008 很多同行會問起頁拆分的相關的問題,自己對頁拆分頁迷迷糊糊,有點雲裡霧裡的感覺,今天來測試測試。首先生成測試資料 使用testdb資料庫來測試 usetestdb godrop table tb01 go 建立測試表tb01 create table ...
蛋疼的遞迴
幾個經典的遞迴場景 1.斐波那契 2.二叉樹的周遊 深度 前中後序 3.全排列問題 非簡單的全排列 允許字母重複 4.尋找滿足條件的n個數 第乙個的變種 跳台階 題目 乙個台階總共有n 級,如果一次可以跳1 級,也可以跳 2 級。求總共有多少總跳法,並分析演算法的時間複雜度。第二個要複習下非遞迴的寫...
曲苑雜壇 收縮資料庫檔案
很多人在刪除大量資料後收縮資料庫,卻發現沒法收縮到預期效果。由於使用dbcc shrinkfile來收縮資料檔案時,是針對資料區來收縮,因此可以先使用dbcc showfilestats來檢視檔案中未使用的分割槽數 totalextents usedextents 如果刪除大量資料但未使用分割槽數比...