1. 程式人生 > >資料庫優化設計(非常實用)

資料庫優化設計(非常實用)

一、樹型關係的資料表

       不少程式設計師在進行資料庫設計的時候都遇到過樹型關係的資料,例如常見的類別表,即一個大類,下面有若干個子類,某些子類又有子類這樣的情況。當類別不確定,使用者希望可以在任意類別下新增新的子類,或者刪除某個類別和其下的所有子類,而且預計以後其數量會逐步增長,此時我們就會考慮用一個數據表來儲存這些資料。按照教科書上的教導,第二類程式設計師大概會設計出類似這樣的資料表結構:

類別表_1(Type_table_1)

這裡寫圖片描述

這樣的設計短小精悍,完全滿足3NF,而且可以滿足使用者的所有要求。是不是這樣就行呢?答案是NO!Why?

我們來估計一下使用者希望如何羅列出這個表的資料的。對使用者而言,他當然期望按他所設定的層次關係一次羅列出所有的類別,例如這樣:  總類別    類別1      類別1.1        類別1.1.1      類別1.2    類別2      類別2.1    類別3      類別3.1      類別3.2    ……

看看為了實現這樣的列表顯示(樹的先序遍歷),要對上面的表進行多少次檢索?注意,儘管類別1.1.1可能是在類別3.2之後新增的記錄,答案仍然是N次。這樣的效率對於少量的資料沒什麼影響,但是日後型別擴充到數十條甚至上百條記錄後,單單列一次型別就要檢索數十次該表,整個程式的執行效率就不敢恭維了。或許第二類程式設計師會說,那我再建一個臨時陣列或臨時表,專門儲存型別表的先序遍歷結果,這樣只在第一次執行時檢索數十次,再次羅列所有的型別關係時就直接讀那個臨時陣列或臨時表就行了。其實,用不著再去分配一塊新的記憶體來儲存這些資料,只要對資料表進行一定的擴充,再對新增型別的數量進行一下約束就行了,要完成上面的列表只需一次檢索就行了。下面是擴充後的資料表結構:

類別表_2(Type_table_2)

這裡寫圖片描述

按照這樣的表結構,我們來看看上面例子記錄在表中的資料是怎樣的:

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  ……

現在按type_layer的大小來檢索一下: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  10 類別1.1.1 3 010101  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  ……

現在列出的記錄順序正好是先序遍歷的結果。在控制顯示類別的層次時,只要對type_layer欄位中的數值進行判斷,每2位一組,如大於0則向右移2個空格。當然,我這個例子中設定的限制條件是最多3層,每層最多可設99個子類別,只要按使用者的需求情況修改一下type_layer的長度和位數,即可更改限制層數和子類別數。其實,上面的設計不單單隻在類別表中用到,網上某些可按樹型列表顯示的論壇程式大多采用類似的設計。

或許有人認為,Type_table_2中的type_father欄位是冗餘資料,可以除去。如果這樣,在插入、刪除某個類別的時候,就得對type_layer 的內容進行比較繁瑣的判定,所以我並沒有消去type_father欄位,這也正符合資料庫設計中適當保留冗餘資料的來降低程式複雜度的原則,後面我會舉一個故意增加資料冗餘的案例。

二、商品資訊表的設計

        假設你是一家百貨公司電腦部的開發人員,某天老闆要求你為公司開發一套網上電子商務平臺,該百貨公司有數千種商品出售,不過目前僅打算先在網上銷售數十種方便運輸的商品,當然,以後可能會陸續在該電子商務平臺上增加新的商品出售。現在開始進行該平臺數據庫的商品資訊表的設計。每種出售的商品都會有相同的屬性,如商品編號,商品名稱,商品所屬類別,相關資訊,供貨廠商,內含件數,庫存,進貨價,銷售價,優惠價。你很快就設計出4個表:商品型別表(Wares_type),供貨廠商表(Wares_provider),商品資訊表(Wares_info):

商品型別表(Wares_type)

這裡寫圖片描述

供貨廠商表(Wares_provider)

這裡寫圖片描述

商品資訊表(Wares_info)

這裡寫圖片描述

你拿著這3個表給老闆檢查,老闆希望能夠再新增一個商品圖片的欄位,不過只有一部分商品有圖片。OK,你在商品資訊表(Wares_info)中增加了一個haspic的BOOL型欄位,然後再建了一個新表——商品圖片表(Wares_pic):

商品圖片表(Wares_pic)

這裡寫圖片描述

程式開發完成後,完全滿足老闆目前的要求,於是正式啟用。一段時間後,老闆打算在這套平臺上推出新的商品銷售,其中,某類商品全部都需新增“長度”的屬性。第一輪折騰來了……當然,你按照新增商品圖片表的老方法,在商品資訊表(Wares_info)中增加了一個haslength的BOOL型欄位,又建了一個新表——商品長度表(Wares_length):

商品長度表(Wares_length)

這裡寫圖片描述

剛剛改完沒多久,老闆又打算上一批新的商品,這次某類商品全部需要新增“寬度”的屬性。你咬了咬牙,又照方抓藥,添加了商品寬度表(Wares_width)。又過了一段時間,老闆新上的商品中有一些需要新增“高度”的屬性,你是不是開始覺得你所設計的資料庫按照這種方式增長下去,很快就能變成一個迷宮呢?那麼,有沒有什麼辦法遏制這種不可預見性,但卻類似重複的資料庫膨脹呢?我在閱讀《敏捷軟體開發:原則、模式與實踐》中發現作者舉過類似的例子:7.3 “Copy”程式。其中,我非常贊同敏捷軟體開發這個觀點:在最初幾乎不進行預先設計,但是一旦需求發生變化,此時作為一名追求卓越的程式設計師,應該從頭審查整個架構設計,在此次修改中設計出能夠滿足日後類似修改的系統架構。下面是我在需要新增“長度”的屬性時所提供的修改方案:

