MySQL索引原理及慢查詢優(yōu)化--整理

摘自:
MySQL索引原理及慢查詢優(yōu)化
MySQL EXPLAIN詳解

1 索引是什么

索引是一種單獨(dú)的丈探、物理的對(duì)數(shù)據(jù)庫表中一列或多列的值進(jìn)行排序的一種存儲(chǔ)結(jié)構(gòu)郑临,它是某個(gè)表中一列或若干列值的集合和相應(yīng)的指向表中物理標(biāo)識(shí)這些值的數(shù)據(jù)頁的邏輯。索引的作用相當(dāng)于圖書的目錄摘悴,可以根據(jù)目錄快速找到所需的內(nèi)容赘被。
索引提供指向存儲(chǔ)在表的指定列中的數(shù)據(jù)值的指針,然后根據(jù)您指定的排序順序?qū)@些指針排序----《百度百科》

2 用什么索引策略

目的铡羡,一個(gè)字---快。

2.1 分段索引

分段索引——最簡單的如果1000條數(shù)據(jù)意鲸,1到100分成第一段烦周,101到200分成第二段,201到300分成第三段......這樣查第250條數(shù)據(jù)怎顾,只要找第三段就可以了读慎,一下子去除了90%的無效數(shù)據(jù)。但如果是1千萬的記錄呢杆勇,分成幾段比較好贪壳?稍有算法基礎(chǔ)的同學(xué)會(huì)想到搜索樹饱亿,其平均復(fù)雜度是lgN蚜退,具有不錯(cuò)的查詢性能。但這里我們忽略了一個(gè)關(guān)鍵的問題彪笼,復(fù)雜度模型是基于每次相同的操作成本來考慮的钻注,數(shù)據(jù)庫實(shí)現(xiàn)比較復(fù)雜,數(shù)據(jù)保存在磁盤上配猫,而為了提高性能幅恋,每次又可以把部分?jǐn)?shù)據(jù)讀入內(nèi)存來計(jì)算,因?yàn)槲覀冎涝L問磁盤的成本大概是訪問內(nèi)存的十萬倍左右泵肄,所以簡單的搜索樹難以滿足復(fù)雜的應(yīng)用場景捆交。

2.2 磁盤IO與預(yù)讀

磁盤讀取數(shù)據(jù)靠的是機(jī)械運(yùn)動(dòng),每次讀取數(shù)據(jù)花費(fèi)的時(shí)間可以分為尋道時(shí)間腐巢、旋轉(zhuǎn)延遲品追、傳輸時(shí)間三個(gè)部分,尋道時(shí)間指的是磁臂移動(dòng)到指定磁道所需要的時(shí)間冯丙,主流磁盤一般在5ms以下肉瓦;旋轉(zhuǎn)延遲就是我們經(jīng)常聽說的磁盤轉(zhuǎn)速,比如一個(gè)磁盤7200轉(zhuǎn),表示每分鐘能轉(zhuǎn)7200次泞莉,也就是說1秒鐘能轉(zhuǎn)120次哪雕,旋轉(zhuǎn)延遲就是1/120/2 = 4.17ms;傳輸時(shí)間指的是從磁盤讀出或?qū)?shù)據(jù)寫入磁盤的時(shí)間鲫趁,一般在零點(diǎn)幾毫秒斯嚎,相對(duì)于前兩個(gè)時(shí)間可以忽略不計(jì)。那么訪問一次磁盤的時(shí)間挨厚,即一次磁盤IO的時(shí)間約等于5+4.17 = 9ms左右孝扛,聽起來還挺不錯(cuò)的,但要知道一臺(tái)500 -MIPS的機(jī)器每秒可以執(zhí)行5億條指令幽崩,因?yàn)橹噶钜揽康氖请姷男再|(zhì)苦始,換句話說執(zhí)行一次IO的時(shí)間可以執(zhí)行40萬條指令,數(shù)據(jù)庫動(dòng)輒十萬百萬乃至千萬級(jí)數(shù)據(jù)慌申,每次9毫秒的時(shí)間陌选,顯然是個(gè)災(zāi)難。

考慮到磁盤IO是非常高昂的操作蹄溉,計(jì)算機(jī)操作系統(tǒng)做了一些優(yōu)化咨油,當(dāng)一次IO時(shí),不光把當(dāng)前磁盤地址的數(shù)據(jù)柒爵,而是把相鄰的數(shù)據(jù)也都讀取到內(nèi)存緩沖區(qū)內(nèi)役电,因?yàn)榫植款A(yù)讀性原理告訴我們,當(dāng)計(jì)算機(jī)訪問一個(gè)地址的數(shù)據(jù)的時(shí)候棉胀,與其相鄰的數(shù)據(jù)也會(huì)很快被訪問到法瑟。每一次IO讀取的數(shù)據(jù)我們稱之為一頁(page)。具體一頁有多大數(shù)據(jù)跟操作系統(tǒng)有關(guān)唁奢,一般為4k或8k霎挟,也就是我們讀取一頁內(nèi)的數(shù)據(jù)時(shí)候,實(shí)際上才發(fā)生了一次IO麻掸,這個(gè)理論對(duì)于索引的數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)非常有幫助酥夭。

