慢SQL優化實戰筆記

1、存在問題

通過sql慢查詢的優化,咱們系統中發現瞭如下幾種類型的問題:php

1.未建索引:整張表沒有建索引;

2.索引未命中:有索引,可是部分查詢條件下索引未命中;

3.搜索了額外的非必要字段,致使回表;

4.排序,聚合致使慢查詢;

5.相同內容屢次查詢數據庫;

6.未消限制搜索範圍或者限制的搜索範圍在預期以外,致使所有掃描;

2、解決方案

1.優化索引,增長或者修改當前的索引;         

2.重寫sql;

3.利用redis緩存,減小查詢次數;

4.增長條件,避免非必要查詢;

5.增長條件,減小查詢範圍;                          

3、案例分析

(一)藥材搜索接口

完整sql語句在附錄,爲方便閱讀和脫敏,部分經常使用字段採用中文。mysql

這兒主要講一下咱們拿到Sql語句後的整個分析過程,思考邏輯,而後進行調整的過程和最後解決的辦法。redis

給你們提供一些借鑑,也但願你們可以提出更好的建議。                sql

這個sql語句要求是根據醫生搜索的拼音或者中文,進行模糊查詢,找到藥材,而後根據醫生選擇的藥庫,查找下面的供應商,而後根據供應商,進行藥材匹配,排除掉供應商沒有的藥材,而後根據真名在前,別名在後,徹底匹配在前,部分匹配在後,附加醫生最近半年的使用習慣,把藥材排序出來。最後把不一樣名稱的同一味藥聚合起來,以真名(另名)的形式展示。數據庫

1.分析sql

  • (1)14-8

第14排,id爲8的explain結果分析:緩存

①Explain
8,DERIVED,ssof,range,"ix_district,ix_供應商id",ix_district,8,NULL,18,Using where; Using index; Using temporary
②Sql
SELECT DISTINCT (ssof.供應商id) AS 供應商id FROM  藥庫供應商關係表 AS ssof  WHERE ssof.藥庫id IN (  1, 2, 8, 9, 10, 11, 12, 13, 14, 15, 17, 22, 24, 25, 26, 27, 31, 33)  AND ssof.藥方劑型id IN (1)
③索引
PRIMARY KEY (`id`),    UNIQUE KEY `ix_district` (        `藥庫id`, `藥方劑型id`, `供應商id`    ) USING BTREE,KEY `ix_供應商id` (`供應商id`) USING BTREE
④分析

使用了索引,創建了臨時表,這個地方索引已經徹底覆蓋了,可是還有回表操做。函數

緣由是用in,這個致使了回表。若是in能夠被mysql 自動優化爲等於,就不會回表。若是沒法優化,就回表。性能

臨時表是由於有distinct,因此沒法避免。大數據

同時使用in須要注意,若是裏面的值數量比較多,有幾萬個。即便區分度高,就會致使索引失效,這種狀況須要屢次分批查詢。優化

2. 12-7

  • (1)Explain
7,DERIVED,<derived8>,ALL,NULL,NULL,NULL,NULL,18,Using temporary; Using filesort
  • (2)Sql
INNER JOIN (上面14-8臨時表) tp ON tp.供應商id= ms.供應商id
  • (3)索引

  • (4)分析

對臨時表操做,無索引,用了文件排序。

這一部分是對臨時表和藥材表進行關聯操做的一部分,有文件排序是由於須要對藥材表id進行group by 致使的。

   一、默認狀況下,mysql在使用group by以後,會產生臨時表,然後進行排序(此處排序默認是快排),這會消耗的性能。

   二、group by本質是先分組後排序【而不是先排序後分組】。

   三、group by column 默認會按照column分組, 而後根據column升序排列;  group by column order by null 則默認按照column分組,而後根據標的主鍵ID升序排列。

3. 13-7

  • (1)Explain
7,DERIVED,ms,ref,"ix_title,idx_audit,idx_mutiy",idx_mutiy,5,"tp.供應商id,const",172,NULL
  • (2)Sql
