MySQL索引原理及SQL優化
目錄
- 索引(Index)
- 索引的原理
- b+樹
- MySQL如何使用索引
- 如何優化
- 索引雖好,不可濫用
- 如何驗證索引使用情況?
- 索引的原理
- SQL優化
- explain查詢執行計劃
- id
- select_type
- table
- type
- possible_keys
- key
- key_len
- ref
- rows
- Extra
- 優化資料庫結構
- 優化資料大小
- 優化資料型別
- explain查詢執行計劃
索引(Index)
MySQL官方對索引的定義為:索引(Index)是幫助MySQL高效獲取資料的資料結構。索引的建立對於MySQL的高效執行是很重要的,索引可以大大提高MySQL的檢索速度。
拿漢語字典的目錄頁(索引)打比方,我們可以按拼音、筆畫、偏旁部首等排序的目錄(索引)快速查詢到需要的字。
索引分單列索引和組合索引。單列索引,即一個索引只包含單個列,一個表可以有多個單列索引,但這不是組合索引。組合索引,即一個索引包含多個列。
建立索引時,你需要確保該索引是應用在 SQL 查詢語句的條件(一般作為 WHERE 子句的條件)。
實際上,索引也是一張表,該表儲存了主鍵與索引欄位,並指向實體表的記錄。
索引的原理
索引用於快速查詢具有特定列值的行。如果沒有索引,MySQL必須從第一行開始,然後讀取整個表以查詢相關行。表越大,成本越高。如果表中有相關列的索引,MySQL可以快速確定要在資料檔案中間尋找的位置,而無需檢視所有資料。這比按順序讀取每一行要快得多。
MySQL常用的是B+ Tree索引,下面詳細介紹。
b+樹
如上圖,是一顆b+樹,淺藍色的塊我們稱之為一個磁碟塊,可以看到每個磁碟塊包含幾個資料項(深藍色所示)和指標(黃色所示),如磁碟塊1包含資料項17和35,包含指標P1、P2、P3,P1表示小於17的磁碟塊,P2表示在17和35之間的磁碟塊,P3表示大於35的磁碟塊。真實的資料存在於葉子節點即3、5、9、10、13、15、28、29、36、60、75、79、90、99。非葉子節點不儲存真實的資料,只儲存指引搜尋方向的資料項,如17、35並不真實存在於資料表中。
上圖中,如果要查詢資料項29,那麼首先會把磁碟塊1由磁碟載入到記憶體,此時發生一次IO,在記憶體中用二分查詢確定29在17和35之間,鎖定磁碟塊1的P2指標,記憶體時間因為非常短(相比磁碟的IO)可以忽略不計,通過磁碟塊1的P2指標的磁碟地址把磁碟塊3由磁碟載入到記憶體,發生第二次IO,29在26和30之間,鎖定磁碟塊3的P2指標,通過指標載入磁碟塊8到記憶體,發生第三次IO,同時記憶體中做二分查詢找到29,結束查詢,總計三次IO。真實的情況是,3層的b+樹可以表示上百萬的資料,如果上百萬的資料查詢只需要三次IO,效能提高將是巨大的,如果沒有索引,每個資料項都要發生一次IO,那麼總共需要百萬次的IO,顯然成本非常非常高。
通過上面的分析,我們知道IO次數取決於b+數的高度h,假設當前資料表的資料為N,每個磁碟塊的資料項的數量是m,則有
當資料量N一定的情況下,m越大,h越小;而m = 磁碟塊的大小 / 資料項的大小,磁碟塊的大小也就是一個數據頁的大小,是固定的,如果資料項佔的空間越小,資料項的數量越多,樹的高度越低。這就是為什麼每個資料項,即索引欄位要儘量的小,比如int佔4位元組,要比bigint8位元組少一半。
當b+樹的資料項是複合的資料結構,常見的就是組合索引,比如我們給某個表添加個組合索引,包括姓名、年齡和性別三列(name,age,sex),b+數是按照從左到右的順序來建立搜尋樹的,比如查詢(where name=‘馬雲’ and age=18 and sex=1),b+樹會優先比較name來確定下一步的檢索方向,如果name相同再依次比較age和sex,最後得到檢索的資料;但如果我們查詢(where age=18 and sex= 1),此時索引是不生效的,因為建立搜尋樹的時候name就是第一個比較因子,必須要先根據name來搜尋才能知道下一步去哪裡查詢。比如當查詢(where name='張三' and sex=2)的時候,b+樹可以用name來指定搜尋方向,但下一個欄位age的缺失,所以只能把名字等於張三的資料都找到,然後再匹配性別是2的資料了, 這個是非常重要的性質,即索引的最左匹配特性。
MySQL如何使用索引
MySQL使用索引進行這些操作:
WHERE
快速 查詢與子句匹配的行。如果在多個索引之間有選擇,MySQL通常使用找到最小行數的索引。
如果表具有多列索引,即組合索引,則優化程式可以使用索引的任何最左字首來查詢行。例如,如果你有一個三列索引上(col1, col2, col3)
,你有索引的搜尋功能
(col1),
(col1, col2)以及
(col1, col2, col3)。在執行連線時從其他表中檢索行。如果宣告它們的型別和大小相同,MySQL可以更有效地使用列上的索引。在這種情況下,
VARCHAR
與CHAR
被認為是相同的,如果它們被宣告為相同的大小。例如,VARCHAR(10)
和CHAR(10)
大小相同,但VARCHAR(10)
與CHAR(15)
不是。對於非二進位制字串列之間的比較,兩列應使用相同的字符集。例如,將
utf8
列與latin1
列進行比較會排除使用索引。不相似列的比較(例如,將字串列與時間或數字列進行比較)可能會在沒有轉換的情況下無法直接比較值時阻止使用索引。對於給定的值,如
1
在數值列,它可能比較等於在字串列,例如任何數量的值'1'
,' 1'
,'00001'
,或'01.e1'
。這排除了對字串列的任何索引的使用。查詢特定索引列的值Min()或 Max()[`值key_col。這是由前處理器優化的,該前處理器檢查您是否正在使用 索引之前出現的所有關鍵部分。在這種情況下,MySQL對每個或 表示式執行單個鍵查詢,並用常量替換它。
對指定索引列進行排序或者分組,ORDER BY或者 GROUP BY
在某些情況下,可以優化查詢在不查詢整行資料的情況下檢索值。(為查詢提供所有必要結果的索引稱為 [覆蓋索引])如果查詢僅使用表中包含某些索引的列,則可以從索引樹中檢索所選值以獲得更快的速度:比如
SELECT key_part3 FROM tbl_name WHERE key_part1 = 1
對於小型表或報表查詢處理大多數或所有行的大型表的查詢,索引不太重要。當查詢需要訪問大多數行時,順序讀取比通過索引更快。順序讀取可以最大限度地減少磁碟搜尋,即使查詢不需要所有行也是如此。
如何優化
主鍵優化
表的主鍵表示您在最重要的查詢中使用的列或列集。它具有關聯的索引,以實現快速查詢效能。查詢效能受益於
NOT NULL
優化,因為它不能包含任何NULL
值。使用InnoDB
儲存引擎,表資料在物理上進行組織,以根據主鍵或列進行超快速查詢和排序。如果您的表很大且很重要,但沒有明顯的列或列集用作主鍵,則可以建立一個單獨的列,其中包含自動增量值以用作主鍵。使用外來鍵連線表時,這些唯一ID可用作指向其他表中相應行的指標。
外來鍵優化
如果一個表有很多列,並且您查詢了許多不同的列組合,那麼將頻率較低的資料拆分為每個都有幾列的單獨表可能會很有效,並通過外來鍵將它們與主表關聯起來。這樣每個小表都可以有一個主鍵來快速查詢其資料,您可以使用連線操作查詢所需的列集。根據資料的分佈方式,查詢可能會執行較少的I / O並佔用較少的快取記憶體。(為了最大限度地提高效能,查詢嘗試從磁碟中讀取儘可能少的資料塊)。
列索引
最常見的索引型別涉及單個列,在資料結構中儲存該列的值的副本,允許快速查詢具有相應列值的行。B樹資料結構可以讓索引快速查詢特定值,一組值,或值的範圍,例如where條件中
=
,>
,≤
,BETWEEN
,IN
等。每個儲存引擎定義每個表的最大索引數和最大索引長度。所有儲存引擎支援每個表至少16個索引,總索引長度至少為256個位元組。
索引字首
使用 字串列的索引規範中的語法,可以建立僅使用列的前幾個字元的索引 。以這種方式僅索引列值的字首可以使索引檔案更小。索引 或 列時, 必須為索引指定字首長度。
如果搜尋項超過索引字首長度,則索引用於排除不匹配的行,並檢查剩餘的行以查詢可能的匹配項。
FULLTEXT索引
FULLTEXT
索引用於全文搜尋。只有InnoDB
和MyISAM
儲存引擎支援FULLTEXT
索引和僅適用於CHAR,VARCHAR和TEXT型別的列。索引始終發生在整個列上,並且不支援列字首索引。空間索引(Spatial Index)
您可以在空間資料型別上建立索引。
MyISAM和InnoDB
支援空間型別的R樹索引。其他儲存引擎使用B樹來索引空間型別(除了ARCHIVE
)。
多列索引
MySQL可以建立複合索引(即多列索引)。索引最多可包含16列。對於某些資料型別,您可以索引列的字首。
MySQL可以對測試索引中所有列的查詢使用多列索引,或者只測試第一列,前兩列,前三列等的查詢。如果在索引定義中以正確的順序指定列,則單個複合索引可以加速同一表上的多種查詢。
假設一個表具有以下規範:
CREATE TABLE test ( id INT NOT NULL, last_name CHAR(30) NOT NULL, first_name CHAR(30) NOT NULL, PRIMARY KEY (id), INDEX name (last_name,first_name) );
在last_name
和
first_name列建立了一個組合索引,它既可以查詢last_name和
first_name`組合的值,也可以僅查詢last_name,因為該列是索引的最左字首。因此,下面這些查詢是可以用到該索引的://只查詢last_name SELECT * FROM test WHERE last_name='Jones'; //同時查 SELECT * FROM test WHERE last_name='Jones' AND first_name='John'; SELECT * FROM test WHERE last_name='Jones' AND (first_name='John' OR first_name='Jon'); SELECT * FROM test WHERE last_name='Jones' AND first_name >='M' AND first_name < 'N';
但是,該索引 不能用於以下查詢中的查詢:
SELECT * FROM test WHERE first_name='John'; SELECT * FROM test WHERE last_name='Jones' OR first_name='John';
假設您寫了如何SQL語句:
SELECT * FROM tbl_name WHERE col1=val1 AND col2=val2;
如果col1和col2存在組合索引,那麼可以直接獲取相應的行。如果col1和col2每列都存在單列索引,那麼MySQL會優化合並索引,或者嘗試通過確定哪個索引會排除更多的行來查詢限制性最強的索引。
如果表具有多列索引,則優化程式可以使用索引的最左字首來查詢行。例如,如果你有一個三列索引上
(col1, col2, col3)
,你有索引的搜尋功能(col1)
,(col1, col2)
以及(col1, col2, col3)
。如果SQL語句不適用索引的最左字首,則MySQL無法使用索引執行查詢。例如以下查詢語句:
//使用索引 SELECT * FROM tbl_name WHERE col1=val1; //使用索引 SELECT * FROM tbl_name WHERE col1=val1 AND col2=val2; //不使用索引 SELECT * FROM tbl_name WHERE col2=val2; //不使用索引 SELECT * FROM tbl_name WHERE col2=val2 AND col3=val3;
如果存在索引
(col1, col2, col3)
,則只有前兩個查詢使用索引。第三和第四個查詢確實包括索引的列,但不使用索引來進行查詢,因為(col2)
和(col2, col3)
不是的最左邊的字首(col1, col2, col3)
。
索引雖好,不可濫用
儘管為查詢中使用的每個可能列建立索引很有誘惑力,但不必要的索引會浪費空間並浪費時間讓MySQL確定要使用哪些索引。索引還會增加插入,更新和刪除的成本,因為必須更新每個索引。您必須找到適當的平衡,以使用最佳索引集實現快速查詢。
如何驗證索引使用情況?
我們建立了索引,但是我們如何確定mysql使用了索引? 答案是 使用explain語句。
下面會詳細介紹Explain,以及如何優化SQL。
SQL優化
explain查詢執行計劃
舉個例子,最基礎的主鍵查詢
EXPLAIN SELECT
*
FROM
`subject`
WHERE
id = 1
執行結果如下:
再舉個關聯查詢的例子
EXPLAIN SELECT
a.*
FROM
`subject` a
LEFT JOIN subject_role_0 b ON a.id = b.subject_id
WHERE
a.id < 3
執行結果如下:
屬性 | 說明 |
---|---|
id | 查詢的序列號 |
select_type | 查詢的型別 |
table | 輸出結果集的表 |
rows | 掃描的行數 |
type | 連線型別,all表示採用全表掃描的方式。 |
possible_keys | 可能使用的索引 |
key | 實際使用的索引 |
key_len | 索引欄位的長度 |
ref | 列與索引的比較 |
Extra | 額外資訊,比如使用了where語句,使用了join buffer等 |
id
id是sql執行順序的標識,按id從大到小的順序執行,在id相同時,執行順序是由上至下
select_type
select 查詢的型別,主要是用於區別普通查詢,聯合查詢,巢狀的複雜查詢
型別 | 說明 |
---|---|
simple | 簡單的select 查詢,查詢中不包含子查詢或者union |
primary | 查詢中若包含任何複雜的子查詢,最外層查詢則被標記為primary |
subquery | 在select或where 列表中包含了子查詢 |
derived | 在from列表中包含的子查詢被標記為derived(衍生)MySQL會遞迴執行這些子查詢,把結果放在臨時表裡。 |
union | 若第二個select出現在union之後,則被標記為union,若union包含在from子句的子查詢中,外層select將被標記為:derived |
union result | 從union表獲取結果的select |
table
查詢的資料庫的表的名稱,如果沒有給表指定別名,那麼table值為表的名稱;否則table值為你指定的別名
type
表示MySQL在表中找到所需行的方式,這是一個非常重要的引數,常見的有:all , index , range , ref , eq_ref , const , system , null 八個級別。
常用的型別有: all、index、range、 ref、eq_ref、const、system、null(從左到右,效能從差到好)
型別 | 說明 |
---|---|
all | 全表掃描找到匹配的行,效能最差 |
index | 全索引掃描,從索引樹找資料,比all效能好 |
range | 只掃描指定範圍的行,使用索引來匹配行,常見使用between,in,>,<等關鍵字 |
ref | 非唯一性索引掃描,本質上也是一種索引訪問,返回所有匹配某個單獨值的行。 |
eq_ref | 唯一性索引掃描,對於每個索引鍵,表中有一條記錄與之匹配。 |
const | 表示通過索引一次就可以找到,const用於比較primary key 或者unique索引。因為只匹配一行資料,所以很快 |
system | 表只有一條記錄(等於系統表),這是const型別的特列,平時不會出現 |
null | MySQL在優化過程中分解語句,執行時甚至不用訪問表或索引,例如從一個索引列裡選取最小值可以通過單獨索引查詢完成。 |
possible_keys
指出MySQL能使用哪個索引在表中找到記錄,查詢涉及到的欄位上若存在索引,則該索引將被列出,但不一定被查詢使用(該查詢可以利用的索引,如果沒有任何索引顯示 null)
key
key列顯示MySQL實際決定使用的索引,必然包含在possible_keys中
key_len
顯示索引中使用的位元組數,可通過key_len計算查詢中使用的索引長度。在不損失精確性的情況下索引長度越短越好。key_len 顯示的值為索引欄位的最可能長度,並非實際使用長度,即key_len是根據表定義計算而得,並不是通過表內檢索出的。
ref
顯示索引的哪一列或常量被用於查詢索引列上的值。
rows
很重要的一個引數,根據表統計資訊及索引選用情況,大致估算出找到所需的記錄所需要讀取的行數,值越大說明掃描的行數越多,效能越差
Extra
該列包含MySQL解決查詢的詳細資訊,有以下幾種情況:
值 | 說明 |
---|---|
Using where | 不用讀取表中所有資訊,僅通過索引就可以獲取所需資料 |
Using temporary | 表示需要使用臨時表來儲存結果集,常見於排序和分組查詢,如group by ,order by |
Using filesort | 當查詢中包含 order by 操作,且無法利用索引完成的排序操作稱為“檔案排序” |
Using join buffer | 在獲取連線條件時沒有使用索引,並且需要連線緩衝區來儲存中間結果。如果出現了這個值,那應該注意,根據查詢的具體情況可能需要新增索引來改進能 |
Impossible where | 強調了where語句會導致沒有符合條件的行 |
Select tables optimized away | 這個值意味著僅通過使用索引,優化器可能僅從聚合函式結果中返回一行 |
No tables used | Query語句中使用from dual 或不含任何from子句 |
優化資料庫結構
優化資料大小
以最小佔用磁碟空間來設計表,這樣可以減少磁碟寫入和讀取來實現效能的提升。較小的表通常需要較少的主記憶體,同時索引也比較小,便於更快的處理。
通過使用此處列出的技術,您可以獲得更好的表效能並最大限度地減少儲存空間:
表列
- 儘可能使用最有效(最小)的資料型別。MySQL有許多專門的型別可以節省磁碟空間和記憶體。例如,如果可能,請使用較小的整數型別。mediumint通常是一個更好的選擇,它比int使用的列空間減少25%。
- 儘量使用NOT NULL列,它通過更好地使用索引並避免測試每個值是否為NULL來獲取更快的速度。
索引
- 表的主索引應儘可能短。這使得每行的識別變得簡單而有效。
- 僅建立提高查詢效能所需的索引。索引適用於檢索,但會降低插入和更新操作的速度。如果您主要通過搜尋列的組合來訪問表,請在它們上建立單個複合索引,而不是為每列建立單獨的索引。索引的第一部分應該是最常用的列。如果從表中選擇時總是使用多列,則索引中的第一列應該是具有最多重複的列,以獲得更好的索引壓縮。
- 如果長字串列很可能在第一個字元數上有唯一的字首,那麼最好只使用MySQL支援在列的最左邊部分建立索引來索引此字首,較短的索引更快,不僅因為它們需要更少的磁碟空間,而且因為它們還會在索引快取中為您提供更多的命中,從而減少磁碟搜尋次數。
Join
- 在某些情況下,分成兩個經常掃描的表可能是有益的,如果它是動態格式表,則尤其如此,並且可以使用較小的靜態格式表,該表可用於在掃描表時查詢相關行。
- 在具有相同資料型別的不同表中宣告具有相同資訊的列,可以加速連線。
- 保持列名簡單,以便您可以在不同的表中使用相同的名稱並簡化連線查詢。例如表customer
,使用列名
name而不是
customer_name。
正常化
- 通常,儘量保持所有資料不冗餘(第三正規化),儘量通過引用join子句中的ID來連線查詢中的表。
- 如果速度比磁碟空間更重要,並且保留多個數據副本,那麼可以建立彙總表到獲得更快的速度。
優化資料型別
對於唯一的id,首選使用數字列,而不是字串,這是因為數字比字串佔用更少的位元組,傳輸和比較速度更快,佔用的記憶體更少。
優化字元和字串型別
對於字元和字串列,請遵循以下準則:
- 比較來自不同列的值時,請儘可能宣告具有相同字符集和排序規則的列,以避免在執行查詢時進行字串轉換。
- 對於小於8KB的列值,請使用binary
VARCHAR
而不是BLOB
。 - 如果表包含字串列(如名稱和地址),但許多查詢不檢索這些列,請考慮將字串列拆分為單獨的表,並在必要時使用帶有外來鍵的連線查詢。可以減少了常見查詢的磁碟I / O和記憶體使用。
優化BLOB型別
- 儲存包含文字資料的大blob時,請考慮先壓縮它。
- 對於具有多個列的表,要減少使用BLOB列的查詢,請考慮將BLOB列拆分為單獨的表,並在需要時使用連線查詢引用它。