2.3 索引的數(shù)據(jù)結(jié)構(gòu)--b+樹

我們現(xiàn)在總結(jié)一下,我們需要這種數(shù)據(jù)結(jié)構(gòu)能夠做些什么脊奋,其實(shí)很簡單熬北,那就是:每次查找數(shù)據(jù)時(shí)把磁盤IO次數(shù)控制在一個(gè)很小的數(shù)量級(jí)妖谴,最好是常數(shù)數(shù)量級(jí)啃擦。那么我們就想到如果一個(gè)高度可控的多路搜索樹是否能滿足需求呢?就這樣沸柔,b+樹應(yīng)運(yùn)而生最楷。


image.png

如上圖整份,是一顆b+樹待错,關(guān)于b+樹的定義可以參見B+樹,這里只說一些重點(diǎn)烈评,淺藍(lán)色的塊我們稱之為一個(gè)磁盤塊火俄,可以看到每個(gè)磁盤塊包含幾個(gè)數(shù)據(jù)項(xiàng)(深藍(lán)色所示)和指針(黃色所示),如磁盤塊1包含數(shù)據(jù)項(xiàng)17和35讲冠,包含指針P1瓜客、P2、P3竿开,P1表示小于17的磁盤塊谱仪,P2表示在17和35之間的磁盤塊,P3表示大于35的磁盤塊否彩。真實(shí)的數(shù)據(jù)存在于葉子節(jié)點(diǎn)即3疯攒、5、9列荔、10敬尺、13、15贴浙、28砂吞、29、36崎溃、60蜻直、75、79袁串、90概而、99。非葉子節(jié)點(diǎn)只不存儲(chǔ)真實(shí)的數(shù)據(jù)般婆,只存儲(chǔ)指引搜索方向的數(shù)據(jù)項(xiàng)到腥,如17、35并不真實(shí)存在于數(shù)據(jù)表中蔚袍。

2.4 b+樹的查找過程

如圖所示,如果要查找數(shù)據(jù)項(xiàng)29配名,那么首先會(huì)把磁盤塊1由磁盤加載到內(nèi)存啤咽,此時(shí)發(fā)生一次IO,在內(nèi)存中用二分查找確定29在17和35之間渠脉,鎖定磁盤塊1的P2指針宇整,內(nèi)存時(shí)間因?yàn)榉浅6蹋ㄏ啾却疟P的IO)可以忽略不計(jì),通過磁盤塊1的P2指針的磁盤地址把磁盤塊3由磁盤加載到內(nèi)存芋膘,發(fā)生第二次IO鳞青,29在26和30之間霸饲,鎖定磁盤塊3的P2指針,通過指針加載磁盤塊8到內(nèi)存臂拓,發(fā)生第三次IO厚脉,同時(shí)內(nèi)存中做二分查找找到29,結(jié)束查詢胶惰,總計(jì)三次IO傻工。真實(shí)的情況是,3層的b+樹可以表示上百萬的數(shù)據(jù)孵滞,如果上百萬的數(shù)據(jù)查找只需要三次IO中捆,性能提高將是巨大的,如果沒有索引坊饶,每個(gè)數(shù)據(jù)項(xiàng)都要發(fā)生一次IO泄伪,那么總共需要百萬次的IO,顯然成本非常非常高匿级。

2.5 b+樹性質(zhì)

1.通過上面的分析臂容,我們知道IO次數(shù)取決于b+樹的高度h,假設(shè)當(dāng)前數(shù)據(jù)表的數(shù)據(jù)為N根蟹,每個(gè)磁盤塊的數(shù)據(jù)項(xiàng)的數(shù)量是m脓杉,則有h=㏒(m+1)N(m+1是下標(biāo)),當(dāng)數(shù)據(jù)量N一定的情況下简逮,m越大球散,h越小散庶;而m = 磁盤塊的大小 / 數(shù)據(jù)項(xiàng)的大小蕉堰,磁盤塊的大小也就是一個(gè)數(shù)據(jù)頁的大小,是固定的悲龟,如果數(shù)據(jù)項(xiàng)占的空間越小屋讶,數(shù)據(jù)項(xiàng)的數(shù)量越多,樹的高度越低须教。這就是為什么每個(gè)數(shù)據(jù)項(xiàng)皿渗,即索引字段要盡量的小,比如int占4字節(jié)轻腺,要比bigint8字節(jié)少一半乐疆。這也是為什么b+樹要求把真實(shí)的數(shù)據(jù)放到葉子節(jié)點(diǎn)而不是內(nèi)層節(jié)點(diǎn),一旦放到內(nèi)層節(jié)點(diǎn)贬养,磁盤塊的數(shù)據(jù)項(xiàng)會(huì)大幅度下降挤土,導(dǎo)致樹增高。當(dāng)數(shù)據(jù)項(xiàng)等于1時(shí)將會(huì)退化成線性表误算。

