[TOC]html
本文有參考網上其餘相關文章,本文最後有附參考的連接mysql
MySQL支持諸多存儲引擎,而各類存儲引擎對索引的支持也各不相同,所以MySQL數據庫支持多種索引類型,如BTree索引,哈希索引,全文索引等等。爲了不混亂,本文將只關注於BTree索引,由於這是日常使用MySQL時主要打交道的索引。redis
MySQL官方對索引的定義爲:索引(Index)是幫助MySQL高效獲取數據的數據結構。提取句子主幹,就能夠獲得索引的本質:索引是數據結構。算法
索引的目的在於提升查詢效率,能夠類比字典,若是要查「mysql」這個單詞,咱們確定須要定位到m字母,而後從下往下找到y字母,再找到剩下的sql。若是沒有索引,那麼你可能須要把全部單詞看一遍才能找到你想要的,若是我想找到m開頭的單詞呢?或者ze開頭的單詞呢?是否是以爲若是沒有索引,這個事情根本沒法完成?sql
我們去圖書館借書也是同樣,若是你要借某一本書,必定是先找到對應的分類科目,再找到對應的編號,這是生活中活生生的例子,通用索引,能夠加快查詢速度,快速定位。數據庫
全部索引原理都是同樣的,經過不斷的縮小想要得到數據的範圍來篩選出最終想要的結果,同時把隨機的事件變成順序的事件,也就是咱們老是經過同一種查找方式來鎖定數據。緩存
數據庫也是同樣,但顯然要複雜許多,由於不只面臨着等值查詢,還有範圍查詢(>、<、between)、模糊查詢(like)、並集查詢(or)、多值匹配(in【in本質上屬於多個or】)等等。數據庫應該選擇怎麼樣的方式來應對全部的問題呢?咱們回想字典的例子,能不能把數據分紅段,而後分段查詢呢?最簡單的若是1000條數據,1到100分紅第一段,101到200分紅第二段,201到300分紅第三段……這樣查第250條數據,只要找第三段就能夠了,一會兒去除了90%的無效數據。但若是是1千萬的記錄呢,分紅幾段比較好?稍有算法基礎的同窗會想到搜索樹,其平均複雜度是lgN,具備不錯的查詢性能。但這裏咱們忽略了一個關鍵的問題,複雜度模型是基於每次相同的操做成原本考慮的,數據庫實現比較複雜,數據保存在磁盤上,而爲了提升性能,每次又能夠把部分數據讀入內存來計算,由於咱們知道訪問磁盤的成本大概是訪問內存的十萬倍左右,因此簡單的搜索樹難以知足複雜的應用場景。安全
任何一種數據結構都不是憑空產生的,必定會有它的背景和使用場景,咱們如今總結一下,咱們須要這種數據結構可以作些什麼,其實很簡單,那就是:每次查找數據時把磁盤IO次數控制在一個很小的數量級,最好是常數數量級。那麼咱們就想到若是一個高度可控的多路搜索樹是否能知足需求呢?就這樣,b+樹應運而生。bash
淺藍色的塊咱們稱之爲一個磁盤塊,能夠看到每一個磁盤塊包含幾個數據項(深藍色所示)和指針(黃色所示),如磁盤塊1包含數據項17和35,包含指針P一、P二、P3,P1表示小於17的磁盤塊,P2表示在17和35之間的磁盤塊,P3表示大於35的磁盤塊。真實的數據存在於葉子節點即三、五、九、十、1三、1五、2八、2九、3六、60、7五、7九、90、99。非葉子節點不存儲真實的數據,只存儲指引搜索方向的數據項,如1七、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,顯然成本很是很是高。
經過上面的分析,咱們知道間越小,數據項的數量越多,樹的高度越低。這就是爲何每一個數據項,即索引字段要儘可能的小,好比int佔4字節,要比bigint8字節少一半。這也是爲何b+樹要求把真實的數據放到葉子節點而不是內層節點,一旦放到內層節點,磁盤塊的數據項會大幅度降低,致使樹增高。當數據項等於1時將會退化成線性表。
當b+樹的數據項是複合的數據結構,好比(name,age,sex)的時候,b+數是按照從左到右的順序來創建搜索樹的,好比當(張三,20,F)這樣的數據來檢索的時候,b+樹會優先比較name來肯定下一步的所搜方向,若是name相同再依次比較age和sex,最後獲得檢索的數據;但當(20,F)這樣的沒有name的數據來的時候,b+樹就不知道下一步該查哪一個節點,由於創建搜索樹的時候name就是第一個比較因子,必需要先根據name來搜索才能知道下一步去哪裏查詢。好比當(張三,F)這樣的數據來檢索時,b+樹能夠用name來指定搜索方向,但下一個字段age的缺失,因此只能把名字等於張三的數據都找到,而後再匹配性別是F的數據了, 這個是很是重要的性質,即索引的最左匹配特性。
在MySQL中,索引屬於存儲引擎級別的概念,不一樣存儲引擎對索引的實現方式是不一樣的,本文主要討論MyISAM和InnoDB兩個存儲引擎的索引實現方式。
MyISAM引擎使用B+Tree做爲索引結構,葉節點的data域存放的是數據記錄的地址。 下圖是MyISAM索引的原理圖:
這裏設表一共有三列,假設咱們以Col1爲主鍵,則上圖即是一個MyISAM表的主索引(Primary key)示意圖。能夠看出MyISAM的索引文件僅僅保存數據記錄的地址。在MyISAM中,主索引和輔助索引(Secondary key)在結構上沒有任何區別,只是主索引要求key是惟一的,而輔助索引的key能夠重複。若是咱們在Col2上創建一個輔助索引,則此索引的結構以下圖所示:
一樣也是一顆B+Tree,data域保存數據記錄的地址。所以,MyISAM中索引檢索的算法爲首先按照B+Tree搜索算法搜索索引,若是指定的Key存在,則取出其data域的值,而後以data域的值爲地址,讀取相應數據記錄。
MyISAM的索引方式也叫作「非彙集」的,之因此這麼稱呼是爲了與InnoDB的彙集索引區分。
雖然InnoDB也使用B+Tree做爲索引結構,但具體實現方式卻與MyISAM大相徑庭。
第一個重大區別是InnoDB的數據文件自己就是索引文件。從上文知道,MyISAM索引文件和數據文件是分離的,索引文件僅保存數據記錄的地址。而在InnoDB中,表數據文件自己就是按B+Tree組織的一個索引結構,這棵樹的葉節點data域保存了完整的數據記錄。這個索引的key是數據表的主鍵,所以InnoDB表數據文件自己就是主索引。
上圖是InnoDB主索引(同時也是數據文件)的示意圖,能夠看到葉節點包含了完整的數據記錄。這種索引叫作彙集索引。由於InnoDB的數據文件自己要按主鍵彙集,因此InnoDB要求表必須有主鍵(MyISAM能夠沒有),若是沒有顯式指定,則MySQL系統會自動選擇一個能夠惟一標識數據記錄的列做爲主鍵,若是不存在這種列,則MySQL自動爲InnoDB表生成一個隱含字段做爲主鍵,這個字段長度爲6個字節,類型爲長整形。
第二個與MyISAM索引的不一樣是InnoDB的輔助索引data域存儲相應記錄主鍵的值而不是地址。換句話說,InnoDB的全部輔助索引都引用主鍵做爲data域。例如,下圖爲定義在Col3上的一個輔助索引:
這裏以英文字符的ASCII碼做爲比較準則。彙集索引這種實現方式使得按主鍵的搜索十分高效,可是輔助索引搜索須要檢索兩遍索引:首先檢索輔助索引得到主鍵,而後用主鍵到主索引中檢索得到記錄。
瞭解不一樣存儲引擎的索引實現方式對於正確使用和優化索引都很是有幫助,例如知道了InnoDB的索引實現後,就很容易明白爲何不建議使用過長的字段做爲主鍵,由於全部輔助索引都引用主索引,過長的主索引會令輔助索引變得過大。再例如,用非單調的字段做爲主鍵在InnoDB中不是個好主意,由於InnoDB數據文件自己是一顆B+Tree,非單調的主鍵會形成在插入新記錄時數據文件爲了維持B+Tree的特性而頻繁的分裂調整,十分低效,而使用自增字段做爲主鍵則是一個很好的選擇。
一個最重要的原則是最左前綴原理,在提這個以前要先說下聯合索引,MySQL中的索引能夠以必定順序引用多個列,這種索引叫作聯合索引,通常的,一個聯合索引是一個有序元組<a1, a2, …, an>,其中各個元素均爲數據表的一列。另外,單列索引能夠當作聯合索引元素數爲1的特例。
索引匹配的最左原則具體是說,假如索引列分別爲A,B,C,順序也是A,B,C:
- 那麼查詢的時候,若是查詢【A】【A,B】 【A,B,C】,那麼能夠經過索引查詢
- 若是查詢的時候,採用【A,C】,那麼C這個雖然是索引,可是因爲中間缺失了B,所以C這個索引是用不到的,只能用到A索引
- 若是查詢的時候,採用【B】 【B,C】 【C】,因爲沒有用到第一列索引,不是最左前綴,那麼後面的索引也是用不到了
- 若是查詢的時候,採用範圍查詢,而且是最左前綴,也就是第一列索引,那麼能夠用到索引,可是範圍後面的列沒法用到索引
複製代碼
由於索引雖然加快了查詢速度,但索引也是有代價的:索引文件自己要消耗存儲空間,同時索引會加劇插入、刪除和修改記錄時的負擔,另外,MySQL在運行時也要消耗資源維護索引,所以索引並非越多越好
在使用InnoDB存儲引擎時,若是沒有特別的須要,請永遠使用一個與業務無關的自增字段做爲主鍵。若是從數據庫索引優化角度看,使用InnoDB引擎而不使用自增主鍵絕對是一個糟糕的主意。
InnoDB使用匯集索引,數據記錄自己被存於主索引(一顆B+Tree)的葉子節點上。這就要求同一個葉子節點內(大小爲一個內存頁或磁盤頁)的各條數據記錄按主鍵順序存放,所以每當有一條新的記錄插入時,MySQL會根據其主鍵將其插入適當的節點和位置,若是頁面達到裝載因子(InnoDB默認爲15/16),則開闢一個新的頁(節點)。若是表使用自增主鍵,那麼每次插入新的記錄,記錄就會順序添加到當前索引節點的後續位置,當一頁寫滿,就會自動開闢一個新的頁。以下:
這樣就會造成一個緊湊的索引結構,近似順序填滿。因爲每次插入時也不須要移動已有數據,所以效率很高,也不會增長不少開銷在維護索引上。
若是使用非自增主鍵(若是身份證號或學號等),因爲每次插入主鍵的值近似於隨機,所以每次新紀錄都要被插到現有索引頁得中間某個位置,以下:
此時MySQL不得不爲了將新記錄插到合適位置而移動數據,甚至目標頁面可能已經被回寫到磁盤上而從緩存中清掉,此時又要從磁盤上讀回來,這增長了不少開銷,同時頻繁的移動、分頁操做形成了大量的碎片,獲得了不夠緊湊的索引結構,後續不得不經過OPTIMIZE TABLE來重建表並優化填充頁面。
所以,只要能夠,請儘可能在InnoDB上採用自增字段作主鍵。
最左前綴匹配原則,很是重要的原則,mysql會一直向右匹配直到遇到範圍查詢(>、<、between、like)就中止匹配,好比a = 1 and b = 2 and c > 3 and d = 4 若是創建(a,b,c,d)順序的索引,d是用不到索引的,若是創建(a,b,d,c)的索引則均可以用到,a,b,d的順序能夠任意調整。
=和in能夠亂序,好比a = 1 and b = 2 and c = 3 創建(a,b,c)索引能夠任意順序,mysql的查詢優化器會幫你優化成索引能夠識別的形式
儘可能選擇區分度高的列做爲索引,區分度的公式是count(distinct col)/count(*),表示字段不重複的比例,比例越大咱們掃描的記錄數越少,惟一鍵的區分度是1,而一些狀態、性別字段可能在大數據面前區分度就是0,那可能有人會問,這個比例有什麼經驗值嗎?使用場景不一樣,這個值也很難肯定,通常須要join的字段咱們都要求是0.1以上,即平均1條掃描10條記錄
索引列不能參與計算,保持列「乾淨」,好比from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,緣由很簡單,b+樹中存的都是數據表中的字段值,但進行檢索時,須要把全部元素都應用函數才能比較,顯然成本太大。因此語句應該寫成create_time = unix_timestamp(’2014-05-29’);
儘可能的擴展索引,不要新建索引。好比表中已經有a的索引,如今要加(a,b)的索引,那麼只須要修改原來的索引便可,固然要考慮原有數據和線上使用狀況
配置優化指的MySQL 的 server端的配置,通常對於業務方而言,能夠不用關注,畢竟會有專門的DBA來處理,可是對於原理的瞭解,我想,咱們開發,是須要了解的
通常要進行SQL調優,那麼就說有慢查詢的SQL,系統或者server能夠開啓慢查詢日誌,尤爲是線上系統,通常都會開啓慢查詢日誌,若是有慢查詢,能夠經過日誌來過濾。可是知道了有須要優化的SQL後,下面要作的就是如何進行調優
在平常工做中,咱們有時會開慢查詢去記錄一些執行時間比較久的SQL語句,找出這些SQL語句並不意味着完事了,咱們經常用到explain這個命令來查看一個這些SQL語句的執行計劃,查看該SQL語句有沒有使用上了索引,有沒有作全表掃描,這均可以經過explain命令來查看。因此咱們深刻了解MySQL的基於開銷的優化器,還能夠得到不少可能被優化器考慮到的訪問策略的細節,以及當運行SQL語句時哪一種策略預計會被優化器採用。
使用explain 只須要在原有select 基礎上加上explain關鍵字就能夠了,以下:
mysql> explain select * from servers;
+----+-------------+---------+------+---------------+------+---------+------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+------+---------------+------+---------+------+------+-------+
| 1 | SIMPLE | servers | ALL | NULL | NULL | NULL | NULL | 1 | NULL |
+----+-------------+---------+------+---------------+------+---------+------+------+-------+
1 row in set (0.03 sec)
複製代碼
簡要解釋下explain各個字段的含義
EXPLAIN的特性
假若有以下表結構
circlemessage_idx_0 | CREATE TABLE `circlemessage_idx_0` (
`circle_id` bigint(20) unsigned NOT NULL COMMENT '羣組id',
`from_id` bigint(20) unsigned NOT NULL COMMENT '發送用戶id',
`to_id` bigint(20) unsigned NOT NULL COMMENT '指定接收用戶id',
`msg_id` bigint(20) unsigned NOT NULL COMMENT '消息ID',
`type` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '消息類型',
PRIMARY KEY (`msg_id`,`to_id`),
KEY `idx_from_circle` (`from_id`,`circle_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
複製代碼
經過執行計劃explain分析以下查詢語句
mysql> explain select msg_id from circlemessage_idx_0 where to_id = 113487 and circle_id=10019063 and msg_id>=6273803462253938690 and from_id != 113487 order by msg_id asc limit 30;
+----+-------------+---------------------+-------+-------------------------+---------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------------------+-------+-------------------------+---------+---------+------+--------+-------------+
| 1 | SIMPLE | circlemessage_idx_0 | range | PRIMARY,idx_from_circle | PRIMARY | 16 | NULL | 349780 | Using where |
+----+-------------+---------------------+-------+-------------------------+---------+---------+------+--------+-------------+
1 row in set (0.00 sec)
複製代碼
mysql> explain select msg_id from circlemessage_idx_0 where to_id = 113487 and circle_id=10019063 and from_id != 113487 order by msg_id asc limit 30;
+----+-------------+---------------------+-------+-----------------+---------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------------------+-------+-----------------+---------+---------+------+------+-------------+
| 1 | SIMPLE | circlemessage_idx_0 | index | idx_from_circle | PRIMARY | 16 | NULL | 30 | Using where |
+----+-------------+---------------------+-------+-----------------+---------+---------+------+------+-------------+
1 row in set (0.00 sec)
複製代碼
經過上面兩個執行計劃能夠發現當沒有msg_id >= xxx這個查詢條件的時候,檢索的rows要少不少,而且二者查詢的時候都用到了索引,並且用到的還只是主鍵索引。那說明索引應該是不合理的,沒有發揮最大做用。
分析這個執行計劃能夠看到,當包含msg_id >= xxx 查詢條件的時候,rows有34w多行,這種狀況,說明檢索太多,要麼就是表裏面確實有這麼大,要麼就是索引不合理沒有用到索引,大都狀況是沒用合理用到索引。列中所用到的索引也是PRIMARY,那就多是(msg_id
,to_id
)的其中一個,注意咱們創建表的時候msg_id索引的順序是在to_id前面的,所以MySQL查詢必定會優先用msg_id索引,在使用了msg_id索引後,就已經檢索出了34w行,而且因爲msg_id的查詢條件是大於等於,所以,再這個查詢條件後,就不能再用到to_id的索引。
而後再看key_len長度爲16,結合 key爲PRIMARY,那麼能夠分析得知,只有一個主鍵索引被用到。
最後看看 type 值,是range,那麼就說明這個查詢要麼是範圍查詢,要麼就是多值匹配。
請注意,from_id != xxx 這樣的語句,是沒法用到索引的。 只有from_id = xxx就能夠用到因此,所以from id 的索引其實能夠不用,創建索引的時候就要考慮清楚
既然知道索引不合理,那麼就要分析並調整索引。通常而言,咱們既然要從單表裏面查詢,那麼就須要可以知道大致,單表裏面大體會有哪些數據,如今的量級大概是多少。
而後開始下一步的分析,既然msgid是被設置爲了主鍵,那必定是全局惟一的,全部,有多少數據量就至少會有多少條msgid;那麼檢索msg_id基本就是檢索整個表了。咱們要作的優化就是要儘可能減小索引,減小查詢的行數;那麼就須要思考,經過查詢哪些字段纔可以減小行數?好比,一個張表裏面,所屬某個用戶的數據,會不會比查詢msgid的行數要少? 查詢某個用戶而且是屬於某個圈子的,那會不會就更少了? 等等。。。
而後根據實際狀況分析,單表裏面命中to_id 的行數應該是會小於命中msg_id的,所以要首先保證可以使用到to_id的索引,爲此,能夠設置主鍵的時候把msg_id和to_id的順序交互一下;可是,因爲已是線上的表,已經有了大量數據,而且業務開始運行,這種狀況下,修改主鍵會引起不少問題(固然修改索引是OK的),所以,不建議直接修改主鍵。那麼,爲了保證有效使用to_id的索引,就要新建一個聯合索引;那麼新建的聯合索引的第一索引字段必然是to_id,針對此業務場景,最好可以再加上circle_id索引,這樣能夠快速索引;這樣就獲得了新的聯合索引(to_id,circle_id)的索引,而後,由於要找msg_id,爲此,在此基礎上,再加上msg_id。最終獲得的聯合索引爲(to_id,circle_id,msg_id);這樣的話,就可以快速檢索這樣的查詢語句了:where to_id = xxx and circle_id = xxx and msgId >= xxx
固然,索引的創建,也不是說某個sql 語句須要啥索引,就創建某個聯合索引,這樣的話,索引太多的話,寫的性能受影響(插入、刪除、修改),而後存儲空間也會相應增大;另外mysql在運行時也會消耗資源維護索引,因此,索引並非越多越好,須要結合查詢最頻繁、最影響性能的sql來創建合適的索引。須要再說明的是,一個聯合索引或者一組主鍵就是一個btree,多個索引就是多個btree
首先咱們須要深刻理解索引的原理和實現,當理解了原理後,纔可以更有助於咱們創建合適的索引。而後咱們創建索引的時候,不要想固然,要先想清楚業務邏輯,再創建對應的表結構和索引。 須要再次強調以下幾點:
感謝參考文章的原做者
【"歡迎關注個人微信公衆號:Linux 服務端系統研發,後面會大力經過微信公衆號發送優質文章"】