網上都說學會mysql須要學會兩個部分,索引和事務,其實在最近的Mysql學習過程當中,我以爲應該是有三個部分的,索引,查詢,事務.其中的查詢主要是指查詢優化即編寫高效率的SQL語句.mysql
本文記錄一下學習MySQL的索引過程當中的一些知識.主要爲閱讀《高性能MySQL》的一些理解和擴展.sql
索引是存儲引擎用於快速找到記錄的一種數據結構.數據庫
這是MySQL官方對於索引的定義,能夠看到索引是一種數據結構,那麼咱們應該怎樣理解索引呢?一個常見的例子就是書的目錄.咱們都已經養成了看目錄的習慣,拿到一本書時,咱們首先會先去查看他的目錄,而且當咱們要查找某個內容時,咱們會在目錄中查找,而後找到該片斷對應的頁碼,再根據相應的頁碼去書中查找.若是沒有索引(目錄)的話,咱們就只能一頁一頁的去查找了.數組
在MySQL中,假設咱們有一張以下記錄的表:緩存
id | name | age |
---|---|---|
1 | huyan | 10 |
2 | huiui | 18 |
3 | lumingfei | 20 |
4 | chuzihang | 15 |
5 | nono | 21 |
若是咱們但願查找到年齡爲15的人的名字,在沒有索引的狀況下咱們只能遍歷全部的數據去作逐一的對比,那麼時間複雜度是O(n).bash
而若是咱們在插入數據的過程當中, 額外維護一個數組,將age字段有序的存儲.獲得以下數組.服務器
[10,15,18,20,21]
| | | | |
[x1,x4,x2,x3,x5]
複製代碼
下面的x是模擬數據再磁盤上的存儲位置.這個時候若是咱們須要查找15歲的人的名字.咱們能夠對蓋數組進行二分查找.衆所周知,二分查找的時間複雜度爲O(logn).查找到以後再根據具體的位置去獲取真正的數據.數據結構
PS:MySQL中的索引不是使用的數組,而是使用的B+樹(後面講),這裏用數組舉例只是由於比較好理解.性能
如上面所說,索引能幫助咱們快速的查找到數據.其次由於索引中的值是順序儲存,那麼能夠幫助咱們進行orderby操做.並且索引中也是存儲了真正的值的,所以有一些的查詢直接能夠在索引中完成(也就是覆蓋索引的概念,後面會提到).學習
總結一下索引的優勢就是(《高性能》書中總結的):
索引有哪些缺點呢?
首先索引也是數據,也須要存儲,所以會帶來額外的存儲空間佔用.其次,在插入,更新和刪除操做的同時,須要維護索引,所以會帶來額外的時間開銷.
總結一下:
實際上,在必定數據範圍內(索引沒有超級多的狀況下),創建索引帶來的開銷是遠遠小於它帶來的好處的,可是咱們仍然要防止索引的濫用.
對於MySQL來講,在服務器層並不實現索引,而是交給存儲引擎來實現的,所以不一樣的存儲引擎實現的索引類型不太同樣.InnoDB做爲當前使用最爲普遍的存儲引擎,使用的是B+樹索引,所以咱們大部分時間提到的索引也都是指的它.
MySQL主要有如下幾種索引:
本文只學習B-樹索引和B+樹索引.
這裏不會特別詳細的解釋B-樹和B+樹的數據結構原理,有興趣的小夥伴能夠移步參考文章中的文章.或者經過google自行了解.
B-樹是一棵多路平衡查找樹,對於一棵M階的B-樹有如下的性質:
這麼說可能會有一些難理解,能夠將B-樹理解爲一棵更加矮胖的二叉搜索樹.
B+樹是B-樹的進階版本,在B-樹的基礎上又作了以下的限制:
這樣能夠帶來什麼好處呢?
因爲優化索引和優化查詢通常是分不開的,所以這一塊可能會包含部分的查詢優化內容.
若是但願給一個很長的字符串上添加索引,那麼能夠考慮使用前綴索引.在正式介紹前綴索引以前,咱們先大概考慮一下索引的工做步驟,數據庫使用索引進行查找的時候,通常是以下幾步:
卡塞爾學院
的一條記錄,而且拿到這條數據在磁盤上的地址.那麼假如在全部的學校名稱的值中,卡塞爾
就能夠惟一的標識這條數據,那麼用卡塞爾
來作索引是否能夠達到和卡塞爾學院
作索引相同的效果?
答案是確定的,而使用卡塞爾
的話,是能夠減小索引的大小到原來的60%的.這就是前綴索引的做用.
前綴索引: 在對一個比較長的字符串進行索引時,能夠僅索引開始的一部分字符,這樣能夠大大的節約索引空間,從而提升索引效率.可是這樣也會下降索引的選擇性.
索引的選擇性: 不重複的值/全部的值. 能夠看出索引的選擇性爲0-1
,最高的就是該列惟一,沒有重複值.因此惟一索引的效率是比較好的.
可是在通常狀況下,較長的字符串的一些前綴的選擇性也是比較好的,這個咱們能夠算出來.使用下面的語句:
select
count(distinct left(school_name,3))/count(*) as sch3,
count(distinct left(school_name,4))/count(*) as sch4,
count(distinct left(school_name,5))/count(*) as sch5,
count(distinct school_name)/count(*) as original
from
user;
複製代碼
其中查找到的original
就是本來的選擇性,sch3,sch4,sch5
分別是取該列的前3,4,5個字符做爲索引的時候的選擇性.逐步增長這個數值,當選擇性與原來相差不大的時候,就是一個比較合適的前綴索引的長度.(通常狀況下是這樣,可是也有例外,當數據極其不均勻時,這樣的前綴索引會在某個特殊的case上表現不好勁).
找到合適的長度以後,就能夠建立一個前綴索引了:alter table user add index sch_pre3(`school(3)`)
注意:前綴索引和覆蓋索引是很難一塊兒使用的,我今天早上剛試過,對索引的優化進行到這一步以後無功而返,具體的緣由在下面介紹完覆蓋索引以後解釋.
通常咱們都是有對多個列進行索引
的需求的,由於查詢的需求多種多樣.這個時候咱們能夠選擇創建多個獨立的索引或者創建一個聯合索引.大多數時候都是聯合索引更加合適一些.
假設咱們要執行這個語句:select * from user where school_name = '卡塞爾' and age > 20
,咱們在school
和age
上分別創建兩個獨立的索引,那麼咱們預期這條查詢語句會命中兩個索引,可是使用explain命令查看會發現不必定.這是一個玄學的過程.我的沒有研究清楚.
從理論上來說,MySQL在5.0以後的版本里面對支持合併索引,也就是同時使用兩個索引,可是MySQL的優化器不必定這樣認爲,他可能會認爲,查詢兩次B+樹的代價高於查詢一次索引以後去數據表進行過濾
,所以會選擇只用一個索引.(我在本身的5張表上作了相似此case的測試,結果都是隻使用了一個索引.)
建立聯合索引的語法:alter table user add index school_age(`school`,`age`)
.
使用聯合索引的時候,有一個很是重要的因素就是全部的索引列只能夠進行最左前綴匹配,例如上面的school_age
聯合索引,當僅使用age做爲查詢條件的時候是不能使用的,也就是說select * from user where age =20
是不能命中上面的聯合索引的.
在不考慮任何查詢的狀況下,咱們應該講選擇性高的列放在聯合索引的前面,可是實際上咱們更多的是經過查詢來反推索引,以使某個固定的查詢能夠儘量的命中索引以提升查詢速度
.畢竟咱們創建索引的目的也是爲了加快查詢的速度.
所以聯合索引的優化更多的是根據某個或者某些語句來優化的,不具有一個通用的法則.
當數據列有序的時候,mysql可使用索引,那麼假設咱們創建了school_age
索引,示例數據以下:
school | age |
---|---|
a | 12 |
b | 12 |
b | 14 |
b | 15 |
c | 1 |
在這份數據中,school字段是徹底有序的,索引school可使用索引.
而從全表來看,age字段不是有序的,所以沒法直接使用索引,那麼觀察一下數據表,在何時age有序呢?在school進行定值匹配的時候,例如當school=b
的時候,對於這三條數據而言,age是有序的,所以可使用age索引.這就是最左前綴的原理.
此外,最左前綴索引只能使用一個範圍查詢,例如select * from user where school > a
, select * from user where school = a and age > 12
,都是能夠命中索引的,可是select * from user where school > a and age > 12
中,僅school能夠命中索引,這也能夠從上面得出結論.由於當school是範圍匹配的時候,mysql沒法確認age字段是否嚴格有序,好比 school的範圍匹配命中了b,c的四條數據,那麼age就不是有序的.沒法使用後續的索引.
聚簇索引不是一種索引類型,而是一種存儲數據的方式.Innodb的聚簇索引是在同一個數據結構中保存了索引和數據.
由於數據真正的數據只能有一種排序方式,因此一個表上只能有一個聚簇索引.Innodb使用主鍵來進行聚簇索引,沒有主鍵的話就會選擇一個惟一的非空索引,若是還尚未,innodb會選擇生成一個隱式的主鍵來進行聚簇索引.爲何innodb這麼執着的須要搞一個聚簇索引呢,由於一個數據表中的數據總得有且只有一種排序方式來存儲在磁盤上,所以這是必須的.
這也是innodb推薦咱們使用自增主鍵的緣由,由於自增主鍵自增且連續,在插入的時候只須要不斷的在數據後面追加便可.設想一下使用UUID來做爲主鍵,那麼每一次的插入操做,都須要找到當前主鍵在已排序的主鍵中的位置,而後插入,而且要移動該主鍵後的數據,以使得數據和主鍵保持相同的順序,這無疑是代價很是高的.
也是由於這個緣由,在其餘索引的葉子節點中,存儲的"數據"其實不是該數據的真實物理地址,而是該數據的主鍵,查找到主鍵以後,再根據主鍵進行一次索引,拿到數據.
聚簇索引和非聚簇索引的區別能夠用一個簡單的例子來講明:
當咱們拿到一本書的時候,目錄就是主鍵,是一個聚簇索引,由於在目錄中連續的內容,在正文中也是連續的,當咱們想要查看迎着陽光盛大逃亡
章節,只須要在目錄中找到它對應的頁面,好比459,而後去對應的頁碼查看正文便可.
而非聚簇索引呢,則相似於書後面的附錄專有名詞索引同樣(二級普通索引),當你查找邦達列夫
的時候,附錄會告訴你,這個名詞出如今了迎着陽光盛大逃亡
一節,而後你須要去目錄(主鍵索引)中再次查找到對應的頁碼.
當一個索引包含(或者說是覆蓋)須要查詢的全部字段的值時,咱們稱之爲覆蓋索引.
設想有以下的查詢語句:
select
school_name,age
from
user
where
school_name = '金色鶯尾花學院'
複製代碼
這個語句根據學校名稱來查詢數據行的學校名稱和年齡,從上面的數據查詢的步驟咱們能夠知道,當在索引中找到要求的值的時候,還須要根據主鍵去進行一次索引,以拿到所有的數據,而後從其中挑選出須要的列,返回.可是如今索引中已經包含了全部的須要返回的列,那麼就不用進行回數據表查詢的操做了,此外索引的大小通常是遠遠小於真正的數據大小的,覆蓋索引能夠極大的減小從磁盤加載數據的數量.
爲何前綴索引和覆蓋索引沒法一塊兒使用?
由於前綴索引的目的是用前綴來表明真正的值,他們在選擇性上幾乎沒有區別,可是MySQL仍然沒法判斷真正的數據是什麼,好比阿里巴巴
和阿里媽媽
在前綴爲2的時候是同樣的,可是爲了確保你查詢阿里巴巴的時候不會出現阿里媽媽的內容,是須要回到數據表拿到數據再次進行一個精準匹配來進行過濾的.
所以,覆蓋索引沒法和列前綴索引一塊兒使用,這是我用一個早晨的時間測試得出的結論.
有一些索引是從未在查詢中使用過,卻白白增長數據插入時開銷的,對於這種索引咱們應該及時的進行刪除.
好比在主鍵上再創建一個普通索引,無疑是毫無做用的.
還好比在有聯合索引school_age
的狀況下,再創建一個school
的獨立索引,由於索引的最左前綴匹配原則,school_age
是徹底能夠命中對school
的單獨查詢的,所以後者能夠刪掉.
在mysql中可使用show index from table_name
來查看某個表上的索引,它將會有以下的輸出:
或者使用show create table table_name
來查看建表語句,其中包含建立索引的語句.
在5.0之後的版本中,咱們能夠經過查看information_schema.TABLES
表中的數據來獲取更加詳細的數據.
該表各字段的含義以下表:
字段 | 含義 |
---|---|
Table_catalog | 數據表登記目錄 |
Table_schema | 數據表所屬的數據庫名 |
Table_name | 表名稱 |
Table_type | 表類型[system view |
Engine | 使用的數據庫引擎[MyISAM |
Version | 版本,默認值10 |
Row_format | 行格式[Compact |
Table_rows | 表裏所存多少行數據 |
Avg_row_length | 平均行長度 |
Data_length | 數據長度 |
Max_data_length | 最大數據長度 |
Index_length | 索引長度 |
Data_free | 空間碎片 |
Auto_increment | 作自增主鍵的自動增量當前值 |
Create_time | 表的建立時間 |
Update_time | 表的更新時間 |
Check_time | 表的檢查時間 |
Table_collation | 表的字符校驗編碼集 |
Checksum | 校驗和 |
Create_options | 建立選項 |
Table_comment | 表的註釋、備註 |
咱們能夠經過一些查詢語句來獲取詳細的信息,好比:
// 查看當前MySQL服務器全部索引的大小(以MB爲單位,默認是字節)
SELECT CONCAT(ROUND(SUM(index_length)/(1024*1024), 2), ' MB') AS 'Total Index Size' FROM TABLES
// 查看某一個庫的全部大小
SELECT CONCAT(ROUND(SUM(index_length)/(1024*1024), 2), ' MB') AS 'Total Index Size' FROM TABLES WHERE table_schema = 'XXX';
// 查看某一個表的索引大小
SELECT CONCAT(ROUND(SUM(index_length)/(1024*1024), 2), ' MB') AS 'Total Index Size' FROM TABLES WHERE table_schema = 'yyyy' and table_name = "xxxxx";
// 彙總查看一個庫中的數據大小及索引大小
SELECT CONCAT(table_schema,'.',table_name) AS 'Table Name', CONCAT(ROUND(table_rows/1000000,4),'M') AS 'Number of Rows', CONCAT(ROUND(data_length/(1024*1024*1024),4),'G') AS 'Data Size', CONCAT(ROUND(index_length/(1024*1024*1024),4),'G') AS 'Index Size', CONCAT(ROUND((data_length+index_length)/(1024*1024*1024),4),'G') AS'Total'FROM information_schema.TABLES WHERE table_schema LIKE 'xxxxx';
複製代碼
對tables表的數據的全部查看方式都是能夠的,其中還包含了一些表格自己的數據信息,可是由於和本文的主題不符合,這裏就不舉例子了.
注意:上面的表格是有緩存的,當更新數據庫索引以後,最好執行analyze table xxxx
,而後再進行查看.MySQL會在表格數據發生較大的變化時才更新此表(大小變化超過1/16或者插入20億行).
在索引的建立刪除過程當中,不可避免的會產品索引碎片,固然還有數據碎片,咱們能夠經過執行optimize table xxx
來從新整理索引及數據,對於不支持此命令的存儲引擎來講,能夠經過一條無心義的alter語句來觸發整理,好比:將表的存儲引擎更換爲當前的引擎,alter table xxxx engine=innodb
.
書籍《高性能MySQL(第三版)》
以上皆爲我的所思所得,若有錯誤歡迎評論區指正。
歡迎轉載,煩請署名並保留原文連接。
聯繫郵箱:huyanshi2580@gmail.com
更多學習筆記見我的博客------>呼延十