mysql高性能索引

轉自:http://blog.csdn.net/lemon89/article/details/50193891

理解磁盤IO

主軸讓磁盤盤片轉動,然后傳動手臂可伸展讓讀取頭在盤片上進行讀寫操作。每個盤片有兩面,都可記錄信息,所以一張盤片對應著兩個磁頭。

磁盤物理結構如下圖:

image
image

扇區:盤片被分為許多扇形的區域,每個區域叫一個扇區,硬盤中每個扇區的大小固定為512字節
磁道:盤片表面上以盤片中心為圓心,不同半徑的同心圓環稱為磁道。


image

磁盤垂直視角

一個I/O請求所花費的時間=尋道時間+旋轉延遲+數據傳輸時間(約10ms)

當需要從磁盤讀取數據時,系統會將數據邏輯地址傳給磁盤,磁盤的控制電路按照尋址邏輯將邏輯地址翻譯成物理地址,即確定要讀的數據在哪個磁道,哪個扇區。為了讀取這個扇區的數據,需要將磁頭放到這個扇區上方,為了實現這一點,磁頭需要移動對準相應磁道,這個過程叫做尋道,所耗費時間叫做尋道時間,然后磁盤旋轉將目標扇區旋轉到磁頭下,這個過程耗費的時間叫做旋轉時間。

尋道時間(Tseek) :

將讀寫磁頭移動至正確的磁道上所需要的時間。尋道時間越短,I/O操作越快,目前磁盤的平均尋道時間一般在3-15ms。

旋轉延遲(Trotation)

指盤片旋轉將請求數據所在的扇區移動到讀寫磁盤下方所需要的時間。旋轉延遲取決于磁盤轉速,通常用磁盤旋轉一周所需時間的1/2表示。比如:7200rpm的磁盤平均旋轉延遲大約為60*1000/7200/2 = 4.17ms,而轉速為15000rpm的磁盤其平均旋轉延遲為2ms。

數據傳輸時間(Transfer)

是指完成傳輸所請求的數據所需要的時間,它取決于數據傳輸率,其值等于數據大小除以數據傳輸率。數據傳輸時間通常遠小于前兩部分消耗時間。簡單計算時可忽略。

預讀

當一次IO時,不光把當前磁盤地址的數據,而是把相鄰的數據也都讀取到內存緩沖區內,因為局部預讀性原理告訴我們,當計算機訪問一個地址的數據的時候,與其相鄰的數據也會很快被訪問到。每一次IO讀取的數據我們稱之為一頁(page)。

IOPS與吞吐量

連續讀寫性能很好,但隨機讀寫性能很差
機械硬盤的連續讀寫性能很好,但隨機讀寫性能很差,這主要是因為磁頭移動到正確的磁道上需要時間,隨機讀寫時,磁頭需要不停的移動,時間都浪費在了磁頭尋址上,所以性能不高。

IOPS

IOPS(Input/Output Per Second)即指每秒內系統能處理的I/O請求數量。 隨機讀寫頻繁的應用,如小文件存儲等,關注隨機讀寫性能,IOPS是關鍵衡量指標。

可以推算出磁盤的IOPS = 1000ms / (Tseek + Trotation + Transfer)

常見磁盤的隨機讀寫最大IOPS為:

7200rpm的磁盤 IOPS = 76 IOPS
10000rpm的磁盤IOPS = 111 IOPS
15000rpm的磁盤IOPS = 166 IOPS

磁盤吞吐量

指單位時間內可以成功傳輸的數據數量。

磁盤陣列與服務器之間的數據通道對吞吐量影響很大。
順序讀寫頻繁的應用,如視頻點播,關注連續讀寫性能、數據吞吐量是關鍵衡量指標。

InnoDB索引——B+Tree索引

B+Tree,簡單來說就是一種為磁盤或者其他存儲設備而設計的一種平衡二叉樹。
由二叉樹,平衡二叉樹,BTree演化而來。

二叉樹 要保證父節點大于左子結點,小于右子節點。

平衡二叉樹 在二叉樹的基礎上,還要保證任一結點的兩個兒子字樹高度差不大于1。

BTree 是一種自平衡二叉樹,繼承了上述平衡二叉樹的特性,另外并保證了每個葉子結點到根節點的距離相同。

BTree vs B+Tree:

B+樹與BTree主要不同就是data的存放位置,以及葉子結點的指針構成鏈表。