SELECT ms.藥材表id, max(ms.audit) AS audit, max(ms.price) AS price, max(ms.market_price) AS market_price,max(ms.is_granule) AS is_granule,max(ms.is_decoct) AS is_decoct, max(ms.is_slice) AS is_slice,max(ms.is_cream) AS is_cream, max(ms.is_extract) AS is_extract,max(ms.is_cream_granule) AS is_cream_granule, max(ms.is_extract_granule) AS is_extract_granule,max(ms.is_drychip) AS is_drychip,            max(ms.is_pill) AS is_pill,max(ms.is_powder) AS is_powder, max(ms.is_bolus) AS is_bolus FROM 供應商藥材表 AS ms INNER JOIN (                SELECT                    DISTINCT (ssof.供應商id) AS 供應商id                FROM                    藥庫供應商關係表 AS ssof WHERE  ssof.藥庫id IN (  1, 2, 8, 9, 10, 11, 12, 13, 14, 15, 17, 22, 24, 25, 26, 27, 31, 33 ) AND ssof.藥方劑型id IN (1) ) tp ON tp.供應商id= ms.供應商id WHERE  ms.audit = 1  GROUP BY  ms.藥材表id
  • (3)索引
KEY `idx_mutiy` (`供應商id`, `audit`, `藥材表id`)
  • (4)分析

命中了索引,表間鏈接使用了供應商id,創建索引的順序是供應商id,where條件中audit,Group by 條件藥材表id。

這部分暫時不須要更改。

4.10-6

  • (1)Explain
6,DERIVED,r,range,"PRIMARY,id,idx_timeline,idx_did_timeline,idx_did_isdel_statuspay_timecreate_payorderid,idx_did_statuspay_ischecked_isdel",idx_did_timeline,8,NULL,546,Using where; Using index; Using temporary; Using filesort
  • (2)Sql
SELECT 
       count(*) AS total, 
       rc.i AS m藥材表id 
     FROM 
        處方藥材表 AS rc 
        INNER JOIN 藥方表AS r ON r.id = rc.藥方表_id 
     WHERE 
         r.did = 40 
         AND r.timeline > 1576115196 
         AND rc.type_id in (1, 3) 
         GROUP BY 
    rc.i
  • (3)索引
KEY `idx_did_timeline` (`did`, `timeline`),
  • (4)分析

驅動表與被驅動表,小表驅動大表。

先了解在join鏈接時哪一個表是驅動表,哪一個表是被驅動表:

1.當使用left join時,左表是驅動表,右表是被驅動表;

2.當使用right join時,右表時驅動表,左表是驅動表;

3.當使用join時,mysql會選擇數據量比較小的表做爲驅動表,大表做爲被驅動表;

4. in後面跟的是驅動表, exists前面的是驅動表;

5. 11-6

  • (1)Explain
6,DERIVED,rc,ref,"orderid_藥材表,藥方表_id",藥方表_id,5,r.id,3,Using where
  • (2)Sql

同上

  • (3)索引
KEY `idx_藥方表_id` (`藥方表_id`, `type_id`) USING BTREE,
  • (4)分析

索引的順序沒有問題,仍舊是in 致使了回表。

6.8-5

  • (1)Explain
5,UNION,malias,ALL,id_tid,NULL,NULL,NULL,4978,Using where
  • (2)Sql
SELECT 
      mb.id, 
      mb.sort_id, 
      mb.title, 
      mb.py, 
      mb.unit, 
      mb.weight, 
      mb.tid, 
      mb.amount_max, 
      mb.poisonous, 
      mb.is_auxiliary, 
      mb.is_auxiliary_free, 
      mb.is_difficult_powder, 
      mb.brief, 
      mb.is_fixed_recipe, 
      ASE WHEN malias.py = 'GC' THEN malias.title ELSE CASE WHEN malias.title = 'GC' THEN malias.title ELSE '' END END AS atitle, 
      alias.py AS apy, 
      CASE WHEN malias.py = 'GC' THEN 2 ELSE CASE WHEN malias.title = 'GC' THEN 2 ELSE 1 END END AS ttid 
 FROM 
      藥材表 AS mb 
      LEFT JOIN 藥材表 AS malias ON malias.tid = mb.id 
WHERE 
      alias.title LIKE '%GC%' 
      OR malias.py LIKE '%GC%'
  • (3)索引 
KEY `id_tid` (`tid`) USING BTREE,
  • (4)分析

