最近學習極客時間上面的專欄,準備把每篇文章寫成博客,加上自己的理解,寫的循序沒有按照專欄的來,會持續跟新!
今天討論的是如何在郵箱這樣的字段上建立合理的索引:
用戶表這樣定義:
mysql> create table SUser(
ID bigint unsigned primary key,
email varchar(64),
...
)engine=innodb;
如果使用郵箱登錄,業務代碼中一定會出現類似的語句:
mysql> select f1, f2 from SUser where email='xxx';
如果email這個字段上沒有索引,那麼這個語句就只能做全表掃描。
同時,mysql支持前綴索引,也就是說,你可以定義字符串的一部分作爲索引,默認地,你創建的索引的語句不指定前綴長度,那麼索引就會包含整個字符串。
比如,這兩個email字段上創建索引的語句:
mysql> alter table SUser add index index1(email);
或
mysql> alter table SUser add index index2(email(6));
第一個語句創建的index1索引裏面,包含了每個記錄的整個字符串;而第二個語句創建的index2索引裏面,對於每個記錄只取前6個字節。
兩種不同的定義在數據結構和存儲上有什麼區別呢?如下去所示:
從圖中可以看出,由於email(6)這個索引結構中每個郵箱字段都只取前6個字節(即:zhangs),所以佔用的空間更小,這就是使用前綴索引的優勢。
但是,這同時帶來的損失是,可能會增加額外的記錄掃描次數。
接下來,再看看下面的語句,在兩個索引的定義下是怎樣執行的。
select id,name,email from SUser where email='[email protected]';
如果使用index1 :
1、從index1索引樹找到滿足索引值是‘[email protected]’的這條記錄,取得ID2的值
2、到主鍵上查找主鍵值得是ID2的行,判斷email的值是正確的,將這行記錄加入結果集
3、取index1索引樹上剛剛查到的位置的下一條記錄,發現已經不滿足條件了,循環結果結束。
這個過程中,只需要回主鍵索引取一次數據,所以系統認爲只掃描了一行。
如果使用的時index2 :
1、從index2索引樹找到滿足索引值的是‘zhangs’的記錄,找到第一個是ID1;
2、到主鍵上查到的主鍵值是ID1的行,判斷出的email的值不是’[email protected]’,這行記錄丟棄;
3、取index2上剛剛查到的位置的下一條記錄,發現仍舊是‘zhangs’,取出ID2,再到ID索引上取整行然後判斷,這次值就對了,將這行數據加到結果集中。
4、重複上一步,直到idex2上取到的值不是‘zahngs’時,循環結束。
在這個過程中,要回主鍵索引取4次數據,也就是掃描了4行。
通過這個對比,可以發現,使用前綴索引後,可能會導致查詢語句讀數據的次數變多。
但是,對於這個查詢語句來書,如果你定義的index2不是email(6)而是email(7),也就是說取email字段的前7個字節構建索引的話,即滿足‘zhangss’的記錄只有一個,也能夠直接查到ID2,只掃描一行就結束了。
也就是說使用前綴索,定義好長度,就可以做到節省空間,又不用額外增加太多的查詢成本。
問題是:
當給字符串創建索引時,有什麼方法能夠確定我應該使用多長的前綴呢?
實際上,我們建立索引是關注的時區分度,區分度越高越好,因爲區分度越高,意味着重複的值就越少,因此,我們可以聽過統計索引上有多少個不同的值來判斷要使用多長的前綴。
首先,你可以使用下面的語句,算出這個列上有多少個不同的值:
select count(distinct email) as L from SUser;
然後,依次選取長度的後綴來看這個值,比如看4~7個字節的前綴索引,可以用這個語句:
select
count(distinct left(email,4))as L4,
count(distinct left(email,5))as L5,
count(distinct left(email,6))as L6,
count(distinct left(email,7))as L7,
from SUser;
當然,使用前綴索引很可能損失區分度,所以要先設定一個可以接受的損失比例,比如5%;在返回的L4-L7中,找出不小於L*95%的值,假設這裏L6、L7都滿足,你就可以選擇前綴長度爲6。
前綴索引對覆蓋索引的影響
前綴索引可能會增加掃描行數,這回印象性能,其實前綴索引的影響不止於此,
第一個sql
select id,email from SUser where email='[email protected]';
前面一個sql:
select id,name,email from SUser where email='[email protected]';
相比這個sql只要返回id和email字段;
所以,如果使用index1的話,可以利用覆蓋索引,從index1查到結果後直接就返回了,不需要回到ID索引再去查一次。如果使用index2的話,就不得不回到ID索引再去判斷email字段的值。
即使你將index2的定義修改爲email(18)的前綴索引,這時候雖然index2已經包含了所有的信息,但是InnoDB還是要回到id索引在查一次,因爲系統不知道是否截取了全部的信息。
也就是說,使用前綴索引就用不上覆蓋索引對查詢性能的優化了,這也是你在選擇是否使用前綴索引時需要考慮的因素。
其他方式:
對於類似郵箱這樣的字段來說,使用前綴索引的效果可能不錯,但是,遇到前綴的區分蘇不夠好的情況時,我們該怎麼辦?
比如我國的身份證號碼,前面6位是地址碼,所以一個縣的人的前6位是一樣的。這個索引區分度就非常低了。
按照前面的方法,可能需要創建長度爲12的前綴索引才能滿足區分度的要求。
但是索引越長,佔用的空間就越大,相同的數據頁放的數據就越少,搜索的效率就越低
那麼什麼方法既可以佔用更小的空間,又能達到相同的搜索效率。
第一種方式:
倒序存儲:如果你存儲的身份證號碼的時候把它倒過來存,每次查詢的時候,你可以寫:
select field_list from t where id_card = reverse('input_id_card_string');
由於身份證號的最後6位沒有地址碼的重複邏輯,所以後6位可以提供足夠的區分度。
當然,實踐中不要忘了使用count(distinct)方法做個驗證。
第二種方式:
hash字段:那可以在表上再創建一個整數字段,來保存身份證的校驗碼,同時在這個字段上創建索引。
alter table t add id_card_crc int unsigned, add index(id_card_crc);
然後每次插入新記錄的時候,都使用crc32()函數得到校驗碼,由於校驗碼可能存在衝突,也就是說不同的身份證也可能得到的結果是相同的,所以你查詢語句where部分要加上id_card的值是否精確相等。
select field_list from t where id_card_crc=crc32('input_id_card_string') and id_card='input_id_card_string'
這樣,索引長度爲4個字節,比原來小很多。
接下來看看這兩種方式的異同點:
相同點:
都不支持範圍查找了;
不同點:
1、從佔用額外空間來說,倒序存儲方式在主鍵索引上,不會消耗額外的存儲空間,而hash字段方法需要增加一個字段,當然,倒序存儲使用4個字節的前綴索引應該是不夠的,如果再長一點,這個消耗跟額外這個hash字段也差不多抵消了;
2、在cpu消耗方面;倒序方式每次寫和讀的時候,都額外調用一次reverse函數,而hash字段的方式需要額外一次調用crc32()函數,如果只從這兩個函數的計算複雜度來說,reverse函數 額外消耗的cpu資源會更小一些
3、從查詢效率來說,使用hash字段的方式來查詢性能相對更穩定一些,因爲crc32算出來的值雖然有衝突的概率,但是概率非常小,可以認爲每次查詢的平均掃描行數接近1.而倒序存儲方式畢竟還是使用前綴索引的方式,也就是說還是會增加掃描行數。
小結:
1、直接創建完整索引,這樣可能會比較佔用空間
2、創建前綴索引,節省空間,但會增加掃描行行數,並且蹦年使用覆蓋索引
3、倒序存儲,再創建前綴索引,用於繞過字符串本身前綴的區分度不夠的問題
4、創建hash字段索引,查詢性能穩定,有額外的存儲計算的消耗,跟第三種方式一樣,不支持範圍查詢。
來源:極客時間《MySQL實戰45講》
擴展:
關於前綴索引長度的選擇方法,可以學習《高性能MySQL》P153 的前綴索引和索引選擇性
CRC的概念:
CRC全稱爲Cyclic Redundancy Check,又叫循環冗餘校驗。CRC32是CRC算法的一種,常用於校驗網絡上傳輸的文件。
CRC32的基本特徵:
#1.CRC32函數返回值的範圍是0-4294967296(2的32次方減1)
#2.相比MD5,CRC32函數很容易碰撞