前言
前面已經寫了有兩篇章長度的文章,第三篇我一直在尋思着要寫什麼(其實並無),按照腦圖來的話,這篇文章咱們該來說講關於索引的知識了,這但是 MySQL 性能優化很關鍵的知識點,千萬千萬不要錯過,不過我這裏會相對比較深刻地探究,相信你們讀完以後多少會有點收穫。php
先送上兩張飛機票🛬還沒讀過前面文章的夥伴能夠先前往閱讀,由淺入深: MySQL相關(一)- 一條查詢語句是如何執行的 MySQL相關(二)- 一條更新語句是如何執行的html
因爲索引的知識點比較多,官網的內容也不少,若是你們想詳細瞭解能夠到官網,想先通讀了解的話能夠先看看我對索引的總結,這一章節分爲三部分來說:mysql
- innodb 邏輯存儲結構須要瞭解,做爲番外篇 MySQL相關(番外篇)- innodb 邏輯存儲結構;
- 索引的數據結構也做爲另外的篇章,經過對查詢算法的數據模型進行演算分析 MySQL相關(三)- 索引數據模型推演及 B+Tree 的詳細介紹;
- 對索引的使用及優化規則也會做爲單獨的篇章 MySQL相關(四)- 性能優化關鍵點索引
前面提到的腦圖以下,想要完整高清圖片能夠到微信個人公衆號下【6曦軒】下回復 MySQL 腦圖獲取: 程序員
正文
索引究竟是什麼?
索引的定義
- 咱們先來看看維基百科對索引的定義:
數據庫索引,是數據庫管理系統(DBMS)中一個排序的數據結構,以協助快速查詢、 更新數據庫表中數據。面試
- 那麼咱們要怎麼來理解這個定義呢?
首先數據是以文件的形式存放在磁盤上面的,每一行數據都有它的磁盤地址。若是 沒有索引的話,要從 500 萬行數據裏面檢索一條數據,只能依次遍歷這張表的所有數據, 直到找到這條數據。 可是有了索引以後,只須要在索引裏面去檢索這條數據就好了,由於它是一種特殊 的專門用來快速檢索的數據結構,咱們找到數據存放的磁盤地址之後,就能夠拿到數據 了。就像咱們從一本 500 頁的書裏面去找特定的一小節的內容,確定不可能從第一頁開 始翻。那麼這本書有專門的目錄,它可能只有幾頁的內容,它是按頁碼來組織的,能夠根據拼音或者偏旁部首來查找,只要肯定內容對應的頁碼,就能很快地找到咱們想要的 內容。算法
索引的類型與方法
索引類型:Normal、Unique、Fulltextsql
怎麼建立一個索引
在 Navicat 中能夠直接建立索引,第一個是索引的名稱,第二個是索引的列,好比咱們是要對 id 建立索引仍是對 name 建立索引。後面兩個很重要,一個叫索引類型。數據庫
在 InnoDB 裏面,索引類型有三種,普通索引、惟一索引(主鍵索引是特殊的惟一 索引)、全文索引。性能優化
普通(Normal):也叫非惟一索引,是最普通的索引,沒有任何的限制。微信
惟一(Unique):惟一索引要求鍵值不能重複。另外須要注意的是,主鍵索引是一 種特殊的惟一索引,它還多了一個限制條件,要求鍵值不能爲空。主鍵索引用 primay key 建立。
全文(Fulltext):針對比較大的數據,好比咱們存放的是消息內容,有幾 KB 的數 據的這種狀況,若是要解決 like 查詢效率低的問題,能夠建立全文索引。只有文本類型 的字段才能夠建立全文索引,好比 char、varchar、text。
用命令行建立索引以下:
create table m3 ( name varchar(50), fulltext index(name) );
全文索引的使用
SELECT * FROM fulltext_test WHERE MATCH(content) against('6曦軒' IN NATURAL LANGUAGE MODE);
MyISAM 和 InnoDB 支持全文索引。 這個是索引的三種類型:普通、惟1、全文。
咱們說索引是一個數據結構,那麼它到底該選擇一種什麼數據結構才能實現數據的高效索引呢?咱們繼續往下看。
在這個篇章裏咱們經過一些數據結構來一步一步演算出 MySQL 爲啥要用 B+tree 做爲索引的數據結構以及對 B+tree 的詳細介紹,篇幅較長,咱們用另外的篇章來說述:
飛機票🛬:MySQL索引數據模型推演
什麼叫作彙集索引(聚簇索引)?
彙集索引是指索引鍵值的邏輯順序跟表數據行的物理存儲順序是一致的。(好比字典的目錄是按拼音排序的,內容也是按拼音排序的,按拼音排序的這種目錄就叫彙集索引)。
在 InnoDB 裏面,它組織數據的方式叫作叫作(彙集)索引組織表(clustered index organize table),因此主鍵索引是彙集索引,非主鍵都是非彙集索引。
若是 InnoDB 裏面主鍵是這樣存儲的,那主鍵以外的索引,好比咱們在 name 字段上面建的普通索引,又是怎麼存儲和檢索數據的呢? InnoDB 中,主鍵索引和輔助索引是有一個主次之分的。
輔助索引存儲的是輔助索引和主鍵值。若是使用輔助索引查詢,會根據主鍵值在主鍵索引中查詢,最終取得數據。
好比咱們用 name 索引查詢 name= 'Jack',它會在葉子節點找到主鍵值,也就是id=1,而後再到主鍵索引的葉子節點拿到數據。
- 爲何在輔助索引裏面存儲的是主鍵值而不是主鍵的磁盤地址呢?若是主鍵的數據類型比較大,是否是比存地址更消耗空間呢?咱們前面說到 B Tree 是怎麼實現一個節點存儲多個關鍵字,還保持平衡的呢?
是由於有分叉和合並的操做,這個時候鍵值的地址會發生變化,因此在輔助索引裏面不能存儲地址。
- 另外一個問題,若是一張表沒有主鍵怎麼辦?
- 若是咱們定義了主鍵(PRIMARY KEY),那麼 InnoDB 會選擇主鍵做爲彙集索引。
- 若是沒有顯式定義主鍵,則 InnoDB 會選擇第一個不包含有 NULL 值的惟一索引做爲主鍵索引。
- 若是也沒有這樣的惟一索引,則 InnoDB 會選擇內置 6 字節長的 ROWID 做爲隱藏的彙集索引,它會隨着行記錄的寫入而主鍵遞增。
select _rowid name from t2;
索引使用原則
咱們容易有以一個誤區,就是在常用的查詢條件上都創建索引,索引越多越好,那究竟是不是這樣呢?
列的離散(sàn)度
第一個叫作列的離散度,咱們先來看一下列的離散度的公式: count(distinct(column_name)) : count(*),列的所有不一樣值和全部數據行的比例。
數據行數相同的狀況下,分子越大,列的離散度就越高。
簡單來講,若是列的重複值越多,離散度就越低,重複值越少,離散度就越高。
瞭解了離散度的概念以後,咱們再來思考一個問題,咱們在 name 上面創建索引和在 gender 上面創建索引有什麼區別。
當咱們用在 gender 上創建的索引去檢索數據的時候,因爲重複值太多,須要掃描的行數就更多。例如,咱們如今在 gender 列上面建立一個索引,而後看一下執行計劃。
ALTER TABLE user_innodb DROP INDEX idx_user_gender; ALTER TABLE user_innodb ADD INDEX idx_user_gender (gender); -- 耗時比較久 EXPLAIN SELECT * FROM `user_innodb` WHERE gender = 0;
show indexes from user_innodb;
而 name 的離散度更高,好比「Jack」的這名字,只須要掃描一行。
ALTER TABLE user_innodb DROP INDEX idx_user_name; ALTER TABLE user_innodb ADD INDEX idx_user_name (name); EXPLAIN SELECT * FROM `user_innodb` WHERE name = 'Jack';
查看錶上的索引,Cardinality [kɑ:dɪ'nælɪtɪ] 表明基數,表明預估的不重複的值 的數量。索引的基數與表總行數越接近,列的離散度就越高。
show indexes from user_innodb;
若是在 B+Tree 裏面的重複值太多,MySQL 的優化器發現走索引跟使用全表掃描差不了多少的時候,就算建了索引,也不必定會走索引。
https://www.cs.usfca.edu/~galles/visualization/BPlusTree.html
- 這個給咱們的啓發是什麼?
創建索引,要使用離散度(選擇度)更高的字段。
聯合索引最左匹配
前面咱們說的都是針對單列建立的索引,但有的時候咱們的多條件查詢的時候,也會創建聯合索引。單列索引能夠當作是特殊的聯合索引。
好比咱們在 user 表上面,給 name 和 phone 創建了一個聯合索引。
ALTER TABLE user_innodb DROP INDEX comidx_name_phone; ALTER TABLE user_innodb add INDEX comidx_name_phone (name,phone);
聯合索引在 B+Tree 中是複合的數據結構,它是按照從左到右的順序來創建搜索樹的(name 在左邊,phone 在右邊)。
從這張圖能夠看出來,name 是有序的,phone 是無序的。當 name 相等的時候, phone 纔是有序的。
這個時候咱們使用 where name= '青山' and phone = '136xx '去查詢數據的時候, B+Tree 會優先比較 name 來肯定下一步應該搜索的方向,往左仍是往右。若是 name 相同的時候再比較 phone。可是若是查詢條件沒有 name,就不知道第一步應該查哪一個節點,由於創建搜索樹的時候 name 是第一個比較因子,因此用不到索引。
何時用到聯合索引
因此,咱們在創建聯合索引的時候,必定要把最經常使用的列放在最左邊。
好比下面的三條語句,能用到聯合索引嗎?
1)使用兩個字段,能夠用到聯合索引:
EXPLAIN SELECT * FROM user_innodb WHERE name= '權亮' AND phone = '15204661800';
2)使用左邊的 name 字段,能夠用到聯合索引:
EXPLAIN SELECT * FROM user_innodb WHERE name= '權亮'
3)使用右邊的 phone 字段,沒法使用索引,全表掃描:
EXPLAIN SELECT * FROM user_innodb WHERE phone = '15204661800'
如何建立聯合索引
有一天咱們的 DBA 找到我,說咱們的項目裏面有兩個查詢很慢。
SELECT * FROM user_innodb WHERE name= ? AND phone = ?; SELECT * FROM user_innodb WHERE name= ?;
按照咱們的想法,一個查詢建立一個索引,因此咱們針對這兩條 SQL 建立了兩個索引,這種作法以爲正確嗎?
CREATE INDEX idx_name on user_innodb(name); CREATE INDEX idx_name_phone on user_innodb(name,phone);
當咱們建立一個聯合索引的時候,按照最左匹配原則,用左邊的字段 name 去查詢 的時候,也能用到索引,因此第一個索引徹底不必。
至關於創建了兩個聯合索引(name),(name,phone)。
若是咱們建立三個字段的索引 index(a,b,c),至關於建立三個索引:
index(a) index(a,b) index(a,b,c)
用 where b=? 和 where b=? and c=? 和 where a=? and c=?是不能使用到索引的。不能不用第一個字段,不能中斷。
這裏就是 MySQL 聯合索引的最左匹配原則。
覆蓋索引
回表:
非主鍵索引,咱們先經過索引找到主鍵索引的鍵值,再經過主鍵值查出索引裏面沒有的數據,它比基於主鍵索引的查詢多掃描了一棵索引樹,這個過程就叫回表。
例如:select * from user_innodb where name = 'Jack';
在輔助索引裏面,無論是單列索引仍是聯合索引,若是 select 的數據列只用從索引中就可以取得,沒必要從數據區中讀取,這時候使用的索引就叫作覆蓋索引,這樣就避免了回表。
咱們先來建立一個聯合索引:
--建立聯合索引 ALTER TABLE user_innodb DROP INDEX comixd_name_phone; ALTER TABLE user_innodb add INDEX `comixd_name_phone` (`name`,`phone`);
這三個查詢語句都用到了覆蓋索引:
EXPLAIN SELECT name,phone FROM user_innodb WHERE name= '青山' AND phone = ' 13666666666'; EXPLAIN SELECT name FROM user_innodb WHERE name= '青山' AND phone = ' 13666666666'; EXPLAIN SELECT phone FROM user_innodb WHERE name= '青山' AND phone = ' 13666666666';
Extra 裏面值爲「Using index」表明使用了覆蓋索引。 select * ,用不到覆蓋索引。
很明顯,由於覆蓋索引減小了 IO 次數,減小了數據的訪問量,能夠大大地提高查詢效率。
前綴索引
當字段值比較長的時候,創建索引會消耗不少的空間,搜索起來也會很慢。咱們能夠經過截取字段的前面一部份內容創建索引,這個就叫前綴索引。
建立一張商戶表,由於地址字段比較長,在地址字段上創建前綴索引:
create table shop(address varchar(120) not null); alter table shop add key (address(12));
問題是,截取多少呢?截取得多了,達不到節省索引存儲空間的目的,截取得少了,
重複內容太多,字段的散列度(選擇性)會下降。怎麼計算不一樣的長度的選擇性呢?
先看一下字段在所有數據中的選擇度:
select count(distinct address) / count(*) from shop;
經過不一樣長度去計算,與全表的選擇性對比:
select count(distinct left(address,10))/count(*) as sub10, count(distinct left(address,11))/count(*) as sub11, count(distinct left(address,12))/count(*) as sub12, count(distinct left(address,13))/count(*) as sub13 from shop;
只要截取前 13 個字段,就已經有比較高的選擇性了(這裏的數據只是舉例)。
索引條件下推(ICP)
https://dev.mysql.com/doc/refman/5.7/en/index-condition-pushdown-optimization.html
再來看這麼一張表,在 last_name 和 first_name 上面建立聯合索引。
drop table employees; CREATE TABLE `employees`( `emp_no` INT(11) NOT NULL , `birth_date` date NULL , `first_name` VARCHAR(14) NOT NULL , `last_name` VARCHAR(16) NOT NULL , `gender` ENUM('M' , 'F') NOT NULL , `hire_date` date NULL , PRIMARY KEY(`emp_no`) ) ENGINE = INNODB DEFAULT CHARSET = latin1; ALTER TABLE employees ADD INDEX idx_lastname_firstname(last_name , first_name); INSERT INTO `employees`( `emp_no` , `birth_date` , `first_name` , `last_name` , `gender` , `hire_date` ) VALUES (1 , NULL , '698' , 'liu' , 'F' , NULL); INSERT INTO `employees`( `emp_no` , `birth_date` , `first_name` , `last_name` , `gender` , `hire_date` ) VALUES (2 , NULL , 'd99' , 'zheng' , 'F' , NULL); INSERT INTO `employees`( `emp_no` , `birth_date` , `first_name` , `last_name` , `gender` , `hire_date` ) VALUES (3 , NULL , 'e08' , 'huang' , 'F' , NULL); INSERT INTO `employees`( `emp_no` , `birth_date` , `first_name` , `last_name` , `gender` , `hire_date` ) VALUES (4 , NULL , '59d' , 'lu' , 'F' , NULL); INSERT INTO `employees`( `emp_no` , `birth_date` , `first_name` , `last_name` , `gender` , `hire_date` ) VALUES (5 , NULL , '0dc' , 'yu' , 'F' , NULL); INSERT INTO `employees`( `emp_no` , `birth_date` , `first_name` , `last_name` , `gender` , `hire_date` ) VALUES (6 , NULL , '989' , 'wang' , 'F' , NULL); INSERT INTO `employees`( `emp_no` , `birth_date` , `first_name` , `last_name` , `gender` , `hire_date` ) VALUES (7 , NULL , 'e38' , 'wang' , 'F' , NULL); INSERT INTO `employees`( `emp_no` , `birth_date` , `first_name` , `last_name` , `gender` , `hire_date` ) VALUES (8 , NULL , '0zi' , 'wang' , 'F' , NULL); INSERT INTO `employees`( `emp_no` , `birth_date` , `first_name` , `last_name` , `gender` , `hire_date` ) VALUES (9 , NULL , 'dc9' , 'xie' , 'F' , NULL); INSERT INTO `employees`( `emp_no` , `birth_date` , `first_name` , `last_name` , `gender` , `hire_date` ) VALUES (10 , NULL , '5ba' , 'zhou' , 'F' , NULL);
關閉 ICP:
set optimizer_switch='index_condition_pushdown=off';
查看參數:
show variables like 'optimizer_switch';
如今咱們要查詢全部姓 wang,而且名字最後一個字是 zi 的員工,好比王胖子,王瘦子。查詢的 SQL:
select * from employees where last_name='wang' and first_name LIKE '%zi' ;
- 這條 SQL 有兩種執行方式:
- 根據聯合索引查出全部姓 wang 的二級索引數據,而後回表,到主鍵索引上查詢所有符合條件的數據(3 條數據)。而後返回給 Server 層,在 Server 層過濾出名字以 zi 結尾的員工。
- 根據聯合索引查出全部姓 wang 的二級索引數據(3 個索引),而後從二級索引中篩選出 first_name 以 zi 結尾的索引(1 個索引),而後再回表,到主鍵索引上查詢所有符合條件的數據(1 條數據),返回給 Server 層。
很明顯,第二種方式到主鍵索引上查詢的數據更少。 注意,索引的比較是在存儲引擎進行的,數據記錄的比較,是在 Server 層進行的。而當 first_name 的條件不能用於索引過濾時,Server 層不會把 first_name 的條件傳遞給存儲引擎,因此讀取了兩條沒有必要的記錄。 這時候,若是知足 last_name='wang'的記錄有 100000 條,就會有 99999 條沒有必要讀取的記錄。
執行如下 SQL,Using where:
explain select * from employees where last_name='wang' and first_name LIKE '%zi' ;
Using Where 表明從存儲引擎取回的數據不所有知足條件,須要在 Server 層過濾。先用 last_name 條件進行索引範圍掃描,讀取數據表記錄,而後進行比較,檢查是否符合 first_name LIKE '%zi' 的條件。此時 3 條中只有 1 條符合條件。
開啓 ICP:
set optimizer_switch='index_condition_pushdown=on';
此時的執行計劃,Using index condition: 把 first_name LIKE '%zi'下推給存儲引擎後,只會從數據表讀取所需的 1 條記錄。
索引條件下推(Index Condition Pushdown)是 5.6 之後完善的功能。只適用於二級索引。ICP 的目標是減小訪問表的完整行的讀數量從而減小 I/O 操做。
總結
由於索引對於改善查詢性能的做用是巨大的,因此咱們的目標是儘可能使用索引。
- 合理建立索引須要注意的點
- 在用於 where 判斷 order 排序和 join 的(on)字段上建立索引
- 索引的個數不要過多。 ——浪費空間,更新變慢。
- 區分度低的字段,例如性別,不要建索引。 ——離散度過低,致使掃描行數過多。
- 頻繁更新的值,不要做爲主鍵或者索引。 ——頁分裂
- 組合索引把散列性高(區分度高)的值放在前面。
- 建立複合索引,而不是修改單列索引。
- 過長的字段,怎麼創建索引?
- 爲何不建議用無序的值(例如身份證、UUID )做爲索引?
- 何時用不到索引?
- 索引列上使用函數(replace\SUBSTR\CONCAT\sum count avg)、表達式、計算(+ - * /):
explain SELECT * FROM `t2` where id+1 = 4;
- 字符串不加引號,出現隱式轉換
ALTER TABLE user_innodb DROP INDEX comidx_name_phone; ALTER TABLE user_innodb add INDEX comidx_name_phone (name,phone);explain SELECT * FROM `user_innodb` where name = 136; explain SELECT * >FROM `user_innodb` where name = '136';
- like 條件中前面帶%
- where 條件中 like abc%,like %2673%,like %888 都用不到索引嗎?爲何?
explain select *from user_innodb where name like 'wang%'; explain select *from user_innodb where name like '%wang';過濾的開銷太大,因此沒法使用索引。這個時候能夠用全文索引。 4. 負向查詢
- NOT LIKE 不能:
explain select *from employees where last_name not like 'wang'
- != (<>)和 NOT IN 在某些狀況下能夠:
explain select *from employees where emp_no not in (1) explain select *from employees where emp_no <> 1
注意一個 SQL 語句是否使用索引,跟數據庫版本、數據量、數據選擇度都有關係。其實,用不用索引,最終都是優化器說了算。
- 優化器是基於什麼的優化器?
基於 cost 開銷(Cost Base Optimizer),它不是基於規則(Rule-Based Optimizer),也不是基於語義。怎麼樣開銷小就怎麼來。 https://docs.oracle.com/cd/B10501_01/server.920/a96533/rbo.htm#38960 https://dev.mysql.com/doc/refman/5.7/en/cost-model.html
By the way
有問題?能夠給我留言或私聊 有收穫?那就順手點個讚唄~
固然,也能夠到個人公衆號下「6曦軒」,
回覆「學習」,便可領取一份 【Java工程師進階架構師的視頻教程】~
回覆「面試」,能夠得到: 【本人嘔心瀝血整理的 Java 面試題】
回覆「MySQL腦圖」,能夠得到 【MySQL 知識點梳理高清腦圖】
因爲我咧,科班出身的程序員,php,Android以及硬件方面都作過,不過最後仍是選擇專一於作 Java,因此有啥問題能夠到公衆號提問討論(技術情感傾訴均可以哈哈哈),看到的話會盡快回復,但願能夠跟你們共同窗習進步,關於服務端架構,Java 核心知識解析,職業生涯,面試總結等文章會不按期堅持推送輸出,歡迎你們關注~~~