由於like是左右like,沒法創建索引,因此只能建tid。Type是all,遍歷全表以找到匹配的行,左右表大小同樣,估算的找到所需的記錄所須要讀取的行數有4978。這個由於是like的緣故,沒法優化,這個語句並無走索引,藥材表 AS mb FORCE INDEX (id_tid) 改成強制索引,讀取的行數減小了700行。

7.9-5

  • (1)Explain
5,UNION,mb,eq_ref,"PRIMARY,ix_id",PRIMARY,4,malias.tid,1,NULL
  • (2)Sql

同上

  • (3)索引
PRIMARY KEY (`id`) USING BTREE,
  • (4)分析

走了主鍵索引,行數也少,經過。

8.7-4

  • (1)Explain
4,DERIVED,mb,ALL,id_tid,NULL,NULL,NULL,4978,Using where
  • (2)Sql           
SELECT 
       mb.id, 
       mb.sort_id, 
       mb.title, 
       mb.py, 
       mb.unit, 
       mb.weight, 
       mb.tid, 
       mb.amount_max, 
       mb.poisonous, 
       mb.is_auxiliary, 
       mb.is_auxiliary_free, 
       mb.is_difficult_powder, 
       mb.brief, 
       mb.is_fixed_recipe, 
       '' AS atitle, 
       '' AS apy, 
       CASE WHEN mb.py = 'GC' THEN 3 ELSE CASE WHEN mb.title = 'GC' THEN 3 ELSE 1 END END AS ttid 
    FROM 
       藥材表 AS mb 
      WHERE 
       mb.tid = 0 
       AND (
           mb.title LIKE '%GC%' 
           OR mb.py LIKE '%GC%'
                                )
  • (3)索引
KEY `id_tid` (`tid`) USING BTREE,
  • (4)分析

 tid int(11) NOT NULL DEFAULT '0' COMMENT '真名藥品的id',

他也是like,這個無法優化。

9.6-3

  • (1)Explain
3,DERIVED,<derived4>,ALL,NULL,NULL,NULL,NULL,9154,Using filesort
  • (2)Sql

  UNION ALL

  • (3)索引

  • (4)分析

就是把真名搜索結果和別人搜索結果合併。避免用or鏈接,加快速度 造成一個munion的表,初步完成藥材搜索,接下去就是排序。

這一個進行了2次查詢,而後用union鏈接,能夠考慮合併爲一次查詢。用case when進行區分,計算出權重。

這邊是一個優化點。

10.4-2

  • (1)Explain
2,DERIVED,<derived3>,ALL,NULL,NULL,NULL,NULL,9154,NULL
  • (2)Sql
SELECT 
       munion.id, 
       munion.sort_id, 
       case when length(
         trim(
              group_concat(munion.atitle SEPARATOR ' ')
                        )
                    )> 0 then concat(
                        munion.title, 
                        '(', 
                        trim(
                            group_concat(munion.atitle SEPARATOR ' ')
                        ), 
                        ')'
                    ) else munion.title end as title, 
          munion.py, 
          munion.unit, 
          munion.weight, 
          munion.tid, 
          munion.amount_max, 
          munion.poisonous, 
          munion.is_auxiliary, 
          munion.is_auxiliary_free, 
          munion.is_difficult_powder, 
          munion.brief, 
          munion.is_fixed_recipe, 
          --  trim( group_concat( munion.atitle SEPARATOR ' ' ) ) AS atitle,
                    ##  --  
           trim(
                 group_concat(munion.apy SEPARATOR ' ')
                 ) AS apy, 
             ##   
               max(ttid) * 100000 + id AS ttid 
         FROM 
                munion <derived4>
             GROUP BY 
                id -- 所有實名藥材 結束##
  • (3)索引

  • (4)分析

這裏所有在臨時表中搜索了。

11.5-2

  • (1)Explain
2,DERIVED,<derived6>,ref,<auto_key0>,<auto_key0>,5,m.id,10,NULL
  • (2)Sql
Select fields from 所有實名藥材表 as m  LEFT JOIN ( 我的使用藥材統計表 ) p ON m.id = p.m藥材表id
  • (3)索引

  • (4)分析

2張虛擬表left join

使用了優化器爲派生表生成的索引<auto_key0>

