數據庫哈希設計

1 用哈希的key代替字符串上的索引,提高查詢效率。

索引時一種最爲常見的提高查詢效率的方式,但在長字符串字段上的索引效果較差,如果多個這樣的字段上的聯合索引則效果更差。例如,

column namedata type
name1 varchar(50)
name2 varchar(100)
other column varchar(32)

如果經常需用name1和name2上查詢,比如經常執行

select * from table where name1=? and name2=?
這種情況就需要在這兩個字段上加索引,
create index myindex on table1 (name1,name2)

 

索引提高性能,但當數據量巨大時比如上億,查詢性能就會降低的很快,而且索引也佔用存儲空間,超大數據量也會使索引佔用空間膨脹。比如在name1和 name2上建立的聯合索引一行就要使用最少150個字符的空間,數據量大了後空間的蠶食可想而知。據一篇文章說“建立索引,系統要佔用大約爲表的1.2倍的硬盤和內存空間來保存索引”

因此,我們來使用hash key

1.1 什麼是hash

簡單說,就是給個字符串,通過一種算法,返回一個整數。返回的整數和給出的字符串就相當於有了一種對應關係。哈希算法有很多種,詳見這裏 http://en.wikipedia.org/wiki/Hash_function#Hash_function_algorithms

SQLSever中提供了checksum()函數來產生hash key.例如:

select checksum('abc','Tom and Jack')
返回:1094199073
在SQLServer2005上測試結果(以下同)

 

用這種哈希函數,你就可以把長字符串列映射到一個哈希值上來,這個值是唯一的,如果改動字符串的內容,哪怕一個字母,新的哈希值也會不同。實際上,你還可以映射非字符串列,因爲哈希算法本質上是對任意長度二進制的映射算法,一切數據類型都可以用二進制來表示。

需要注意的是,實際應用中,有可能出現兩段不同的二進制分別經過哈希運算,竟然生成同一個哈希key,這種機率不大,這稱爲hash collision.它的出現原因也不復雜,主要是由於實際應用中的哈希輸出一般都有固定的最大長度限制,超過此長度的哈希輸出可能會被進行剪斷等操作。這樣就有可能本來不應該相同的哈希key經過剪斷變成相等的了。具體可參看相關資料,這裏不詳述,但我們需要當心實際中的哈希值不一定唯一這一點。

1.2 如何在數據庫設計中使用hash

讓我們繼續開始的那個表。當數據量爆多時,建立在name1和name2字段上的索引就有性能問題了。那麼根據hash原理,我們對這個表增加一列哈希列,然後對這一列增加索引,而不是對name1和name2增加索引,成爲這樣,

column namedata type
name1 varchar(50)
name2 varchar(100)
other column varchar(32)
hashkey integer
create index hashkeyindex on table(hashkey)
hashkey的內容是name1和name2的哈希key,可以通過數據庫的checksum來實現。
update table set hashkey=checksum(name1,name)

上面這條語句就會給所有的哈希列賦值。當然更好的辦法是建立觸發器,每次在插入一條新的記錄的時候都對哈希列通過checksum(name1,name2)計算而賦值。

這樣,本來建立在兩個字段上的索引變成了建立在一個字段上的索引,更重要的是hashkey只有幾個byte的大小,比起原來name1+name2=150個字符的大小來說,索引的存儲量大大降低了。其實,索引本身也需要一定的搜索,而索引本身越小,搜索一定也就越快。以按名稱查詢爲例,有了哈希列後就可以這麼查詢

select * from table where hashkey=checksum('張三','地方是飛啊飛鵝的')

由於前面我們提到的哈希key的不一定唯一性,即多個輸入可能會被運算成同一個輸出(即哈希key),反過來說,一個哈希key有可能對應多條記錄,那麼上面的這個查詢就可能查出不止一條記錄,怎麼做呢?很簡單

select * from table where hashkey=checksum('張三','地方是飛啊飛鵝的') and name1='張三' and name2='地方是飛啊飛鵝的'

where子句中的哈希key的條件已經使查詢大大的減少了返回的行數,再通過真實條件過濾,很快就能查到你真正想要的。這種做法有人會覺得不錯,但畢竟在數據庫中增加了一列,而且還要寫個觸發器每次往新增列裏更新哈希值。有些麻煩。那這裏就可以利用計算列來解決這樣的問題。