2.當(dāng)b+樹的數(shù)據(jù)項(xiàng)是復(fù)合的數(shù)據(jù)結(jié)構(gòu)仰美,比如(name,age,sex)的時(shí)候迷殿,b+數(shù)是按照從左到右的順序來建立搜索樹的,比如當(dāng)(張三,20,F)這樣的數(shù)據(jù)來檢索的時(shí)候咖杂,b+樹會(huì)優(yōu)先比較name來確定下一步的所搜方向庆寺,如果name相同再依次比較age和sex,最后得到檢索的數(shù)據(jù)翰苫;但當(dāng)(20,F)這樣的沒有name的數(shù)據(jù)來的時(shí)候止邮,b+樹就不知道下一步該查哪個(gè)節(jié)點(diǎn),因?yàn)榻⑺阉鳂涞臅r(shí)候name就是第一個(gè)比較因子奏窑,必須要先根據(jù)name來搜索才能知道下一步去哪里查詢导披。比如當(dāng)(張三,F)這樣的數(shù)據(jù)來檢索時(shí),b+樹可以用name來指定搜索方向埃唯,但下一個(gè)字段age的缺失撩匕,所以只能把名字等于張三的數(shù)據(jù)都找到,然后再匹配性別是F的數(shù)據(jù)了墨叛, 這個(gè)是非常重要的性質(zhì)止毕,即索引的最左匹配特性。

3 慢查詢優(yōu)化
3.1 建索引的幾大原則