這邊比較浪費性能,每次查詢,都要對醫生歷史開方記錄進行統計,而且統計仍是幾張大表計算後的結果。可是若是隻是sql優化,這邊暫時沒法優化。

12.2-1

  • (1)Explain
1,PRIMARY,<derived7>,ALL,NULL,NULL,NULL,NULL,3096,Using where; Using temporary; Using filesort
  • (2)Sql
  • (3)索引
  • (4)分析

臨時表操做

13.3-1

  • (1)Explain
1,PRIMARY,<derived2>,ref,<auto_key0>,<auto_key0>,4,msu.藥材表id,29,NULL
  • (2)Sql
  • (3)索引
  • (4)分析

臨時表操做

14.null

  • (1)Explain
NULL,UNION RESULT,"<union4,5>",ALL,NULL,NULL,NULL,NULL,NULL,Using temporary
  • (2)Sql
  • (3)索引
  • (4)分析

臨時表

(二)優化sql

上面咱們只作索引的優化,遵循的原則是:

1.最左前綴匹配原則,很是重要的原則,mysql會一直向右匹配直到遇到範圍查詢(>、<、between、like)就中止匹配,好比a = 1 and b = 2 and c > 3 and d = 4 若是創建(a,b,c,d)順序的索引,d是用不到索引的,若是創建(a,b,d,c)的索引則均可以用到,a,b,d的順序能夠任意調整。

2.=和in能夠亂序,好比a = 1 and b = 2 and c = 3 創建(a,b,c)索引能夠任意順序,mysql的查詢優化器會幫你優化成索引能夠識別的形式。

3.儘可能選擇區分度高的列做爲索引,區分度的公式是count(distinct col)/count(*),表示字段不重複的比例,比例越大咱們掃描的記錄數越少,惟一鍵的區分度是1,而一些狀態、性別字段可能在大數據面前區分度就是0,那可能有人會問,這個比例有什麼經驗值嗎?使用場景不一樣,這個值也很難肯定,通常須要join的字段咱們都要求是0.1以上,即平均1條掃描10條記錄。

4.索引列不能參與計算,保持列「乾淨」,好比from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,緣由很簡單,b+樹中存的都是數據表中的字段值,但進行檢索時,須要把全部元素都應用函數才能比較,顯然成本太大。因此語句應該寫成create_time = unix_timestamp(’2014-05-29’)。

5.儘可能的擴展索引,不要新建索引。好比表中已經有a的索引,如今要加(a,b)的索引,那麼只須要修改原來的索引便可。

查詢優化神器 - explain命令

關於explain命令相信你們並不陌生,具體用法和字段含義能夠參考官網explain-output,這裏須要強調rows是核心指標,絕大部分rows小的語句執行必定很快(有例外,下面會講到)。因此優化語句基本上都是在優化rows。

化基本步驟:

0.先運行看看是否真的很慢,注意設置SQL_NO_CACHE

1.where條件單表查,鎖定最小返回記錄表。這句話的意思是把查詢語句的where都應用到表中返回的記錄數最小的表開始查起,單表每一個字段分別查詢,看哪一個字段的區分度最高;

2.explain查看執行計劃,是否與1預期一致(從鎖定記錄較少的表開始查詢);

3.order by limit 形式的sql語句讓排序的表優先查;

4.瞭解業務方使用場景;

5.加索引時參照建索引的幾大原則;

6.觀察結果,不符合預期繼續從0分析;

上面已經詳細的分析了每個步驟,根據上面的sql,去除union操做, 增長索引。能夠看出,優化後雖然有所改善。可是距離咱們的但願還有很大距離,可是光作sql優化,感受也沒有多少改進空間,因此決定從其餘方面解決。

(三)拆分sql

   因爲速度仍是不領人滿意,尤爲是我的用藥狀況統計,其實不必每次都所有統計一次,再要優化,只靠修改索引應該是不行的了,因此考慮使用緩存。

接下來是修改php代碼,把所有sql語句拆分,而後再組裝。

  • (1)搜索真名,別名(緩存)
