數據表還有哈希索引,沒聽過?

哈希索引是基於哈希表構建的,僅在按列進行精確查找時有用。對於每一行,存儲引擎都給指定的索引列生成了一個哈希值,這個哈希值與其他行相比,由於索引列的值不同而不同。存儲引擎將這個哈希值存儲在索引中,並且在哈希表中存儲了一個指向數據行的指針。

在MySQL中,只有Memory存儲引擎明確支持哈希索引,這是Memory數據表中默認的索引類型(該存儲引擎也支持二叉樹索引)。Memory存儲引擎只是重複的哈希索引,這在數據庫界中並不常見。如果不同行的索引列的值相同,它們也就有相同的哈希值,因此這些行的指針會通過鏈表存入同一個哈希表的索引中。
下面是一個例子:

CREATE TABLE testhash (
 fname VARCHAR(50) NOT NULL,
 lname VARCHAR(50) NOT NULL,
 KEY USING HASH(fname)
) ENGINE=MEMORY

數據表包括如下數據:

mysql> SELECT * FROM testhash;
fname lname
Arjen Lentz
Baron Schwartz
Peter Zaitsev
Vadim Tkachenko

假設索引使用一個虛構的哈希函數f(),對應不同的fname返回的結果如下所示:

f('Arjen')= 2323
f('Baron')= 7437
f('Peter')= 8784
f('Vadim')= 2458

則索引的數據結構類型下面的表格:

Slot Value
2323 指向第一行
2458 指向第4行
7437 指向第2行
8784 指向第3行

需要注意的是哈希值的Slot是有序的,但是數據行並不是有序的。假設我們執行下面的SQL:

mysql> SELECT lname FROM testhash WHERE fname='Peter';

MySQL首先會計算Peter字符串的哈希值,以用來找到對應的數據行。由於f('Peter')=8784,因此MySQL會找到8784對應的Slot,然後找到第3行,最後一個步驟是比較第3行的列的值是否與Peter一致。
由於索引自身只存儲短的哈希值,因此哈希索引十分簡潔。其結果是,查找起來像閃電一般快!有得有失,哈希索引還有一些限制:

  • 由於索引僅僅包含哈希值和數據行指針,因此讀取完索引後不能得到數據行的值。幸運的是,讀取記憶行(in-memory rows)的速度很快,因此這個限制一般不會有什麼影響。
  • MySQL無法使用哈希索引進行排序,因爲他們並沒有將數據行按序排列。
  • 哈希索引不支持部分值匹配,這是因爲他們是計算完整索引列值的哈希值。所以,如果你有一個索引在(A,B)上,那WHERE語句中如果只使用了A,索引不會有任何幫助。
  • 哈希索引只支持相等的比較操作符,如=,IN(),和<=>(注意這個操作符和<>並不相同)。因此在範圍查詢時並不起作用,例如WHERE price > 100。
  • 訪問哈希索引的數據是很快,但是如果出現了哈希衝突(很多值擁有相同的哈希值)另說了。當有衝突時,存儲引擎必須在數據行鏈表裏逐個比較行的值是否與查詢的值一致。
  • 如果存在很多哈希衝突,一些索引的維護操作可能會很慢。例如,如果你在一個列創建了哈希索引,但這個列的差異性很低(相同值過多,存在很多哈希衝突)。如果要從數據表刪除一行,通過索引找到這一行的代價可能會很高。存儲引擎需要對索引指向的數據行鏈表進行遍歷,知道找到要刪除的行。

這些限制使得哈希索引只在特殊的場景中有用。然而,如果他們適用於應用需求,可以極大地改善性能。一個例子是在數據倉庫應用中,一個典型的“star”事務需要連接很多表查詢,這個時候如果建立哈希索引將十分高效。

除了Memory存儲引擎明確支持哈希索引外,NDB集羣存儲引擎也支持唯一哈希索引。InnoDB存儲引擎有個特性叫做自適應哈希索引。當InnoDB引擎發現有些索引值經常被訪問,它就會在內存中構建二叉樹之上的索引,這賦予了二叉樹一些哈希索引的特性,例如快速的哈希查找。這個過程是全自動的,你不可以控制或配置,只是可以禁用這一特性。