1.最左前綴匹配原則 非常重要的原則漠趁,mysql會(huì)一直向右匹配直到遇到范圍查詢(>扁凛、<、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的順序可以任意調(diào)整。

2.=和in可以亂序 比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意順序共缕,mysql的查詢優(yōu)化器會(huì)幫你優(yōu)化成索引可以識(shí)別的形式

3.盡量選擇區(qū)分度高的列作為索引 區(qū)分度的公式是count(distinct col)/count(*) 表示字段不重復(fù)的比例洗出,比例越大我們掃描的記錄數(shù)越少,唯一鍵的區(qū)分度是1图谷,而一些狀態(tài)翩活、性別字段可能在大數(shù)據(jù)面前區(qū)分度就是0,那可能有人會(huì)問蜓萄,這個(gè)比例有什么經(jīng)驗(yàn)值嗎隅茎?使用場景不同,這個(gè)值也很難確定嫉沽,一般需要join的字段我們都要求是0.1以上,即平均1條掃描10條記錄俏竞;

4.索引列不能參與計(jì)算绸硕,保持列“干凈”堂竟,比如from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,原因很簡單玻佩,b+樹中存的都是數(shù)據(jù)表中的字段值出嘹,但進(jìn)行檢索時(shí),需要把所有元素都應(yīng)用函數(shù)才能比較咬崔,顯然成本太大税稼。所以語句應(yīng)該寫成create_time = unix_timestamp(’2014-05-29’);

5.盡量的擴(kuò)展索引,不要新建索引 比如表中已經(jīng)有a的索引垮斯,現(xiàn)在要加(a,b)的索引郎仆,那么只需要修改原來的索引即可

3.2 查詢優(yōu)化神器 - explain命令
3.2.1 慢查詢優(yōu)化基本步驟
  • 0.先運(yùn)行看看是否真的很慢,注意設(shè)置SQL_NO_CACHE
  • 1.where條件單表查兜蠕,鎖定最小返回記錄表扰肌。這句話的意思是把查詢語句的where都應(yīng)用到表中返回的記錄數(shù)最小的表開始查起,單表每個(gè)字段分別查詢熊杨,看哪個(gè)字段的區(qū)分度最高
  • 2.explain查看執(zhí)行計(jì)劃曙旭,是否與1預(yù)期一致(從鎖定記錄較少的表開始查詢)
  • 3.order by limit 形式的sql語句讓排序的表優(yōu)先查
  • 4.了解業(yè)務(wù)方使用場景
  • 5.加索引時(shí)參照建索引的幾大原則
  • 6.觀察結(jié)果,不符合預(yù)期繼續(xù)從0分析
3.2.2 explain 的重要參數(shù)
image.png

id
id是用來順序標(biāo)識(shí)整個(gè)查詢中SELELCT 語句的晶府,在嵌套查詢中id越大的語句越先執(zhí)行桂躏。該值可能為NULL,如果這一行用來說明的是其他行的聯(lián)合結(jié)果川陆。

select_type
表示查詢的類型

image.png

table
對(duì)應(yīng)行正在訪問哪一個(gè)表剂习,表名或者別名
關(guān)聯(lián)優(yōu)化器會(huì)為查詢選擇關(guān)聯(lián)順序,左側(cè)深度優(yōu)先
當(dāng)from中有子查詢的時(shí)候书劝,表名是derivedN的形式进倍,N指向子查詢,也就是explain結(jié)果中的下一列
當(dāng)有union result的時(shí)候购对,表名是union 1,2等的形式猾昆,1,2表示參與union的query id
注意:MySQL對(duì)待這些表和普通表一樣,但是這些“臨時(shí)表”是沒有任何索引的骡苞。

type
type顯示的是訪問類型垂蜗,是較為重要的一個(gè)指標(biāo),結(jié)果值從好到壞依次是:
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL 解幽,一般來說贴见,得保證查詢至少達(dá)到range級(jí)別,最好能達(dá)到ref躲株。

image.png

possible_keys

顯示查詢使用了哪些索引片部,表示該索引可以進(jìn)行高效地查找,但是列出來的索引對(duì)于后續(xù)優(yōu)化過程可能是沒有用的

key

key列顯示MySQL實(shí)際決定使用的鍵(索引)霜定。如果沒有選擇索引档悠,鍵是NULL廊鸥。要想強(qiáng)制MySQL使用或忽視possible_keys列中的索引,在查詢中使用FORCE INDEX辖所、USE INDEX或者IGNORE INDEX惰说。

key_len

key_len列顯示MySQL決定使用的鍵長度。如果鍵是NULL缘回,則長度為NULL吆视。使用的索引的長度。在不損失精確性的情況下酥宴,長度越短越好 啦吧。

ref

ref列顯示使用哪個(gè)列或常數(shù)與key一起從表中選擇行。

rows

rows列顯示MySQL認(rèn)為它執(zhí)行查詢時(shí)必須檢查的行數(shù)幅虑。注意這是一個(gè)預(yù)估值丰滑。

Extra

Extra是EXPLAIN輸出中另外一個(gè)很重要的列,該列顯示MySQL在查詢過程中的一些詳細(xì)信息倒庵,MySQL查詢優(yōu)化器執(zhí)行查詢的過程中對(duì)查詢計(jì)劃的重要補(bǔ)充信息褒墨。

image.png
3.3 例子

下面幾個(gè)例子詳細(xì)解釋了如何分析和優(yōu)化慢查詢

復(fù)雜語句寫法
很多情況下,我們寫SQL只是為了實(shí)現(xiàn)功能擎宝,這只是第一步郁妈,不同的語句書寫方式對(duì)于效率往往有本質(zhì)的差別,這要求我們對(duì)mysql的執(zhí)行計(jì)劃和索引原則有非常清楚的認(rèn)識(shí)绍申,請(qǐng)看下面的語句

select
   distinct cert.emp_id 
from
   cm_log cl 
inner join
   (
      select
         emp.id as emp_id,
         emp_cert.id as cert_id 
      from
         employee emp 
      left join
         emp_certificate emp_cert 
            on emp.id = emp_cert.emp_id 
      where
         emp.is_deleted=0
   ) cert 
      on (
         cl.ref_table='Employee' 
         and cl.ref_oid= cert.emp_id
      ) 
      or (
         cl.ref_table='EmpCertificate' 
         and cl.ref_oid= cert.cert_id
      ) 
where
   cl.last_upd_date >='2013-11-07 15:03:00' 
   and cl.last_upd_date<='2013-11-08 16:00:00';

0.先運(yùn)行一下噩咪,53條記錄 1.87秒,又沒有用聚合語句极阅,比較慢

53 rows in set (1.87 sec)
1.explain

+----+-------------+------------+-------+---------------------------------+-----------------------+---------+-------------------+-------+--------------------------------+
| id | select_type | table      | type  | possible_keys                   | key                   | key_len | ref               | rows  | Extra                          |
+----+-------------+------------+-------+---------------------------------+-----------------------+---------+-------------------+-------+--------------------------------+
|  1 | PRIMARY     | cl         | range | cm_log_cls_id,idx_last_upd_date | idx_last_upd_date     | 8       | NULL              |   379 | Using where; Using temporary   |
|  1 | PRIMARY     | <derived2> | ALL   | NULL                            | NULL                  | NULL    | NULL              | 63727 | Using where; Using join buffer |
|  2 | DERIVED     | emp        | ALL   | NULL                            | NULL                  | NULL    | NULL              | 13317 | Using where                    |
|  2 | DERIVED     | emp_cert   | ref   | emp_certificate_empid           | emp_certificate_empid | 4       | meituanorg.emp.id |     1 | Using index                    |
+----+-------------+------------+-------+---------------------------------+-----------------------+---------+-------------------+-------+--------------------------------+

簡述一下執(zhí)行計(jì)劃胃碾,首先mysql根據(jù)idx_last_upd_date索引掃描cm_log表獲得379條記錄;然后查表掃描了63727條記錄筋搏,分為兩部分仆百,derived表示構(gòu)造表,也就是不存在的表奔脐,可以簡單理解成是一個(gè)語句形成的結(jié)果集俄周,后面的數(shù)字表示語句的ID。derived2表示的是ID = 2的查詢構(gòu)造了虛擬表髓迎,并且返回了63727條記錄峦朗。我們?cè)賮砜纯碔D = 2的語句究竟做了寫什么返回了這么大量的數(shù)據(jù),首先全表掃描employee表13317條記錄排龄,然后根據(jù)索引emp_certificate_empid關(guān)聯(lián)emp_certificate表波势,rows = 1表示,每個(gè)關(guān)聯(lián)都只鎖定了一條記錄,效率比較高艰亮。獲得后闭翩,再和cm_log的379條記錄根據(jù)規(guī)則關(guān)聯(lián)挣郭。從執(zhí)行過程上可以看出返回了太多的數(shù)據(jù)迄埃,返回的數(shù)據(jù)絕大部分cm_log都用不到,因?yàn)閏m_log只鎖定了379條記錄兑障。
如何優(yōu)化呢侄非?可以看到我們?cè)谶\(yùn)行完后還是要和cm_log做join,那么我們能不能之前和cm_log做join呢?仔細(xì)分析語句不難發(fā)現(xiàn)流译,其基本思想是如果cm_log的ref_table是EmpCertificate就關(guān)聯(lián)emp_certificate表逞怨,如果ref_table是Employee就關(guān)聯(lián)employee表,我們完全可以拆成兩部分福澡,并用union連接起來叠赦,注意這里用union,而不用union all是因?yàn)樵Z句有“distinct”來得到唯一的記錄革砸,而union恰好具備了這種功能除秀。如果原語句中沒有distinct不需要去重,我們就可以直接使用union all了算利,因?yàn)槭褂胾nion需要去重的動(dòng)作册踩,會(huì)影響SQL性能。
優(yōu)化過的語句如下

