一、樹型關系的數據表
不少程序員在進行數據庫設計的時候都遇到過樹型關系的數據,例如常見的類別表,即一個大類,下面有若干個子類,某些子類又有子類這樣的情況。當類別不確定,用戶希望可以在任意類別下添加新的子類,或者刪除某個類別和其下的所有子類,而且預計以后其數量會逐步增長,此時我們就會考慮用一個數據表來保存這些數據。
設計結構:
名稱 | 類型 | 約束條件 | 說明 |
type_id | int | 無重復 | 類別標識,主鍵 |
type_name | char(50) | 不允許為空 | 類型名稱,不允許重復 |
type_father | int | 不允許為空 | 該類別的父類別標識,如果是頂節點的話設定為某個唯一值 |
type_layer | char(6) | 限定3層,初始值為000000 | 類別的先序遍歷,主要為減少檢索數據庫的次數 |
這樣設計的好處就是遍歷方便,只需要一個檢索即可,通過設置type_layer即可設定遍歷順序,000000為3層,若要求多則可增加,每一層允許最多99個子類。010101表示為第三層。
檢索過程:SELECT * FROM Type_table_2 ORDER BY type_layer
列出記錄集如下:
type_id type_name type_father type_layer
1 總類別 0 000000
2 類別1 1 010000
3 類別1.1 2 010100
4 類別1.2 2 010200
5 類別2 1 020000
6 類別2.1 5 020100
7 類別3 1 030000
8 類別3.1 7 030100
9 類別3.2 7 030200
10 類別1.1.1 3 010101
……
二、商品信息表的設計(如何使數據表的屬性可擴展)
假設你是一家百貨公司電腦部的開發人員,某天老板要求你為公司開發一套網上電子商務平台,該百貨公司有數千種商品出售,不過目前僅打算先在網上銷售數十種方便運輸的商品,當然,以后可能會陸續在該電子商務平台上增加新的商品出售。現在開始進行該平台數據庫的商品信息表的設計。每種出售的商品都會有相同的屬性,如商品編號,商品名稱,商品所屬類別,相關信息,供貨廠商,內含件數,庫存,進貨價,銷售價,優惠價。
商品類型表(Wares_type)
名稱 | 類型 | 約束條件 | 說明 |
type_id | int | 無重復 | 類別標識,主鍵 |
type_name | char(50) | 不允許為空 | 類型名稱,不允許重復 |
type_father | int | 不允許為空 | 該類別的父類別標識,如果是頂節點的話設定為某個唯一值 |
type_layer | char(6) | 限定3層,初始值為000000 | 類別的先序遍歷,主要為減少檢索數據庫的次數 |
供貨廠商表(Wares_provider)
名稱 | 類型 | 約束條件 | 說明 |
provider_id | int | 無重復 | 供貨商標識,主鍵 |
provider_name | char(100) | 不允許為空 | 供貨商名稱 |
商品信息表(Wares_info)
名稱 | 類型 | 約束條件 | 說明 |
wares_id | int | 無重復 | 商品標識,主鍵 |
wares_name | char(100) | 不允許為空 | 商品名稱 |
wares_type | int | 不允許為空 | 商品類型標識,和Wares_type.type_id關聯 |
wares_info | char(200) | 允許為空 | 相關信息 |
provider | int | 不允許為空 | 供貨廠商標識,和Wares_provider.provider_id關聯 |
setnum | int | 初始值為1 | 內含件數,默認為1 |
stock | int | 初始值為0 | 庫存,默認為0 |
buy_price | money | 不允許為空 | 進貨價 |
sell_price | money | 不允許為空 | 銷售價 |
discount | money | 不允許為空 | 優惠價 |
你拿着這3個表給老板檢查,老板希望能夠再添加一個商品圖片的字段,不過只有一部分商品有圖片。OK,你在商品信息表(Wares_info)中增加了一個haspic的BOOL型字段,然后再建了一個新表——商品圖片表(Wares_pic):
商品圖片表(Wares_pic)
名稱 | 類型 | 約束條件 | 說明 |
pic_id | int | 無重復 | 商品圖片標識,主鍵 |
wares_id | int | 不允許為空 | 所屬商品標識,和Wares_info.wares_id關聯 |
pic_address | char(200) | 不允許為空 | 圖片存放路徑 |
程序開發完成后,完全滿足老板目前的要求,於是正式啟用。一段時間后,老板打算在這套平台上推出新的商品銷售,其中,某類商品全部都需添加“長度”的屬性。第一輪折騰來了……當然,你按照添加商品圖片表的老方法,在商品信息表(Wares_info)中增加了一個haslength的BOOL型字段,又建了一個新表——商品長度表(Wares_length):
商品長度表(Wares_length)
名稱 | 類型 | 約束條件 | 說明 |
length_id | int | 無重復 | 商品圖片標識,主鍵 |
wares_id | int | 不允許為空 | 所屬商品標識,和Wares_info.wares_id關聯 |
length | char(20) | 不允許為空 | 商品長度說明 |
剛剛改完沒多久,老板又打算上一批新的商品,這次某類商品全部需要添加“寬度”的屬性。你咬了咬牙,又照方抓葯,添加了商品寬度表(Wares_width)。又過了一段時間,老板新上的商品中有一些需要添加“高度”的屬性,你是不是開始覺得你所設計的數據庫按照這種方式增長下去,很快就能變成一個迷宮呢?那么,有沒有什么辦法遏制這種不可預見性,但卻類似重復的數據庫膨脹呢?我在閱讀《敏捷軟件開發:原則、模式與實踐》中發現作者舉過類似的例子:7.3 “Copy”程序。其中,我非常贊同敏捷軟件開發這個觀點:在最初幾乎不進行預先設計,但是一旦需求發生變化,此時作為一名追求卓越的程序員,應該從頭審查整個架構設計,在此次修改中設計出能夠滿足日后類似修改的系統架構。下面是我在需要添加“長度”的屬性時所提供的修改方案:
去掉商品信息表(Wares_info)中的haspic字段,添加商品額外屬性表(Wares_ex_property)和商品額外信息表(Wares_ex_info)2個表來完成添加新屬性的功能。
商品額外屬性表(Wares_ex_property)
名稱 | 類型 | 約束條件 | 說明 |
ex_pid | int | 無重復 | 商品額外屬性標識,主鍵 |
p_name | char(20) | 不允許為空 | 額外屬性名稱 |
商品額外信息表(Wares_ex_info)
名稱 | 類型 | 約束條件 | 說明 |
ex_iid | int | 無重復 | 商品額外信息標識,主鍵 |
wares_id | int | 不允許為空 | 所屬商品標識,和Wares_info.wares_id關聯 |
property_id | int | 不允許為空 | 商品額外屬性標識,和Wares_ex_property.ex_pid關聯 |
property_value | char(200) | 不允許為空 | 商品額外屬性值 |
在商品額外屬性表(Wares_ex_property)中添加2條記錄:
ex_pid p_name
1 商品圖片
2 商品長度
再在整個電子商務平台的后台管理功能中追加一項商品額外屬性管理的功能,以后添加新的商品時出現新的屬性,只需利用該功能往商品額外屬性表(Wares_ex_property)中添加一條記錄即可。
三、多用戶及其權限管理的設計
要求:該數據庫管理軟件的系統管理員可以自行添加新用戶,修改已有用戶的權限,刪除已有用戶。
首先,分析用戶需求,列出該數據庫管理軟件所有需要實現的功能;然后,根據一定的聯系對這些功能進行分類,即把某類用戶需使用的功能歸為一類;
功能表(Function_table)
名稱 | 類型 | 約束條件 | 說明 |
f_id | int | 無重復 | 功能標識,主鍵 |
f_name | char(20) | 不允許為空 | 功能名稱,不允許重復 |
f_desc | char(50) | 允許為空 | 功能描述 |
用戶組表(User_group)
名稱 | 類型 | 約束條件 | 說明 |
group_id | int | 無重復 | 用戶組標識,主鍵 |
group_name | char(20) | 不允許為空 | 用戶組名稱 |
group_power | char(100) | 不允許為空 | 用戶組權限表,內容為功能表f_id的集合 |
用戶表(User_table)
名稱 | 類型 | 約束條件 | 說明 |
user_id | int | 無重復 | 用戶標識,主鍵 |
user_name | char(20) | 無重復 | 用戶名 |
user_pwd | char(20) | 不允許為空 | 用戶密碼 |
user_type | int | 不允許為空 | 所屬用戶組標識,和User_group.group_id關聯 |
采用這種用戶組的架構設計,當需要添加新用戶時,只需指定新用戶所屬的用戶組;當以后系統需要添加新功能或對舊有功能權限進行修改時,只用操作功能表和用戶組表的記錄,原有用戶的功能即可相應隨之變化。
四、簡潔的批量m:n設計
碰到m:n的關系,一般都是建立3個表,m一個,n一個,m:n一個。但是,m:n有時會遇到批量處理的情況,例如到圖書館借書,一般都是允許用戶同時借閱n本書,如果要求按批查詢借閱記錄,即列出某個用戶某次借閱的所有書籍,該如何設計呢?讓我們建好必須的3個表先:
書籍表(Book_table)
名稱 | 類型 | 約束條件 | 說明 |
book_id | int | 無重復 | 書籍標識,主鍵 |
book_no | char(20) | 無重復 | 書籍編號 |
book_name | char(100) | 不允許為空 | 書籍名稱 |
…… |
借閱用戶表(Renter_table)
名稱 | 類型 | 約束條件 | 說明 |
renter_id | int | 無重復 | 用戶標識,主鍵 |
renter_name | char(20) | 不允許為空 | 用戶姓名 |
…… |
借閱記錄表(Rent_log)
名稱 | 類型 | 約束條件 | 說明 |
rent_id | int | 無重復 | 借閱記錄標識,主鍵 |
r_id | int | 不允許為空 | 用戶標識,和Renter_table.renter_id關聯 |
b_id | int | 不允許為空 | 書籍標識,和Book_table.book_id關聯 |
batch_no | int | 不允許為空 | 批量借閱編號,同一批借閱的batch_no相同 |
rent_date | datetime | 不允許為空 | 借閱時間 |
…… |
其中,同一次借閱的batch_no和該批第一條入庫的rent_id相同。舉例:假設當前最大rent_id是64,接着某用戶一次借閱了3本書,則批量插入的3條借閱記錄的batch_no都是65。之后另外一個用戶租了一套碟,再插入出租記錄的rent_id是68。采用這種設計,查詢批量借閱的信息時,只需使用一條標准T_SQL的嵌套查詢即可。
五、冗余數據的取舍
我原先所在的公司為了解決員工的工作餐,和附近的一家小餐館聯系,每天吃飯記賬,費用按人數平攤,月底由公司現金結算,每個人每個月的工作餐費從工資中扣除。當然,每天吃飯的人員和人數都不是固定的,而且,由於每頓工作餐的所點的菜色不同,每頓的花費也不相同。例如,星期一中餐5人花費40元,晚餐2人花費20,星期二中餐6人花費36元,晚餐3人花費18元。為了方便計算每個人每個月的工作餐費,我寫了一個簡陋的就餐記賬管理程序,數據庫里有3個表:
員工表(Clerk_table)
名稱 | 類型 | 約束條件 | 說明 |
clerk_id | int | 無重復 | 員工標識,主鍵 |
clerk_name | char(10) | 不允許為空 | 員工姓名 |
每餐總表(Eatdata1)
名稱 | 類型 | 約束條件 | 說明 |
totle_id | int | 無重復 | 每餐總表標識,主鍵 |
persons | char(100) | 不允許為空 | 就餐員工的員工標識集合 |
eat_date | datetime | 不允許為空 | 就餐日期 |
eat_type | char(1) | 不允許為空 | 就餐類型,用來區分中、晚餐 |
totle_price | money | 不允許為空 | 每餐總花費 |
persons_num | int | 不允許為空 | 就餐人數 |
就餐計費細表(Eatdata2)
名稱 | 類型 | 約束條件 | 說明 |
id | int | 無重復 | 就餐計費細表標識,主鍵 |
t_id | int | 不允許為空 | 每餐總表標識,和Eatdata1.totle_id關聯 |
c_id | int | 不允許為空 | 員工標識標識,和Clerk_table.clerk_id關聯 |
price | money | 不允許為空 | 每人每餐花費 |
其中,就餐計費細表(Eatdata2)的記錄就是把每餐總表(Eatdata1)的一條記錄按就餐員工平攤拆開,是個不折不扣的冗余表。當然,也可以把每餐總表(Eatdata1)的部分字段合並到就餐計費細表(Eatdata2)中,這樣每餐總表(Eatdata1)就成了冗余表,不過這樣所設計出來的就餐計費細表重復數據更多,相比來說還是上面的方案好些。但是,就是就餐計費細表(Eatdata2)這個冗余表,在做每月每人餐費統計的時候,大大簡化了編程的復雜度,只用類似這么一條查詢語句即可統計出每人每月的寄餐次數和餐費總帳:
SELECT clerk_name AS personname,COUNT(c_id) as eattimes,SUM(price) AS ptprice FROM Eatdata2 JOIN Clerk_tabsle ON (c_id=clerk_id) JOIN eatdata1 ON (totleid=tid) WHERE eat_date>=CONVERT(datetime,'"&the_date&"') AND eat_date<DATEADD(month,1,CONVERT(datetime,'"&the_date&"')) GROUP BY c_id
想象一下,如果不用這個冗余表,每次統計每人每月的餐費總帳時會多麻煩,程序效率也夠嗆。那么,到底什么時候可以增加一定的冗余數據呢?我認為有2個原則:
1、用戶的整體需求。當用戶更多的關注於,對數據庫的規范記錄按一定的算法進行處理后,再列出的數據。如果該算法可以直接利用后台數據庫系統的內嵌函數來完成,此時可以適當的增加冗余字段,甚至冗余表來保存這些經過算法處理后的數據。要知道,對於大批量數據的查詢,修改或刪除,后台數據庫系統的效率遠遠高於我們自己編寫的代碼。
2、簡化開發的復雜度。現代軟件開發,實現同樣的功能,方法有很多。盡管不必要求程序員精通絕大部分的開發工具和平台,但是還是需要了解哪種方法搭配哪種開發工具的程序更簡潔,效率更高一些。冗余數據的本質就是用空間換時間,尤其是目前硬件的發展遠遠高於軟件,所以適當的冗余是可以接受的。不過我還是在最后再強調一下:不要過多的依賴平台和開發工具的特性來簡化開發,這個度要是沒把握好的話,后期維護升級會栽大跟頭的。
參考:https://www.cnblogs.com/hoojjack/p/4705830.html