1.b-樹索引
索引首先要回顧一下b樹b+樹的特點和區別,數據庫引擎用b+樹的好處有查詢時間比較穩定,b+樹比較適合范圍查詢,b+樹比較矮胖,b樹的高度代表隨機io的次數,相對的查詢時間會比較短。
如圖是索引的一般設計
非葉子也擁有指向葉子頁的指針,葉子頁之間也存在指針。
文中說道:邏輯頁依賴于不同的存儲引擎,對于InnoDB為16k。對此“頁”的概念做一些資料查找。
頁是innodb存儲引擎的最小存儲單位,有數據頁,undo頁,系統頁,事務處理頁。默認的頁是16kb,每個頁上至少有2條以上的記錄。
b-樹適合的查詢類型
- 全鍵值
- 鍵值范圍
- 鍵值前綴(最左前綴)
其中具體包含如下(索引為lastname,firstname,birthday)
- 全部鍵值 查找lastname=Allen,firstname=Cuba,birthday='1960-01-01'
- 匹配最左前綴 查找lastname=Allen
- 匹配列前綴 查找lastname like"A%"
- 匹配范圍值 查找lastname>'A' and lastname<'D'
- 精確匹配某一列并范圍匹配另一列 lastname='Allen' and firstname like "f%"
- 覆蓋索引的查詢
orderBy是否能用到索引的條件和查詢是否能用到索引的條件一致
不能使用索引的限制
- 不是從最左開始 查找 firstname='Cuba'
- 不能跳過索引中的列 查找 lastname='Allen' and birthday='2019-01-01'
- 查詢中某一列用了范圍查詢,右邊的所有列無法用索引再優化 查找 lastname='A' and firstname like "A%" and birthday = '2019-01-01'
2.哈希索引
哈希索引基于哈希表實現,只有精確匹配索引所有列才有效。對于每一行數據,對所有索引列計算哈希碼,哈希索引將哈希碼存儲在索引中,同時在哈希表中保存指向每個數據行的指針。
Memory引擎顯示支持哈希索引,同時也支持B-tree索引。而且Memory引擎支持非唯一哈希索引,索引會以鏈表的方式存在同一條哈希條目中。
哈希索引速度非常快,但是限制如下:
- 哈希索引只包含哈希值和行指針,不能在索引中直接查到數據行(對速度影響較小)。
- 哈希索引不是按照索引值順序排序的,無法用于排序。
- 不支持單個索引列查找,只有部分列是無法使用索引的
- 只支持等值查詢,不支持范圍查詢
- 哈希沖突時查詢速度會變慢
innodb有個特殊功能叫"自適應哈希索引(adaptive hash index)",當Innodb注意到某些索引值使用非常頻繁的時候,會在內存基于B-tree索引之上創建一個哈希索引,讓B-Tree索引具有哈希索引的優點,這個功能是完全自動的,內部行為,用戶無法控制或者配置。
這里有個擴展知識就是在平時使用Mysql時我們對一個字段添加了hash索引的話,重新show INDEXES from tableA 會發現的index_type還是BTREE,這個就是上面所說的自適應哈希,其實還是在BTREE的基礎上做了哈希索引。
在InnoDB中如何優化如下查詢
select id from url where url = "http://www.mysql.com";
可以在該數據庫手動建立一個哈希索引列,基于這一列來查詢會更快
select id from url where url = "http://www.mysql.com" and url_crc=CRC32("http://www.mysql.com")
這種方案麻煩的地方在于需要手動維護url_crc
全文索引
更類似于搜索引擎做的事情,不是簡單的where匹配,全文索引適用于match against操作。
索引的優點
- 減少服務器需要掃描的數據量
- 幫助服務器避免排序和臨時表
- 索引可以將隨機IO變為順序IO
如何才能使用到索引
1.獨立的列
索引列不能是表達式的一部分,也不能是函數的參數
select * from where To_DAYS(current_date) < 10
2.前綴索引和索引選擇性
索引選擇性就是說用了這個索引可以篩選多少行,可以過濾多少行,不重復的索引值越多,選擇性越大。
文中的例子是對city這個地段做索引,可以用這個公式來測算索引選擇性
select count(distinct city)/count(*) from city_demo
計算前綴索引長度的一個建議就是計算完整列的選擇性,使得前綴的選擇接近于完整列。
select count(distinct left(city,3))/count(*) as sel3,
count(distinct left(city,4))/count(*) as sel4 from city_demo
當索引字節增加,索引選擇性沒有太大變化時,即認為當前長度足夠了。
alter table city_demo add key (city(7));
3.多列索引
4.B—tree索引選擇合適的索引列順序
確定索引列的順序的其中一個原則是將選擇性最高的列放在最前面。
以下面查詢為例
select * from payment where staff_id = 2 and customer_id = 584;
是應該創建(staff_id,customer_id)的索引還是顛倒下順序。
實際上如果staff_id和customer_id的組合數接近總行數,那就說明數據沒什么區分度
select count(distinct staff_id,customer_id)/count(*) 比值非常小,說明staff_id,customer_id的組合重復非常多的時候
那么這個聯合索引就沒啥用,反而如果customer_id的區分度很高,只用customer_id做索引會是查詢效率提高很多
聚簇索引
聚簇索引是一種數據存儲方式,當表有聚簇索引時,數據行實際存放在索引的葉子頁。屬于聚簇表示數據行和相鄰的鍵值緊湊地存儲在一起。因為無法把數據行放在兩個不同的地方,所以一個表只能有一個聚簇索引。
聚簇索引實際上是b-tree+數據行
Mysql的內建存儲引擎都不支持服務器選擇哪個索引作為聚簇索引,InnoDB通過主鍵聚集數據,即上圖中被索引的列是主鍵列。
聚簇索引的優點:
- 可以把相關數據保存在一起,例如根據用戶id來查找用戶的郵件,如果沒有使用聚簇索引,可能每封郵件都需要一次IO。(讀到這里我是有疑問的,前面說innodb聚簇索引是主鍵,那主鍵做索引查郵件只可能是一次IO,一個用戶id就一行數據。這里我認為應該是我的誤解,這個說法是針對所有聚簇索引的,而不是單指innodb,而innodb是基于主鍵的)
- 數據訪問更快,索引中直接包含數據,比普通索引更快
- 使用覆蓋索引掃描的查詢可以直接使用頁節點的值
聚簇索引的缺點
- 聚簇索引是最大限度提高了IO密集型應用的性能,但是如果數據都在內存里,訪問順序就不重要了,聚簇索引也沒什么優勢。
- 插入速度依賴于插入順序,innodb是主鍵做聚簇索引,索引按照主鍵順序插入是最快的
- 更新聚簇索引列代價比較高。 比如Innodb中的主鍵
- 基于聚簇索引插入行,會面臨"頁分裂"問題,當主鍵插入一個已經滿了的頁中,存儲引擎會將該頁分為兩個頁面,會造成占用更多的磁盤空間。
- 聚簇索引導致全表掃描變慢,由于頁存儲不連續的時候。
- 非聚簇索引會比較大,因為二級索引包含引用行的主鍵列
- 二級索引訪問需要兩次索引查找(innodb一次b-tree,找到主鍵后查找聚簇索引的b-tree,對于innodb,自適應哈希能夠減少這種重復工作)
下面看下MyISAM和Innodb對于索引實現的區別,創建如下的數據表,插入10000行數據
create table layout_test(
col1 int not null,
clo2 int not null,
PRIMARY KEY(col1),
KEY(col2)
)
對于MyISAM引擎,如下:
數據列如下
主鍵索引如下
col2索引如下
可以看到主鍵索引和普通索引沒有什么區別,葉子頁都存放的是行號,用來索引到具體的數據行。
對于Innodb引擎,聚簇索引如下:
可以看到innodb主鍵索引的葉子節點存放的就是行數據。每個葉子節點都包含了主鍵值,事務ID,用于事務和MVCC的回滾的指針,以及剩余的列。
還有一個MyISAM和Innodb的二級索引和聚簇索引很不相同。Innodb的二級索引的葉子節點不是行指針,而是主鍵值,并用主鍵值當做行指針,這樣就減少了當前行移動或者數據頁分裂時二級索引的維護工作。innodb在移動行時不需要更新二級索引上的指針。
二級索引列的數據如下:
下圖可以清楚看出MyISAM和Innodb的區別
一個問題:在InnoDB表中按主鍵順序插入行的兩種方案,一種主鍵是遞增的id,一種主鍵是uuid,哪種插入速度會比較快?
兩者的區別如下
順序:
uuid:
- 寫入的目標頁可能已經刷到磁盤并從緩存刪除,Innodb在插入前不得不找到并且從目標頁存到內存中,這會導致大量隨機IO。
- 插入是亂序的,Innodb會不得不頻繁地做頁分裂操作,為新的行分配空間,一次插入最少修改三個頁而不是一個頁。
- 頻繁的頁分離會造成頁變得稀疏,最終會有碎片。
所以當隨機值加入后,需要做一次OPTIMIZE TABLE來重新建表并優化頁的填充
覆蓋索引
如果索引的葉子節點就包含所有要查詢的數據,就稱改索引為"覆蓋索引"。例如對表中某兩個字段(A,B)做了聯合索引,則如果一個Sql語句是查詢A,B兩個字段,那就可以用到覆蓋索引。
覆蓋索引的好處:
- 索引條目遠小于數據行大小,用覆蓋索引會極大減小數據訪問量。
- 索引是順序存儲的,查詢的IO會比從磁盤讀小很多
- MyISAM這種數據內存中只緩存索引,如果調用數據需要系統調用,會有很大的損耗,直接查索引可以減小很多開銷。
- InnoDB是聚簇索引,覆蓋索引可以避免對主鍵索引的二次查詢。
覆蓋索引必須要存索引列的值,這個在MySQL只能用B-Tree,哈希全文空間都不行。
在使用覆蓋索引時,explain時會看到extra有"Using index"
如果想查一個表的所有數據,但是又想用到覆蓋索引,可以用延遲關聯的方法
innodb所有二級索引都包含主鍵,所以本來用到覆蓋索引的再增加主鍵值,依然可以用到覆蓋索引。
使用索引做排序
在rental表添加一個包含三個列的索引(rental_date,inventory_id,customer_id)
如下語句可以用到索引
select * from rental where rental_date='2019-01-01' order by inventory_id,customer_id
下面這個也可以,滿足了最左前綴的要求
select * from rental where rental_date='2019-01-01' order by inventory_id desc
下面這個也可以,因為orderBy使用的也是最左前綴
select * from rental where rental_date>'2019-01-01' order by rental_date,inventory_id desc
下面這個不能用,因為orderBy的排序方向不同,索引列都是正向排序的
select * from rental where rental_date>'2019-01-01' order by inventory_id desc,customer_id ASC
這個也不行,用到了一個非索引的列
select * from rental where rental_date>'2019-01-01' order by inventory_id ,staff_id
這個也不行,無法組成最左前綴
select * from rental where rental_date>'2019-01-01' order by customer_id
這個也不行,第一列是范圍查詢
select * from rental where rental_date>'2019-01-01' order by inventory_id desc,customer_id
這個也不行,也是一種范圍查詢
select * from rental where rental_date='2019-01-01' and inventory_id in (1,2) order by inventory_id desc,customer_id
壓縮索引
MyIsam用前綴壓縮減小索引大小,從而讓更多索引放入內存
冗余和重復索引
Mysql可以對相同列創建多個索引,主鍵、唯一鍵也是通過索引實現的。
例如(A,B)和A這種索引就是冗余索引,因為對于(A,B)只查A也走索引
另外一種情況是(A,ID)也是冗余的,因為InnoDB主鍵列在二級索引中。
索引和鎖
InnoDB只有在訪問行的時候會對其加鎖,但是這個只有在InnoDB在存儲引擎層能過濾掉所有不需要的行時才有效。在InnoDB檢索到數據行返回給服務層,MySQL服務器才能應用Where子句。這時候很多行已經被鎖住,直到服務器過濾掉行之后才釋放。
set autocommit=0;
begin;
select actor_id from actor where actor_id<5 and actor_id <>1 for update
只返回3行記錄,但是實際上會鎖住1-4這四行。通過explain可以看出Extra有"UsingWhere",說明MysSQL是InnoDB將行返回再應用where條件過濾。
set autocommit=0;
begin;
select actor_id from actor where actor_id=1 for update
這個程序會掛起,直到第一個事務釋放了鎖。
如果說查詢根本沒用到索引,情況會更糟糕,Innodb會全表掃描并鎖住所有的行,這個在生產環境是很危險的。所以寫for update語句一定要看看有沒有用到索引。
一個很少人知道的細節:InnoDB在二級索引上使用讀鎖,在訪問主鍵索引需要排他鎖,這消除了使用覆蓋索引的可能性,并且使得select for update 比 lock in share mode 慢很多
總結
Mysql大多數情況下都會使用B-Tree索引,其他索引都只是適用于特殊目的。
選擇索引和編寫索引的查詢時,有以下三條原則:
- 單行訪問是很慢的,如果訪問數據頁的某一塊只為獲得某一行,那就浪費了很多IO的工作。
- 順序訪問數據很快,1不需要多次磁盤尋道,所以比隨機IO快,尤其機械硬盤2其次是無需額外的排序操作。
- 索引覆蓋查詢非常快,避免了單行訪問,減少了IO的次數。
祝大家編碼愉快,工作愉快,歡迎關注我的公眾號,一起分享交流