select
   emp.id 
from
   cm_log cl 
inner join
   employee emp 
      on cl.ref_table = 'Employee' 
      and cl.ref_oid = emp.id  
where
   cl.last_upd_date >='2013-11-07 15:03:00' 
   and cl.last_upd_date<='2013-11-08 16:00:00' 
   and emp.is_deleted = 0  
union
select
   emp.id 
from
   cm_log cl 
inner join
   emp_certificate ec 
      on cl.ref_table = 'EmpCertificate' 
      and cl.ref_oid = ec.id  
inner join
   employee emp 
      on emp.id = ec.emp_id  
where
   cl.last_upd_date >='2013-11-07 15:03:00' 
   and cl.last_upd_date<='2013-11-08 16:00:00' 
   and emp.is_deleted = 0

4.不需要了解業(yè)務(wù)場景效拭,只需要改造的語句和改造之前的語句保持結(jié)果一致

5.現(xiàn)有索引可以滿足暂吉,不需要建索引

6.用改造后的語句實(shí)驗(yàn)一下,只需要10ms 降低了近200倍缎患!

+----+--------------+------------+--------+---------------------------------+-------------------+---------+-----------------------+------+-------------+
| id | select_type  | table      | type   | possible_keys                   | key               | key_len | ref                   | rows | Extra       |
+----+--------------+------------+--------+---------------------------------+-------------------+---------+-----------------------+------+-------------+
|  1 | PRIMARY      | cl         | range  | cm_log_cls_id,idx_last_upd_date | idx_last_upd_date | 8       | NULL                  |  379 | Using where |
|  1 | PRIMARY      | emp        | eq_ref | PRIMARY                         | PRIMARY           | 4       | meituanorg.cl.ref_oid |    1 | Using where |
|  2 | UNION        | cl         | range  | cm_log_cls_id,idx_last_upd_date | idx_last_upd_date | 8       | NULL                  |  379 | Using where |
|  2 | UNION        | ec         | eq_ref | PRIMARY,emp_certificate_empid   | PRIMARY           | 4       | meituanorg.cl.ref_oid |    1 |             |
|  2 | UNION        | emp        | eq_ref | PRIMARY                         | PRIMARY           | 4       | meituanorg.ec.emp_id  |    1 | Using where |
| NULL | UNION RESULT | <union1,2> | ALL    | NULL                            | NULL              | NULL    | NULL                  | NULL |             |
+----+--------------+------------+--------+---------------------------------+-------------------+---------+-----------------------+------+-------------+
53 rows in set (0.01 sec)

明確應(yīng)用場景
舉這個(gè)例子的目的在于顛覆我們對(duì)列的區(qū)分度的認(rèn)知慕的,一般上我們認(rèn)為區(qū)分度越高的列,越容易鎖定更少的記錄挤渔,但在一些特殊的情況下肮街,這種理論是有局限性的

select
   * 
from
   stage_poi sp 
where
   sp.accurate_result=1 
   and (
      sp.sync_status=0 
      or sp.sync_status=2 
      or sp.sync_status=4
   );

0.先看看運(yùn)行多長時(shí)間,951條數(shù)據(jù)6.22秒,真的很慢

951 rows in set (6.22 sec)
1.先explain蚂蕴,rows達(dá)到了361萬低散,type = ALL表明是全表掃描

+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows    | Extra       |
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
|  1 | SIMPLE      | sp    | ALL  | NULL          | NULL | NULL    | NULL | 3613155 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+

2.所有字段都應(yīng)用查詢返回記錄數(shù),因?yàn)槭菃伪聿樵?0已經(jīng)做過了951條

3.讓explain的rows 盡量逼近951

看一下accurate_result = 1的記錄數(shù)

select count(*),accurate_result from stage_poi  group by accurate_result;
+----------+-----------------+
| count(*) | accurate_result |
+----------+-----------------+
|     1023 |              -1 |
|  2114655 |               0 |
|   972815 |               1 |
+----------+-----------------+

我們看到accurate_result這個(gè)字段的區(qū)分度非常低骡楼,整個(gè)表只有-1,0,1三個(gè)值熔号,加上索引也無法鎖定特別少量的數(shù)據(jù)

再看一下sync_status字段的情況

select count(*),sync_status from stage_poi  group by sync_status;
+----------+-------------+
| count(*) | sync_status |
+----------+-------------+
|     3080 |           0 |
|  3085413 |           3 |
+----------+-------------+

同樣的區(qū)分度也很低,根據(jù)理論鸟整,也不適合建立索引