SELECT  mb.id,  mb.sort_id,  mb.title,  mb.py,  mb.unit,  mb.weight,  mb.tid,  mb.amount_max,  mb.poisonous,  mb.is_auxiliary,  mb.is_auxiliary_free,  mb.is_difficult_powder,  mb.brief,  mb.is_fixed_recipe,  IFNULL(group_concat(malias.title),'') atitle,  IFNULL(group_concat(malias.py),'') apy  FROM  藥材表 AS mb  LEFT JOIN 藥材表 AS malias ON malias.tid = mb.id  WHERE  mb.tid = 0  AND (  malias.title LIKE '%GC%'  OR malias.py LIKE '%GC%'  or mb.title LIKE '%GC%'  OR mb.py LIKE '%GC%'  )  group by  mb.id

  • (2)若是命中有藥材
①排序

真名在前,別名在後,徹底匹配在前,部分匹配在後

//對搜索結果進行處理,增長權重
②對供應商藥材搜索
SELECT ms.藥材表id, max( ms.audit ) AS audit, max( ms.price ) AS price, max( ms.market_price ) AS market_price, max( ms.is_granule ) AS is_granule, max( ms.is_decoct ) AS is_decoct, max( ms.is_slice ) AS is_slice, max( ms.is_cream ) AS is_cream, max( ms.is_extract ) AS is_extract, max( ms.is_cream_granule) AS is_cream_granule, max( ms.is_extract_granule) AS is_extract_granule, max( ms.is_drychip ) AS is_drychip, max( ms.is_pill ) AS is_pill, max( ms.is_powder ) AS is_powder, max( ms.is_bolus ) AS is_bolus  FROM 供應商藥材表 AS ms WHERE ms.audit = 1 AND ms.供應商idin (  SELECT DISTINCT  ( ssof.供應商id) AS 供應商id FROM  藥庫供應商關係表 AS ssof  WHERE  ssof.藥庫id IN ( 1,2,8,9,10,11,12,13,14,15,17,22,24,25,26,27,31,33 )  AND ssof.藥方劑型id IN (1) ) AND ms.藥材表id IN ( 78,205,206,207,208,209,334,356,397,416,584,652,988,3001,3200,3248,3521,3522,3599,3610,3624,4395,4396,4397,4398,4399,4400,4401,4402,4403,4404,4405,4406,4407,4408,5704,5705,5706,5739,5740,5741,5742,5743,6265,6266,6267,6268,6514,6515,6516,6517,6518,6742,6743 ) AND ms.is_slice = 1  GROUP BY ms.藥材表id  

③拿醫生歷史開方藥材用量數據(緩存)
SELECT  count( * ) AS total,  rc.i AS 藥材表id FROM  處方藥材表 AS rc  INNER JOIN 藥方表AS r ON r.id = rc.藥方表_id WHERE  r.did = 40  AND r.timeline > 1576116927  AND rc.type_id in (1,3) GROUP BY  rc.i

④  裝配及排序微調

  • (3)小結

運行速度,對於開方量不是特別多的醫生來講,二者速度都是0.1秒左右.可是若是碰到開方量大的醫生,優化後的sql速度比較穩定,能始終維持在0.1秒左右,優化前的sql速度會超過0.2秒.速度提高約一倍以上。 

最後對搜索結果和未優化前的搜索結果進行比對,結果數量和順序徹底一致.本次優化結束。

4、附錄:

SELECT sql_no_cache 
    *
