1. 程式人生 > 實用技巧 >mysql5.7的sql優化

mysql5.7的sql優化

1.explain

(1).準備基礎資料(建立表,在c1欄位插入重複資料,並在c1欄位建立索引)

use testdb;
create table t1_explain(id int,c1 char(20),c2 char(20),c3 char(20));
insert into t1_explain values(10,'a','b','c');
insert into t1_explain values(10,'a','b','c');
insert into t1_explain values(10,'a','b','c');
insert into t1_explain values(10,'a
','b','c'); insert into t1_explain values(10,'a','b','c'); insert into t1_explain values(10,'a','b','c'); create index idx_c1 on t1_explain(c1);

(2).通過執行計劃看一下cost值的消耗(explain format=json)

mysql> explain format=json select * from t1_explain where c1='a'\G
*************************** 1. row ***************************
EXPLAIN: { "query_block": { "select_id": 1, "cost_info": { "query_cost": "1.10" }, "table": { "table_name": "t1_explain", "access_type": "ref", "possible_keys": [ "idx_c1" ], "key": "idx_c1", "used_key_parts": [ "c1" ]
, "key_length": "81", "ref": [ "const" ], "rows_examined_per_scan": 6, "rows_produced_per_join": 6, "filtered": "100.00", "index_condition": "(`testdb`.`t1_explain`.`c1` = 'a')", "cost_info": { "read_cost": "0.50", "eval_cost": "0.60", "prefix_cost": "1.10", "data_read_per_join": "1K" }, "used_columns": [ "id", "c1", "c2", "c3" ] } } } 1 row in set, 1 warning (0.00 sec) mysql>

(3).刪除索引,並通過執行計劃檢視cost值的消耗

mysql> ALTER TABLE `testdb`.`t1_explain` DROP INDEX `idx_c1` ;
Query OK, 0 rows affected (0.28 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> 
mysql> explain format=json select * from t1_explain where c1='a'\G
*************************** 1. row ***************************
EXPLAIN: {
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "0.85"
    },
    "table": {
      "table_name": "t1_explain",
      "access_type": "ALL",
      "rows_examined_per_scan": 6,
      "rows_produced_per_join": 1,
      "filtered": "16.67",
      "cost_info": {
        "read_cost": "0.75",
        "eval_cost": "0.10",
        "prefix_cost": "0.85",
        "data_read_per_join": "248"
      },
      "used_columns": [
        "id",
        "c1",
        "c2",
        "c3"
      ],
      "attached_condition": "(`testdb`.`t1_explain`.`c1` = 'a')"
    }
  }
}
1 row in set, 1 warning (0.01 sec)

mysql> 

兩次查詢的cost值不同,通過索引查詢的cost值比全表掃描的cost值大。這是因為當通過索引查詢時索引資料都是重複的(基數很低),所以要做一個索引全掃描;還因為“SELECT *”掃描完索引後要回表查詢id, c2,c3這幾個欄位。就好比你要讀完一本書,不會先把目錄全部讀一遍,然後再把後面的內容都讀一遍。

(4).如果將c1欄位的值改成不重複的,來看一下效果

重新寫入基礎資料

truncate table t1_explain;
insert into t1_explain values(10,'a','b','c');
insert into t1_explain values(10,'b','b','c');
insert into t1_explain values(10,'c','b','c');
insert into t1_explain values(10,'d','b','c');
insert into t1_explain values(10,'e','b','c');
insert into t1_explain values(10,'f','b','c');
create index idx_c1 on t1_explain(c1);

檢視執行計劃

mysql> explain format=json select * from t1_explain where c1='a'\G
*************************** 1. row ***************************
EXPLAIN: {
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "0.35"
    },
    "table": {
      "table_name": "t1_explain",
      "access_type": "ref",
      "possible_keys": [
        "idx_c1"
      ],
      "key": "idx_c1",
      "used_key_parts": [
        "c1"
      ],
      "key_length": "81",
      "ref": [
        "const"
      ],
      "rows_examined_per_scan": 1,
      "rows_produced_per_join": 1,
      "filtered": "100.00",
      "index_condition": "(`testdb`.`t1_explain`.`c1` = 'a')",
      "cost_info": {
        "read_cost": "0.25",
        "eval_cost": "0.10",
        "prefix_cost": "0.35",
        "data_read_per_join": "248"
      },
      "used_columns": [
        "id",
        "c1",
        "c2",
        "c3"
      ]
    }
  }
}
1 row in set, 1 warning (0.00 sec)

mysql> 

刪除索引,並通過執行計劃檢視cost值的消耗