問題分析到這引镊,好像得出了這個(gè)表無法優(yōu)化的結(jié)論,兩個(gè)列的區(qū)分度都很低,即便加上索引也只能適應(yīng)這種情況弟头,很難做普遍性的優(yōu)化吩抓,比如當(dāng)sync_status 0、3分布的很平均赴恨,那么鎖定記錄也是百萬級(jí)別的

4.找業(yè)務(wù)方去溝通疹娶,看看使用場景。業(yè)務(wù)方是這么來使用這個(gè)SQL語句的伦连,每隔五分鐘會(huì)掃描符合條件的數(shù)據(jù)雨饺,處理完成后把sync_status這個(gè)字段變成1,五分鐘符合條件的記錄數(shù)并不會(huì)太多,1000個(gè)左右惑淳。了解了業(yè)務(wù)方的使用場景后额港,優(yōu)化這個(gè)SQL就變得簡單了,因?yàn)闃I(yè)務(wù)方保證了數(shù)據(jù)的不平衡歧焦,如果加上索引可以過濾掉絕大部分不需要的數(shù)據(jù)

5.根據(jù)建立索引規(guī)則移斩,使用如下語句建立索引

alter table stage_poi add index idx_acc_status(accurate_result,sync_status);

6.觀察預(yù)期結(jié)果,發(fā)現(xiàn)只需要200ms,快了30多倍绢馍。

952 rows in set (0.20 sec)
我們?cè)賮砘仡櫼幌路治鰡栴}的過程向瓷,單表查詢相對(duì)來說比較好優(yōu)化,大部分時(shí)候只需要把where條件里面的字段依照規(guī)則加上索引就好痕貌,如果只是這種“無腦”優(yōu)化的話风罩,顯然一些區(qū)分度非常低的列,不應(yīng)該加索引的列也會(huì)被加上索引舵稠,這樣會(huì)對(duì)插入超升、更新性能造成嚴(yán)重的影響,同時(shí)也有可能影響其它的查詢語句哺徊。所以我們第4步調(diào)差SQL的使用場景非常關(guān)鍵室琢,我們只有知道這個(gè)業(yè)務(wù)場景,才能更好地輔助我們更好的分析和優(yōu)化查詢語句落追。

無法優(yōu)化的語句

select
   c.id,
   c.name,
   c.position,
   c.sex,
   c.phone,
   c.office_phone,
   c.feature_info,
   c.birthday,
   c.creator_id,
   c.is_keyperson,
   c.giveup_reason,
   c.status,
   c.data_source,
   from_unixtime(c.created_time) as created_time,
   from_unixtime(c.last_modified) as last_modified,
   c.last_modified_user_id  
from
   contact c  
inner join
   contact_branch cb 
      on  c.id = cb.contact_id  
inner join
   branch_user bu 
      on  cb.branch_id = bu.branch_id 
      and bu.status in (
         1,
      2)  
   inner join
      org_emp_info oei 
         on  oei.data_id = bu.user_id 
         and oei.node_left >= 2875 
         and oei.node_right <= 10802 
         and oei.org_category = - 1  
   order by
      c.created_time desc  limit 0 ,
      10;

還是幾個(gè)步驟
0.先看語句運(yùn)行多長時(shí)間盈滴,10條記錄用了13秒,已經(jīng)不可忍受

10 rows in set (13.06 sec)
1.explain

+----+-------------+-------+--------+-------------------------------------+-------------------------+---------+--------------------------+------+----------------------------------------------+
| id | select_type | table | type   | possible_keys                       | key                     | key_len | ref                      | rows | Extra                                        |
+----+-------------+-------+--------+-------------------------------------+-------------------------+---------+--------------------------+------+----------------------------------------------+
|  1 | SIMPLE      | oei   | ref    | idx_category_left_right,idx_data_id | idx_category_left_right | 5       | const                    | 8849 | Using where; Using temporary; Using filesort |
|  1 | SIMPLE      | bu    | ref    | PRIMARY,idx_userid_status           | idx_userid_status       | 4       | meituancrm.oei.data_id   |   76 | Using where; Using index                     |
|  1 | SIMPLE      | cb    | ref    | idx_branch_id,idx_contact_branch_id | idx_branch_id           | 4       | meituancrm.bu.branch_id  |    1 |                                              |
|  1 | SIMPLE      | c     | eq_ref | PRIMARY                             | PRIMARY                 | 108     | meituancrm.cb.contact_id |    1 |                                              |
+----+-------------+-------+--------+-------------------------------------+-------------------------+---------+--------------------------+------+----------------------------------------------+

從執(zhí)行計(jì)劃上看轿钠,mysql先查org_emp_info表掃描8849記錄巢钓,再用索引idx_userid_status關(guān)聯(lián)branch_user表,再用索引idx_branch_id關(guān)聯(lián)contact_branch表疗垛,最后主鍵關(guān)聯(lián)contact表症汹。
rows返回的都非常少,看不到有什么異常情況贷腕。我們?cè)诳匆幌抡Z句背镇,發(fā)現(xiàn)后面有order by + limit組合咬展,會(huì)不會(huì)是排序量太大搞的?于是我們簡化SQL瞒斩,去掉后面的order by 和 limit破婆,看看到底用了多少記錄來排序

select
  count(*)
from
   contact c  
inner join
   contact_branch cb 
      on  c.id = cb.contact_id  
