工作中遇到的問題:
為調用方提供一個分頁接口時,調用方一直反應有部分數據取不到,且取到的數據有重復的內容,於是我按以下步驟排查了下錯誤。
1.檢查分頁頁碼生成規則是否正確。
2.檢查SQL語句是否正確。(后來確認是SQL中order by作祟,犯了想當然的錯誤,認為SQL是最不可能出問題的地方,因為分頁SQL格式與老代碼分頁SQL格式一樣,所以沒有懷疑。)
3.檢查調用方入參是否正確。
4.檢查調用方循環遍歷邊界。
5.在上述步驟驗證沒問題后,懷疑ibatis,調試到ibatis中,花費大量時間。
6.再次驗證SQL,發現問題。
經過這么多步驟,發現自己考慮問題都想復雜了,最簡單的錯誤原因往往就是其錯誤原因,那么我們就來分析為什么 order by 會造成分頁SQL出錯。
分頁SQL: SELECT * FROM (SELECT t.*, ROWNUM AS rowno FROM (select * from table ORDER BY LIST_ORDER) t WHERE ROWNUM<#endRow# ) WHERE rowno>=#startRow# 看似這個SQL沒有什么問題,
執行過程: select * from table ORDER BY LIST_ORDER 1.首先取出table表的所有數據,並按照list_order排序,其中list_order可以取0,1,2,3,4,5這六個數 SELECT t.*, ROWNUM AS rowno FROM (.....) t WHERE ROWNUM<#endRow# 2.取出table表中前#endRow#個數據。 SELECT * FROM (......) WHERE rowno>=#startRow# 3.取出從第#startRow#個數據后的所有數據。 於是這樣就取出了table中#startRow#到#endRow#的所有數據,可是我們忽略了這個問題,ROWNUM是不變的嗎?答案是order by 會導致 rownum發生變化
驗證一下 比較兩個SQL 的結果。
1.SELECT t.*, ROWNUM AS rowno FROM (select * from table ORDER BY LIST_ORDER) t WHERE ROWNUM<6
ID | CATEGORY_NAME | LIST_ORDER | ROWNO |
23794 | fdfdf | 0 | 1 |
22899 | 上裝1 | 0 | 2 |
5260 | 薯片 | 0 | 3 |
5094 | 廚房家電 | 0 | 4 |
23029 | 涼血止血 | 0 | 5 |
2.SELECT t.*, ROWNUM AS rowno FROM (select * from table ORDER BY LIST_ORDER) t WHERE ROWNUM<11
ID | CATEGORY_NAME | LIST_ORDER | ROWNO |
23794 | fdfdf | 0 | 1 |
23204 | 子目錄222-22 | 0 | 2 |
23203 | 子目錄222-21 | 0 | 3 |
23202 | 子目錄222-20 | 0 | 4 |
23200 | 子目錄222-18 | 0 | 5 |
23198 | 子目錄222-16 | 0 | 6 |
22899 | 上裝1 | 0 | 7 |
5260 | 薯片 | 0 | 8 |
5094 | 廚房家電 | 0 | 9 |
23029 | 涼血止血 | 0 | 10 |
結果很明顯:
以“涼血止血”為例,在第一個SQL中,“涼血止血”的rownum為5, 而在第二個SQL中“涼血止血”的rownum為10,他的rownum 發生了變化
於是這樣在第三步,我們取第#startRow#個數據后的所有數據時,就會一直把最后面的“涼血止血”類似的數據給取出來,導致出現重復的錯誤,並且前面的數據會有取不到的可能性。那么為什么rownum會發生變化呢?
對於rownum來說它是oracle系統順序分配為從查詢返回的行的編號,返回的第一行分配的是1,第二行是2,依此類推,這個偽字段可以用於限制查詢返回的總行數,且rownum不能以任何表的名稱作為前綴。
聽起來很繞口對吧,其實簡單的說就是,你去查數據庫,rownum就是oracle根據返回數據的順序給他的一個編號,誰先返回誰就是1,如果不存在order by排序條件那么它就是oracle的存儲順序。
錯誤導致原因分析:於是當本文中取出的數據的list_order這個字段的值是一樣的時候,oracle在返回數據時,返回數據順序不是固定的,我們取前5個數據的時候,數據庫返回數據的順序,與我們取前11個數據時,數據庫返回數據的順序是完全不同的,於是他生成的rownum偽列的編號就完全不一樣,就導致了這樣的錯誤。
造成這種錯誤前提:
1.order by 排序字段不唯一
2.分頁使用的是類似以下SQL的結構
SELECT * FROM (SELECT t.*, ROWNUM AS rowno FROM ( select * from table ORDER BY LIST_ORDER) t WHERE ROWNUM<#endRow# ) WHERE rowno>=#startRow#
3.數據庫的數據足夠多,這樣才比較容易發生rownum生成不一致
解決辦法:
1.提取rownum到外部:
SELECT * FROM (SELECT t.*, ROWNUM AS rowno FROM (select * from table ORDER BY LIST_ORDER) t ) WHERE rowno>=#startRow# AND ROWNUM<#endRow#
優點:適用各種order by不同字段,因為內部取值SQL是不變的,所以取值順序是不變的,分頁肯定不會出錯
缺點:SQL效率變低,每次都相當於取出了所有的數據,然后再進行遍歷比較,依賴於oracle的存儲順序,當oracle存儲順序發生變化時,需要注意。(當然那時候很多類型的SQL都要注意了)
2.order by后面加上唯一性字段(類似主鍵id) :
SELECT * FROM (SELECT t.*, ROWNUM AS rowno FROM (select * from table ORDER BY LIST_ORDER,id) t ) WHERE rowno>=#startRow# AND ROWNUM<#endRow#
優點:修改簡單,原來的代碼不用做過多更改
缺點:sql效率有可能會比第一種修改方式更加低,因為在根據list_order排序后,還要根據id再排一次序,當數據量比較多時,SQL可能會很慢。