只是,爲什麼數據庫不直接在聯合索引中存放哈希值呢?使用者不就更省事了嗎?

1.3 使用計算列

什麼是計算列?就是通過計算而得來的列,並非真實存在列(但可以使用 PERSISTED關鍵字把計算列實際存放在表中)。比如第三列是前兩列的和,那麼第三列就可設計成計算列,每次查詢出的第三列的值實際上是通過計算前兩列之和而得出的。這種設計可減少額外存儲,減小DB size.

在哈希設計中,就可以創建一個哈希計算列,然後對該哈希計算列創建索引,這樣它就不需要真正的存放在表裏,實現了只是用來提高查詢效率的哈希列與業務表完全分離。

對計算列創建索引需要一些條件,比如計算列的表達式必須有確定性、精確、不能用字符串列等,而哈希key都容易滿足。

CREATE TABLE mytable(
    name1 varchar(50),
    name2 varchar(150),
    myhashkey AS checksum(name1,name2)
)
Go
CREATE INDEX hashkeyIndex ON mytable(myhashkey)

1.4 如何在多表關聯中使用哈希

多表關聯中可以使用hash join,是否使用hash join一般是有數據庫系統自己決定。也可以自己手工指定,手工指定方式如下

SELECT a.au_id FROM A a JOIN B b
ON a.name = b.name OPTION (HASH JOIN)

2 庫表散列

當數據庫表大的一定程度的時候,比如大到上億或幾百億的記錄,查詢的性能就會驟降,即使正確的使用索引也無濟於事。這個時候就需要使用庫表散列的方式來進行處理,實質上就是對錶進行拆分,然後按照一定的策略對拆分的表進行查詢和其它處理。這裏我主要講哈希策略。

哈希策略就是通過哈希計算,直接定位表。例如:用戶表,數據量巨大,需要拆分。以用戶的id爲哈希計算的關鍵字,比如userid 是32位的字符串. SDDKSL3KDKSLDKE2FKQKSLEJF9ELAPEK. 最簡單的哈希策略是用userid左邊第一位的字母作爲新表明的後綴。這樣,就有了 USERS表。26個字母+10個阿拉伯數字,這樣就可以把用戶表拆分成36個表。如果查詢 userid=UDKLSK3DKSLKDE2FKQSKLEJ9FELAPEKC的記錄,可以馬上定位到USERU表 上去。這只是舉個簡單的例子來說明庫表散列,實際中往往用用戶名來查,比如根據張三,李四來查,這需要數據庫迅速定位張三、李四分別在哪張表中。爲了使多個用戶均勻的分佈在不同的表中,需要一個比較好的哈希函數,我們就不自己設計了,就用oracle的orahash函數來舉例。

2.1 ORAHASH 函數

 

函數的用法:
select orahash('張三',100,0) from dual

 

結果:79

 

含義:oracle的哈希函數計算出'張三'的哈希值,然後把它放進79號桶中。 100意味着最多隻能允許有100個桶。0是種子,用來計算哈希值和安排不同的桶。返回值就是分配的桶號。所謂桶,無非就是組,即把不同的哈希值均勻的放進不同的組裏。

 

標準描述:orahash(exp,maxbucket,seedvalue)

 

2.2 哈希拆分例1

有了這個現成的函數,hash拆表就比較簡單了。如果你想拆成1000個表,那只需要把maxbucket設置成1000即可。

首先,要建1000個user表,分別是 user000,user001,user002,user003….user999. 用戶插入的時候,先根據用戶名計算它要插入那個表。比如用戶名是:'王老虎'.

ora_hash('王老虎',1000,0)的結果是159,那麼就需要把該條記錄插入到 user159表中。

insert into user159 (name) values ('王老虎') –實際中還有別的字段
查詢的時候,要先定位表,比如查找用戶名爲 '王老虎', id='SDFEW23SDF' 的記錄,同理先通過orahash函數計算,得到159,然後就執行:

 

select * from user159 where name='王老虎' and id='SDFEW23SDF'