去掉商品資訊表(Wares_info)中的haspic欄位,新增商品額外屬性表(Wares_ex_property)和商品額外資訊表(Wares_ex_info)2個表來完成新增新屬性的功能。

商品額外屬性表(Wares_ex_property)

這裡寫圖片描述

商品額外資訊表(Wares_ex_info)

這裡寫圖片描述

在商品額外屬性表(Wares_ex_property)中新增2條記錄:  ex_pid 和p_name  1 商品圖片  2 商品長度

再在整個電子商務平臺的後臺管理功能中追加一項商品額外屬性管理的功能,以後新增新的商品時出現新的屬性,只需利用該功能往商品額外屬性表(Wares_ex_property)中新增一條記錄即可。不要害怕變化,被第一顆子彈擊中並不是壞事,壞的是被相同軌道飛來的第二顆、第三顆子彈擊中。第一顆子彈來得越早,所受的傷越重,之後的抵抗力也越強8)

三、多使用者及其許可權管理的設計

       開發資料庫管理類的軟體,不可能不考慮多使用者和使用者許可權設定的問題。 儘管目前市面上的大、中型的後臺資料庫系統軟體都提供了多使用者,以及細至某個資料庫內某張表的許可權設定的功能,我個人建議:一套成熟的資料庫管理軟體,還是應該自行設計使用者管理這塊功能,原因有二:

1.那些大、中型後臺資料庫系統軟體所提供的多使用者及其許可權設定都是針對資料庫的共有屬性,並不一定能完全滿足某些特例的需求;

2.不要過多的依賴後臺資料庫系統軟體的某些特殊功能,多種大、中型後臺資料庫系統軟體之間並不完全相容。否則一旦日後需要轉換資料庫平臺或後臺資料庫系統軟體版本升級,之前的架構設計很可能無法重用。

下面看看如何自行設計一套比較靈活的多使用者管理模組,即該資料庫管理軟體的系統管理員可以自行新增新使用者,修改已有使用者的許可權,刪除已有使用者。首先,分析使用者需求,列出該資料庫管理軟體所有需要實現的功能;然後,根據一定的聯絡對這些功能進行分類,即把某類使用者需使用的功能歸為一類;最後開始建表:

這裡寫圖片描述

採用這種使用者組的架構設計,當需要新增新使用者時,只需指定新使用者所屬的使用者組;當以後系統需要新增新功能或對舊有功能許可權進行修改時,只用操作功能表和使用者組表的記錄,原有使用者的功能即可相應隨之變化。當然,這種架構設計把資料庫管理軟體的功能判定移到了前臺,使得前臺開發相對複雜一些。但是,當用戶數較大(10人以上),或日後軟體升級的概率較大時,這個代價是值得的。

四、簡潔的批量m:n設計

       碰到m:n的關係,一般都是建立3個表,m一個,n一個,m:n一個。但是,m:n有時會遇到批量處理的情況,例如到圖書館借書,一般都是允許使用者同時借閱n本書,如果要求按批查詢借閱記錄,即列出某個使用者某次借閱的所有書籍,該如何設計呢?讓我們建好必須的3個表先:

這裡寫圖片描述

為了實現按批查詢借閱記錄,我們可以再建一個表來儲存批量借閱的資訊,例如:

這裡寫圖片描述

其中,同一次借閱的batch_no和該批第一條入庫的rent_id相同。舉例:假設當前最大rent_id是64,接著某使用者一次借閱了3本書,則批量插入的3條借閱記錄的batch_no都是65。之後另外一個使用者租了一套碟,再插入出租記錄的rent_id是68。採用這種設計,查詢批量借閱的資訊時,只需使用一條標準T_SQL的巢狀查詢即可。當然,這種設計不符合3NF,但是和上面標準的3NF設計比起來,哪一種更好呢?答案就不用我說了吧。

五、冗餘資料的取捨

       上篇的“樹型關係的資料表”中保留了一個冗餘欄位,這裡的例子更進一步——添加了一個冗餘表。先看看例子:我原先所在的公司為了解決員工的工作餐,和附近的一家小餐館聯絡,每天吃飯記賬,費用按人數平攤,月底由公司現金結算,每個人每個月的工作餐費從工資中扣除。當然,每天吃飯的人員和人數都不是固定的,而且,由於每頓工作餐的所點的菜色不同,每頓的花費也不相同。例如,星期一中餐5人花費40元,晚餐2人花費20,星期二中餐6人花費36元,晚餐3人花費18元。為了方便計算每個人每個月的工作餐費,我寫了一個簡陋的就餐記賬管理程式,資料庫裡有3個表:

這裡寫圖片描述

其中,就餐計費細表(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、簡化開發的複雜度。現代軟體開發,實現同樣的功能,方法有很多。儘管不必要求程式設計師精通絕大部分的開發工具和平臺,但是還是需要了解哪種方法搭配哪種開發工具的程式更簡潔,效率更高一些。冗餘資料的本質就是用空間換時間,尤其是目前硬體的發展遠遠高於軟體,所以適當的冗餘是可以接受的。不過我還是在最後再強調一下:不要過多的依賴平臺和開發工具的特性來簡化開發,這個度要是沒把握好的話,後期維護升級會栽大跟頭的。

(沒有找到原作者,略表歉意和敬意)