1. 程式人生 > >MySQL查詢分頁,同時獲取總記錄數

MySQL查詢分頁,同時獲取總記錄數

Mysql分頁查詢獲取totalCount大幅提升效能的辦法總結

MySQL查詢分頁,通常在MySQL中獲取記錄總數都會使用SELECT COUNT(*) FROM tblName之類的語句

這類語句的缺點是:記錄集還需要單獨的查詢來獲取,相當於查詢兩次,推薦如下寫法:

SELECT   SQL_CALC_FOUND_ROWS fldName1, fldName2 FROM tblName WHERE fldName3 = 1 LIMIT 10, OFFSET 20;

SELECT FOUND_ROWS();

雖然有兩條SQL語句,但實際上只執行了一次資料庫查詢

做分頁查詢中,一般情況下需要兩個sql,查當前頁資料 和 查記錄總條數;但後者的查詢結果變化有時候並不大,而且count還佔用了很大一部分的查詢時間;主要是想用一種省時簡便的方法查詢符合條件的記錄總數,

查詢資料使用的sql為:

  1. SELECT SUBSTRING_INDEX(`url`,'/',3) AS host,COUNT(*) AS count FROM `tab` WHERE `type`=4 GROUP BY host HAVING(count >= 5) ORDER BY count desc LIMIT 0,10

以下是網上查到的一些嘗試過的方法(不過後來都感覺不太合適,所以,亮點在最後):

方法一: 一般情況下可以使用DISTINCT來查詢總數

  1. select count(DISTINCT SUBSTRING_INDEX(`url`,'/',3)) as c from tab where type = 4

但是 查詢資料中的sql 有 having 子句,這樣得到的總數是沒有經過條件篩選的。這個結果是錯誤的。

方法二: 通過 SQL_CALC_FOUND_ROWS 選項忽略 LIMIT 子句,然後通過FOUND_ROWS()獲得查詢總數,那麼sql改為:

  1. SELECT SQL_CALC_FOUND_ROWS SUBSTRING_INDEX(`url`,'/',3) AS host,COUNT(*) AS count FROM `tab` WHERE `type`=4 GROUP BY host HAVING(count >= 5) ORDER BY count desc LIMIT 0,10

再通過 select FOUND_ROWS(); 獲得總數

這樣獲得的總數沒問題,但是由於分頁程式需要先獲得符合條件的總數,才能生成 page_list ,以及驗證offset,和總頁數等資訊,所以不能先查詢資料再得總數。

方法三:和上邊的方法類似,只是第一次使用sql獲得總數

先:

  1. SELECT SUBSTRING_INDEX(`url`,'/',3) AS host,COUNT(*) AS count FROM `tab` WHERE `type`=4 GROUP BY host HAVING(count >= 5)

然後:

  1. select FOUND_ROWS();

最後:

  1. SELECT SUBSTRING_INDEX(`url`,'/',3) AS host,COUNT(*) AS count FROM `tab` WHERE `type`=4 GROUP BY host HAVING(count >= 5) ORDER BY count desc LIMIT 0,10

這個沒有問題,也可以避免方法二中的問題,但是會返回全部的符合條件的資料,並且返回的資料沒有任何作用,只是查詢一次總數,所以也不可取。

方法四:使用子查詢

  1. select count(*) as count from (select SUBSTRING_INDEX(url,'/',3) as host,count(*) as c from tab where type=4 group by host having(c >= 5)) as temp

這個基本滿足了需要,但是效率不是很高,如果子集很大的話,效能上是個問題。

以上4種方法,是網上查到的,但感覺都不是特別好或特別通用;後來經多方努力查詢和學習,選用了自己寫一套智慧生產count查詢語句的方案;

該方案採用了第三方包jsqlparser來解析sql結構並智慧拼接count查詢語句;

以我現在使用的java語言mybatis框架為示例:

框架中分頁查詢的count語句是這樣產生的:

  1. String count_sql = dialect.getCountString(sql);  

mybatis分頁外掛paginator中,mysql方言是這樣實現的:

  1. /**  
  2.      * 將sql轉換為總記錄數SQL  
  3.      * @param sql SQL語句  
  4.      * @return 總記錄數的sql  
  5.      */  
  6.     public String getCountString(String sql){  
  7.         return "select count(1) from (" + sql + ") tmp_count";  
  8.     }  

當我看到這段原始碼的時候,有種想罵孃的感覺,mybatis官方提供的這種count寫法,效能真不敢恭維!

於是乎親自動手覆蓋瞭如下方法:

  1. /** 
  2.  * 優化父類的getCountString效能 
  3.  */  
  4. public String getCountString(String sql) {  
  5.     try {  
  6.         boolean queryCacheable = queryCachedFlag.get() != null && queryCachedFlag.get();  
  7.         queryCachedFlag.remove();// 使用一次清理一次  
  8.         return MySqlSmartCountUtil.getSmartCountSql(sql, queryCacheable);  
  9.     } catch (JSQLParserException e) {  
  10.         e.printStackTrace();  
  11.     } catch (Exception e) {  
  12.         e.printStackTrace();  
  13.     }  
  14.     return "select count(*) from (" + sql + ") tmp_count";  
  15. }  