inner join
   branch_user bu 
      on  cb.branch_id = bu.branch_id 
      and bu.status in (
         1,
      2)  
   inner join
      org_emp_info oei 
         on  oei.data_id = bu.user_id 
         and oei.node_left >= 2875 
         and oei.node_right <= 10802 
         and oei.org_category = - 1  
+----------+
| count(*) |
+----------+
|   778878 |
+----------+
1 row in set (5.19 sec)

發(fā)現(xiàn)排序之前居然鎖定了778878條記錄,如果針對(duì)70萬的結(jié)果集排序胸囱,將是災(zāi)難性的祷舀,怪不得這么慢,那我們能不能換個(gè)思路旺矾,先根據(jù)contact的created_time排序蔑鹦,再來join會(huì)不會(huì)比較快呢?
于是改造成下面的語句箕宙,也可以用straight_join來優(yōu)化

select
c.id,
c.name,
c.position,
c.sex,
c.phone,
c.office_phone,
c.feature_info,
c.birthday,
c.creator_id,
c.is_keyperson,
c.giveup_reason,
c.status,
c.data_source,
from_unixtime(c.created_time) as created_time,
from_unixtime(c.last_modified) as last_modified,
c.last_modified_user_id
from
contact c
where
exists (
select
1
from
contact_branch cb
inner join
branch_user bu
on cb.branch_id = bu.branch_id
and bu.status in (
1,
2)
inner join
org_emp_info oei
on oei.data_id = bu.user_id
and oei.node_left >= 2875
and oei.node_right <= 10802
and oei.org_category = - 1
where
c.id = cb.contact_id
)
order by
c.created_time desc limit 0 ,
10;

驗(yàn)證一下效果 預(yù)計(jì)在1ms內(nèi),提升了13000多倍铺纽!

10 rows in set (0.00 sec)
本以為至此大工告成柬帕,但我們?cè)谇懊娴姆治鲋新┝艘粋€(gè)細(xì)節(jié),先排序再join和先join再排序理論上開銷是一樣的狡门,為何提升這么多是因?yàn)橛幸粋€(gè)limit陷寝!大致執(zhí)行過程是:mysql先按索引排序得到前10條記錄,然后再去join過濾其馏,當(dāng)發(fā)現(xiàn)不夠10條的時(shí)候凤跑,再次去10條,再次join叛复,這顯然在內(nèi)層join過濾的數(shù)據(jù)非常多的時(shí)候仔引,將是災(zāi)難的,極端情況褐奥,內(nèi)層一條數(shù)據(jù)都找不到咖耘,mysql還傻乎乎的每次取10條,幾乎遍歷了這個(gè)數(shù)據(jù)表撬码!
用不同參數(shù)的SQL試驗(yàn)下

select
   sql_no_cache   c.id,
   c.name,
   c.position,
   c.sex,
   c.phone,
   c.office_phone,
   c.feature_info,
   c.birthday,
   c.creator_id,
   c.is_keyperson,
   c.giveup_reason,
   c.status,
   c.data_source,
   from_unixtime(c.created_time) as created_time,
   from_unixtime(c.last_modified) as last_modified,
   c.last_modified_user_id    
from
   contact c   
where
   exists (
      select
         1        
      from
         contact_branch cb         
      inner join
         branch_user bu                     
            on  cb.branch_id = bu.branch_id                     
            and bu.status in (
               1,
            2)                
         inner join
            org_emp_info oei                           
               on  oei.data_id = bu.user_id                           
               and oei.node_left >= 2875                           
               and oei.node_right <= 2875                           
               and oei.org_category = - 1                
         where
            c.id = cb.contact_id           
      )        
   order by
      c.created_time desc  limit 0 ,
      10;
Empty set (2 min 18.99 sec)

2 min 18.99 sec儿倒!比之前的情況還糟糕很多。由于mysql的nested loop機(jī)制呜笑,遇到這種情況夫否,基本是無法優(yōu)化的。這條語句最終也只能交給應(yīng)用系統(tǒng)去優(yōu)化自己的邏輯了叫胁。
通過這個(gè)例子我們可以看到凰慈,并不是所有語句都能優(yōu)化,而往往我們優(yōu)化時(shí)曹抬,由于SQL用例回歸時(shí)落掉一些極端情況溉瓶,會(huì)造成比原來還嚴(yán)重的后果急鳄。所以,第一:不要指望所有語句都能通過SQL優(yōu)化堰酿,第二:不要過于自信疾宏,只針對(duì)具體case來優(yōu)化,而忽略了更復(fù)雜的情況触创。

慢查詢的案例就分析到這兒坎藐,以上只是一些比較典型的案例。我們?cè)趦?yōu)化過程中遇到過超過1000行哼绑,涉及到16個(gè)表join的“垃圾SQL”岩馍,也遇到過線上線下數(shù)據(jù)庫差異導(dǎo)致應(yīng)用直接被慢查詢拖死,也遇到過varchar等值比較沒有寫單引號(hào)抖韩,還遇到過笛卡爾積查詢直接把從庫搞死蛀恩。再多的案例其實(shí)也只是一些經(jīng)驗(yàn)的積累,如果我們熟悉查詢優(yōu)化器茂浮、索引的內(nèi)部原理双谆,那么分析這些案例就變得特別簡單了。

