在關系型資料庫中,資料結構有邏輯結構和實體結構。實體結構指存儲在實體媒體上的資料檔案的結構。邏輯結構即關系,也就是一張張的二維表。表中的一列即為一個字段(屬性),代表的是實體的一個屬性。表中的一行即為一條記錄。
如:學生表中(學号,姓名,年齡,性别),在該表中有4個字段,代表學生實體的4個屬性。表中的一行資料(001,張三,男,20),即一條記錄,表示的是張三這個學生的資訊。
在表中,用來唯一辨別一條記錄的字段集,叫做主關鍵字或者主關鍵碼,簡稱主鍵(主碼),而主鍵包含的屬性(字段)叫做主屬性,其他的則為非主屬性。
在這裡說明一下,既然是“字段集”,那麼主鍵就可能是一個字段或者多個字段。例如上面的學生表,學号就是能唯一辨別一個學生的字段,學号字段就是該表的主鍵。由于學生的姓名重名是不可避免的,是以一般不用姓名來作為唯一辨別一個學生的主鍵。
再舉個例子說明一下多個字段作為主鍵。例如學生選課表(學号,課程号,成績),在學生選課表中,主關鍵字是(學号,課程号)。原因顯而易見,一個學生可以選擇多門課程,一個課程可以被多個學生選擇。如果單單是用學号或者課程号,都不足以唯一辨別一條記錄。
對于外鍵的了解,在這裡我也舉個例子。假設有兩個表,學生表(學号,姓名,年齡,性别,專業編号),專業資訊表(專業編号,專業名稱,專業備注資訊)。學生表中主鍵是學号,專業資訊表中主鍵是專業編号。學生表中的非主屬性專業編号恰好是專業資訊表中的主鍵。我們就稱這個專業編号是學生表的外鍵。像這樣,一個表的非主屬性是另一個表的主屬性,該非主屬性就是外鍵。
資料庫的限制有三種,實體完整性限制、參照完整性限制和使用者自定義限制。
1.實體完整性限制指的是主鍵不能為空,如果主鍵為空了還怎麼唯一辨別一條記錄。
2.參照完整性限制,即外鍵的限制,某一外鍵的值必須在它引用的主鍵字段中存在。如,學生表中專業編号屬性的值,必須都存于專業資訊表中的專業編号屬性中。想一想也就明白了,一個學生(大學生)怎麼可能屬于一個不存在的專業。
3.使用者自定義完整性限制,指的是一些使用者自己設定的限制,例如字段是否可以為空,字段值的取值範圍(如:人的性别隻能取男、女)。
---------------------
定義主鍵和外鍵主要是為了維護關系資料庫的完整性,總結一下:
1.主鍵是能确定一條記錄的唯一辨別,比如,一條記錄包括身份正号,姓名,年齡。
身份證号是唯一能确定你這個人的,其他都可能有重複,是以,身份證号是主鍵。
2.外鍵用于與另一張表的關聯。是能确定另一張表記錄的字段,用于保持資料的一緻性。
比如,a表中的一個字段,是b表的主鍵,那他就可以是a表的外鍵。
聚集索引和非聚集索引的差別?
聚集索引一定是唯一索引。但唯一索引不一定是聚集索引。
聚集索引,在索引頁裡直接存放資料,而非聚集索引在索引頁裡存放的是索引,這些索引指向專門的資料頁的資料。
三、資料庫中主鍵和外鍵的設計原則
主鍵和外鍵是把多個表組織為一個有效的關系資料庫的粘合劑。主鍵和外鍵的設計對實體資料庫的性能和可用性都有着決定性的影響。
必須将資料庫模式從理論上的邏輯設計轉換為實際的實體設計。而主鍵和外鍵的結構是這個設計過程的症結所在。一旦将所設計的資料庫用于了生産環境,就很難對這些鍵進行修改,是以在開發階段就設計好主鍵和外鍵就是非常必要和值得的。
主鍵:
關系資料庫依賴于主鍵---它是資料庫實體模式的基石。
主鍵在實體層面上隻有兩個用途:
1. 惟一地辨別一行。
2. 作為一個可以被外鍵有效引用的對象。
基于以上這兩個用途,下面給出了我在設計實體層面的主鍵時所遵循的一些原則:
1. 主鍵應當是對使用者沒有意義的。如果使用者看到了一個表示多對多關系的連接配接表中的資料,并抱怨它沒有什麼用處,那就證明它的主鍵設計地很好。
2. 主鍵應該是單列的,以便提高連接配接和篩選操作的效率。
注:使用複合鍵的人通常有兩個理由為自己開脫,而這兩個理由都是錯誤的。其一是主鍵應當具有實際意義,然而,讓主鍵具有意義隻不過是給人為地破壞資料庫提供了友善。其二是利用這種方法可以在描述多對多關系的連接配接表中使用兩個外部鍵來作為主鍵,我也反對這種做法,理由是:複合主鍵常常導緻不良的外鍵,即當連接配接表成為另一個從表的主表,而依據上面的第二種方法成為這個表主鍵的一部分,然,這個表又有可能再成為其它從表的主表,其主鍵又有可能成了其它從表主鍵的一部分,如此傳遞下去,越靠後的從表,其主鍵将會包含越多的列了。
3. 永遠也不要更新主鍵。實際上,因為主鍵除了惟一地辨別一行之外,再沒有其他的用途了,是以也就沒有理由去對它更新。如果主鍵需要更新,則說明主鍵應對使用者無意義的原則被違反了。
注:這項原則對于那些經常需要在資料轉換或多資料庫合并時進行資料整理的資料并不适用。
4. 主鍵不應包含動态變化的資料,如時間戳、建立時間列、修改時間列等。
5. 主鍵應當有計算機自動生成。如果由人來對主鍵的建立進行幹預,就會使它帶有除了惟一辨別一行以外的意義。一旦越過這個界限,就可能産生認為修改主鍵的動機,這樣,這種系統用來連結記錄行、管理記錄行的關鍵手段就會落入不了解資料庫設計的人的手中。
四、資料庫主鍵選取政策
我們在建立資料庫的時候,需要為每張表指定一個主鍵,所謂主鍵就是能夠唯一辨別表中某一行的屬性或屬性組,一個表隻能有一個主鍵,但可以有多個候選索引。因為主鍵可以唯一辨別某一行記錄,是以可以確定執行資料更新、删除的時候不會出現張冠李戴的錯誤。當然,其它字段可以輔助我們在執行這些操作時消除共享沖突,不過就不在這裡讨論了。主鍵除了上述作用外,常常與外鍵構成參照完整性限制,防止出現資料不一緻。是以資料庫在設計時,主鍵起到了很重要的作用。
常見的資料庫主鍵選取方式有:
· 自動增長字段
· 手動增長字段
· uniqueidentifier
· “comb(combine)”類型
1自動增長型字段
很多資料庫設計者喜歡使用自動增長型字段,因為它使用簡單。自動增長型字段允許我們在向資料庫添加資料時,不考慮主鍵的取值,記錄插入後,資料庫系統會自動為其配置設定一個值,確定絕對不會出現重複。如果使用sql server資料庫的話,我們還可以在記錄插入後使用@@identity全局變量擷取系統配置設定的主鍵鍵值。
盡管自動增長型字段會省掉我們很多繁瑣的工作,但使用它也存在潛在的問題,那就是在資料緩沖模式下,很難預先填寫主鍵與外鍵的值。假設有兩張表:
order(orderid, orderdate)
orderdetial(orderid, linenum, productid, price)
order表中的orderid是自動增長型的字段。現在需要我們錄入一張訂單,包括在order表中插入一條記錄以及在orderdetail表中插入若幹條記錄。因為order表中的orderid是自動增長型的字段,那麼我們在記錄正式插入到資料庫之前無法事先得知它的取值,隻有在更新後才能知道資料庫為它配置設定的是什麼值。這會造成以下沖突發生:
首先,為了能在orderdetail的orderid字段中添入正确的值,必須先更新order表以擷取到系統為其配置設定的orderid值,然後再用這個orderid填充orderdetail表。最後更新oderdetail表。但是,為了確定資料的一緻性,order與orderdetail在更新時必須在事務保護下同時進行,即確定兩表同時更行成功。顯然它們是互相沖突的。
除此之外,當我們需要在多個資料庫間進行資料的複制時(sql server的資料分發、訂閱機制允許我們進行庫間的資料複制操作),自動增長型字段可能造成資料合并時的主鍵沖突。設想一個資料庫中的order表向另一個庫中的order表複制資料庫時,orderid到底該不該自動增長呢?
ado.net允許我們在dataset中将某一個字段設定為自動增長型字段,但千萬記住,這個自動增長字段僅僅是個占位符而已,當資料庫進行更新時,資料庫生成的值會自動取代ado.net配置設定的值。是以為了防止使用者産生誤解,建議大家将ado.net中的自動增長初始值以及增量都設定成-1。此外,在ado.net中,我們可以為兩張表建立datarelation,這樣存在級聯關系的兩張表更新時,一張表更新後另外一張表對應鍵的值也會自動發生變化,這會大大減少了我們對存在級聯關系的兩表間更新時自動增長型字段帶來的麻煩。
2手動增長型字段
既然自動增長型字段會帶來如此的麻煩,我們不妨考慮使用手動增長型的字段,也就是說主鍵的值需要自己維護,通常情況下需要建立一張單獨的表存儲目前主鍵鍵值。還用上面的例子來說,這次我們建立一張表叫intkey,包含兩個字段,keyname以及keyvalue。就像一個hashtable,給一個keyname,就可以知道目前的keyvalue是什麼,然後手工實作鍵值資料遞增。在sql server中可以編寫這樣一個存儲過程,讓取鍵值的過程自動進行。代碼如下:
create procedure [getkey]
@keyname char(10),
@keyvalue int output
as
update intkey set @keyvalue = keyvalue = keyvalue + 1 where keyname = @keyname
go
這樣,通過調用存儲過程,我們可以獲得最新鍵值,確定不會出現重複。若将orderid字段設定為手動增長型字段,我們的程式可以由以下幾步來實作:首先調用存儲過程,獲得一個orderid,然後使用這個orderid填充order表與orderdetail表,最後在事務保護下對兩表進行更新。
使用手動增長型字段作為主鍵在進行資料庫間資料複制時,可以確定資料合并過程中不會出現鍵值沖突,隻要我們為不同的資料庫配置設定不同的主鍵取值段就行了。但是,使用手動增長型字段會增加網絡的roundtrip,我們必須通過增加一次資料庫通路來擷取目前主鍵鍵值,這會增加網絡和資料庫的負載,當處于一個低速或斷開的網絡環境中時,這種做法會有很大的弊端。同時,手工維護主鍵還要考慮并發沖突等種種因素,這更會增加系統的複雜程度。
3使用uniqueidentifier
sql server為我們提供了uniqueidentifier資料類型,并提供了一個生成函數newid( ),使用newid( )可以生成一個唯一的uniqueidentifier。uniqueidentifier在資料庫中占用16個位元組,出現重複的機率非常小,以至于可以認為是0。我們經常從系統資料庫中看到類似
{45f0eb02-0727-4f2e-aab5-e8aedee0cec5}
的東西實際上就是一個uniqueidentifier,windows用它來做com元件以及接口的辨別,防止出現重複。在.net裡管uniqueidentifier稱之為guid(global unique identifier)。在c#中可以使用如下指令生成一個guid:
guid u = system.guid.newguid();
對于上面提到的order與orderdetail的程式,如果選用uniqueidentifier作為主鍵的話,我們完全可以避免上面提到的增加網絡roundtrip的問題。通過程式直接生成guid填充主鍵,不用考慮是否會出現重複。
uniqueidentifier字段也存在嚴重的缺陷:首先,它的長度是16位元組,是整數的4倍長,會占用大量存儲空間。更為嚴重的是,uniqueidentifier的生成毫無規律可言,要想在上面建立索引(絕大多數資料庫在主鍵上都有索引)是一個非常耗時的操作。有人做過實驗,插入同樣的資料量,使用uniqueidentifier型資料做主鍵要比使用integer型資料慢,是以,出于效率考慮,盡可能避免使用uniqueidentifier型資料庫作為主鍵鍵值。
4使用“comb(combine)”類型
既然上面三種主鍵類型選取政策都存在各自的缺點,那麼到底有沒有好的辦法加以解決呢?答案是肯定的。通過使用comb類型(資料庫中沒有comb類型,它是jimmy nilsson在他的“the cost of guids as primary keys”一文中設計出來的),可以在三者之間找到一個很好的平衡點。
comb資料類型的基本設計思路是這樣的:既然uniqueidentifier資料因毫無規律可言造成索引效率低下,影響了系統的性能,那麼我們能不能通過組合的方式,保留uniqueidentifier的前10個位元組,用後6個位元組表示guid生成的時間(datetime),這樣我們将時間資訊與uniqueidentifier組合起來,在保留uniqueidentifier的唯一性的同時增加了有序性,以此來提高索引效率。也許有人會擔心uniqueidentifier減少到10位元組會造成資料出現重複,其實不用擔心,後6位元組的時間精度可以達到1/300秒,兩個comb類型資料完全相同的可能性是在這1/300秒内生成的兩個guid前10個位元組完全相同,這幾乎是不可能的!在sql server中用sql指令将這一思路實作出來便是:
declare @aguid uniqueidentifier
set @aguid = cast(cast(newid() as binary(10))
+ cast(getdate() as binary(6)) as uniqueidentifier)
經過測試,使用comb做主鍵比使用int做主鍵,在檢索、插入、更新、删除等操作上仍然顯慢,但比unidentifier類型要快上一些。關于測試資料可以參考我2004年7月21日的随筆。
除了使用存儲過程實作comb資料外,我們也可以使用c#生成comb資料,這樣所有主鍵生成工作可以在用戶端完成。c#代碼如下:
//================================================================
///
/// 傳回 guid 用于資料庫操作,特定的時間代碼可以提高檢索效率
/// comb (guid 與時間混合型) 類型 guid 資料
public static guid newcomb()
{
byte[] guidarray = system.guid.newguid().tobytearray();
datetime basedate = new datetime(1900,1,1);
datetime now = datetime.now;
// get the days and milliseconds which will be used to build the byte string
timespan days = new timespan(now.ticks - basedate.ticks);
timespan msecs = new timespan(now.ticks - (new datetime(now.year, now.month, now.day).ticks));
// convert to a byte array
// note that sql server is accurate to 1/300th of a millisecond so we pide by 3.333333
byte[] daysarray = bitconverter.getbytes(days.days);
byte[] msecsarray = bitconverter.getbytes((long)(msecs.totalmilliseconds/3.333333));
// reverse the bytes to match sql servers ordering
array.reverse(daysarray);
array.reverse(msecsarray);
// copy the bytes into the guid
array.copy(daysarray, daysarray.length - 2, guidarray, guidarray.length - 6, 2);
array.copy(msecsarray, msecsarray.length - 4, guidarray, guidarray.length - 4, 4);
return new system.guid(guidarray);
}
/// 從 sql server 傳回的 guid 中生成時間資訊
///包含時間資訊的 comb
/// 時間
public static datetime getdatefromcomb(system.guid guid)
byte[] daysarray = new byte[4];
byte[] msecsarray = new byte[4];
byte[] guidarray = guid.tobytearray();
// copy the date parts of the guid to the respective byte arrays.
array.copy(guidarray, guidarray.length - 6, daysarray, 2, 2);
array.copy(guidarray, guidarray.length - 4, msecsarray, 0, 4);
// reverse the arrays to put them into the appropriate order
// convert the bytes to ints
int days = bitconverter.toint32(daysarray, 0);
int msecs = bitconverter.toint32(msecsarray, 0);
datetime date = basedate.adddays(days);
date = date.addmilliseconds(msecs * 3.333333);
return date;