MySqlSmartCountUtil就是今天介紹的大神,是用jsqlparser寫的智慧生產count語句的工具類,採用了mysql查詢快取和獲取count語句靜態快取的策略,大大提升了只能生產count語句的時間,和count查詢的時間;原始碼分享給大家:

  1. public class MySqlSmartCountUtil {  
  2.     // countSql快取  
  3.     private static HashMap<String, String> countSqlCache = new HashMap<String, String>();  
  4.     private static HashMap<String, String> queryCacheableCountSqlCache = new HashMap<String, String>();  
  5.     private static final List<SelectItem> countItem = new ArrayList<SelectItem>();  
  6.     private static final List<SelectItem> sqlCachedCountItem = new ArrayList<SelectItem>();  
  7.     static {  
  8.         countItem.add(new SelectExpressionItem(new Column("count(*) as totalX")));  
  9.         sqlCachedCountItem.add(new SelectExpressionItem(new Column("sql_cache count(*) as totalX")));  
  10.     }  
  11.     private static void cacheSmartCountSql(String srcSql, String countSql, boolean queryCacheable) {  
  12.         if (queryCacheable)  
  13.             queryCacheableCountSqlCache.put(srcSql, countSql);  
  14.         else  
  15.             countSqlCache.put(srcSql, countSql);  
  16.     }  
  17.     private static List<SelectItem> getCountItem(boolean queryCacheable) {  
  18.         return queryCacheable ? sqlCachedCountItem : countItem;  
  19.     }  
  20.     private static void smartCountPlainSelect(PlainSelect plainSelect, boolean queryCacheable) throws JSQLParserException{  
  21.         // 去掉orderby  
  22.         OrderByUtil.removeOrderBy(plainSelect);  
  23.         // 判斷是否包含group by  
  24.         if(GMUtil.isEmpty(plainSelect.getGroupByColumnReferences())){  
  25.             plainSelect.setSelectItems(getCountItem(queryCacheable));  
  26.         } else {  
  27.             throw new JSQLParserException("不支援智慧count的sql格式: GROUP BY ");  
  28.         }  
  29.     }  
  30.     public static String getSmartCountSql(String srcSql, boolean queryCacheable) throws JSQLParserException {  
  31.         // 直接從快取中取  
  32.         if(!queryCacheable && countSqlCache.containsKey(srcSql))  
  33.             return countSqlCache.get(srcSql);  
  34.         if(queryCacheable && queryCacheableCountSqlCache.containsKey(srcSql))  
  35.             return queryCacheableCountSqlCache.get(srcSql);  
  36.         Statement stmt = CCJSqlParserUtil.parse(srcSql);  
  37.         Select select = (Select) stmt;  
  38.         SelectBody selectBody = select.getSelectBody();  
  39.         if (selectBody instanceof PlainSelect) {  
  40.             PlainSelect plainSelect = ((PlainSelect) selectBody);  
  41.             smartCountPlainSelect(plainSelect, queryCacheable);  
  42.         } else if (selectBody instanceof SetOperationList) {  
  43.             SetOperationList setOperationList = (SetOperationList) selectBody;  
  44.             boolean isUnion = false;  
  45.             for (SetOperation o : setOperationList.getOperations()) {  
  46.                 isUnion = (o.toString().contains("UNION"));  
  47.                 if (!isUnion)  
  48.                     break;  
  49.             }  
  50.             // union all 語句的智慧count  
  51.             if(isUnion){  
  52.                 for (PlainSelect ps : setOperationList.getPlainSelects()) {  
  53.                     smartCountPlainSelect(ps, false);// TODO 強制不允許快取  
  54.                 }  
  55.                 String resultSql = "select sum(totalX) from (" + select.toString() + ") as t ";  
  56.                 cacheSmartCountSql(srcSql, resultSql, false);// TODO 強制不允許快取  
  57.                 return resultSql;  
  58.             } else {  
  59.                 throw new JSQLParserException("不支援智慧count的sql格式");  
  60.             }  
  61.         } else {  
  62.             throw new JSQLParserException("不支援智慧count的sql格式");  
  63.         }  
  64.         cacheSmartCountSql(srcSql, select.toString(), queryCacheable);  
  65.         return select.toString();  
  66.     }  
  67. }  

目前該工具類可以支援簡單的select查詢,group by查詢,union查詢,更為複雜的查詢還沒有測試過,不過即使你的sql很複雜,最悲催的結局就是工具類丟擲異常,方言類中會使用paginator古老的count語句為你服務!