簡介
索引在mysql叫做“鍵(key)”是存儲引擎用於快速查找記錄的一種數據結構,這是索引的基本功能,下面將討論索引的其他有用的屬性。
當表的數據量較小時索引對查詢效率的提升並不明顯,當數據量越來越大時索引可以將查詢效率提高几個數量級,但當數據量超大時索引開始變得無助了,因爲數據量大到一定程度後索引結構開始變得十分複雜,管理索引變得較爲喫力,反而可能對查詢效率起到反作用。
索引基礎
怎麼理解索引是如何工作的呢?翻開書的目錄可以大概瞭解,索引就是對一堆數據進行區分每個區界進行貼標籤,下次來時就可以快速的找到。
如下面的語句:
mysql> select user_name from user where user_id=5
mysql將使用索引找到user_id=5的數據行,然後再找到你想要的列。
索引類型
在mysql中,索引是在存儲引擎層實現的,不同的存儲引擎工作的方式並不一樣,並不是所有存儲引擎都支持所有類型索引,就算不同存儲引擎都支持一種索引,他們的實現方式很可能會不同。
下面我們先看看mysql支持的索引類型,以及他們的優點和缺點。
B-Tree 索引
B-Tree 索引是最常見的索引,大多數mysql索引都支持這種索引,它是用數據結構來存儲數據的。
我們使用“B-Tree”是因爲create table和其他語句使用該關鍵字,但不代表存儲引擎都是使用這種數據結構存儲索引的,如NDB集羣存儲引擎內部實際上使用了T-Tree存儲索引,即使它的名字是BTree,InnoDB則是使用B+Tree。
存儲引擎以不同的方式使用B-Tree ,性能也有所不同,例如MyISAM使用前綴壓縮技術使得索引更小,但InnoDB則按照原數據格式進行存儲。再如MyISAM索引通過數據的物理位置引用被索引的行,InnoDB則根據主鍵引用被索引的行。
B-Tree通常意味着所有的值都是按順序存儲的,並且每一個葉子頁到根的距離相同。下面圖大致反映InnoDB索引是如何工作的。MyISAM結構有所不同但基本思想類似。
從圖可以看出來索引葉節點最終指向的是數據,B-Tree提供一種存儲的數據結構,左邊的指向的是小於key1的值,中間的是大於等於key1小於keyN的值,右邊的是大於keyN的值,從而可以證明B-Tree所有的值都是按照順序進行存儲,然後給這些值畫界限,想要的時候根據界限快速定位,避免按順序掃描,所以很適合查找範圍數據。
create table people (
last_name vachar(50) not null,
fist_name vachar(50) not null,
dob date not null ,
gender enum('m','f')not null,
key(last_name,fist_name,dob)
);
可以發現索引對多個值進行排序的依據是create table語句中定義的順序,先按last_name再按fist_name,最後按dob。
B-Tree索引值適用於全鍵值、健值範圍或鍵前查找。
全值匹配
指的是和索引中的所有列進行匹配,如查找姓名+出生的人。
匹配最左前綴
如查找所有姓名爲Allen的人,即使用索引的第一列。
匹配範圍值
如查找姓名在Allen和Barrymore之間的人。
下面是一些關於B-Tree索引的限制:
- 如果不是按照索引最左列開始查找,則無法使用該索引,如上面的例子你無法使用該索引查找名字爲bill或者特定生日的人。也就說如果你想組合索引來進行查詢一定要涉及最左列開始,不能跳過左邊的索引列,如你的查詢條件可以是
where last_name='xx' and fist_name='xx'
不能是where fist_name='xx' and dob='xx'
- 如果查詢中要查詢某個列的範圍,則該列右邊的列將使用不上索引,如
where last_name='Smith' and fist_name='J%' and dob='1976-1cha2-23'
,這是內部設計的問題可能會在更新版本中解決,如果範圍查找的範圍有限可以使用多個等值條件來代替範圍查找。
讀到這裏可以發現索引順序是多麼重要,所以在使用時可能爲了解決這個問題會多弄幾個相同列但順序不同的索引。
哈希索引
哈希索引是基於哈希表實現的,由於存儲引擎會對所有的索引列生成一個哈希碼,所以只有精確匹配索引所有列索引纔會有效。哈希碼是一個較小的值,並且不同的鍵值行計算出來的哈希碼也不一樣。哈希表把所有哈希碼存儲在索引中並保存指向數據行的指針。
目前只有Memory引擎顯示的支持哈希索引,這也是Memory默認的索引類型。Memory引擎支持非唯一哈希索引,這是數據庫世界裏比較與衆不同的,如果多個列的哈希值相同,索引會以鏈表的方式存儲多個記錄到哈希條目中。
create table testhash(
fname varchar(50) not null,
lname varchar(50) not null,
key using hash(fname)
)engine=memory;
select * from testhash;
fname | lanme |
---|---|
Arjen | Lentz |
Baron | Schwartz |
Peter | Zaitsev |
Vadim | Tkacheko |
哈希索引的數據結構如下(哈希碼是假設的):
哈希碼 | 值 |
---|---|
2568 | 指向第1行的指針 |
3256 | 指向第4行的指針 |
4567 | 指向第3行的指針 |
4589 | 指向第2行的指針 |
注意哈希碼排列是順序的,但是指向數據行的指針不是。
哈希索引的限制
- 哈希索引只包含哈希行碼和指向行的指針十分緊湊,索引想要得到查詢的值必須訪問數據行,內存中的還好,磁盤中的效率就沒那麼高了。
- 哈希索引數據不是按照列值順序存儲的,所以無法用於排序。
- 哈希索引的哈希值是根據所有索引列生產的,所以不支持部分索引列匹配查找。
- 哈希索引查找數據的速度非常快,除非存在哈希碼相同需要通過鏈表的指針遍歷數據行,但是已經縮小了範圍。
- 如果哈希碼有很多組重複的話,索引維護也會帶來較高代價,例如刪除某個哈希值重複的數據需要刪除相應索引,這又要遍歷數據行進行確定了。
- 哈希索引只支持等值比較,包括=、in()、<=>。
除了Memory引擎,InnoDB有種特殊的索引“自適應哈希索引”,當InnoDB注意到某個索引值被使用的非常頻繁,它會在內存中基於B-Tree之上再建一個哈希索引,這樣能提高查詢速率,這是InnoDB的自發行爲,不需要的話可以關閉。
創建自定義哈希索引,如果存儲引擎不支持哈希索引。則可以模擬InnoDB一樣創建哈希索引,這樣就可以用很小的索引爲超長的健創建索引。
思路很簡單:在B-Tree之上建立一個僞哈希索引,實際上還是通過B-Tree查找,但它使用的是哈希碼而不是鍵本身進行查找。
例如: 使用B-Tree來存儲URL,那麼存儲的內容就會很大,因爲URL本身就很長
select id from url where url="https://www.baidu.com/";
若刪除原來url列上的索引,新增一個被索引的url_crc列,使用crc32做哈希索引
select id from url where url="https://www.baidu.com/" and url_crc=crc32("https://www.baidu.com/");
這樣查詢效率會非常高。mysql優化器會選擇性能高的url_crc完成查找,由於哈希值可能會重複,專業維護起來比較麻煩,可以手動維護也可以加觸發器。
索引的優點
- 索引最基本的優點是提高查詢速度,減少要掃描的數據量。
- 索引可以幫助排序和避免服務器建臨時表,如B-Tree索引。
- 索引隨機I/O變成順序I/O。
如何評價某個索引是否合適某個查詢呢?
《數據庫索引設計與優化》一書中介紹了“三星評價”:
一星——將相關的索引記錄放在一起(組合索引);
二星——索引數據的順序和查詢排列順序一致(B-Tree索引)。
三星——查詢的列索引已經存儲。
高性能的索引策略
獨立的列
索引的列不能是表達式的一部分,也不能是函數參數,因爲mysql無法識別處理,如下:
select * from user where user_id+1=6;
select * from user where todays(current_date)-todays(user_birth)=100;
前綴索引和索引的選擇性
前綴索引顧明思議就是截取列數據左邊一部分作爲索引存儲數據。爲什麼搞這麼麻煩?因爲當爲比較長數據列做索引時需要存儲較大會影響索引速度,那麼有什麼辦法可以解決呢,那就是截取部分,雖然會出現相同前綴但是隻要足夠長可以把不應該出現相同的概率降到很低,怎麼評判這個標準?那就是索引的選擇性。
索引的選擇性,名字聽上去很拗口,其實就是索引不重複值和數據表的記錄總數的比值,該比值要儘量接近於索引列原來不重複值和數據表的記錄總數的比值。如下:
select count(distnct titlename)/count(titlename) from title
原來比值:count(distnct titlename)/count(titlename)=0.312
select
count(distnct left(titlename,4))/count(titlename),
count(distnct left(titlename,5))/count(titlename),
count(distnct left(titlename,6))/count(titlename),
count(distnct left(titlename,7))/count(titlename)
from title
各前綴索引比值:
count(distnct left(titlename,4))/count(titlename)=0.223
count(distnct left(titlename,5))/count(titlename)=0.256
count(distnct left(titlename,6))/count(titlename)=0.282
count(distnct left(titlename,7))/count(titlename)=0.310
所以截取長度7就差不多了
alter table title add key(titlename(7))
前綴索引可以使索引更小、更快,但是無法用於group by 、order by 和覆蓋掃描。
order by 排序是不大可能的,因爲就算前綴的比值和原來一樣,後面的部分是不確定的,group by 到是可能可以,前面部分都不同了後面就不用管了。
多列索引
很多人對多列索引理解不夠,常見的錯誤就算爲每個列創建單獨索引,或者按照錯誤的順序創建多列索引。
在更早版本的msql中只能使用其中某一單列索引,但是在多個條件查詢時沒有哪一個獨立索引是非常有效的,例如在student 表中爲user_id 和 user_phone做單列索引,下面語句在老的mysql版本中將會全表掃描也就是說不會用上索引。
select user_id ,user_phone from user where user_id=‘1’or phone=‘12345678912’
除非你改成
select user_id ,user_phone from user where user_id=‘1’
union all
select user_id ,user_phone from user where phone=‘12345678912’ and user_id<>‘1’
但在mysql 5.0 和以上版本中可以使用單列索引,它會將結果進行合併,如語句中使用 and 或者 or 。通過explain可以看到:
extra:using union(primary,idx_user_phone) ;using where
索引合併策略是一種 優化的結果,但更多的說明了索引設計的不好:
- 當查詢涉及多列時需要設計多列索引,而不是爲每個列設計單獨索引。
- 合併策略是一種妥協策略,它是先根絕單獨列查出來,然後進行合併,需要進行緩存,合併時還要檢查是否滿足要求(剔除重複等)效率不高,有時還不如全表掃描。
如果在explain中看到合併索引應該進行優化,也可以通過optimizer_switch來關閉索引合併功能,或者使用ignore index提示讓優化器忽略某些索引。
選擇合適的索引列順序
索引順序適用於B-Tree索引。索引的順序依賴於使用該索引的查詢,並同時需要考慮更好的滿足排序和分組。
當不考慮分組和排序時,將選擇性最高的列放在最左邊,這樣能快速的過濾。如下:
select
count(distinct user_id)/count(*),
count(distinct user_phone)/count(*)
from user
count(distinct user_id)/count(*)=1
count(distinct user_phone)/count(*)=0.658
所以當查詢條件涉及user_id 和 user_phone時創建的索引要把user_id 放在前面。
聚簇索引
聚簇索引不是單獨的一種索引類型,而是一種數據的存儲方式。具體要看其實現方式,InnoDB的聚簇索引就是B-Tree索引保存了整個數據行。
數據的物理順序和索引順序一樣,物理順序只有一種,所以一個表只有一個聚簇索引,一般就是主鍵索引。
下圖展示了聚簇索引中的記錄是如何存放的,被索引的列是主鍵列:
如果沒有定義的主鍵,InnoDB會找一個唯一非空的列來代替,如果沒有就創建一個隱式的主鍵。索引如果換了存儲引擎將可能導致表不可用。
聚集索引的優點:
- 根據主鍵縮小查找範圍,避免磁盤IO負載太大。
- 聚簇索引包含了數據行,訪問數據快。
聚集索引的缺點:
- 插入速度嚴重依賴於插入順序,按照默認的主鍵順序插入是加載數據到InnoDB表中最快的,但是如果不是按照順序加載,最好在加載完成後使用optimizer table命令重新組織下。
- 更新聚簇索引列的代價比較高,因爲涉及的是整個數據行,所有有一定的I/O代價。
- 聚簇索引在頁滿時插入新的數據時會新創建頁,只放一條數據佔用磁盤。
- 聚簇索引可能會導致全表掃描速度慢,尤其是頁分裂導致數據存儲不連續。
- 二級索引可能比想象的要大,因爲在二級索引的葉子中包含了引用行的主鍵列。
- 二級索引訪問需要兩次索引查找,而不是一次。
覆蓋索引
所謂的覆蓋索引就是指,索引的內容包括了你想要查詢的列,所以覆蓋索引不僅考慮到了查詢條件還考慮到要查詢的列,這樣就不用再去讀取數據行了,速度肯定更快,但是索引包含的內容不要太多,不然內存放不下就麻煩了。
例子:
有個覆蓋索引(user_id,user_name)
select user_id,user_name from user
explain下會看到extra列可以看到using index
優點:
- 索引條目通常遠小於數據行,所以可以幫助I/O密集型應用減少數據的搬運,更容易放進內存,所以提升效率明顯。
- 覆蓋索引可以對InooDB的聚簇索引進行改進,InooDB的二級索引在葉子節點保存了行的主鍵,爲啥不可以學覆蓋索引包含點其他內容呢?
不是所有的存儲擎都支持覆蓋索引,不是所有索引類型都支持覆蓋索引,當然B-Tree支持。
有時覆蓋索引無法使用,雖然覆蓋了查詢條件,如下:
select * from user where user_id='xx' and user_name like 'feng%'
無法使用索引的原因:覆蓋索引沒有覆蓋所有查詢的列。(mysql 5.5及以下版本索引支持簡單的比較操作(>,<,=,!=)和一個通配符在右邊(‘xx%’))
那麼沒辦法解決嗎?覆蓋索引只要滿足查詢條件我們還是可以做一定的調整進行利用的,記住主鍵是默認有索引的,可以利用上。
select *
from user
join (
select user_id
from user
where user_id='xx' and user_name like 'feng%'
) as u
on u.user_id=user.user_id
這樣本來就縮小了範圍加上user_id本來就有索引索引查詢效率還是可以的。
使用索引掃描進行排序
如果Extra出來的type的值爲index,則說明Mysql使用了索引掃描排序。
掃描索引本身是很快的,但是如果需要查詢全部列那麼需要頻繁隨機的去讀取數據行,所以按順序全表掃描通常比索引掃描排序快。
MySql要儘可能滿足排序和查找行。
索引的列的順序和排序方向要和order by 一樣,如果查詢涉及多張表,只有Order by 所要的字段都只在一個表時才能使用。order by個查詢限制一樣:滿足最左前綴要求。
有一種情況order by可以不滿足最左前綴的要求,就是前導列爲常量。如下:
表有包含(user_birth,user_id,user_name)
select * from user where user_birth='1999-09-21'
order by user_id,user_name
下面是一些不能使用索引做排序的查詢:
- 查詢使用了不同方向的排序
...where user_birth='1999-09-21' order by user_id desc,user_name asc
- 使用了索引不存在的列
...where user_birth='1999-09-21' order by user_id,user_address
- 無法合成最左前綴
...where user_birth='1999-09-21' order user_name
- 不是常數,後面的索引列無法使用
...where user_birth>'1999-09-21' order user_name
- user_id上用了範圍查找
...where user_birth='1999-09-21' and user_id in ('xx','yy') order by user_name
壓縮(前綴壓縮)索引
MyISAM使用前綴壓縮來減少索引的大小,減少佔用內存空間,減少從磁盤掉索引進內存的次數,提高查詢速度。默認只壓縮字符串,但通過參數設置也可以對整數壓縮。
方法:通過比較兩個值,如果存在兩個前綴一樣,那麼相同的地方可以只存一份,相同的部分用字節數表示。如common,community,可以這樣表示”common”,”4,unity”(但是被壓縮的要有指針指向沒被壓縮的——我自己想的),MyISAM對指針也採用類似的方法進行壓縮。
缺點:
這樣的缺點就是要想確認被壓縮的值還要去找沒被壓縮的值,所以有利就有弊,沒有完美的事物。
可以在create_table語句中指定pack_keys參數來控制索引壓縮的方式。
冗餘和重複索引
MySql允許在同一個列上創建多個索引,所以你會有意無意的創建重複索引而不受限制。MySql需要維護重複索引,並且優化器在查詢的時候需要考慮用哪個,所以會影響性能。
重複索引指的是在相同列上創建相同順序相同類型的索引,應該避免這樣的徒勞工,如下:
create table user{
user_id int not null primary key,
...
unique(user_id),
index(user_id)
}engine=InnoDB;
主鍵和唯一限制都是通過索引來實現的,所以沒必要重複弄,主鍵本來就不能重複。
冗餘索引和重複索引有所不同,冗餘並不是完全相同,只是可以替代。如創建了索引(A,B),再創建索引(A)就是冗餘了。
一般情況下不需要冗餘索引,應該儘量對現有索引進行擴展,但是有時候擴展會對使用現有索引方帶來影響,例如:在原來的int單列上擴展一個很長的varchar,那麼性能可能會下降,首先裝進內存就比原來慢了,特別是做覆蓋索引的時候。
解決冗餘索引和重複索引就是刪掉,那麼怎麼找到這些不必要的索引呢?可以通過寫一些 複雜的訪問INFORMATION_SCHEMA表的查詢來找,不過還有兩個更簡單的方法。可使用Shlomi Noach的common_schema中的一些視圖來定位,common_schema是一系列可以安裝到服務器上常用的存儲和視圖。此外PerconaToolkit中的pt_duplicate_key_checker,該工具通過分析表結構來找出冗餘和重複的索引。
索引和鎖
索引可以讓查詢鎖定更少的行,鎖定超過需要的行會增加鎖爭用並減少併發性。