mysql排序不穩定問題

現象

當order by中的列具有相同的值時,每次查詢到的順序存在不一致的現象,如下:
在這裏插入圖片描述
在這裏插入圖片描述
類似的,在分頁排序中,有的數據會在頁面連續出現多次,有的數據則在數據中一次也不能出現。

爲什麼會出現此現象

PageHelper首先將前端傳遞的參數保存到page這個對象中,接着將page的副本存放入ThreadLoacl中,這樣可以保證分頁的時候,參數互不影響,接着利用了mybatis提供的攔截器,取得ThreadLocal的值,重新拼裝分頁SQL,完成分頁。
其中mysql通過採用limit分頁。

public String getPageSql(String sql, Page page, RowBounds rowBounds, CacheKey pageKey) {
    StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
    sqlBuilder.append(sql);
    sqlBuilder.append(" limit ?,?");
    return sqlBuilder.toString();
}

通過explain,查看sql的執行計劃,在extra中顯示如圖
在這裏插入圖片描述
Using temporary表示由於排序沒有走索引,因此創建了一個內部臨時表。注意這裏的臨時表可能是內存上的臨時表,也有可能是硬盤上的臨時表。
Using filesort表示沒有使用索引排序,它並不意味着在硬盤上排序,filesort與文件無關。消除Using filesort的方法就是讓查詢sql的排序走索引。

外部排序模式

mysql使用的排序模式有四種:

  • 第一種,回表排序模式
    對需要排序的記錄生成<sort_key,rowid>的元數據進行排序,該元數據僅包含排序字段和rowid。排序完成後只有按字段排序的rowid,因此還需要通過rowid進行回表操作獲取所需要的列的值,可能會導致大量的隨機IO讀消耗;
  • 第二種,不回表排序模式
    不回表排序是回表排序模式的改進,對需要排序的記錄生成<sort_key,additional_fields>的元數據,該元數據包含排序字段和需要返回的所有列。排序完後不需要回表,但是元數據要比第一種方法長得多,採用的是用空間換時間的方法,需要更多的空間用於排序。
    但是由於sort buffer就那麼大,如果用戶要查詢的數據非常大的話,很多時間浪費在多次磁盤外部排序,導致更多的IO操作,效率可能還不如第一種方式。
    所以,MySQL給用戶提供了一個max_length_for_sort_data的參數。當“排序的鍵值對大小” > max_length_for_sort_data時,MySQL認爲磁盤外部排序的IO效率不如回表的效率,會選擇第一種排序模式;反之,會選擇第二種不回表的模式。
  • 第三種,打包數據排序模式
    與第二種區別,僅僅在於將char和varchar字段存到sort buffer中時,更加緊縮。
    在之前的兩種模式中,存儲了“yes”3個字符的定義爲VARCHAR(255)的列會在內存中申請255個字符內存空間,但是5.7.3改進後,只需要存儲2個字節的字段長度和3個字符內存空間(用於保存”yes”這三個字符)就夠了,內存空間整整壓縮了50多倍,可以讓更多的鍵值對保存在sort buffer中。
  • 第四種,優先隊列排序
    在前三種方式中,都需要將滿足條件的記錄先排序再返回結果。而在5.6版本中針對Order by limit M,N語句,在空間層面做了優化,使用一種新的排序方式–優先隊列(priority queue) 。
    優先隊列排序採用堆排序實現,堆排序算法特徵正好可以解決limit M,N 這類排序的問題。雖然仍然需要所有元素參與排序,但是隻需要M+N個元組的sort buffer空間即可,對於M,N很小的場景,基本不會因爲sort buffer不夠而導致需要臨時文件進行歸併排序的問題。
    其中對於升序,採用大頂堆,最終堆中的元素組成了最小的N個元素,對於降序,採用小頂堆,最終堆中的元素組成了最大的N的元素。

排序算法

前三種排序方式使用的算法是QuickSort,即對需要排序的記錄生成元數據進行分塊排序,然後再使用mergesort方法合併塊,即使用到了快速排序及歸併排序。而優先隊列則使用堆排序。

其中快速排序,堆排序是不穩定的。
排序算法的穩定性指的是能保證排序前2個相等的數其在序列的前後位置順序和排序後它們兩個的前後位置順序相同。即如果Ai = Aj,Ai原來在位置前,排序後Ai還是要在Aj位置前。

快速排序的不穩定性發生在key與相遇點進行交換時。
堆排序的不穩定性發生在子節點與父節點進行交換時,將兩個相同元素順序打亂,5(left) 6(root) 5(right) -> 5 5 6,此時5(right)位於5(left)的左側。
在這裏插入圖片描述

如何解決此問題

  • 使用索引, 因爲索引本身也是有序的,如果在需要排序的字段上面建立了合適的索引,那麼就可以跳過排序的過程,提高SQL的查詢速度。(強制走索引:force index)
    在這裏插入圖片描述
    B+ tree其中一個特點,即所有葉子節點增加了一個鏈指針,以此保證了索引的有序性。

  • 如果實在不能走索引,可以加一個唯一字段(例如id)進行排序

題外話

  • pagehelper進入doProcessPage方法後,通過反射機制,會先查詢出數據總數量,然後再進行分頁SQL的拼裝,MappedStatement的getBoundSql。
    查詢出的總數量是爲了返回分頁信息用,如總頁數等。

  • 當一個數據庫表過於龐大,LIMIT offset, length中的offset值過大,則SQL查詢語句會非常緩慢。此事可以將上一頁的最大值當成參數作爲查詢條件或者採用子查詢優化法。

  • 查詢緩存,可以看作SQL文本和查詢結果的映射。如果第二次查詢的SQL和第一次查詢的SQL完全相同(注意必須是完全相同,即使多一個空格或者大小寫不同都認爲不同)且開啓了查詢緩存,那麼第二次查詢就直接從查詢緩存中取結果

https://dev.mysql.com/doc/refman/5.7/en/order-by-optimization.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章