mysql> drop index idx_c1 on t1_explain;
Query OK, 0 rows affected (0.24 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> explain format=json select * from t1_explain where c1='a'\G
*************************** 1. row ***************************
EXPLAIN: {
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "0.85"
    },
    "table": {
      "table_name": "t1_explain",
      "access_type": "ALL",
      "rows_examined_per_scan": 6,
      "rows_produced_per_join": 1,
      "filtered": "16.67",
      "cost_info": {
        "read_cost": "0.75",
        "eval_cost": "0.10",
        "prefix_cost": "0.85",
        "data_read_per_join": "248"
      },
      "used_columns": [
        "id",
        "c1",
        "c2",
        "c3"
      ],
      "attached_condition": "(`testdb`.`t1_explain`.`c1` = 'a')"
    }
  }
}
1 row in set, 1 warning (0.00 sec)

mysql> 

這次c1欄位的值不重複(基數較高),則通過索引查詢的cost值比全表掃描的cost值小。

這裡可能沒有體現出選擇性,我們說基數高比較好,但是要有一個衡量目標。例如,某一欄位的基數是幾十萬條,但是表中資料有幾十億條,在這個欄位上建立索引就不是很合適,因為選擇性比較低,通過索引查詢在索引中可能就要掃描上億條資料。

通常在建立索引時要考慮以上內容(回表、基數、選擇性),在MySQL中可以通過系統表innodb_index_stats來檢視索引選擇性如何,並且可以看到組合索引中每一個欄位的選擇性如何,還可以計算索引的大小(單位:M)。

select stat_value as pages,
       index_name,
       stat_value * @@innodb_page_size / 1024 / 1024 as size
  from mysql.innodb_index_stats 
 where table_name ='t1_explain'
   and database_name = 'testdb'
   and stat_name = 'size'
   and stat_description = 'Number of pages in the index'
 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 ='t#P%'
   and database_name = 'test'
   and stat_name = 'size'
   and stat_description = 'Number of pages in the index'
 group by index_name;

也可以通過show index from table_name 檢視Cardinality欄位的值,以及欄位的基數是多少。

2.MySQL中的優化特性

(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),且會遵循下面一些原則:

1.連線型別為ALL、index、range,會使用到Join Buffer。
2.Join Buffer是由join_buffer_size 變數控制的。
3.每次連線都使用一個Join Buffer,多表的連線可以使用多個Join Buffer。
4.Join Buffer只儲存與查詢操作相關的欄位資料,而不是整行記錄。

BNL演算法的虛擬碼如下:

for each row in t1 matching range{
  for each row in t2 matching reference key {
    store used columns for 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
    }
  }
}

對上面的過程解釋如下:

①將t1、t2的連線結果放到緩衝區中,直到緩衝區滿為止。
②遍歷t3,與緩衝區內的資料匹配,找到匹配的行,傳送到客戶端。
③清空緩衝區。
④重複上面的步驟,直至緩衝區不滿。
⑤處理緩衝區中剩餘的資料,重複步驟②。

假設S是每次儲存t1、t2組合的大小,C是組合的數量,則t3被掃描的次數為:(S * C)/join_buffer_size+ 1。

由此可見,隨著join_buffer_size的增大,t3被掃描的次數會減少,如果join_buffer_size足夠大,大到可以容納所有t1和t2連線產生的資料,那麼t3只會被掃描一次。

(3).MySQL中的優化特性

1).Index Condition Pushdown(ICP,索引條件下推)

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

①根據索引讀取一條索引記錄,然後使用索引的葉子節點中的主鍵值回表讀取整個錶行。
②判斷這行記錄是否符合where條件。

有ICP後處理過程如下圖所示:

①根據索引讀取一條索引記錄,但並不回表取出整行資料。
②判斷記錄是否滿足where條件的一部分,並且只能使用索引欄位進行檢查。如果不滿足條件,則繼續獲取下一條索引記錄。
③如果滿足條件,則使用索引回表取出整行資料。
④再判斷where條件的剩餘部分,選擇滿足條件的記錄。

ICP的意思就是篩選欄位在索引中的where條件從伺服器層下推到儲存引擎層,這樣可以在儲存引擎層過濾資料。由此可見,ICP可以減少儲存引擎訪問基表的次數和MySQL伺服器訪問儲存引擎的次數。

ICP的使用場景如下:

1.組合索引(a,b)where條件中的a欄位是範圍掃描,那麼後面的索引欄位b則無法使用到索引。在沒有ICP時需要把滿足a欄位條件的資料全部提取到伺服器層,並且會有大量的回表操作;而有了ICP之後,則會將b欄位條件下推到儲存引擎層,以減少回表次數和返回給伺服器層的資料量。
2.組合索引(a,b)的第一個欄位的選擇性非常低,第二個欄位查詢時又利用不到索引(%b%),在這種情況下,通過ICP也能很好地減少回表次數和返回給伺服器層的資料量。

ICP的使用限制如下:

1.只能用於InnoDB和MyISAM。
2.適用於range、ref、eq_ref和ref_or_null訪問方式,並且需要回表進行訪問。
3.適用於二級索引。
4.不適用於虛擬欄位的二級索引。

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所控制的。