Mysql版本差異導致groupBy+orderBy結果不一致的問題

發現個問題。
Mysql版本導致orderBy + groupBy之後的結果不一致
在Mysql5.6版本和5.7版本穩定復現。

舉個栗子🌰:
tests表中數據如下

想查找每個relate_id下created_at最大的數據(即查詢id 爲4 的記錄)

sql如下

select  id,relate_id from (select id,relate_id from tests ORDER BY created_at desc) as t GROUP BY relate_id;

在5.6版本運行結果如下:

可以看出是能按照我們的需求找到ID最大的,也就是created_at最大的數據的(即group by 取的是分組的第一條記錄)

但是在5.7及以上版本的時候,結果卻完全相反:

取的是最小的那個,看上去似乎是我的orderby desc並沒有生效。

執行了一下explain

在網上查詢了一下關於這個的原因,也就是Mysql 5.7中的Derived table。
Derived table是個啥玩意呢?其實你可以理解爲一個子查詢,位於sql語句的from子句裏面,在5.7之前的版本都是把Derived table 進行Materialize,生成一個臨時表保存Derived table的結果,然後進行join之類的操作完成整個查詢。
但是5.7版本之後呢,對Derived table做了一個新特性,這個新特新就允許符合條件的子表直接和父查詢的表進行join等操作。

Merge Derived table有兩種方式進行控制。第一種,通過開關optimizer_switch=’derived_merge=on|off’來進行控制。
第二種,在CREATE VIEW的時候指定ALGORITHM=MERGE | TEMPTABLE, 默認是MERGE方式。如果指定是TEMPTABLE,將不會對VIEW進行Merge Derived table操作。

但是仍然有很多限制,只要Derived table裏不包含如下條件就可以利用該特性進行優化:

  • UNION clause
  • GROUP BY
  • DISTINCT
  • Aggregation
  • LIMIT or OFFSET

Derived table裏面包含用戶變量的設置。
也就是如上查詢,在5.7版本中只要添加一個最簡單的limit就能使 5.7中Derived table新特性失效,從而達到我們想要的效果~

EXPLAIN select  id,relate_id from (select id,relate_id from tests ORDER BY created_at desc limit 1000) as t GROUP BY relate_id;

看下現在的explain:

當然根據上面幾種給出的情況只要不影響你的sql結果,你添加上相關的查詢就能達到一樣的效果。

那麼追根到底就是說Derived table作爲一個子表join到父查詢的表的時候,忽略了orderBy語句,導致了查詢的差異。以下是我找到的關於這兩個部分處理的原則,僅供參考~

如果Derived table中包含ORDER By語句,處理原則和正常SubQuery的處理方式類似:
    1. 如果Derived table只包含一個表
    2. 並且Derived table不包含聚集函數
   滿足上述兩個條件之後,Derived table將會保留ORDER BY。其他情況subquery中的ORDER BY將會被忽略掉,這也是MySQL5.7區別於MySQL5.6的一點。

    當Derived table保留了Order by,是否能合併到父查詢,需要滿足如下條件:
       1. 父查詢允許做Derived table中的ORDER BY。下面幾種情況不允許做ORDER BY
           a) 如果父查詢包含有自己的ORDER BY
           b) 如果父查詢包含GROUP BY
           c) 如果父查詢包含未被優化掉的DISTINCT
       2. 父查詢不能是UNION操作,因爲UNION默認會做DISTINCT操作
    3. 爲了簡化操作,只有當父查詢只包含Derived table的時候(即FROM子句裏面只有Derived table一個表)纔可以保留ORDER BY。這裏有相當大的改進空間可以儘量的來按照Derived table定義的ORDER BY操作來進行父查詢的操作。比如有兩個表以上,如果父查詢沒有ORDER BY的要求,也可以按照Derived table來對結果進行排序。

建議 :

  • 可以使用limit 加入到order by 的後面(但是如果group by 的分組記錄是範圍,而且每個分組的含有的記錄有很多條,limit 1000可能還不夠)
SELECT SUMS,EPRJ_INFO_ID,sec_uid,sec_name,PROCESS,step 
    FROM
      (SELECT SUMS,EPRJ_INFO_ID, sec_uid,sec_name,PROCESS,step,psq 
      FROM
        EPRJ_LIST 
      WHERE EPRJ_INFO_ID IN (214310, 214311, 214312) 
        AND PARENT_ID = 0 
        AND PROCESS = 1 
        AND STEP = 70 
        AND IS_DELETED = 0 
      ORDER BY eprj_info_id DESC ,psq DESC limit 1000) t -- 這個1000可能還不夠
    GROUP BY t.EPRJ_INFO_ID
  • 在select 裏面加入distinct (已經測試過 , 和limit的效果一樣,加了distinct 之後,explain就是兩個;不加explain就是一條) ,如下 :
EXPLAIN select  id,relate_id from (select DISTINCT id,relate_id from tests ORDER BY created_at desc limit 1000) as t GROUP BY relate_id;

 

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