不知道你們有沒有參與過系統重構或者代碼調優的工做,有幸,最近我接觸了一個公司N久前的一個項目的重構工做,目的就是爲了提高一下響應速度,而後咱們小組拿到這個項目的源代碼以後,大呼:WC,這NM誰寫的代碼啊,太不講究了吧,這SQL都寫了些什麼玩意啊,其實在這些年的工做中,這樣的問題已經不是第一次碰見了,總是被提需求說性能有問題,拿到代碼以後發現問題很簡單,90%都是SQL的問題,當時趕進度,能查詢出來結果就能夠,稍微一優化SQL性能就能提高不少,雖然如今有不少SQL審覈平臺,可是,在面試的過程當中,須要你回答的更加深刻一些,那這裏,我就結合代碼給你講解到算法的層次,在遇到優化,不在害怕任何挑戰mysql
公衆號:Java架構師聯盟,每日更新技術好文面試
SQL優化基礎概念
說到SQL優化,你們首先想到的就是建立索引,但建立索引須要瞭解相關基礎概念。算法
1. 索引sql
咱們知道,MySQL中的索引一般採用B-Tree結構,那麼首先就要清楚B-Tree和B+Tree 結構的區別數據庫
在InnoDB中索引是B+Tree結構的,在該結構中葉子節點包含了非葉子節點的全部數據,而且葉子節點之間會經過指針鏈接。json
之因此採用B+Tree結構,是由於數據庫中有>、<、between … and這類範圍查詢語句,直接掃描葉子節點便可。緩存
2. 彙集索引(主鍵索引)服務器
InnoDB的全部的表都是索引組織表,主鍵與數據存放在一塊兒。InnoDB選擇彙集索引遵循如下原則:架構
• 在建立表時,若是指定了主鍵,則將其做爲彙集索引。oop
• 若是沒有指定主鍵,則選擇第一個NOT NULL的惟一索引做爲彙集索引。
• 若是沒有惟一索引,則內部會生成一個6字節的rowid做爲主鍵。
彙集索引是將主鍵與行記錄存儲在一塊兒的,當根據主鍵進行查詢時, 可直接在表中獲取到數據,不用回表查詢。
3. 二級索引(也稱輔助索引)
二級索引的葉子節點存儲了索引值+rowid(主鍵值)。熟悉MySQL 的讀者在MySQL中建立表時最好本身指定一個顯式的自增主鍵,這樣作的好處是:顯式指定的主鍵能夠是普通的int類型,這樣存儲的空間就是4字節,在二級索引的葉子節點中存儲主鍵值所佔用的空間就會變小。這時可能有人會問:二級索引的葉子節點爲什麼不存儲主鍵的指針呢?緣由是:若是主鍵位置發生了變化,則須要修改二級索引的葉子節點對應存儲的指針;可是若是二級索引的葉子節點自己存儲的是主鍵的值,則不會出現這種情 況。
4. 基數、選擇性、回表
• 基數是字段distinct後的值,主鍵或非NULL的惟一索引的基數等於表的總行數。
• 選擇性是指基數與總行數的比值乘以100%,選擇性一般表示在字段上是否適合建立索引。
• 當要查詢的字段不能在索引中徹底得到時,則須要回表查詢取出所須要的數據。這幾點很重要,由於SQL優化最重要的就是要減小SQL語句的掃描行數,看下面這個例子。
mysql> create table t1 (id int , cl char(20),c2 chaz(20),c3 char(20)) ; Query OK,0 rows affected (0.02 sec) mysql> insert into t1 values (10,'a','b', 'C'); Query OK,1 row affected (0.01 sec) mysql> insert into t1 values (10,'a','b', 'c'); Query OK,1 row affected (0.01 sec) mysql> insert into t1 values (10,'a', 'b' , 'c'); Query OK,1 row affected (0.01 sec) mysql> insert into t1 values (10,'a','b', 'c'); Query OK, 1 row affected (0.01 sec) mysql> insert into t1 values (10,'a', 'b', 'c'); Query OK, 1 row affected (0.01 sec) mysql> insert into t1 values (10,'a', 'b', 'c'); Query OK,1 row affected (0.01 sec) mysql> create index idx_c1 on t1 (c1); Query OK,o rows affected (0.02 sec) Records: 0 Duplicates: 0 warnings: o
建立表,在c1字段插入重複數據,並在c1字段建立索引,咱們經過執行計劃看一下
cost值的消耗。
mysql> explain format=json select * from t1 where c1 = 'a'; "query_block" :{ "select_id": 1,"cost_info":{ "query_cost": "1.10" )
刪除索引,並經過執行計劃查看cost值的消耗。
mysql> drop index idx_c1 on t1; Query OK,0 rows affected (0.02sec)Records: 0 Duplicates: 0 warnings: o mysql> explain format=json select * from t1 where c1 = 'a';( "query_block":{ "select_id":1,"cost_info" :{ "query_cost":"0.85" ) )
兩次查詢的cost值不一樣,經過索引查詢的cost值比全表掃描的cost值大。這是由於當經過索引查詢時索引數據都是重複的(基數很低),因此要作一個索引全掃描;還因
爲「SELECT *」掃描完索引後要回表查詢id, c2,c3這幾個字段。就比如你要讀完一本書,不會先把目錄所有讀一遍,而後再把後面的內容都讀一遍。
若是將c1字段的值改爲不重複的,咱們再來看一下。
mysql> insert into t1 values (10, 'a', 'b', 'c'); Query OK, 1 row affected (0.01 sec) mysql> insert into t1 values (10,'b','b','c'); Query OK,1 row affected (0.01 sec) mysql> insert into t1 values (10,'c','b','c'); Query OK, 1 row affected (0.01 sec) mysql> insert into t1 values (10,'d','b', 'c'); Query OK, 1 row affected (0.00 sec) mysql> insert into t1 values (10,'e', 'b','c'); Query OK,1 row affected (0.01 sec) mysql> explain format=json select * from t1 where c1 = 'a'; "query_block" :{ "select_id": 1,"cost_info":{ "query_cost": "0.35" } mysql> drop index idx_c1 on t1; Query OK,0 rows affected (0.02 sec) Records: 0 Duplicates: 0 warnings:0 mysql>explain format=json select * from t1 where c1 = 'a'; "query_block": { "select_id": 1,"cost_info":( "query_cost":"0.75" )
此次c1字段的值不重複(基數較高),則經過索引查詢的cost值比全表掃描的cost值小。
這裏可能沒有體現出選擇性,咱們說基數高比較好,可是要有一個衡量目標。例如, 某一字段的基數是幾十萬條,可是表中數據有幾十億條,在這個字段上建立索引就不是很合適,由於選擇性比較低,經過索引查詢在索引中可能就要掃描上億條數據。
一般在建立索引時要考慮以上內容(回表、基數、選擇性),在MySQL中能夠經過系統表innodb_index_stats來查看索引選擇性如何,而且能夠看到組合索引中每個字段的選擇性如何,還能夠計算索引的大小
SELECT stat_value AS pages, index_name , stat_value * @@innodb_page_size / 1024/ 1024 AS size FROM mysql.innodb_index_stats WHERE(table_name = 'sbtest1' AND database_name = 'sbtest' AND stat_description = 'Number of pages in the index' AND stat_name = 'size') GROUP BY index_name;
若是是分區表,則使用下面的語句。
SELECT stat_value AS pages, index_name ,SUM(stat_value)* @@innodb_page_size / 1024 / 1024 AS size FROM mysql .innodb_index_stats WHERE(table_name LIKE 't#P%' AND database_name = 'test' AND stat_description = 'Number of pages in the index' AND stat_name = 'size') GROUP BY index_name;
也能夠經過show index from table_name 查看Cardinality字段的值,以及字段的基數是多少。
MySQL中的Join算法
這是一個世紀難題,不少文章或者文檔或者公司規範都是說盡可能不要使用join方法,可是緣由講解都比較粗一些,今天就來看一下他的算法實現是怎麼樣的,爲何要儘可能減小使用頻率
1. Nested-Loop Join Algorithm(嵌套循環Join算法)
最簡單的Join算法及外循環讀取一行數據,根據關聯條件列到內循環中匹配關聯,在這種算法中,咱們一般稱外循環表爲驅動表,稱內循環表爲被驅動表。
Nested-Loop Join算法的僞代碼以下:
for each row in t1 matching range { for each row in t2 matching reference key { for each row in t3 { if row satisfies join conditions,send to client) )
2. Block Nested-Loop Join Algorithm(塊嵌套循環Join算法,即BNL算法)
BNL算法是對Nested-Loop Join算法的優化。
具體作法是將外循環的行緩存起來,讀取緩衝區中的行,減小內循環表被掃描的次數。例如,外循環表與內循環表均有100行記錄,普通的嵌套內循環表須要掃描100次,若是使用塊嵌套循環,則每次外循環讀取10行記錄到緩衝區中,而後把緩衝區數據傳遞給下一個內循環,將內循環讀取到的每行和緩衝區中的10行進行比較,這樣內循環表只須要掃描10次便可完成,使用塊嵌套循環後內循環總體掃描次數少了一個數量級。使用塊嵌套循環,內循環表掃描方式應是全表掃描,由於是內循環表匹配Join Buffer中的數據的。使用塊嵌套循環鏈接,MySQL會使用鏈接緩衝區(Join Buffer),且會遵循下面一些原則:
• 鏈接類型爲ALL、index、range,會使用到Join Buffer。
• Join Buffer是由join_buffer_size 變量控制的。
• 每次鏈接都使用一個Join Buffer,多表的鏈接可使用多個Join Buffer。
• Join Buffer只存儲與查詢操做相關的字段數據,而不是整行記錄。
BNL算法的僞代碼以下:
for each row in t1 matching range { for each row in t2 matching reference key { store used columns from t1, t2 in join buffer if buffer is full { for each row in t3 { for each t1,t2 combination in join buffer { if row satisfies join conditions,send to client } } empty join buffer } } } if buffer is not empty{for each row in t3{ for each t1, t2 combination in join buffer { if row satisfies join conditions,send to client } } }
對上面的過程解釋以下:
①將t一、t2的鏈接結果放到緩衝區中,直到緩衝區滿爲止。
②遍歷t3,與緩衝區內的數據匹配,找到匹配的行,發送到客戶端。
③清空緩衝區。
④重複上面的步驟,直至緩衝區不滿。
⑤處理緩衝區中剩餘的數據,重複步驟②。
假設S是每次存儲t一、t2組合的大小,C是組合的數量,則t3被掃描的次數爲:(*S* *
C)/join_buffer_size+ 1。
因而可知,隨着join_buffer_size的增大,t3被掃描的次數會減小,若是join_buffer_size
足夠大,大到能夠容納全部t1和t2鏈接產生的數據,那麼t3只會被掃描一次。
MySQL中的優化特性
1. Index Condition Pushdown(ICP,索引條件下推)
ICP是MySQL針對索引從表中檢索時的一種優化特性,在沒有ICP時處理過程如圖21- 4所示。
①根據索引讀取一條索引記錄,而後使用索引的葉子節點中的主鍵值回表讀取整個錶行。
②判斷這行記錄是否符合where條件。
有ICP後處理過程
①根據索引讀取一條索引記錄,但並不回表取出整行數據。
②判斷記錄是否知足where條件的一部分,而且只能使用索引字段進行檢查。若是不知足條件,則繼續獲取下一條索引記錄。
③若是知足條件,則使用索引回表取出整行數據。
④再判斷where條件的剩餘部分,選擇知足條件的記錄。
ICP的意思就是篩選字段在索引中的where條件從服務器層下推到存儲引擎層,這樣能夠在存儲引擎層過濾數據。因而可知,ICP能夠減小存儲引擎訪問基表的次數和MySQL服務器訪問存儲引擎的次數。
ICP的使用場景以下:
• 組合索引(a,b)where條件中的a字段是範圍掃描,那麼後面的索引字段b則沒法使用到索引。在沒有ICP時須要把知足a字段條件的數據所有提取到服務器層,而且會有大量的回表操做;而有了ICP以後,則會將b字段條件下推到存儲引擎層,以減小回表次數和返回給服務器層的數據量。
• 組合索引(a,b)的第一個字段的選擇性很是低,第二個字段查詢時又利用不到索引(%b%),在這種狀況下,經過ICP也能很好地減小回表次數和返回給服務器層的數據量。
ICP的使用限制以下:
• 只能用於InnoDB和MyISAM。
• 適用於range、ref、eq_ref和ref_or_null訪問方式,而且須要回表進行訪問。
• 適用於二級索引。
• 不適用於虛擬字段的二級索引。
2. Multi-Range Read(MRR)
若是經過二級索引掃描時須要回表查詢數據,那麼此時因爲主鍵順序與二級索引的順序不一致會致使大量的隨機I/O。而經過Multi-Range Read特性,MySQL會將索引掃描到的數據根據rowid進行一次排序,而後再回表查詢。此方式的好處是將回表查詢從隨機I/O 轉換成順序I/O。
在沒有MRR時,經過索引查詢到數據以後回表形式
從圖中能夠看到,當經過二級索引掃描完數據以後,根據rowid(或者主鍵)回表查詢,可是這個過程是隨機訪問的。若是表數據量很是大,在傳統的機械硬盤中IOPS 不高的狀況下性能會不好。
有了MRR以後,回表形式
根據索引查詢完以後會將rowid放到緩衝區中進行排序,排序以後再回表訪問,此時是順序I/O。這裏排序所用到的緩衝區是由參數read_rnd_buffer_size所控制的。
3. Batched Key Access(BKA)
BKA是對BNL算法的更一步擴展及優化,其做用是在錶鏈接時能夠進行順序I/O,因此BKA是在MRR基礎之上實現的,同時BKA支持內鏈接、外鏈接和半鏈接操做。
當兩個錶鏈接時,在沒有BKA的狀況下如圖所示,能夠看到訪問t2表時是隨機I/O。
有了BKA以後如圖21-9所示,能夠看到對t2表進行鏈接訪問時,先將t1中相關的字段放入Join buffer中,而後利用MRR特性接口進行排序(根據rowid),排序以後便可經過rowid到t2表中進行查找。
這裏也有一個隱含的條件,就是就是關聯字段須要有索引,不然仍是會使用BNL算法的。