3. 查看查詢各階段耗費(fèi)時(shí)間統(tǒng)計(jì)
1. set profiling=1;
Query OK, 0 rows affected, 1 warning (0.00 sec)
2.select * from xg_order0;
+--------+-------------------+---------+-------------+-------------+---------+------------+-------------+------------+
| id     | order_no          | user_id | accept_name | mobile      | address | order_time | total_money | updated_at |
+--------+-------------------+---------+-------------+-------------+---------+------------+-------------+------------+
|      1 | 20180325133505884 |  716996 | xiao716996  | 1379411888  | china   | 1521984905 |      100.00 | 1521984905 |
|      2 | 20180325133531350 |  185892 | xiao185892  | 1379411777  | china   | 1521984931 |      100.00 | 1521984931 |
|      3 | 20180325133531718 |  957900 | xiao957901  | 13794117440 | china   | 1521984931 |      100.00 | 1521984931 |
|      4 | 20180325133531179 |  423416 | xiao957900  | 13794117440 | china   | 1521984931 |      100.00 | 1521984931 |
|      6 | 20180325133853879 |  567792 | xiao957900  | 13794117440 | china   | 1521985133 |      100.00 | 1521985133 |
|      7 | 20180325133854667 |  696240 | xiao696240  | 13794117440 | china   | 1521985134 |      100.00 | 1521985134 |
|      9 | 20180325133855834 |  962108 | xiao962108  | 13794117440 | china   | 1521985135 |      100.00 | 1521985135 |
|     11 | 20180325133856630 |  628040 | xiao628040  | 13794117440 | china   | 1521985136 |      100.00 | 1521985136 |
|     12 | 20180325133856484 |  614532 | xiao614532  | 13794117440 | china   | 1521985136 |      100.00 | 1521985136 |
|     13 | 20180325133855922 |  338180 | xiao338180  | 13794117440 | china   | 1521985135 |      100.00 | 1521985135 |
|     14 | 20180325133856995 |  286092 | xiao286092  | 13794117440 | china   | 1521985136 |      100.00 | 1521985136 |
|     15 | 20180325133856250 |  817376 | xiao817376  | 13794117440 | china   | 1521985136 |      100.00 | 1521985136 |
|     16 | 20180325133856839 |  161336 | xiao161336  | 13794117440 | china   | 1521985136 |      100.00 | 1521985136 |
...
3. show profiles;
+----------+------------+-------------------------+
| Query_ID | Duration   | Query                   |
+----------+------------+-------------------------+
|        1 | 0.00063800 | select * from xg_order0 |
+----------+------------+-------------------------+
1 row in set, 1 warning (0.00 sec)

4.show profile for query 1; #1指的是上一步驟的Query_ID
+----------------------+----------+
| Status               | Duration |
+----------------------+----------+
| starting             | 0.000067 |
| checking permissions | 0.000007 |
| Opening tables       | 0.000020 |
| init                 | 0.000025 |
| System lock          | 0.000010 |
| optimizing           | 0.000005 |
| statistics           | 0.000016 |
| preparing            | 0.000012 |
| executing            | 0.000003 |
| Sending data         | 0.000423 |
| end                  | 0.000005 |
| query end            | 0.000008 |
| closing tables       | 0.000009 |
| freeing items        | 0.000018 |
| cleaning up          | 0.000013 |
+----------------------+----------+
15 rows in set, 1 warning (0.00 sec)
上面就是查詢各階段耗費(fèi)的時(shí)間了席揽,用來輔助優(yōu)化分析顽馋。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市幌羞,隨后出現(xiàn)的幾起案子寸谜,更是在濱河造成了極大的恐慌,老刑警劉巖属桦,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件熊痴,死亡現(xiàn)場離奇詭異,居然都是意外死亡地啰,警方通過查閱死者的電腦和手機(jī)愁拭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來亏吝,“玉大人岭埠,你說我怎么就攤上這事∥蹬福” “怎么了惜论?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長止喷。 經(jīng)常有香客問我馆类,道長,這世上最難降的妖魔是什么弹谁? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任乾巧,我火速辦了婚禮句喜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘沟于。我一直安慰自己咳胃,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布旷太。 她就那樣靜靜地躺著展懈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪供璧。 梳的紋絲不亂的頭發(fā)上存崖,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天夯尽,我揣著相機(jī)與錄音瘩例,去河邊找鬼粪糙。 笑死死讹,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的宴猾。 我是一名探鬼主播辱士,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼契邀,長吁一口氣:“原來是場噩夢啊……” “哼偶房!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起军浆,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤棕洋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后乒融,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體掰盘,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年赞季,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了愧捕。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡申钩,死狀恐怖次绘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情撒遣,我是刑警寧澤邮偎,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站义黎,受9級(jí)特大地震影響禾进,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜廉涕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一泻云、第九天 我趴在偏房一處隱蔽的房頂上張望艇拍。 院中可真熱鬧,春花似錦宠纯、人聲如沸卸夕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽娇哆。三九已至,卻和暖如春勃救,著一層夾襖步出監(jiān)牢的瞬間碍讨,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來泰國打工蒙秒, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留勃黍,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓晕讲,卻偏偏與公主長得像覆获,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子瓢省,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容