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為:
- 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來查詢總數
- select count(DISTINCT SUBSTRING_INDEX(`url`,'/',3)) as c from tab where type = 4
但是 查詢資料中的sql 有 having 子句,這樣得到的總數是沒有經過條件篩選的。這個結果是錯誤的。
方法二: 通過 SQL_CALC_FOUND_ROWS 選項忽略 LIMIT 子句,然後通過FOUND_ROWS()獲得查詢總數,那麼sql改為:
- 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獲得總數
先:
- SELECT SUBSTRING_INDEX(`url`,'/',3) AS host,COUNT(*) AS count FROM `tab` WHERE `type`=4 GROUP BY host HAVING(count >= 5)
然後:
- select FOUND_ROWS();
最後:
- 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
這個沒有問題,也可以避免方法二中的問題,但是會返回全部的符合條件的資料,並且返回的資料沒有任何作用,只是查詢一次總數,所以也不可取。
方法四:使用子查詢
- 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語句是這樣產生的:
- String count_sql = dialect.getCountString(sql);
mybatis分頁外掛paginator中,mysql方言是這樣實現的:
- /**
- * 將sql轉換為總記錄數SQL
- * @param sql SQL語句
- * @return 總記錄數的sql
- */
- public String getCountString(String sql){
- return "select count(1) from (" + sql + ") tmp_count";
- }
當我看到這段原始碼的時候,有種想罵孃的感覺,mybatis官方提供的這種count寫法,效能真不敢恭維!
於是乎親自動手覆蓋瞭如下方法:
- /**
- * 優化父類的getCountString效能
- */
- public String getCountString(String sql) {
- try {
- boolean queryCacheable = queryCachedFlag.get() != null && queryCachedFlag.get();
- queryCachedFlag.remove();// 使用一次清理一次
- return MySqlSmartCountUtil.getSmartCountSql(sql, queryCacheable);
- } catch (JSQLParserException e) {
- e.printStackTrace();
- } catch (Exception e) {
- e.printStackTrace();
- }
- return "select count(*) from (" + sql + ") tmp_count";
- }
MySqlSmartCountUtil就是今天介紹的大神,是用jsqlparser寫的智慧生產count語句的工具類,採用了mysql查詢快取和獲取count語句靜態快取的策略,大大提升了只能生產count語句的時間,和count查詢的時間;原始碼分享給大家:
- public class MySqlSmartCountUtil {
- // countSql快取
- private static HashMap<String, String> countSqlCache = new HashMap<String, String>();
- private static HashMap<String, String> queryCacheableCountSqlCache = new HashMap<String, String>();
- private static final List<SelectItem> countItem = new ArrayList<SelectItem>();
- private static final List<SelectItem> sqlCachedCountItem = new ArrayList<SelectItem>();
- static {
- countItem.add(new SelectExpressionItem(new Column("count(*) as totalX")));
- sqlCachedCountItem.add(new SelectExpressionItem(new Column("sql_cache count(*) as totalX")));
- }
- private static void cacheSmartCountSql(String srcSql, String countSql, boolean queryCacheable) {
- if (queryCacheable)
- queryCacheableCountSqlCache.put(srcSql, countSql);
- else
- countSqlCache.put(srcSql, countSql);
- }
- private static List<SelectItem> getCountItem(boolean queryCacheable) {
- return queryCacheable ? sqlCachedCountItem : countItem;
- }
- private static void smartCountPlainSelect(PlainSelect plainSelect, boolean queryCacheable) throws JSQLParserException{
- // 去掉orderby
- OrderByUtil.removeOrderBy(plainSelect);
- // 判斷是否包含group by
- if(GMUtil.isEmpty(plainSelect.getGroupByColumnReferences())){
- plainSelect.setSelectItems(getCountItem(queryCacheable));
- } else {
- throw new JSQLParserException("不支援智慧count的sql格式: GROUP BY ");
- }
- }
- public static String getSmartCountSql(String srcSql, boolean queryCacheable) throws JSQLParserException {
- // 直接從快取中取
- if(!queryCacheable && countSqlCache.containsKey(srcSql))
- return countSqlCache.get(srcSql);
- if(queryCacheable && queryCacheableCountSqlCache.containsKey(srcSql))
- return queryCacheableCountSqlCache.get(srcSql);
- Statement stmt = CCJSqlParserUtil.parse(srcSql);
- Select select = (Select) stmt;
- SelectBody selectBody = select.getSelectBody();
- if (selectBody instanceof PlainSelect) {
- PlainSelect plainSelect = ((PlainSelect) selectBody);
- smartCountPlainSelect(plainSelect, queryCacheable);
- } else if (selectBody instanceof SetOperationList) {
- SetOperationList setOperationList = (SetOperationList) selectBody;
- boolean isUnion = false;
- for (SetOperation o : setOperationList.getOperations()) {
- isUnion = (o.toString().contains("UNION"));
- if (!isUnion)
- break;
- }
- // union all 語句的智慧count
- if(isUnion){
- for (PlainSelect ps : setOperationList.getPlainSelects()) {
- smartCountPlainSelect(ps, false);// TODO 強制不允許快取
- }
- String resultSql = "select sum(totalX) from (" + select.toString() + ") as t ";
- cacheSmartCountSql(srcSql, resultSql, false);// TODO 強制不允許快取
- return resultSql;
- } else {
- throw new JSQLParserException("不支援智慧count的sql格式");
- }
- } else {
- throw new JSQLParserException("不支援智慧count的sql格式");
- }
- cacheSmartCountSql(srcSql, select.toString(), queryCacheable);
- return select.toString();
- }
- }
目前該工具類可以支援簡單的select查詢,group by查詢,union查詢,更為複雜的查詢還沒有測試過,不過即使你的sql很複雜,最悲催的結局就是工具類丟擲異常,方言類中會使用paginator古老的count語句為你服務!