FROM
    (
        -- mbu start##
        SELECT
            m.*,
            ifnull(p.total, 0) AS total
        FROM
            (
                --
所有實名藥材
開始
##
SELECT
       munion.id,
       munion.sort_id,
       case when length(
        trim(
              group_concat(munion.atitle SEPARATOR ' ')
                 )
             )> 0 then concat(
           munion.title,
          '(',
      trim(
             group_concat(munion.atitle SEPARATOR ' ')
              ),
                ')'
             ) else munion.title end as title,
        munion.py,
        munion.unit,
        munion.weight,
        munion.tid,
        munion.amount_max,
        munion.poisonous,
        munion.is_auxiliary,
        munion.is_auxiliary_free,
        munion.is_difficult_powder,
        munion.brief,
        munion.is_fixed_recipe,
        --  trim( group_concat( munion.atitle SEPARATOR ' ' ) ) AS atitle,##
        --  trim( group_concat( munion.apy SEPARATOR  ' ' ) ) AS apy,##
              max(ttid) * 100000 + id AS ttid
           FROM
              (
                -- #union start
聯合查找
,
獲得所有藥材
##
  (
       SELECT
              mb.id,
              mb.sort_id,
              mb.title,
              mb.py,
              mb.unit,
              mb.weight,
              mb.tid,
              mb.amount_max,
              mb.poisonous,
              mb.is_auxiliary,
              mb.is_auxiliary_free,
              mb.is_difficult_powder,
              mb.brief,
              mb.is_fixed_recipe,
              '' AS atitle,
              '' AS apy,
              CASE WHEN mb.py = 'GC' THEN 3 ELSE CASE WHEN mb.title = 'GC' THEN 3 ELSE 1 END END AS ttid
               FROM
                 藥材表 AS mb
                     WHERE
                         mb.tid = 0
                       AND (
                              mb.title LIKE '%GC%'
                              OR mb.py LIKE '%GC%'
                                )
                        ) --
真名藥材
 
結束
##
 UNION ALL
    (
      SELECT
            mb.id,
            mb.sort_id,
            mb.title,
            mb.py,
            mb.unit,
            mb.weight,
            mb.tid,
            mb.amount_max,
            mb.poisonous,
            mb.is_auxiliary,
            mb.is_auxiliary_free,
            mb.is_difficult_powder,
            mb.brief,
            mb.is_fixed_recipe,
            CASE WHEN malias.py = 'GC' THEN malias.title ELSE CASE WHEN malias.title = 'GC' THEN malias.title ELSE '' END END AS atitle,
            malias.py AS apy,
            CASE WHEN malias.py = 'GC' THEN 2 ELSE CASE WHEN malias.title = 'GC' THEN 2 ELSE 1 END END AS ttid
          FROM
                藥材表 AS mb
                LEFT JOIN 藥材表 AS malias ON malias.tid = mb.id
          WHERE
                malias.title LIKE '%GC%'
                OR malias.py LIKE '%GC%'
                      ) --
其餘藥材結束
##
                 -- #union end##
                ) munion
                GROUP BY
                    id --
所有實名藥材
 
結束
##
                    ) m
            LEFT JOIN (
                --
我的使用藥材統計
 
開始
##
    SELECT
          count(*) AS total,
          rc.i AS m藥材表id
     FROM
           處方藥材表 AS rc
           INNER JOIN 藥方表AS r ON r.id = rc.藥方表_id
      WHERE
           r.did = 40
            AND r.timeline > 1576115196
            AND rc.type_id in (1, 3)
       GROUP BY
              rc.i --
我的使用藥材統計
 
結束
##
           ) p ON m.id = p.m藥材表id -- mbu end ##
            ) mbu
    INNER JOIN (
        -- msu start
供應商藥材篩選
##
        SELECT
            ms.藥材表id,
            max(ms.audit) AS audit,
            max(ms.price) AS price,
            max(ms.market_price) AS market_price,
            max(ms.is_granule) AS is_granule,
            max(ms.is_decoct) AS is_decoct,
            max(ms.is_slice) AS is_slice,
            max(ms.is_cream) AS is_cream,
            max(ms.is_extract) AS is_extract,
            max(ms.is_cream_granule) AS is_cream_granule,
            max(ms.is_extract_granule) AS is_extract_granule,
            max(ms.is_drychip) AS is_drychip,
            max(ms.is_pill) AS is_pill,
            max(ms.is_powder) AS is_powder,
            max(ms.is_bolus) AS is_bolus
        FROM
            供應商藥材表 AS ms
            INNER JOIN (
                SELECT
                    DISTINCT (ssof.供應商id) AS 供應商id
                FROM
                    藥庫供應商關係表 AS ssof
                WHERE
                    ssof.藥庫id IN (
                        1, 2, 8, 9, 10, 11, 12, 13, 14, 15, 17, 22,
                        24, 25, 26, 27, 31, 33
                    )
                    AND ssof.藥方劑型id IN (1)
            ) tp ON tp.供應商id= ms.供應商id
        WHERE
            ms.audit = 1
        GROUP BY
            ms.藥材表id -- msu end ##
            ) msu ON mbu.id = msu.藥材表id
WHERE
    msu.藥材表id > 0
    AND msu.is_slice = 1
order by
    total desc,
    ttid desc
相關文章
相關標籤/搜索