什麼是DOMAINs (領域)?

DOMAIN 這種資料型態在 PostgreSQL 是從 7.2.x 版才出現的東西,但是其實它在 Oracle 則是從 8.0 就出現了。

如果各位有在用pgAdmin,如果你裝PostgreSQL的伺服器是7.2.x以後的版本,你一定會發現在schema項目中,出現了一個叫「領域」 (翻譯得不錯,挺貼切的) 的東西。如果你是用英文版,就會知道這個「領域」的英文原名是DOMAIN。那麼,什麼是DOMAIN?這是SQL 92的Schema物件之一,主要是用在定義某種有其範圍的資料型態。只要是你想定義某個經常使用的,而其資料範圍是「有限」的資料型態,你就可以用它。

我想大部份的人看到這裡可能還是一頭霧水。所以我舉個例可能大家就比較了解。比如說,我要建立一個台灣縣市名的資料型態,讓我的所有的TABLE可以被局限在這25個縣市中,再加上一個「不詳」,這個東西不錯吧。這樣的東西在SQL 92中,就是以DOMAIN來實作的。

我們先回歸資料庫設計面。我們定義資料的限制範圍,通常是用DDL在建立TABLE時,設定其限制條件。在PostgreSQL (SQL 92),限制條件的種類有很多種。

第一種是NULL | NOT NULL | UNIQUE | PRIMARY KEY ,這些基本的限制,用來限定該欄位是否空值或獨一無二,甚至是主鍵值。這一類的限定範圍最小。

第二種是用CHECK來限定該欄位的資料範圍。比如說,該欄位不允許負值出現,這時候,我們就要定義

CHECK(colname >= 0)

來做限定。CHECK算是可以限制細一點的範圍。當然我們也可以把第一類和第二類的這些限制組合起來,然後用CONSTRAINT關鍵字替它取名字,這樣的定義更清楚。

至於第三種,則是參照另一個資料表的某一欄位,也就是referential integrity,以REFERENCE關鍵字,建立FOREIGN KEY,來參考另一個表格的某一個欄位,限制這個FOREIGN KEY欄位的值一定要在被參照表格中的被參照欄位中有出現。Referential integrity是另一門學問,一般的Database的書一定都有,只是寫得多寫得少而已,在這裡我就先不多談,有空再寫這方面的東西。

使用Referential integrity其實是很好的選擇,它其實已經很詳實的定義該欄位的值的範圍了,而且還是動態改變的。這也就是關聯式資料庫的精髓。這也就是我一直不認為MySQL在4.1版之前算是RDBMS的原因,因為在這個版本之前,MySQL對Referential integrity的支援不足,根本沒資格稱為關聯式資料庫。

但是用FOREIGN KEY來定義有缺點。缺點就在於上一段我提到的「動態改變」。因為為了動態改變而使用這個架構,一定會在更新 (包括插入、刪除) 效能上造成影響。所以,當我已經確定,這個欄位的「值域」是有限的 (finite),如果沒有DOMAIN的RDBMS,我還是得再建立一個TABLE,讓我來一一輸入值域中的所有清單、組合,而無法顧及是否造成效能降低。其實,這樣做並非完全沒優點,等一下文章最後我會提到此時用FOREIGN KEY的優點,但目前看來用FOREIGN KEY的確會在更新時會造成若干效能上的降低,好像不是一種很好的方案。

所以,這時候比較好的方法是剛才提到的,把第一類的限制,和CHECK這種第二類的限制組合起來。但是這種方法無法像使用Referential integrity這樣,可以讓多個TABLE同時參照,當我們遇到像我剛才提到的,有很多TABLE都有「縣市」欄位時,那麼我每定義一次TABLE,就必須重覆輸入一次。天啊,這...這...這實在太可怕了。工程師都是很會偷懶的,有沒有其它的好方法可以偷懶呢?

這時候就是DOMAIN的使用時機了。我們可以使用CREATE DOMAIN,然後把之前在CREATE TABLE所要做的範圍限制全搬進DOMAIN中。PostgreSQL的參考手冊中有以下的例子:

CREATE DOMAIN country_code char(2) NOT NULL;
CREATE TABLE countrylist (id integer, country country_code);

這個例子很糟,而且還是錯的,因為你完全看不出這和UDT有何不同,而且你照這樣輸入一定沒辦法成功,資料庫會出現錯誤訊息。我以前最常用的例子是像:

CREATE DOMAIN county_name
AS char(3) <---------- AS千萬不要忘了,這一點和SQL 92的標準有所不同,千萬要注意!
NOT NULL
DEFAULT '不詳 ' <---------- 這裡不要忘了要加一個空白,這樣才會符合char(3)
CHECK( VALUE IN ( '基隆市', '台北縣', '台北市', '宜蘭縣', ...... , '金門縣', '連江縣', '不詳 ' ));
CREATE TABLE contact_list (id serial, name varchar(10), county county_name, address varchar(100), ......);

這樣就可以完成很好的「有限」範圍定義了。

但是這樣會出現另一個問題:如果清單改變了,比如說當初輸入時輸入錯了,那該怎麼辦?或是說,清單中有新增的項目,那該怎麼辦?

我們先回歸基本的資料庫設計上來談。你在考慮要使用Referential integrity或使用DOMAIN時,有兩個考量。第一點,這個「有限」的值域的清單有多長?如果清單太長了,可能使用Referential integrity加上index可能會比較實用。第二個,如果清單在設計完成上線後,還有變動的可能,那麼,你還是考慮使用Referential integrity比較好。

好了,如果當初考量上沒有失誤,TABLE定義最後定案使用DOMAIN,結果發現當初輸入時發生打字錯誤了,那怎麼辦?如果你用剛才範例的定義方法,那一定得用DROP DOMAIN刪掉重新定義了。另一種比較好的方法是把我剛才的範例改成:

CREATE DOMAIN county_name
AS char(3)
CONSTRAINT cc_cnstrnt (
NOT NULL
DEFAULT '不詳 '
CHECK( VALUE IN ( '基隆市', '台北縣', '台北市', '宜蘭縣', ...... , '金門縣', '馬祖縣', '不詳 ' )));
CREATE TABLE contact_list (id serial, name varchar(10), county county_name, address varchar(100), ......);

如果我發現「連江縣」不小心打成「馬祖縣」,那麼按照PostgreSQL參考手用的做法,還可以用:

ALTER DOMAIN DROP CONSTRAINT cc_cnstrnt;
ALTER DOMAIN ADD CONSTRAINT cc_cnstrnt(......);

來補救。但是實作上比這個複雜太多了,這種方法一點都不實用,如果還在設計階段,這樣做很好,很實用。但是如果系統已經上線,這樣做恐怕會被系統擋回來。所以這裡只是提出來供大家做參考而已。實作上是再建立一個新的DOMAIN,然後把TABLE的定義做修改。不過這樣做我沒試過PostgreSQL上,如果系統已經上線了,這樣做資料庫會不會接受。不過理論上應該是不會通過。

除了list一旦建立,系統上線之後,就很難更改這個缺點外,DOMAIN的另一個缺點是:沒有SQL 92的標準指令可以列出清單。因此在寫程式時,如果我想在GUI界面上設計一個下拉式選單,讓end user被限制在只能從有限的清單中輸入,而非用文字輸入方塊那種開放式的輸入方式。這時寫程式的人如果不是你,程式設計師一定會很想殺了你。所以,這時候如果效能上許可下,使用Referential integrity會是比較好的選擇。因為你可以用SELECT指令把值域清單列出來。