如果你的存儲引擎不支持哈希索引,也可以通過模仿InnoDB的方式實現哈希索引,這能夠讓你使用哈希索引的特性。例如給長內容的列建立小的索引。實現的思路比較簡單:在標準的二叉樹上創建一個僞哈希索引,這與真正的哈希索引並不完全一致,因爲還是會用到二叉樹索引進行查詢。但是,它會用到哈希值進行查詢,而不是索引列的值。你所需要做的就是在WHERE條件中手動指定一個哈希函數。

一個運用很好的例子是URL查詢。URL通常會導致二叉樹變得很大,這是因爲URL通常比較長,你可能是這樣查詢URL的:

mysql> SELECT id FROM url WHERE url="http://www.mysql.com";

如果你把url列的索引去掉,而是新增一個url_crc列,然後就可以這樣查詢:

mysql> SELECT id FROM url WHERE url="http://www.mysql.com" AND url_crc=CRC32("http://www.mysql.com");

這會有效改善查詢效率,這是因爲MySQL查詢優化器會發現url_crc列存儲空間更小、且是索引,因此會進行索引查找。即便可能多個行都有相同的url_crc的值,但是通過整數比較找到這些行,再從這些行去查找匹配url行的效率會高很多。另一種方式是啓用URL全文索引,但這樣會更慢。

這種方式的一個缺陷是需要維護哈希值,你可以手動維護,或者在MySQL 5.0及更高版本,你可以使用觸發器。下面的例子是在插入或更新時,如何啓用觸發器去維護url_crc列。

CREATE TABLE pseudohash (
 id int unsigned NOT NULL auto_increment,
 url varchar(255) NOT NULL,
 url_crc int unsigned NOT NULL DEFAULT 0,
 PRIMARY KEY(id)
);

現在來創建觸發器。我們需要臨時修改語句的分隔符,以便於我們使用分號作爲觸發器的分隔符。

DELIMITER //

CREATE TRIGGER pseudohash_crc_ins BEFORE INSERT ON pseudohash FOR EACH ROW BEGIN
SET NEW.url_crc=crc32(NEW.url);
END;
//

CREATE TRIGGER pseudohash_crc_upd BEFORE UPDATE ON pseudohash FOR EACH ROW BEGIN
SET NEW.url_crc=crc32(NEW.url);
END;
//

DELIMITER ;

剩下要做的就是驗證觸發器確實維護了這個哈希值:

mysql> INSERT INTO pseudohash (url) VALUES ('http://www.mysql.com');
mysql> SELECT * FROM pseudohash;

如果看到在url_crc列上生成了一個整數的CRC值,那觸發器就是正常的。

mysql> UPDATE pseudohash SET url='http://www.mysql.com/' WHERE id=1;
mysql> SELECT * FROM pseudohash;

然後,如果對應的url_crc的值發生了改變,就說明更新時觸發器也正常工作了。

採用這種方式要避免使用SHA1或MD5,這是因爲這兩個方法返回的字符串很長,或導致空間浪費和比較查詢時變慢。雖然有很多強加密方法設計去減少衝突,但是在這裏不應該用。簡單的哈希函數只要衝突可接受,性能會更好。如果你的數據表有很多行,而CRC32導致了過多的衝突,可以使用64位的哈希函數。確保這個哈希函數返回整數,而不是字符串。一種方式是隻使用MD5函數的部分返回結果,雖然和你自己寫的方法相比可能未必那麼高效,但是相差不會太多。

如何處理哈希衝突

如果你使用哈希值查找數據,你應該在WHERE語句同時包括哈希值對應的原值:

mysql> SELECT id FROM url WHERE url_crc=CRC32("http://www.mysql.com")
 -> AND url="http://www.mysql.com";

下面的語句有可能不會正常工作,這是因爲其他的行的URL也可能會擁有相同的CRC32值。

mysql> SELECT id FROM url WHERE url_crc=CRC32("http://www.mysql.com");

哈希衝突的增長速度會比你想像中的快,就像是潘多拉的盒子一樣難以控制。CRC32返回一個32位的整數,因此在93000個不同值中衝突的概率會達到1%。這就是爲什麼查詢的時候需要帶上原列進行查詢。爲了減少衝突,可以使用FNV64函數生成哈希值,它是64位的,速度很快,並且產生的衝突會比CRC32少很多。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章