鍵值的拷貝被存儲在內部節點(或稱非葉子結點);鍵值和記錄存儲在葉子節點;
一個葉子節點可以包含一個指針,指向另一個葉子節點以加速順序存取。
二叉樹


image

平衡二叉樹


image

BTree


image

B+Tree


image

索引為什么使用B+Tree?

每個頁的葉子結點包含較多的數據,因此樹的高度較低(3~4),而樹的高度也決定了磁盤IO的次數,從而影響了數據庫的性能。一般情況下,IO次數與樹的高度是一致的
對于組合索引,B+tree索引是按照索引列名進行順序排序的,因此可以將隨機IO轉換為順序IO提升IO效率;并且可以支持order by \group等排序需求;適合范圍查詢


image

聚集索引 與 非聚集索引

聚集索引:

InnfoBD引擎是索引組織表,所有數據都存放在聚集索引中。

準確來說聚集索引并不是某種單獨的索引類型,而是一種數據存儲方式。就是指在同一個結構中保存了B+tree索引以及數據行。InnoDB中通常主鍵就是一個聚集索引。

innoDB中,用戶如果沒有設置主鍵索引,會隨機選擇一個唯一的非空索引替代,
如果沒有這樣的索引,會隱式的定義一個主鍵作為隱式的聚集索引。
通常將主鍵設置為一個與業務無關的自增數字,這樣能保證按照主鍵順序插入數據,避免頁分裂以及碎片問題。

主鍵索引的非葉子結點存放的是<.key,address.>,address就是指向下一層的指針。

主鍵索引的葉子結點保存了所有列的信息,因此通過主鍵索引可以快速獲取數據。

輔助索引

(或稱為非聚集索引、二級索引)
輔助索引的葉子結點并沒有存放數據,而是存放了主鍵索引的值信息,而是<.key,address.>的形式。address用于指向對應的主鍵索引的key。

因為二級索引葉子頁中存放了主鍵索引的值信息,如果主鍵索引很大的話,會導致所有索引都比較大。因此主鍵索引盡可能要小

也就是說使用輔助索引查詢,會通過葉子結點找到對應的主鍵,在主鍵索引中找到最終的數據。

為什么要使用索引

  • 使用索引可以大大減少服務器需要掃描的數據量。
  • 使用索引可以幫助服務器避免排序或者臨時表
  • 索引是隨機I\O變為 順序I\O.

索引的適用范圍

索引并不是適用于任何情況。對于中型、大型表適用。對于小型表全表掃描更高效。而對于特大型表,考慮”分區”技術。

高性能索引策略(其他類型索引:略)

以下講解使用如下表作為示例:

mysql> show create table people \G
*************************** 1. row ***************************
Table: people
Create Table: CREATE TABLE `people` (
  `last_name` varchar(50) NOT NULL,
  `first_name` varchar(50) NOT NULL,
  `dob` date NOT NULL,
  `gender` enum('m','f') NOT NULL,
  KEY `last_name` (`last_name`,`first_name`,`dob`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

最左前綴匹配原則

對于KEY last_name (last_name,first_name,dob),where 后的謂詞必須包含last_name(組合索引的最左列),否則無法使用這個索引.

示例:

select ... from people where .... last_name="..."....

這個語句將使用 last_name (last_name,first_name,dob)索引。

無法跳過某個列使用后續索引列

示例:

SELECT ... FROM people WHERE last_name="..." AND dob="..."

這個語句只使用了last_name (last_name,first_name,dob)的last_name列,因為缺少first_name,所以后續dob列也無法從索引中搜索。

范圍查詢后的列無法使用索引

示例:

SELECT ... FROM people WHERE last_name > "..."  AND first_name= "..." AND dob= "..."

這個語句只使用了last_name (last_name,first_name,dob)的last_name列,因為last_name 使用了范圍查詢,所以后續索引的兩個列無法使用。

什么事范圍查詢:
使用了范圍查詢的語句。范圍查詢指使用 “>” 、”<”、“between” “like”的查詢。注意“in”不算范圍查詢,屬于多值查詢條件。
列作為函數參數或表達式的一部分

列作為函數參數或表達式的一部分無法正常使用索引。
示例1:

SELECT ... FROM people WHERE last_name+1 = "1001"

示例2:

SELECT ... FROM people FORCE INDEX(last_name) WHERE LEFT(last_name,3) = "..."

以上示例均無法使用last_name (last_name,first_name,dob)索引。explain 展示位全表掃描。

前綴索引與索引選擇性

什么是索引的選擇性:

索引的基數(explain 中的一列:Cardinality) / 表的總記錄數(#T)
select count(Distinct columnName)/count(*) from Table

范圍從 1#T ~ 1 ,值越高查詢效率越高。唯一索引的選擇性是:1.

注:索引的基數(Cardinality)不重復的索引值。此處計算的基數(Cardinality),與SHOW INDEX 語句中的Cardinality并不一致!explain中只是預估值。

一般情況,將選擇性高的列放在左邊,選擇性高代表這個列的過濾性較好,盡可能的盡快過濾掉無用的數據。

前綴索引

對于 較大的Varchar類型、Text類型、Blob類型,需要建立索引時必須使用前綴索引,因為mysql不允許索引完整大小,而且索引字段越大效率越差。

可以索引開始的部分字符串(取代全部),大大節約索引空間,提高索引效率。但這樣會降低索引的選擇性。

所以,對比較長的 (Varchar、Text、BLOB等等數據類型)列查詢,要保證索引的選擇性,又要不能太長以節省空間。所以“前綴”需要選的恰到好處:

“前綴索引”的基數應該接近完整的列索引的基數。

示例:前7個字符的前綴索引

mysql> select count(Distinct last_name)/count(*) from people ;
+------------------------------------+
| count(Distinct last_name)/count(*) |
+------------------------------------+
|                             0.7059 |
+------------------------------------+
1 row in set (0.07 sec)
------------------------------------------------------------
mysql> select count(Distinct left(last_name,5))/count(*), count(Distinct left(la
st_name,6))/count(*) ,count(Distinct left(last_name,7))/count(*)  from people \G

*************************** 1. row ***************************
count(Distinct left(last_name,5))/count(*): 0.6471
count(Distinct left(last_name,6))/count(*): 0.7059
count(Distinct left(last_name,7))/count(*): 0.7059
1 row in set (0.00 sec)
mysql> alter table people add key (last_name(6))

所以使用前6個字符即可達到完整字段的過濾性。

注意:
前綴索引是能夠使索引更小,更快的方法,但是無法使用前綴索引做 Group By\Order By,也不能用前綴索引做覆蓋查詢(Using Index)。

除了使用前綴索引的方式處理這類大字段索引的情況,還有如下方式:

偽哈希索引

step1建表語句
-- step1建表語句

CREATE TABLE `people` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `last_name` VARCHAR(50) COLLATE utf8_bin NOT NULL,
  `first_name` VARCHAR(50) COLLATE utf8_bin NOT NULL,
  `dob` DATE NOT NULL,
  `gender` ENUM('m','f') COLLATE utf8_bin NOT NULL,
  `blog_url` VARCHAR(128) COLLATE utf8_bin DEFAULT NULL,
  `crc32_url` BIGINT(20) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `last_name` (`last_name`,`first_name`,`dob`),
  KEY `crc32_url` (`crc32_url`)
) ENGINE=INNODB

因為blog_url是一個較長的字符串,所以直接將blog_url作為索引列會影響索引的整體效率。現在,嘗試用一個偽hash值做一個偽hash索引。

step2 建立觸發器,用于每次插入\更新維護hash值
-- step2 建立觸發器,用于維護hash值

DELIMITER //
    CREATE TRIGGER pseudohash_crc32_ins BEFORE INSERT
    ON people FOR EACH ROW
    BEGIN
      SET New.crc32_url = CRC32(New.blog_url);
    END //
    CREATE TRIGGER pseudohash_crc32_upd BEFORE UPDATE
    ON people FOR EACH ROW
    BEGIN
      SET New.crc32_url = CRC32(New.blog_url);
    END //
DELIMITER;

效果如下,非常明顯:

SELECT COUNT(1) FROM people ;
/**
 636480
*/


SELECT blog_url FROM people WHERE blog_url="http://www.-588141732.com" AND crc32_url=1790086969
/**

sql執行時間:
執行耗時   : 0.004 sec
傳送時間   : 0 sec
總耗時      : 0.004 sec

explain:
"id"    "select_type"   "table" "partitions"    "type"  "possible_keys" "key"   "key_len"   "ref"   "rows"  "filtered"  "Extra"
"1" "SIMPLE"    "people"    \N  "ref"   "crc32_url" "crc32_url" "9" "const" "1" "10.00" "Using where"
*/

EXPLAIN SELECT blog_url FROM people WHERE blog_url="http://www.-588141732.com" 

/**
sql執行時間:
執行耗時   : 0.413 sec
傳送時間   : 0 sec
總耗時      : 0.413 sec

explain:
"id"    "select_type"   "table" "partitions"    "type"  "possible_keys" "key"   "key_len" "ref" "rows"   "filtered" "Extra"
"1" "SIMPLE"    "people"    \N  "ALL"       \N  \N  \N     \N   "630928"  "10.00"   "Using where"
*/

crc函數當數據量達到93000時,會產生1%的沖突。
如果要避免hash沖突的概率可以使用MD5()截取的方式取代crc()。
如:

SELECT CONV(RIGHT(MD5("http://www.-aqq3feaeaff41732fff.com"),16),16,10) AS hashCode;

三星索引(關系型數據庫索引設計及優化)

  • 一星 使用where后的謂詞列,按照選擇性構造索引。
  • 二星 如果語句中有排序操作,使用索引自帶的順序的排序(消除fileSort)。
  • 三星 如果可以的話,將select后的還不在索引中的列名放到索引后邊,可以覆蓋索引(using index),而不需要讀取表數據。
    如下這個sql就是一個三星索引。
SELECT last_name,first_name,dob FROM people WHERE last_name=”101899” AND first_name=”10189900” AND dob=”2017-08-03” ORDER BY dob

在實際應用中,無法保證三個星每個星都滿足。需要權衡取舍。
冗余與重復索引

重復索引:相同列上按照相同順序創建的相同類型的索引。
冗余索引:已有索引(A,B),現在 創建索引 (A)就是一個冗余索引,因為,索引(A)完全可以被 (A,B)替代。然而,(B,A)、(B) 并不是 (A,B)的冗余索引。
另外當Id列是主鍵,(A,Id)是冗余索引,因為二級緩存的葉子節點包含了主鍵值。直接使用(A)作為索引即可。
未使用的索引 也是累贅。建議刪除。

Explain output \Profile

關于explain的詳細解釋,請參考:
explain 詳解
或者查看官網文檔
explain-output

除了explain,還可以查看sql耗時分布情況。

Show Profiles \Show profile queryId


SHOW PROFILES;
/**
"Query_ID"  "Duration"  "Query"
...
"19"    "0.00007350"    "select ...."
"20"    "0.00026150"    "select state, round(sum(duration),5) ....."
...
*/
SHOW PROFILE FOR QUERY 24
/**
"Status"                "Duration"
"Creating sort index"   "0.748448"
"freeing items"         "0.001355"
"starting"              "0.000029"
"cleaning up"           "0.000019"
"init"                  "0.000015"
"statistics"            "0.000012"
"end"                   "0.000008"
"preparing"             "0.000007"
"Opening tables"        "0.000007"
"closing tables"        "0.000006"
"Sending data"          "0.000005"
"query end"             "0.000005"
"optimizing"            "0.000004"
"System lock"           "0.000004"
"Sorting result"        "0.000003"
"checking permissions"  "0.000001"
"checking permissions"  "0.000001"
"executing"             "0.000001"
*/

高性能SQL

理解sql執行過程

Step1:客戶端向Mysql服務器發送SQL語句。

使用”半雙工”通信方式,客戶端或服務端在一個連接上同一時刻只允許一方進行數據傳輸,并且直到數據傳輸完成,另一方才能執行傳輸。

當語句太長,超過 max_allowed_packet ,服務端會拒絕接收。
通常建議加上limit,可以減少不必要的數據從服務端發送到客戶端。

Step2:服務器收到后先查詢”查詢緩存“,如果命中,從緩存中直接返回sql執行的結果集。否則,進入Step3。

這個緩存通過一個對大小寫敏感的hash算法實現,及時只有一個字節不匹配,那也無法命中。

Step3:服務器解析、預處理、優化sql執行計劃,然后將處理好的sql放入查詢的執行計劃中。
在這個階段,sql會被轉換為一個執行計劃,使用這個執行計劃于具體的存儲引擎進行交換。這個階段包括,解析、預處理、優化sql執行計劃這三個子任務。

Step4:執行引擎通過調用”存儲引擎”(如,innodb、myisam等)提供的API去執行這個計劃。

Step5:服務器返回結果給客戶端
這里寫圖片描述

慢SQL優化步驟

Step1:explain查看 (show profile可以查看耗時分布)

Step2:確認優化目標\方向,對于復雜sql需要理清執行步驟

目標1. type是否能夠按照 const>eq_reg>ref>range>index>ALL的順序優化,最差也要達到range級別。
目標2. 避免filesort的出現、避免rows數據量太大等負面字段、索引選擇性是否足夠、對于關聯查詢盡量保證關聯字段在第二張表上有可用索引(原因:NestLoop)。

Step3:遵照SQL索引原則增加或調整SQL,常見如下(可參考上文去理解)

保證where后的謂詞盡可能出現在索引中,并且組合索引按選擇性順序排序,范圍查詢條件盡量放在后邊
(如果sql中有排序語句)是否能夠通過索引解決排序問題
是否能使用use index,全部通過索引獲取數據
NestLoop

除了full Join,其他所有類型的查詢SQL,都以類似的方式執行。

NestLoop (內嵌套循環)算法,簡單來說就是逐行查詢處理,或者內嵌逐行查詢。對于高版本的使用join buffer對上層表數據緩存,無需多次遍歷上層表,下層表直接使用(Block NestLoop)。

以下以兩個示例詳細說明執行計劃,其他join以及單表查詢原理也是類似的!

Join執行順序偽代碼演示

示例1:內關聯inner join

SELECT 
    people.id,user.id
FROM
    user
        INNER JOIN
    people ON user.name = people.name
WHERE
    user.enumType = 'orange'
        AND people.enumType = 'orange';

示例1對應偽代碼:

//先掃描先執行的表,優化器通常選擇關聯的較小的表,explain第一行。
people_iterator=people_table.iterator();
//逐行遍歷,并且丟棄where篩選不通過的行

while(people_iterator.hashNext()){

    people_item=people_iterator.next();

    if(people_item.enumType=='orange'){
        //篩選通過后,在進入第二個嵌套
        user_iterator=user_table.iterator()
        //逐行遍歷第二個表
        while(user_iterator.hashNext()){

            user_item=user_iterator.next();
            //過濾:on 的條件匹配以及當前表的where條件
            if(user_item.name==people_item.name&&user_item.enumType=='orange' ){

                output(people.id,user.id);
                                            }
                                        }
                                    }
                                }

示例2:非內關聯

SELECT 
    people.id,user.id
FROM
    user
        LEFT JOIN
    people ON user.name = people.name
WHERE
    user.enumType = 'orange'
        AND people.enumType = 'orange';

示例2偽代碼

//先掃描先執行的表,優化器通常選擇關聯的較小的表,explain第一行。
people_iterator=people_table.iterator();
//逐行遍歷,并且丟棄where篩選不通過的行
while(people_iterator.hashNext()){

people_item=people_iterator.next();

if(people_item.enumType=='orange'){
    //篩選通過后,在進入第二個嵌套
    user_iterator=user_table.iterator()
    //逐行遍歷第二個表
    while(user_iterator.hashNext()){

        user_item=user_iterator.next();
        //過濾:on 的條件匹配以及當前表的where條件
        if(user_item.name==people_item.name&&user_item.enumType=='orange' ){

            output(people.id,user.id);
                                        }
        //與innerjoin不同的,leftJoin需要即使on條件不成立,也要保留左邊數據
        else if(!is_innerJoin){
            output(people.id,null);//保留左邊數據
        }

                                    }
                                }
                            }   

注意:盡量保證關聯字段在第二張表上有可用索引。
(因為第一張表示全表掃描,然后會對第二張表用關聯字段查詢,詳情請看NestLoop理解關聯過程)

SQL使用常用策略

1.通常情況下,使用一個性能好的sql去做更多的事情,而不是使用多個sql。

除非這個sql過長效率低下或者對于delete這種語句,過長的delete會導致太多的數據被鎖定,耗盡資源,阻塞其他sql。

2.分解關聯查詢。
將關聯(** join……)放在應用中處理,執行小而簡單的sql,好處是:

分解后的sql通常由于簡單固定,能更好的使用mysql緩存。
執行拆分后的sql,可以減少鎖的競爭。
程序具備更強的擴展性
關聯sql使用的是內嵌循環算法nestloop,而應用中可以使用hashmap等結構處理數據,效率更高
關于Count()
count()函數有兩種含義:統計行數、統計列數。
比如:count(*)代表統計的行數;count(talbe.cloumn)代表統計的是這個列不為null的數量。
關于Limit
在使用Limit 1000,20這種操作的時候,mysql會掃描偏移量(1000條無效查詢)數據,而只取后20條,盡量避免這種寫法,想辦法規避。
關于Union
需要將where、order by、limit 這些限制放入到每個子查詢,才能重分提升效率。另外如非必須,盡量使用Union all,因為union會給每個子查詢的臨時表加入distinct,對每個臨時表做唯一性檢查,效率較差。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容