面試無憂:源碼+實踐,講到MySQL調優的底層算法實現

不知道你們有沒有參與過系統重構或者代碼調優的工做,有幸,最近我接觸了一個公司N久前的一個項目的重構工做,目的就是爲了提高一下響應速度,而後咱們小組拿到這個項目的源代碼以後,大呼:WC,這NM誰寫的代碼啊,太不講究了吧,這SQL都寫了些什麼玩意啊,其實在這些年的工做中,這樣的問題已經不是第一次碰見了,總是被提需求說性能有問題,拿到代碼以後發現問題很簡單,90%都是SQL的問題,當時趕進度,能查詢出來結果就能夠,稍微一優化SQL性能就能提高不少,雖然如今有不少SQL審覈平臺,可是,在面試的過程當中,須要你回答的更加深刻一些,那這裏,我就結合代碼給你講解到算法的層次,在遇到優化,不在害怕任何挑戰mysql

公衆號:Java架構師聯盟,每日更新技術好文面試

SQL優化基礎概念

說到SQL優化,你們首先想到的就是建立索引,但建立索引須要瞭解相關基礎概念。算法

1. 索引sql

咱們知道,MySQL中的索引一般採用B-Tree結構,那麼首先就要清楚B-Tree和B+Tree 結構的區別數據庫

面試無憂:源碼+實踐,講到MySQL調優的底層算法實現

 

在InnoDB中索引是B+Tree結構的,在該結構中葉子節點包含了非葉子節點的全部數據,而且葉子節點之間會經過指針鏈接。json

之因此採用B+Tree結構,是由於數據庫中有>、<、between … and這類範圍查詢語句,直接掃描葉子節點便可。緩存

2. 彙集索引(主鍵索引)服務器

InnoDB的全部的表都是索引組織表,主鍵與數據存放在一塊兒。InnoDB選擇彙集索引遵循如下原則:架構

• 在建立表時,若是指定了主鍵,則將其做爲彙集索引。oop

• 若是沒有指定主鍵,則選擇第一個NOT NULL的惟一索引做爲彙集索引。

• 若是沒有惟一索引,則內部會生成一個6字節的rowid做爲主鍵。

彙集索引是將主鍵與行記錄存儲在一塊兒的,當根據主鍵進行查詢時, 可直接在表中獲取到數據,不用回表查詢。

面試無憂:源碼+實踐,講到MySQL調優的底層算法實現

 

3. 二級索引(也稱輔助索引)

二級索引的葉子節點存儲了索引值+rowid(主鍵值)。熟悉MySQL 的讀者在MySQL中建立表時最好本身指定一個顯式的自增主鍵,這樣作的好處是:顯式指定的主鍵能夠是普通的int類型,這樣存儲的空間就是4字節,在二級索引的葉子節點中存儲主鍵值所佔用的空間就會變小。這時可能有人會問:二級索引的葉子節點爲什麼不存儲主鍵的指針呢?緣由是:若是主鍵位置發生了變化,則須要修改二級索引的葉子節點對應存儲的指針;可是若是二級索引的葉子節點自己存儲的是主鍵的值,則不會出現這種情 況。

面試無憂:源碼+實踐,講到MySQL調優的底層算法實現

 

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 PushdownICP,索引條件下推)

ICP是MySQL針對索引從表中檢索時的一種優化特性,在沒有ICP時處理過程如圖21- 4所示。

①根據索引讀取一條索引記錄,而後使用索引的葉子節點中的主鍵值回表讀取整個錶行。

②判斷這行記錄是否符合where條件。

面試無憂:源碼+實踐,講到MySQL調優的底層算法實現

 

有ICP後處理過程

面試無憂:源碼+實踐,講到MySQL調優的底層算法實現

 

①根據索引讀取一條索引記錄,但並不回表取出整行數據。

②判斷記錄是否知足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 ReadMRR

若是經過二級索引掃描時須要回表查詢數據,那麼此時因爲主鍵順序與二級索引的順序不一致會致使大量的隨機I/O。而經過Multi-Range Read特性,MySQL會將索引掃描到的數據根據rowid進行一次排序,而後再回表查詢。此方式的好處是將回表查詢從隨機I/O 轉換成順序I/O。

在沒有MRR時,經過索引查詢到數據以後回表形式

面試無憂:源碼+實踐,講到MySQL調優的底層算法實現

 

從圖中能夠看到,當經過二級索引掃描完數據以後,根據rowid(或者主鍵)回表查詢,可是這個過程是隨機訪問的。若是表數據量很是大,在傳統的機械硬盤中IOPS 不高的狀況下性能會不好。

有了MRR以後,回表形式

面試無憂:源碼+實踐,講到MySQL調優的底層算法實現

 

根據索引查詢完以後會將rowid放到緩衝區中進行排序,排序以後再回表訪問,此時是順序I/O。這裏排序所用到的緩衝區是由參數read_rnd_buffer_size所控制的。

3. Batched Key AccessBKA

BKA是對BNL算法的更一步擴展及優化,其做用是在錶鏈接時能夠進行順序I/O,因此BKA是在MRR基礎之上實現的,同時BKA支持內鏈接、外鏈接和半鏈接操做。

當兩個錶鏈接時,在沒有BKA的狀況下如圖所示,能夠看到訪問t2表時是隨機I/O。

面試無憂:源碼+實踐,講到MySQL調優的底層算法實現

 

有了BKA以後如圖21-9所示,能夠看到對t2表進行鏈接訪問時,先將t1中相關的字段放入Join buffer中,而後利用MRR特性接口進行排序(根據rowid),排序以後便可經過rowid到t2表中進行查找。

面試無憂:源碼+實踐,講到MySQL調優的底層算法實現

 

這裏也有一個隱含的條件,就是就是關聯字段須要有索引,不然仍是會使用BNL算法的。

相關文章
相關標籤/搜索