其它數據庫沒有orahash這樣的函數,所以要自己設計或借用其它的散列函數。 實際上orahash函數可能被oracle的哈希分區(hash partition)功能使用。而 hash partition就是oracle的一種把大表數據分而治之的方式。即把一個大表劃分成好多個分區。所以在oracle數據庫設計中,也就不需要把一個達標拆分成多個小表,而是直接把一個大表拆分成多個分區,然後讓oracle自動去管理就行了。但如果做分佈式數據庫,那麼很多工作還是需要自己來做。

2.3 哈希拆分例2

還有一種方式是在每次插入數據的時候才進行判斷是否新建hash表。比如有一個叫王剛的記錄要被插入,僞碼如下:

String userHashName = yourHashFun('王剛');
String tableName = "user_"+userHashName;
if (!exists(tableName)){
    create table...
}
insert into ...

這樣做,由於需要做表是否已經存在的判斷,所以有一定的性能損耗。

2.4 除留餘數法

orahash函數好,但屬於oracle專有,離開了oracle,有時我們需要設計自己的hash函數。除留餘數法簡單易用。

H(k)=k mod p  p<=m

如果userid是數字,則可直接對userid進行此法。比如想要拆分10個表,就取p=10. 比如userid=11,11/10餘1,那麼就把userid=11的記錄分配到表 user1。如userid=23,23/10餘3,那麼把userid=23的記錄分配到表 user3. 除留餘數法的好處就是1,簡單;2,能相對均勻的把數據分配到不同的子表中去(前提是userid本身比較均勻)。

如果userid不是數字,那麼就先對userid進行hash計算(你可以使用現成的 hash計算,比如md5,也可以自己設計),得到數字值,然後再對數字值進行除留餘數法。是否均勻,也取決於hash計算的結果是否均勻。

它有一個缺點,原來拆了10個表,系統運行一段後需要拆成50個表會不容易實現。

2.5 拆庫

除了如前所述的拆表操作,還可以進行拆庫操作。就是把一個大表拆分成多個子表放入不同數據庫中。例如user表中的數據可以分別放入5個DB中,分別是 DB1,DB2,DB3,DB4,DB5,每個DB中都有一個user表。5個DB中的user記錄加起來就是所有user的記錄。hash拆庫與拆表方式相同。明白了拆表,拆庫也就明白了。所不同的是,定位庫和定位表方式有差異。庫有可能分佈在不同的機器上,所以需要多個數據源。

下面以一段示例性代碼說明這種情況

public void test(){
    String userid = "123456";
    String hashvalue = hash(userid);
    String db_connection = getConnection(hashvalue);
}
//用戶自己可以實現這個函數(這個函數可以做的很複雜,數據源可以做成動態的)
public abstract Connection getConnection(String db_mark);

//比如下面的一個實現
public Connection getConnection(String db_mark){
    String username="admin";
    String password="admin";
    String ip = "localhost";
    String dbname = "db"+db_mark; //拼湊db name. 
    Class.forName(driveString).newInstance();
    conn = DriverManager.getConnection("jdbc:inetdae7:localhost:1433", username, password);
    conn.setCatalog(dbname);
    return conn;
}

不關怎麼實現,只要你能想辦法找到你要定位的數據庫就行。這個過程就是 DB路由。路由策略根據實際業務情況即可以做的很簡單,也可以做的功能強大、複雜無比(因爲要負載均衡、要讀寫分離、要便於統計、要個摘要表、要個緩衝表,要這個要那個,元素越來越多,設計就越來越複雜)。

Hash就是把一大堆亂七八糟的東西用一個小小的東西(一段數字)來代表。這種一大一小的對應的關係極大的縮減了運算空間,所以極大的提高了運算效率。這可以看作是種壓縮,看做一種以小御大(想象一下你的食指一動,對面的五指山的食指也一動,多麼壯觀)。hash後的key們,就如同是對一個龐大世界的精簡映像。這正好適合於成萬上億的大數據量的處理。而數據庫中的這種被代表現象無處不在。

  1. id. 每個表都有一個id或類似id的唯一標識。代表某一行數據,縮減運算空間。對id的各種運算要比對整條記錄的數據進行運算快的多。
  2. 索引。縮減查詢運算空間。
  3. Hash.可以看做你自己實現的能跨越多表多庫德索引,縮減查詢運算空間。

    以小御大,運用之妙,存乎一心。

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