Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
這一章涵蓋了如何建立資料庫結構。在關連式資料庫中,原始資料儲存在表格之中,所以在這一章裡,主要說明表格如何建立及調整,以及有什麼樣的功能可以操控所存放的資料。再來我們會討論表格如何以結構來管理,以及權限的設定。最後,我們會簡短地看一下其他的功能如何影響資料儲存,像是繼承、表格分割、view、函數、還有觸發函數。
欄位可以指定一個預設值。當新的列被插入,某些欄位卻沒有指定其值時,這些欄位將會被填入相對應的預設值。資料處理的過程中,當有欄位的值不確定時,也會被設定為其預設值。(關於資料處理的詳細內容,請參閱第 6 章。)
如果預設值並沒有明確被指定時,預設值就會是 null。一般來說空值是可接受的情況,因為空值可以表示「未知的資料」的意義。
在表格定義時,預設值接在資料型別後宣告,如下所示:
預設值也可以是運算表示式,會在資料插入的同時進行運算(不是在表格建立時)。常見的例子是 timestamp 欄位,會設定一個 CURRENT_TIMESTAMP 的預設值,使其在資料插入時設定為當下的時間。另一個例子是產生「序列數」,這在 PostgreSQL 中,通常以下列語法來表現:
這裡的 nextval() 函數會從序列物件取得下一個數字(參閱 9.16 節)。這個例子也常簡化為:
有關 SERIAL 的簡寫方式,將在 8.1.4 節中說明。
PostgreSQL 實作了資料表的繼承方式,對於資料庫設計人員來說,將會是很好用的功能。(SQL:1999 之後定義了型別繼承的功能,但和這裡所介紹的方向有許多不同。)
我們直接以一個例子作為開始:假設我們嘗試建立「城市(city)」的資料模型。每一個州(state)都會有許多城市(city),但只會有一個首都(capital)。我們想要很快地可以找到某個州的首都。這件事我們需要建立兩個資料表,一個存放首都,而另一個記載非首都的城市。只是,當我們想要取得的是所有城市,不論是否為首都,似乎會有些麻煩?這時候繼承功能就可以幫助我們解決這個問題。我們可以定義一個資料表 capitals,它是由資料表 cities 繼承而來:
在這個例子中,資料表 capitals 會繼承父資料表 cities 的所有欄位。只是 capitals 會多一個欄位 state,表示它是哪個州的首都。
在 PostgreSQL 裡,一個資料表可以繼承多個資料表,而一個查詢可以引用該資料表裡的所有資料列或在其所屬的資料表的資料列,後者的行為是預設的。舉個例子,下面的查詢可以列出所有海沷在 500 英呎以上的城市名稱,州的首都也包含在內:
使用 2.1 節中的範例資料,將會回傳:
換句話說,下面的查詢就會查出非首都且海沷超過 500 英呎以上的城市:
這裡「ONLY」關鍵字指的是查詢只需要包含資料表 cities 就好,而不是任何繼承 cities 的資料表都包含在內。我們先前介紹過的指令:SELECT、UPDATE、和 DELETE,都可以使用 ONLY 關鍵字。
你也可以在資料表名稱後面加上「*」,明確指出繼承的資料表都需要包含在內:
注意這個「*」並不是必要的,因為這個行為本來就是預設的。這個語法用於相容舊的版本,有些版本的預設行為可能不太一樣。
在某些例子裡,也許你會希望知道哪些資料列來自於哪個資料表。有一個系統欄位稱作 tableoid,每一個資料表都會有,而它可告訴你資料列的來源:
這將會回傳:
(如果你嘗試重覆執行這個例子,你可能會得到不同的 OID 值。)藉由和資料表 pg_class 交叉查詢,你可以看到實際的資料表名稱:
將會回傳:
另一個可以得到相同結果的方式是,使用 regclass 別名型別,這個型別會將 OID 轉換成名稱輸出:
在使用 INSERT 或 COPY 指令時,繼承並不會自動轉存資料。在我們的例子中,下面的 INSERT 指令將會失敗:
我們可能會希望資料以某種方式轉送到資料表 capitals 中,但這不會發生:INSERT 指令永遠只會將資料插入到指定的資料表中。在某些情況下,如果設定了存取規則(第 40 章)的話,那有可能做到類似的效果。然而,在這個例子下是沒有辦法執行的,因為資料表 cities 中並沒有一個欄位稱作 state,所以這個指令將會被拒絕執行,如果沒有其他規則被設定的話。
所有限制條件的檢查,還有非空值的限制,都會自動從父資料表繼承下來,除非特別使用 NO INHERIT 子句來設定拋棄繼承。而其他型態的限制條件(唯一性、主鍵、外部鍵)都不會自動繼承。
一個資料表也可以繼承超過一個資料表,也就是說,它會擁有這些資料表全部的欄位,然後再加上自己所宣告的欄位。如果父資料表有相同名稱的欄位的話,或是父資料表和子資料表有同名的欄位,那麼這些欄位會被合併,它們會被合併為一個欄位。合併的時候,他們的資料型別必須要一致,否則會產生錯誤。被繼承的限制條件和無空值的限制也會用類似的方式合併。舉個例子來說,如果要合併的欄位中,任何一個欄位有 not-null 的設定的話,那麼合併後的欄位就會被設定為 not-null。如果有同名的限制條件要被合併,但他們的內容不相同的話,那麼合併也會失敗。
資料表的繼承一般來說是在子資料表建立時進行的,也就是在 CREATE TABLE 中使用 INHERITES 子句。然而,資料表也可以在 ALTER TABLE 中使用 INHERIT 子句來新增新的父資料表。要進行這個動作,新的子資料表必須已經包含所有父資料表的欄位—相同的欄位名稱及資料型別。還有在 ALTER TABLE 時加入 INHERIT 子句來移除某個欄位的繼承。動態地新增或移除繼承欄位通常是在應用分割表格(table partitioning)時特別好用(請參閱 5.10 節)。
還有一個方便的方式去建立一個相容於之後繼承的資料表,就是在 CREATE TABLE 中使用 LIKE 子句。這個方式會建立一個新的資料表,其欄位和另一個資料表完全相同。如果有任何 CHECK 子句的限制條件的話,就應該在 LIKE 子句中加入 INCLUDING CONSTRAINTS 選項,這樣就會和父資料表完全相容了。
父資料表無法在子資料表仍然存在時被移除。子資料表的欄位和限制條件也不能被移除,如果它們是由其他資料表繼承而來的話。如果你想要移除某個資料表,包含其相關的物件的話,一個簡單的方式就是在移除時加上 CASCADE 選項(請參閱 5.13 節)。
ALTER TABLE 將會讓欄位型態和限制條件的改變,衍生至繼承它的資料表之中。一樣地,移除某個欄位,如果它有被其他資料表繼承的話,那麼就必須要加上 CASCADE 選項才行。ALTER TABLE 會遵循和 CREATE TABLE 一樣的規則,決定重覆的欄位要合併還是拒絕。
指令的繼承權限是依父資料表的權限。舉個例子,當你存取資料表 cities 時,在 cities 上給予 UPDATE 的權限,同時也隱含了賦予 capitals 更新資料的權限。這考量到這些資料也會出現在父資料表,但如果你沒有特別給予 capitals 權限的話,你還是無法直接存取 capitals。類似的情況也會發生在資料列的安全原則(5.7 節),在繼承查詢時,同樣是參考父資料表的安全原則。而子資料表額外的安全原則,只在直接查詢該資料表時有效,同時任何父資料表的安全原則會失效。
外部資料表(5.11 節)也可以是繼承的一部份,父資料表或子資料表,就如同一般的資料表一樣。只是,如果整個繼承結構中,有任何外部資料不支援的操作的話,那麼整個繼承結構就都不支援。
注意,並非所有的 SQL 指令都可以在繼承結構中執行。一般常用的資料查詢,資料更新,或結構調整(像是 SELECT、UPDATE、DELETE,還有多數 ALTER TABLE 的功能,但不包括 INSERT 或 ALTER TABLE ...... RENAME),基本上預設都是包含子資料表,也支援使用 ONLY 指示字來排除子資料表。如果是資料庫維護或調教的指令,如 REINDEX、VACUUM,一般就只支援特定且實體的資料表,就不會在繼承結構中衍生其他的動作。這些個別指令相關的行為,請參閱 SQL Commands 內的說明。
繼承功能比較嚴格的限制是索引(包含唯一性索引),還有外部鍵的限制條件,都只能用在單一資料表,而不會衍生至他們的子資料表中。對外部鍵來說,無論引用資料表或是參考資料表的情況都一樣。下面是一些例子說明:
如果我們宣告 cities.name 具備唯一性或是主鍵,這不會限制到 capitals 中有重覆的項目。而這些重覆的資料列就會出現在 cities 的查詢結果中。事實上,預設的 capitals 就沒有唯一性的限制,所以就可能有多個資料列記載相同的名稱。你可以在 capitals 中也加入唯一性索引,但這也無法避免 capitals 和 cities 中有重覆的項目。
同樣地,如果我們指定 cities.name 以外部鍵的方式,參考另一個資料表,而這個外部鍵也不會衍生到 capitals 中。這種情況你就必須在 capitals 中也以 REFERENCES 設定同樣外部鍵的引用。
如果有另一個資料表的欄位設定了 REFERENCES cities(name) 就會允許其他的資料表包含城市名稱,但就沒有首都名稱。在這個情況下,沒有好的解決辦法。
這些缺點可能會在後續的版本中被修正,但在此時此刻,當你需要使用繼承功能讓你的應用設計更好用時,你就必須要同時考慮這些限制。
除了透過 GRANT 指令設定 SQL 標準的權限系統之外,資料表也可以有資料列層級的安全原則,控制每個使用者在資料查詢或變更時,所能接觸到的資料列。這個功能就稱作資料列安全原則(Row-Level Security)。預設上,資料表並不會有這些安全原則,所以只要使用者能存取該資料表,就表示他能存取所有資料列的內容。
當資料列安全原則在資料表裡被啓動後(使用 ALTER TABLE ... ENABLE ROW LEVEL SECURITY),所有資料表的操作,就必須符合資料列安全原則的設定。(當然,資料表的擁有者並不受限於資料列安全原則。)如果資料表中未設定任何原則,那麼預設就是拒絕存取,意思就是任何資料列都不能被看見或修改。但如果是整個資料表的操作行為,像是 TRUNCATE 或 REFERENCES,就不會受到影響。
資料列安全原則可以被設定在命令,使用者角色,或兩者兼具。安全原則也可以使用 ALL 的修飾字,或具體指出是 SELECT、INSERT、UPDATE、或 DELETE。多重角色可以共用一個安全原則,一般使用者或承繼的角色都會被同步影響到。
要設定一個安全原則來指出哪些資料列可見或可修改,是以一個回傳值為布林值的表示式來決定的。這個表示式會計算每一個資料列的結果,在使用者進行任何操作之前。(這個規則唯一的例外是 leakproof 函數,用來確保沒有洩漏資訊;查詢最佳化元件會選押在確任資料列安全原則前就先執行它。)在這個表示式沒有回傳 true 的資料列,都是不能被存取的。獨立的表示式可用於提供資料列專屬的控制,判斷其是否可供讀取或修改。安全原則表示式是查詢的一部份,和使用者執行查詢時一起執行,不過,安全原則表示式是可以存取到該使用者看不到的資料。
超級使用者因為擁有 BYPASSRLS 的屬性,所以永遠可以通過安全原則檢查而存取資料表。資料表的擁有者一般來說也是可以通過檢查,但可以使用 ALTER TABLE ... FORCE ROW LEVEL SECURITY 來強制適用安全原則。
開啓或關閉資料列安全原則的權限,只屬於資料表擁有者。
使用 CREATE POLICY 指令來建立安全原則;使用 ALTER POLICY 指令來修改;使用 DROP POLICY 指令來移除原則。要開啓或關閉安全原則的功能,請使用 ALTER TABLE 指令。
每一個安全原則都有一個名稱,而一個資料表可以定義多個安全原則。安全原則是資料表專屬的,而每一個安全原則在所屬資料表內必須有一個唯一的名稱。不同的資料表下的安全原則可以取相同的名稱。
當多個安全原則使用者某個查詢上時,可能會使用 OR 串接(開放安全原則 permissive policies,這是預設的狀態),也可能以 AND 串接(嚴格安全原則 restrictive policies)。這類似角色授權的情況。有關於開放安全原則與嚴格安全原則的細節,稍後再進行說明。
先進行一個簡單的範例,我們建立一個安全原則在資料表 account 上,它只允許 managers 的使用者可以存取資料列,並且只能存取他自己帳號的資料列:
如果沒有指定角色或使用者時,就會以 PUBLIC 替代,也就是所有使用者都適用。要允許所有使用者存取他們自己的資料列的話,就可以簡化指令為:
想要定義一個安全原則是有別於可見性權限的話,請使用 WITH CHECK 字句。例如希望讓所有人都可以看到所有資料列,但只能修改自己的資料的話:
資料列安全原則也可以透過 ALTER TABLE 指令關閉。不過關閉資料列安全原則,並不會移除任何已定義的原則,只是單純被忽略而已。然後資料表的所有資料列,就只依標準 SQL 的權限系統,決定查詢及修改的權力。
下面是一個較複雜的例子,展示這個功能如何被應用於產品等級的環境裡。資料表 passwd 模擬 Unix 的密碼檔:
對於任何的安全設定,很重要的是,你必須實際測試來確認系統的行為和你預期的相同。使用上面的例子,下面的測試表現出權限設如預期地運作。
所有的安全原則,目前來說都是開放安全原則,意思是當有多個安全原則被引用時,它們會以 OR 運算串連其結果。開放安全原則用於只允許在計畫內的環境使用的話,它會比和嚴格安全原則(把安全原則用 AND 串連起來判斷)一起使用來得簡單。基於上面的列子,我們建立一個嚴格安全原則,它限制管理者只能透過 unix socket 連線才能存取 passwd 資料表:
我們接下來就可以看到,管理者透過一般網路連線,是看不到任何資料的,因為嚴格安全原則:
資料一致性的檢查,像是唯一性、主鍵、以及外部鍵參考,都會略過資料列安全原則,以維持資料的一致性。在發展資料庫結構時應該要特別小心,以資料列安全原則避免透過一致性檢查而產生隱藏通道洩露資訊。
在某些情況,很重要的是要確認安全原則是否被觸發。舉例來說,當進行資料備份流程時,如果安全原則造成某些資料被備份程式忽略了,那可能就會很糟糕。在這種情況下,你可以把 row_security 這個參數設為 off。這並不是避開安全原則,而是在觸發安全原則時,會出現錯誤訊息,使得我們可以發現進而修正原則。
在上面的例子裡,安全原則表示式只引用了目前資料列中的資料。這是最簡單也是最常見的形式,可以的話,最好以這樣的方式來設計安全原則。如果需要參考其他資料列或資料表來做決定的話,那麼可以使用子查詢或函數的方式達成,也就是包含一個 SELECT 的查詢語句在表示式中。要注意到的是,這種方法可能會造成資料庫內交易競爭(race condition)的狀態,不注意的話也可能產生資訊的洩漏。像這樣的例子,試試下面的資料表設計:
現在假設 alice 想要變更"slightly secret"的資訊,但決定不讓 mallory 看到新的內容,所以她這麼做:
看起來很安全,因為沒有窗口讓 mallory 可以看到"secret from mallory",然而,這裡就存在了交易競爭的情況。如果 mallory 也在同時做了:
因為她的交易是屬於 READ COMMITTED 模式,所以她有可能會看到"secret from mallory"。這會剛好發生在,她在 alice 的交易完成前一刻。mallory 的指令會暫時擋下 alice 的提交完成,而因為 FOR UPDATE,她會取得更新後的資訊。所以她並沒有從隱含的使用者執行 SELECT 取得資訊,因為子查詢沒有 FOR UPDATE,使得其他使用者可以從快照裡取得資訊。因為安全原則是以舊的 mallory 權限允許她看見該筆資料。
這個問題有好幾個面向的解決方式。一個簡單的方式就是使用 SELECT ... FOR SHARE 在安全原則的子查詢裡。但這樣就必須要讓使用者擁有 UPDATE 的權限,可能不太合適。(但也可以用另一個安全原則來做更多的限制,又或是把子查詢封裝進另一個安全的函數裡)同時,大量的引用查詢也可能造成效能的問題,特別是更新資料的時候。另一個解決辦法,如果參考的資料表並不是很常更新的話,那麼可以在資料表更新時強制鎖定該資料表,確保沒有其他交易能在同時進行查詢,也就不會洩漏任何資訊。或是等待其他所有交易都完成後,才提交更新變更新的安全方案。
更多詳細,請參閱 CREATE POLICY 和 ALTER TABLE。
「資料表」(table)在關連式資料庫中的角色很接近在紙上畫一個「資料表」:包含了列與欄。欄的數量與次序是固定的,而每個欄位都有一個名稱。列的數量是變動的—它表示在當下有多少資料被存在資料庫中。SQL 並不保證列在資料表中的次序。當讀取資料表的時候,除非明確要求要排序,不然列與列之間是不存在固定的次序。這些將在第 7 章中進一步說明。進一步來說,SQL 並沒有給每一列一個唯一性的識別,所以在資料表中是有可能存在有完全相同內容的列。這是 SQL 架構下的數學模型結果,通常不是理想的結果。在這章之後,我們會說明如何處理這個問題。
每一個欄位都有一個資料型別。資料型別限制了儲存於該欄位的資料內容,同時也設定了資料儲存的型態,使得該資料可以直接用於計算。舉個例子,一個被宣告為數字型別的欄位,就不能放進任何文字字串,而儲存於此欄位中的資料,可用於數學計算。相反地,一個被宣告為字元字串的欄位,可以儲存任何型能的資料,但就無法用於數學計算了,雖然也有其他操作可以進行字串串接。
PostgreSQL 擁有許多內建的資料型別,可以適應許多應用系統。使用者也可以自訂他們所需的資料型別。大多數內建的資料型別都有顯而易見的名稱與用法,所以我們打算在第 8 章再做詳細的說明。有一些常用的資料型別,像是 interger 用於整數,numeric 用於浮點數,text 用於字串,date 則是日期,time 是時間,而 timestamp 則同時包含日期和時間。
要建立一個資料表,你可以使用 CREATE TABLE 指令。這個指令你至少要指定一個名稱給新的資料表,還有每一個欄位的名稱與資料型別。例如:
這個建立一個叫作 my_first_table 的資料表,它包含了兩個欄位。第一個欄位叫作 first_column,其資料型別為 text;第二個欄位名稱為 second_column,資料型別為 integer。表格與欄位名稱的規則依 4.1.1 節中所介紹的識別字語法,但也有一些例外。注意欄位列表是用逗號分隔,並且包含於括號之中。
當然,前面的例子明顯只是做做樣子而已。一般來說,你會將你的資料表欄位以實際用途來命名,所以我們來看一下更實際的例子:
(numeric 資料型別可以儲存浮點數,用於典型的貨幣計量。)
小技巧當你建立了許多相關的資料表時,建立最好選擇一個用於命名表格及欄位的規則。舉例來說,有一個規則是使用單數或複數名詞來取名表格,兩者都有些人喜歡使用。
一個資料表中有多少欄位是有限制的,依欄位型別而定,上限通常是 250 個到 1600 個之間。不過,宣告到這麼多的欄位是非常罕見,而且應該是有問題的設定。
如果你不再需要某個資料表,你可以移除它。請使用 DROP TABLE 指令,如下所示:
企圖要移除一個不存在的資料表,會產生錯誤。不過,在 SQL 腳本中,在建立資料表前嘗試移除是很常見的,通常會忽略錯誤訊息,所以不論資料表是否已經存在,腳本都能如預期執行。(如果你需要的話,你也可以使用 DROP TABLE IF EXISTS 來避免產生錯誤訊息,但這並不是標準 SQL 語法。)
如果你需要變更資料表的結構的話,請參閱本章的 5.5 節。
到目前為止,你已經可以利用工具建立完整功能的資料表。本章接下來的部份會針對附加的功能介紹,像是確保資料完整性、安全性、或方便性。如果你現在急著要將資料存入你的資料表的話,你可以暫時跳過本章,到第 6 章繼續操作。
PostgreSQL 支援基礎的分割資料表。本節描述如何讓分割資料表成為資料庫設計的一部份。
資料表的分割,指的是把一個邏輯上很大的資料表,分割為數個實體的小資料表。分割資料表可以獲得幾點好處:
資料查詢的效能在某些情況下會大幅改善,特別是資料表中有一些資料列是時常被存取的,而它們只存在於某一個單一的分割區,或某一小群分割區。分割資料表的優勢在於大幅降低欄位索引的大小,而當其大小縮小到可以完全在記憶體中執行時,那就會獲得相當大的效能改善。
當資料查詢或更新時,它可能牽連到某一分割區大部份的資料列,效能同樣會獲得改善,它可以直接掃描存取整個「小」區域,而不是在「大」資料表中,以索引逐筆搜尋分散的資料列。
大量載入或移除資料的話,可以直接對整個分割區操作,當然這些資料要能符合分割資料表的設計。使用 ALTER TABLE DETACH PARTITION 或是 DROP TABLE 移除特定的分割資料表,都比進行大量的 DELETE 要快非常多。因為這些指令不會進行資料表的整理,而大量的 DELETE 會引發 VACUUM 的啓動。
少用的資料可以搬到較便宜或比較慢的儲存媒體。
這些優勢通常是在原來資料表特別大是會很明顯,不過實際上會獲得什麼樣的改善,還是要視應用程式而定。一個基本的概念是資料表的大小,如果超過了資料庫主機的記憶體上限,那就最好進行資料表的分割。
PostgreSQL 內建提供的資料表分割方式:
Range Partitioning
資料表是以某個欄位或某些欄位的資料內容範圍來分割,所謂的範圍,就表示彼此之間沒有重疊的部份。舉例來說,你可以以資料的範圍做分割,或是以指定的公司資料 ID 的範圍來分割。
List Partitioning
明確列出有哪些資料的值要被分配在哪些資料表。
如果你的應用需要使用上述兩種以外的分割方式,還有其他方式,像是繼承,UNION ALL views,也可以使用。這些方式提供更多的彈性,但都不如內建分割方式所提升的效能。
PostgreSQL 提供一個方式,可以指定如何將資料表分割為較小的資料表,這些小資料表稱作為分割區(partitions)。被分割的資料表,稱作分割資料表。分割主鍵包含了分割方法與一些欄位內容或是表示式。
所有新插入的資料列將會依分割主鍵的規則,轉送至分割區中。每一個分割區都是所有資料列的子集合,範圍由其定義的資料邊界而定。目前支援的分割方法有 Range 及 List 分割法,也就是每一個分割區都需要指定一個區段或是一個列表。
分割資料表的 CHECK 及 NOT NULL 限制條件,會被其分割區所繼承。 在分割資料區中,把 CHECK 標示為 NO INHERIT 是不被允許的。
在分割資料表新增或移除限制條件時使用 ONLY 的話,只有在其還沒有分割區時是允許的。一旦其下有分割區存在,使用 ONLY 就會產生錯誤。換句話說,當有分割區時,這個執行方式是不被允許的。但是你可以新增或移除分割區裡的限制條件,只要它們並沒有在分割資料表中存在就好。在分割資料表嘗試執行 TRUNCATE ONLY 指令,也會產生錯誤,因為分割資料表並未實際存放資料。
分割區不能有分割資料表裡沒有的欄位。不能在 CREATE TABLE 時建立,也不能使用 ALTER TABLE 增加。資料表能成為一個分割區,它的欄位必須和分割資料表完全吻全,包含 OIDs。
你無法移除分割區中的 NOT NULL 限制條件,如果它是定義在分割資料表中的話。
假設我們為一家大型冰淇淋公司建構一個資料庫。這家公司每天測量最高溫度,同時也統計各區域的銷售情況。概念上,我們需要像這樣的資料表:
我們知道大多數都是在進行近期的資料查詢,如最近一週、最近一個月、或最近一季的資料,這個資料表用於產生管理用的線上報表之用。為了降低需要儲存的資料量,我們決定只保存 3 年內有價值的資料。每一個月開始時,我們就會移除最舊那個月的資料。在這種情況下,我們可以使用分割資料表來幫助我們滿足所有需求。
在這個例子中,使用下列步驟來宣告分割資料表:
建立 measurement 資料表時,使用 PARTITION BY 子句,在本例子使用 RANGE 的分割方法,然後以 logdate 作為分割主鍵。
你也可以使用多個欄位作為分割主鍵來依範圍分割,當然,這會產生相當多的分割區,它可以分割得更小一些。也就是說,使用較少的分割主鍵欄位,是較為粗略的分割。當分割資料表被查詢時,就可以減少分割區存取的數量,如果條件是遍及數個欄位時。舉例來說,可以想像一下一個以範圍分割的資料表,同時以 lastname 及 firstname 兩個欄位作為分割主鍵的情況。
建立分割區時,每一個分割區的定義都必須指定其分割方式的規則與分割主鍵。需要注意的是,如果指定的規則,造成某些分割主鍵的值會落在多個分割區中的話,將會產生錯誤。從分割資料表插入資料時,如果沒有對應到任何一個分割區的話,也會產生錯誤;適當擺放資料的分割區必須要手動加入。
分割區的建立就如同一般的 PostgreSQL 資料表一樣(也可以是外部資料表),也可以指定各自的 tablespace 和儲存參數。
不需要為分割規則設定分割區的限制條件,而是在設定分割方式及規則時,其限制條件就已經隱含在內了。
要實現子分割時,使用 PARTITION BY 子句來建立個別的分割區。舉例來說:
在建立了 measurement_y__2006m02 資料表之後,所有新增到 measurement 資料表中符合分割規則而被派送到 measurementy_2006m02 的資料(或是符合條件的資料直接新增到 measurement_y2006m02),都會再進一步依據 peaktemp 欄位的內容轉存到它的子分割區。這個分割主鍵是可以和其父資料表分割主鍵有重疊的,不過要注意的是,指定子分割區的規則時,資料真的會分配到該子分割區,資料庫系統不會去檢查該分配是不是真的會發生。
為每一個分割區資料表的分割主鍵建立索引。(這並不是一定要做的事,不過對大多數的情況是好的。如果你需要這些值俱備唯一性,那你應該建立唯一索引或是主鍵。)
在上面的例子中,我們需要每個月建立一個分割區,所以如果能再有程序自動建立這些資料表就更好了。
一般來說,分割區在初始建立時,會假設其會不斷地變動。通常會需要定期移除舊的分割區,然後為新的資料加入新的分割區。使用分割資料表時,其中一件很重要的事,就是要能夠很明確地做到這個管理動作,否則大量地實體資料變更,會嚴重拖累資料庫系統的效率。
最簡易移除資料的方式,就是移除分割區:
這可以非常快地移除數百萬筆資料,因為它並不是單獨去移除每一筆資料。注意到的是,這個動作需要父資料表取得 ACCESS EXCLUSIVE 的鎖定。
另一個方式也很常使用,就是把某個分割區從分割資料表中卸載,但仍然保存該分割區的資料表:
這樣可以在資料被移除之前再進行一些其他的操作。舉例來說,很常見的使用情況是備份資料,利用 COPY 指令、pg_dump、或相關的工具;把資料以小單位進行彙總計算產生報表,也是很常用的方式。
接下來,要處理新的資料也是類似的動作,我們可以建立新的資料表,並宣告為分割區來使用,就如同先前我們介紹的設定方式一樣:
另一種更方便的方式是先建立新的資料表,然後再將它掛載為分割區。好處是這樣可以在掛載前先進行資料的載入、檢查和轉換:
在進行 ATTACH PARTITION 指令前,建議最好先設定 CHECK 限制條件,同分割資料表的條件,這樣的話,系統就會跳過隱含的資料檢查過程。如果沒有先設定限制條件的話,資料表會被 ACCESS EXCLUSIVE 鎖定,然後進行全資料掃描以檢查其合法性。最後我們在掛載分割區之後再移除該 CHECK 設定,因為它已經不再需要了。
使用分割資料表是會有下面的受限制的使用情況:
沒有方法可以自動在所有分割區建立需要的索引。每個分割區的索引都需要個別建立。這也代表了,沒有任何方式可以建立主鍵、唯一性限制條件、或其他跨分割區的限制條件需求;只能個別分割區自行維護。
因為分割資料表無法建立主鍵,所以外部鍵就無法支援了,無論是參考其他資料表或被參考,都不支援。
在分割資料表使用 ON CONFLICT 子句的話,會產生錯誤訊息,因為沒有唯一性及除外限制可以使用。目前不支援跨所有分割區的唯一性限制,也包含其他除外限制。
想要利用 UPDATE 改變欄位值,使資料移動到另一個分割區是行不通的。因為隱含的資料限制條件會造成其更新失敗。
資料列的事件觸發函數,必須定義在個別分割區的資料表中,而非分割資料表。
使用內建的分割資料表,基本上適用於大多數的應用情境,也可以使用一些彈性的技巧會更有幫助。分割資料庫也可以用資料表繼承的方式來達成,好處是可以支援一些本來有限制的使用情況,例如:
分割資料表會強制使所有分割區都必須要與父資料表完全一樣的資料結構,但使用繼承的話,就可以允許分割區各自擁有額外的資料欄位。
資料表的繼承可以是多重繼承。
內建的分割資料表只支援列表(list)和範圍(range)兩種資料對應方式,而繼承則可以用自訂的方式來對應資料分區。(注意,如果你的資料對應方式無法適當地利用每個分割區的話,那麼查詢將會很沒有效率。)
內建的分割資料表相對於資料表繼承時,有一些操作需要較嚴格的資料鎖定(lock)。舉例來說,分割資料表在新增或移除分割區時,會使用 ACCESS EXCLUSIVE 等級的資料鎖定,但實際上在資料表繼承維護時,只需要 SHARE UPDATE EXCLUSIVE 等級即可。
以先前使用過的 measurement 資料表作為範例說明,我們要使用繼承功能來完成分割資料表。請參考下列步驟:
建立主資料表(master),所有的分割區將會繼承它,而這個資料表不會儲存任何資料。請不要在這個資料表上定義任何限制條件,除非你希望每一個分割區都要有相等的限制條件。同樣地,也不要定義任何索引或唯一性限制。在這個例子裡,資料表 measurement 就如同先前一開始宣告的一樣。
建立幾個子資料表(child),由主資料表繼承而得。一般來說,這些資料表並不增加額多的欄位。就如同內建的分割資料表一樣,這些子資料表就是一般的 PostgreSQL 資料表(或是外部資料表)。
在每個子資料表(分割區)中,加入明確分隔的欄位值限制條件。
典型的範例如下:
請確認這些限制條件是明確的且彼此不會重疊的。下面是使用範圍分割時常見的錯誤:
這裡的錯誤來自於「200」同時符合兩個分割區的條件。
下面是比較好的寫法:
對每一個分割區,對分割主鍵欄位建立索引,就如同一般的索引建立一樣。
我們希望應用程式可以使用 INSERT INTO measurement ...
的語法,資料則自動轉送到適當的資料表。我們可以在主資料表建立適當的 Trigger 來完成此事。如果資料都會被新增到最新的子資料表中,我們可以建立很簡單的事件觸發函數:
建立這個函數之後,再建立 Trigger:
我們必須每個月都重新定義這個函數,使其都指向最新的分割區,但 Trigger 宣告並不需要更新。
我們也可以在新增資料時,讓它們自動找到適當的分割區,那就需要宣告一個比較複雜的函數,如下:
Trigger 本身的定義仍然是一樣的。要注意的是,每一個 IF 判斷式,都必須要完全符合 CHECK 限制條件的宣告。
這個函數比前一個函數要複雜許多,但它就不需要時常更新了,只要分割區在需要前就被建立就好。
實務上,最好是可以先檢查新建立的分割區,在它要掛載之前。簡化來看,我們在這個例子中使用事件觸發函數(Trigger)來處理這個動作。
另一個作法是在主要的資料表上設定規則,來取代事件觸發函數。例如:
基本上,設定規則對資料庫的負擔是比事件觸發函數更重一點,但其負擔是在於每一次查詢,而非每一個資料列,所以這個方式比較適合一次大量插入資料的情況。不過,在大多數的情況,事件觸發函數會有比較好的效能。
但要注意的是 COPY 指令會忽略規則。如果你要使用 COPY 來插入資料,你應該要從父資料表插入。而 COPY 會觸發事件觸發函數,所以你如果使用 Trigger 的話,那就像一般使用的方式使用就好了。
另一個使用 rule 的缺點是,沒有比較簡單的方法可以強制產生錯誤,如果設定的規則錯誤的話;那些出錯的資料,只會靜靜地留在父資料表中而已。
就如同我們看到的,複雜的分割區結構,可能會需要相當數量的 DDL 宣告。在先前的例子,我們每個月建立一個新的分割區,所以比較聰明的作法是寫一小段程式來自動產生那些指令。
要快速刪除舊資料,可以簡單地移除不再使用的分割區資料表即可:
將一個分割區從分割資料表中卸載,仍然留存該資料表:
要新增一個分割區來處理新的資料,建立一個空的分割區,就如同先前介紹的方式:
另一種更方便的方式是先建立新的資料表,然後再將它掛載為分割區。好處是這樣可以在掛載前先進行資料的載入、檢查和轉換:
如果你使用繼承在實現分割資料表的話,請注意下列項目:
沒有任何自動的方式可以檢驗 CHECK 子句之間是否矛盾。比較建議的作法是程式化控制分割區的建立和維護,而非手動處理。
在這裡所展示的方法都是假設分割主鍵欄位不會改變,也不會需要把某個資料列在分割區間移動。如果你企圖使用 UPDATE 指令,而期待資料列自動移到另一個分割區的話,那將會得到失敗的結果,因為會先被 CHECK 限制條件擋下來。如果你需要做到這樣的效果,那麼你可以建立 UPDATE 事件的觸發函數,但這可能會造成你的資料庫管理更加複雜。
如果你手動執行 VACUUM 或 ANALYZE 指令,不要忘了你需要在每個分割區資料表分別執行。
例如:ANALYZE measurement;
將只會在父資料表執行。
INSERT 指令裡的 ON CONFLICT 子句將無法運作,因為它只能在父資料表產生作用,而不會到子資料表中執行。
事件觸發函數(Trigger)需要建立,負責把資料放在設計好的資料表中,除非應用程式很清楚分割區的結構。事件觸發函數可能會不太好寫,而且也會比使用內建的分割資料表時慢很多。
除外限制(Constraint exclusion)是一種查詢最佳化的技術,用來改善分割資料表的效能。(包含內建分割資料表的方式,以及繼承式的分割資料表)舉個例子如下:
如果沒有除外限制的話,上面的查詢語句將會掃描每一個 measurement 資料表的分割區。而開啓了除外限制的話,查詢前就會先測試限制條件,確認該分割區是否需要掃描,因為有些分割區可能完全沒有資料符合該條件。如果確實有不需要掃描的分割區,那麼它就會在實際查詢時排除在外。
你可以使用 EXPLAIN 指令來比較除外查詢開啓與否的差異。下面是未最佳化的例子:
可以看到有些分割區可能會使用索引掃描來取代全資料表掃描,但這裡的重點是,有些分割區是完全不需要掃描的。當我們開啓除外限制時,很明顯可以得到一個更簡潔的查詢計畫:
要注意的是,除外限制只檢查 CHECK 子句,而不是索引,所以不一定要對主鍵欄位定義索引。索引是否需要在分割區建立,是依據你希望查詢在該分割區大範圍或小範圍被查詢。索引的用處在後者會比較明顯,而不是前者。預設也是建議的選項不是 on 也不是 off,而是使用 partition 子句,讓查詢只在需要執行的分割區執行。設定除外限制為「on」的話,對於大範圍的查詢很有用,但簡單查詢就不見得有好處了。
下面還有幾點注意事項,繼承和分割資料表都適用:
除外限制只適用於 WHERE 子句是常數的條件(或外部引用的參數)。舉例來說,和一個不確定結果的函數比較的話,如 CURRENT_TIMESTAMP,那就無法最佳化,因為查詢計畫無法事先得知執行時的值。
保持分割區限制條件簡潔一些,否則查詢計畫無從查驗該分割區是否需要處理。請在列舉分割時,使用簡單的等式;或在範圍分割時使用簡單的比較式,就如同先前的例子一樣。一個好的規則是只包含分割主鍵的欄位,並且使用 B-tree 可以索引的運算子,也同時宣告在主資料表中,只允許適用於 B-tree 的欄位宣告為分割主鍵。(如果使用內建分割語法的話,這不會有什麼問題,因為系統會自動宣告適合查詢計畫的限制條件。)
由於除外限制會在查詢前檢查所有分割區的限制條件,所以大量的分割區可能會增加查詢計畫的時間。所謂的「大量」,通常幾百個分割區還是可以接受的範圍,但最好不要用於上千個分割區的情境中。
表格是關連式資料庫結構裡的主要物件,因為它負責存放資料,但並不是資料庫中唯一的物件。還有許多其他種的物件存在,讓使用上更方便或管理更有效率。這些其他的物件並不在本章中討論,但我們先在這裡列出讓你知道:
視觀
函數與運算子
資料型別和領域
觸發事件和規則覆寫
關於這些物件的詳細說明安排在。
當你建立了一個表格,而你發現出了點錯,或者應用需求有一些改變,那麼你可以移除它再重新建立。但這可能不會一個好的選擇,當表格中已經儲存了許多資料時,或者表格正在被其他的資料庫物件所參考中(例如外部鍵參考)。所以 PostgreSQL 提供了一系列的指令來修改現存的表格。注意到這和更新表格內資料的概念是不同的:在這裡,我們主要針對的是調整表格的定義或結構。
你可以:
加入欄位
移除欄位
加入限制條件
移除限制條件
改變預設值
改變欄位資料型別
變更欄位名稱
變更表格名稱
所有這些動作都透過 指令來進行,你可以參考該頁面取得詳細資訊。
要加入一個新欄位,請使用下面的指令:
這個新的欄位預設會以預設值填入(如果你沒有使用 DEFAULT 子句來宣告的話,那會使用 NULL)。
你也可以在新增同時建立限制條件:
事實上,所有在 CREATE TABLE 的選項都可以在這裡使用。要記得的是,預設值必須要符合限制條件的設定,否則這個欄位會無法加入。順帶一提的是,你也可以隨後再加入限制條件(隨後說明),在你更新好新的欄位資料內容後。
加入一個欄位,並且設定預設值,會更新表格的裡的每一個資料列(為了存入新的欄位內容)。然而,無預設值的話,PostgreSQL 就不會在實體上真正進行更新的動行。所以如果你的新欄位大多數的內容都不是預設值的話,那麼就建議不要在加入欄位時設定預設值。之後再使用 UPDATE 來分別更新其內容,然後再以隨後的介紹來更新預設值的設定。
要移除一個欄位,請使用下列指令:
不論資料在該欄位是否消滅,表格的限制條件都會同步再次啓動檢查。所以,如果欄位是被外部鍵所參考的話,PostgreSQL 不會就這樣移除它。你可以宣告同步刪去與此欄位相關的物件,加上 CASCADE:
要加入限制條件,請使用表格限制條件的語法,例如:
要加入 NOT NULL 限制條件的話,就不能寫成表格的限制條件,請使用這樣的語法:
加入的限制條件會立即開始檢查,所以當下的資料內容必須要能符合條件才能加入成功。
要移除限制條件,你需要先知道它的名稱。如果你在宣告時有命名的話,那就使用那個名稱,否則你得找出系統自動命名的名稱。其所使用的指令為「\d tablename」,會列出表格相關的資訊。或使用其他的資料庫工具應該也可以找到它。找到之後請使用下列指令來移除限制條件:
(如果你的限制條件名稱像是「$2」這樣的,不要忘記使用雙引號括住,使其可以正確地被識別為是名稱。)
在移除欄位時,你需要加入 CASCADE,如果你需要同步移除相關的限制條件的話。像是外部鍵就會依賴另一個唯一性限制或主鍵的限制條件。
下面這可以用在移除 NOT NULL 限制的欄位:
(記得 NOT NULL 是沒有名稱的。)
要設定新的欄位預設值,請使用下面指令:
注意這並不會影響到已經存在的資料,只有隨後新增的資料才會使用。
要移除任何預設值,請使用:
這個指令會把預設值設為空值。因為預設值本來就設為空值,所以即使刪去一個未設定預設值欄位的預設值,也不會是一種錯誤。
要變更欄位成為另一個資料型別,請使用下列指令:
這只有在欄位內容可以被自動轉換型別時才會成功。如果存在比較複雜的轉換時,你需要加上 USING 子句來指示如何轉換資料內容。
PostgreSQL 會企圖轉換欄位預設值到任何新的型別,而所有的限制條件也會啓動檢查機制。但這些轉換可能會失敗,也可以產生意外的結果。比較好的作法是,先移除限制條件,再變更資料型別,最後再重新加入適當調整後的限制條件。
要變更某個欄位的名稱:
要變更表格的名稱:
資料型別是一種限制資料如何被儲存在表格中的方式。然而,對許多應用來說,這樣的限制還是不夠細膩。舉個例子,一個欄位包含了產品價格,當然它必須只能是正整數,但並沒有標準的資料型別可以只限制在正整數。另一個需求是,你可能想要限制的條件是依據其他的資料而定。舉例來說,在表格中的產品資訊,每一個產品編號都不能重覆。
所以,SQL 允許你在表格和欄位上定義額外的限制條件,它幫助你對資料有更多的控制能力。當某個使用者輸入資料時,違反了限制條件,錯誤訊息就會產生。這些限制條件也會限制預設值的設定。
使用 CHECK 是最普遍的限制條件製定方式,它可以允許你指定某個欄位必須符合某個布林條件式的判斷。舉個例子,要滿足產品價格是正數的話,你可以使用這樣的語法:
如同你所看到的,限制條件會接在資料型別之後,就像是預設值的設定一樣。預設值和限制條件的設定,在語法撰寫上沒有先後次序。檢查限制條件使用關鍵字 CHECK,然後接著是一組以括號括起來的條件式。其條件式應該要包含被限制的欄位,不然就沒有任何意義。
你也可以讓該限制條件擁有另一個名稱,這樣的好處是,當錯誤訊息發生時,你可以明確得到是哪一個限制被違反了:
如上,給予這個限制條件一個名稱,使用關鍵字 CONSTRAINT,緊接著一個限制條件的定義。(如果你沒有自行命名,系統也會自動取一個名字)
一個限制條件可以參考多個欄位。例如你設定了標準價格和優惠價格,而你需要確保優惠價格一定是比標準價格要便宜的話:
前兩個限制條件和前述很類似,而第三個是新的語法。它並不是只參考某個特定的欄位,而是以逗號分隔列出所有需要遵守的條件。欄位的定義和限制條件的定義,撰寫上沒有規定次序。
我們會說前兩個是欄位的限制,而第三個是表格的限制,因為它是獨立於其他的欄位定義的。欄位限制也可以寫成表格的限制方式,不過反過來通常就不行,因為一個欄位的限制,指的就是只參考到語法上它所接續的欄位而已。(PostgreSQL 並沒有強制這樣做,但如果你的語法與其他資料庫共用的話,最好還是依這樣的語法避免混用。)上面的例子也可以改寫成如此:
或等同於:
都可以照你所喜愛的語法撰寫。
命名表格的限制條件和欄位限制條件的命名是一樣的:
應該要注意的是,檢查限制條件是否成立,端看條件表示式在運算後是真值(true)還是空值(null)。因為當有運算元是空值時,多數的運算結果都是空值,所以可能會有空值產生在想要限制條件的欄位之中。要確保欄位中不會出現空值的話,請參閱下一段的說明。
限制無空值只要以下方的語法設定,就可以限制欄位不得存在空值的輸入:
限制無空值的語法,只能使用在欄位限制上。而限制無空值等效於以 CHECK 建立一個限制條件式為(IS NOT NULL),但在 PostgreSQL 明確使用 NOT NULL 語法的話,處理會更快速。只是它的缺點是你無法給予這樣的限制一個自訂的名稱。
當然,一個欄位可以有一個以上的限制條件。只要一個接著一個即可:
撰寫的次序沒有關係,也不需要去計較限制被檢查的次序。
NOT NULL 有一個相反的語法:NULL。這並非表示欄位裡只能是空值,如果這樣的話就完全沒用處了。其實這是一種簡化,將預設值設定為空值。NULL 語法並不是 SQL 標準的一部份,所以請不要用在可移植式的應用程式裡。(這僅是 PostgreSQL 為了相容其他資料庫而增加的功能)然而,有一些使用者喜歡使用它,因為在程序檔的撰寫上,很容易利用這個語法來切換限制條件。舉個例子,你可以先寫下:
然後在需要的時候再適時加入 NOT 關鍵字即可。
在多數資料庫設計原則上,主要欄位都應該被標示為 NOT NULL。
限制唯一性,確保在某個欄位或某一群欄位的資料,是在該表格中獨一無二的。語法如下:
這是欄位限制的語法。而:
則是表格限制的寫法。
如果想要限制一群欄位的唯一性的話,請使用表格限制的語法,欄位名稱以逗號分隔:
這表示這些欄位所包含的內容組合,在整個表格中是具有唯一性的,但任何一個欄位本身並不一定具備唯一性。
你可以命名唯一性的限制條件,語法如下:
一般來說,唯一性被違反的情況是,所限制的欄位在表格中,有超過一列的資料是相等的。不過,空值並不會被計算在內。這表示說,即使設定了唯一性的限制,在被限制的欄位中,還是有可能會有多個列的資料是空值。這個設計源自 SQL 標準,但聽說有其他的 SQL 資料庫並不是這樣的規則。所以,如果要移植這個語法到其他資料庫的話,要注意這項設計有無差異。
主鍵的意思是,某一個欄位或某一群欄位,在整個表格中,其每一列的組合都是唯一的,且有宣告唯一性的限制條件,並且也包含了非空值的條件(UNIQUE 及 NOT NULL)。所以,下面的兩種語法對資料的意義相同:
主鍵也可以包含多個欄位,語法和宣告唯一限制條件類似:
加入主鍵時,會自動建立一個具唯一性的 B-tree 索引,範圍為 PRIMARY KEY 語法所定義的欄位,並且會強制將這些欄位設定為非空值(NOT NULL)。
一個表格只能有一個主鍵。(你可以使用 UNIQUE 及 NOT NULL 設定多個同樣的限制條件,在功能上幾乎是相同的,但只能有一組條件是由 PRIMARY KEY 所定義。)關連式資料庫的理論指出,每一個表格都必須要有一個主鍵。這個規則在 PostgreSQL 中並不是強制的,但通常建議最好遵循這個理論。
主鍵在用戶端文件式的資料處理上是很有用的。舉個例子,一個圖型化介面讓使用者可以修改資料,那麼可能就需要主鍵來確認每一列的唯一性,而不致於產生混淆。也有一些用途是在資料庫系統的管理上,例如,主鍵會用於外部鍵(Foreign Keys)的處理,使其可以處理表格與表格間的資料對應問題。
外部鍵指的是某個欄位或某一群欄位的內容,必須在另一個表格相對欄位之中,存在相同內容的資料。我們會說這樣的行為是在維護兩個表格之間的關連性。
就使用我們已經使用多次的產品表格吧:
讓我們假設你有一個表格用來儲存這些產品的訂單,我們要確保這些訂單內的產品確實存在。所以我們定義一個外部鍵來關連訂單的表格和產品的表格:
這樣的話,如果 product_no 沒有出現在產品表格的話,就無法建立資料了。
我們會說像這樣的情況是,訂單表格是引用表格(referencing table),而產品表格是參考表格(referenced table)。相對地,欄位也稱為引用欄位(referencing columns)及參考欄位(referenced columns)。
你可以將這個語法簡化為:
因為在參考表格中,不在主鍵欄位組合中的欄位,就是參考欄位。
外部鍵也可以參考一組欄位。一般來說,這樣要寫成表格限制條件形式,如下:
當然,組合外部鍵的欄位數量,彼此之間必須要相等。
你可以給外部鍵一個名稱,使用語法與限制條件相同。
一個表格可以有許多個外部鍵,這用於表格之間多對多的關係。例如你有一些表格記錄了很多產品和訂單,但現在你要讓每一筆訂單也可以訂購多項產品(這在先前的語法並不允許)。你也許可以試試這個表格宣告:
注意到這裡的主鍵和外部鍵是重覆的。
我們知道外部鍵不允許沒有關連到產品的訂單,但如果企圖移除一個有訂單的產品會如何呢?SQL 有幾個選項讓你直覺進行這項操作:
不允許移除被參考到的產品
同時也刪去訂單
其他?
要描繪這些情況,讓我們建立如上需求的多對多關連的結構:當某人要移除一個有訂單的產品(以 order_items 關連)時,我們不允許執行。而如果某人移除了一筆訂單,訂單內的項目也會同步被移除:
引用和同步刪除有兩個常見的作法。用「RESTRICT」防止參考的資料被刪除;「NO ACTION」表示當限制條件被違反時,引用欄位的資料仍會留存,然後回傳錯誤訊息,如果未指定處理方式的話,這會是預設的行為(這兩個語法根本上的不同是「NO ACTION」允許延遲檢查到交易事務的最後,而「RESTRICT」則不會。);「CASCADE」指的是當參考的資料列被刪除時,引用的資料列也會同步被刪除。刪除時還有兩個其他的選項:SET NULL 和 SET DEFAULT,表示引用的資料會被更新為空值或其預設值。注意到,這並不是說你就可以違反限制條件。舉個例來說,如果使用了 SET DEFAULT,但預設值卻違反了外部鍵的限制,這個操作將會失敗。
類似的於 ON DELETE 的情況是 ON UPDATE,也就是在參考欄位的資料內容被更新時的情況。可以設定的動作關鍵字是相同的。在這個情況的 CASCADE 指的就是更新參考欄位的資料內容時,引用欄位的內容也會同步被更新為相同的內容。
一般來說,引用的資料列不需要滿足外部鍵的定義,如果其任一欄位內容為空值的話。而如果「MATCH FULL」加到宣告的語法之中的話,引用的資料列就必須要全部都是空值才不受外部鍵的限制(也就是部份空值的資料列就不受限制)。如果要避免空值使得外部鍵失效的話,就應該宣告相關欄位為 NOT NULL。
外部鍵所參考的欄位必須要是主鍵或是宣告其唯一性限制,這表示參考欄位會有索引存在,這使得檢查關連的過程會是很有效率的。因為在刪除或更新參考資料表時,需要檢查引用資料表的情況,所以在引用表格的欄位建立索引,也是常見的作法。因為這並不是一定需要,而還有許多的選擇在於如何索引,所以宣告外部鍵時並不會自行以引用欄位組合建立索引。
除外宣告要確保的是,如果任意兩個資料列在指定的欄位或表示式被比較時,用於特定的運算子,至少有一個比較會回傳假(false)或空值(null)。語法如下:
加入除外宣告時,將會自動建立相對應的索引。
分割區本身也可以是分割資料表,這稱作為次分割(sub-partitioning)。分割區會擁有它們自己的索引,限制條件,以及預設值,是獨立於其他分割區的。索引必須要分別為每個分割區建立。請參閱 進一步瞭解建立分割資料表及分割區的指令。
一般資料表和分割資料表是無法互相轉換的,但你可以使一個已存放資料的一般資料表或分割資料表成為某個分割資料表的新分割區;或是從某個分割資料表移出某個分割區,使其成為獨立的一般資料表。請參閱 瞭解 ATTACH PARTITION 及 DETACH PARTITION 的使用方式。
分割資料表和個別的分割區之間,隱含著繼承的關係;不過它們並無法使用先前章節所介紹過的繼承功能。舉例來說,分割區不能同時是其他分割資料表的子資料表,一般資料表也不能繼承分割資料表。簡單來說,分割資料表及其分割區,都不能和一般資料表有任何繼承的關係。分割區與分割資料表是階層關係,而其分割區也是繼承的階層,所以所有一般的繼承規則,在 中介紹的,都會成立,除了有一些例外,比較重要的如下:
分割區可以是外部資料表(參閱 ),雖然它會有一些使用上的限制。舉例來說,插入資料到分割資料表,資料並不會轉送到外部資料表的分割區處理。
確定 postgresql.conf 中的 設定並未被關閉。如果是關閉狀態的話,查詢最佳化就不會進行。
確認一下 postgresql.conf 中的 並沒有被關閉。如果被關閉的話,查詢就不會最佳化處理。
請參閱 ,瞭解詳細的處理機制。
加入唯一性的限制條件,將會自動建立一個具唯一性的 B-tree 索引,其包含的欄位就如限制條件中所條列的欄位。這樣唯一性限制的語法並不能只限制某部份列的唯一性,但如果使用「 」的話就可以做到。
關於更新資料與刪除資料的細節在。也可以在 語法說明中,找到更多外部鍵的說明。
詳情請參考 中,CONSTRAINT 到 EXCLUDE 的段落。
PostgreSQL 實作了 SQL/MED 的部份標準,讓你可以存取不在 PostgreSQL 管理下的資料,重點是,你仍然只需要使用 SQL 語法。這樣的資料我們稱作為外部資料。(注意這部份的使用不要和外部鍵搞混了,外部鍵是資料庫內部的一種條件限制。)
外部資料的存取是透過「Foreign data wrapper」(外部資料封裝技術)。外部資料封裝技術是一組函式庫,用於和外部的資料源溝通,它封裝了資料連線和存取資料的細節。有一些外部資料封裝的套件收錄在 contrib 模組之中,參閱附件 F。其他種類的外部封裝套件則由第三方產品提供。如果沒有適合你的資料源的套件的話,你也可以自己寫一個,參閱第 56 章。
要存取外部資料,你需要建立外部服務物件,用它來連結特定的外部資料源,也可以對套件進行一些設定。然後你還需要建立幾個外部資料表,用於定義外部資料的資料結構。外部資料表的使用就如一般的表格一樣,只不過它沒有實際儲存任何資料罷了。當外部資料表被查詢時,PostgreSQL 會透過外部資料封裝套件,從外部資料源取得資料,或者傳送資料到外部,進行更新資料。
存取外部資料可能需要對外部資料源進行認證。這可以利用使用者映對(user mapping)的方法,讓每個 PostgreSQL 使用者在使用部資料表時,可以傳送自己的認證資訊。
進一步的資訊,請參閱 CREATE FOREIGN DATA WRAPPER、CREATE SERVER、CREATE USER MAPPING、CREATE FOREIGN TABLE、IMPORT FOREIGN SCHEMA 等內容。
Schema 在台灣並沒有習慣的中文說法,所以仍使用原文,而不翻譯。
PostgreSQL 資料庫叢集(cluster)可以包含一個或多個資料庫。使用者和群組則是共用於叢集的層次,但沒有任何資料面是在資料庫之間能共用的。任何用戶端連到資料庫服務,都只能存取單一資料庫,你必須在連線時指定一個資料庫。
在叢集內的使用者並不需要對每個資料庫都有使用權。使用者共用指的是它們不能有同名的情況,例如在同一個叢集內,不能有兩個使用者名稱都叫 joe。但系統可以只允許 joe 使用某些叢集內的資料庫。
一個資料庫可以包含一個或多個 schema,它會包含一些資料表。Schema 也可以包含一些資料庫物件,像是資料型別、函數、和運算子。同樣的物件名稱在不同的 schema 中是不會衝突的。舉例來說,schema1 和 myschema 都可以擁有一個叫作 mytable 的資料表。和資料庫不同, schema 並不是完全隔離的:使用者可以直接取用他們連接的資料庫中的任何 schema,只要他們擁有足夠的權限。
使用 schema 有幾個好處:
允許多個使用者存取相同資料庫,而不會互相干擾。
將資料庫物件建立邏輯上的管理層,它們會更有彈性。
第三方的應用結構可以放在不同的 schema 中,避免有撞名的情況產生。
Schema 和作業系統裡的資料夾是類似的,只是它不能使用巢狀結構。
要建立 schema,請使用 CREATE SCHEMA 指令。給予一個自訂的名稱。例如:
要在 schema 中建立或存取某個物件,請使用句點(.)將兩者名稱串連起來:
這個形式在任何可以使用資料表的地方都是可以的,包含資料表結構更新指令,以及在接下來章節會討論到的資料處理指令。(我們只提到資料表的部份,但相同的概念用於其他資料庫物件都是一樣的,像是資料型別和函數。)
實際上,更一般化的語法是:
也可以這樣使用,但目前這只是為了符合 SQL 標準而已。如果你填上了資料庫的名稱,也必須填上你所連線的資料庫而已。
所以,要在新的 schema 中建立一個資料表,請使用:
要移除一個 schema,它必須要是空的,也就是所有所屬物件都已經被移除了,請使用:
但你也可以同步移除 schema 及其所屬物件,請使用:
這個部份的機制請參閱 5.13 節,會深入介紹移除時的問題。
通常你會想要建立一個 schema 給某個使用者使用(這是一種藉由命名空間規畫來限制使用者權限的方法)。可以使用下列語法:
你甚至可以省略 schema 名稱,省略的話,schema 名稱會與使用者名稱相同。請參閱後續的 5.8.6 節來瞭解如何使用。
Schema 名稱以「pg_」開頭的,是系統的保留名稱,使用者不能使用這樣的名稱建立 schema。
在前面我們所建立的資料表都沒有指定 schema 名稱。預設使用的 schema 是「public」,每一個資料庫都會有這個 schema。所以,下面兩種寫法是一樣的:
以及:
完整的名稱寫法是冗長而不容易使用的,通常最好不要把一些特別的 schema 名稱寫到應用程式裡。而資料表時常是以簡要的寫法引用,也就是只寫資料表本身的名稱。資料庫系統依據搜尋路徑的規則找到該資料表。在搜尋路徑上所遇到的第一個資料表就會被使用。如果整個搜尋路徑走完都沒有符合的資料表,那麼才會回報錯誤,即使該資料表名稱有出現在資料庫裡的其他 schema 中。
第一個會被搜尋的 schema,就是目前的 schema。除此之外也用於新的資料表建立,當 CREATE TABLE 未指定 schema 名稱的話,也會依搜尋路徑的 schema 建立。
要顯示目前的搜尋路徑,請使用下面的指令:
預設的情況是:
第一個項目指的就是和目前使用者同名的 schema 會被使用,而如果沒有同名的,它就會被忽略。第二個項目則是先前介紹過的公開 schema。第一個被找到的 schema,就會是新建物件時預設的位置,這就是為什麼預設都會被建立在公開的 schema。當某個物件在使用(資料表結構調整、資料更新、或查詢指令)時沒有註明 schema 的話,那也會使用搜尋路徑來找到符合的物件。不過,預設上只會搜尋公開的 schema。
要設定新的搜尋路徑,請使用:
(我們在這邊暫時忽略掉 $user,因為還沒有立即性的需要。)然後我們就可以試著存取資料表而不用加上 schema:
因為 myschema 在搜尋路徑裡是第一個項目,所以新的物件就會被建立在該處。
我們也可以這樣寫:
這樣的話,不指定的話就不再能夠再使用公開的 schema 了。「public」schema 並沒有比較特別,除了它一開始就會存在之外,它也可以被移除。
請參閱 9.25 節,將會介紹其他設定 schema 搜尋路徑的方式。
搜尋路徑也用於資料型別、函數、及運算子的搜尋,就如同在資料表上的行為一樣。資料型別和函數名稱完整的寫法也和資料表相同。如果你需要特別指出運算子的完整路徑的話,它比較特別,你必須這樣寫:
這是為了避免語法上的混淆。如下所示:
實務上我們都還是依賴路徑搜尋來使用運算子,這樣可以避免使用冗長且低可讀性的程式碼。
預設的情況,使用者無法存取任何不屬於他們的 schema 中的物件。要允許存取的話,該 schema 的擁有者必須要授予 USAGE 權限給其他使用者。要允許其他使用者使用某個 schema 中的物件,通常需要額外給予適當的權限。
使用者想要在其他使用者的 schema 中建立新物件的話,就必須要授予 CREATE 的權限。注意,預設上,所有的使用者在 public schema 中,都具備 CREATE 和 USAGE 權限。這使得所有的使用者在連線到某個資料庫之後,就可以在 public schema 上新增物件。如果你不希望這樣,你可以移除這些權限:
前面的「public」指的是 schema,是一個物件識別器;而後面的「PUBLIC」指的是所有使用者,是一個關鍵字。所以使用不同的大小寫,可以再複習 4.1.1 節的內容。
除了 public 以及使用者自行建立的 schema 之外,每一個資料庫還有一個稱作 pg_catalog 的 schema,它包含了系統資訊的資料表和內建的資料型別、函數、及運算子。 pg_catlog 永遠都都是搜尋路徑裡的有效項目。它沒有明確地顯示在搜尋路徑裡,但卻是隱含優先搜尋,在那些明定的搜尋項目之前。這是為了確保內建的物件的名稱都能被找到。然而,你可以把 pg_catlog 放在搜尋路徑的最後面,如果你希望自訂的同名物件能優先被使用的話。
系統用的資料表都以「pg_」開頭,為的就是確保不會有衝突的情況出現,以免將來新的系統資料表和你現在所定義的資料表同名。(以預設的搜尋路徑來說,一個簡單的資料表使用,會直接被同名的系統資料表取代。)系統資料表會一直遵循這個命名規則,就不會產生衝突,只要使用者不使用「 pg_」開頭的命名方式就好了。
Schema 可以在許多方面協助你組織你的資料。有一些巧妙的樣版值得推薦,也很方便以預設的方式支援:
如果你沒有建立任何 schema 的話,那麼所有使用者就是隱含著都使用 public schema。這種情況指的是都沒有設定任何 schema,而主要推薦給在一個資料庫中,只有一個使用者的情況。這樣的樣版設定也適合之後轉換到無 schema 設計的資料庫環境。
你可以為每一個使用者建立一個同名的 schema。回想一下先前介紹的預設搜尋路徑,第一個項目就是 $user,表示該使用者的名稱。所以,每一個使用者有一個專屬的 schema,預設上,他們就只存取他們所擁有的 schema。 如果你使用這個情境樣版,你也許會需要移除 public schema 的權限,甚至直接移除它,讓使用者真正被隔離在他們自己的 schema 中。
要安裝共享的應用程式(每個人共享資料表,有一些第三方提供的延伸套件,或其他的東西。),把他們放到不同的 schema 裡,然後記得要設定好適當的存取權限。使用者可以使用完整的名稱來存取這些共享的應用程式,或把他們加入到搜尋路徑中,由使用者自己來決定。
在標準 SQL 中,在同一個 schema 中的物件,分別被不同使用者擁有,是不被允許的。然而,有一些實作系統甚至不允許使用者建立和自己不同名的 schema。事實上,schema 和使用者的概念,對於只支援基本 schema 的資料庫系統本身而言,幾乎是相同的。所以,許多使用者會認為完整名稱指的是 user_name.table_name。這也就是為什麼 PostgreSQL 建議你這樣為每一個使用者建立他們同名的 schema。
再者,在標準 SQL 裡,也沒有所謂 public schema 的概念。極致相容標準的話,你就不應該使用,或移除 public schema。
當然,也有些 SQL 資料庫並沒有實作 schema,或提供其他跨資料庫存取的命名方式。如果你需要和這些系統共同運作,要提高可攜性的方式就是不要使用任何 schema。
每一個表格都有幾個系統欄位,而它們是由資料庫系統預先定義好的,所以使用者在定義欄位名稱時,不能使用這些名字。(這些限制並不是因為它們是保留關鍵字,所以就算用引號括起來也不能使用。)但在一般使用時,你也不需要特別考慮這些欄位,只要瞭解會有這些欄位存在就好。
oid
每一個資料列會有一個 Object ID,不過這個欄位只有在建立表格時,加上 WITH OIDS 語法才能使用。或者也可以藉由參數 default_with_oids 來切換使用。這個欄位的型別是 oid(和欄位名相同)。參閱 8.18 節瞭解詳細資訊。
tableoid
每個表格也有一個 ID 也會記錄在每一個資料列中。這個欄位特別方便在取得表格的繼承結構(參閱 5.9 節),如果沒有這個欄位的話,要去找出資料列的來源就會很麻煩。tableoid 可以參考 pg_class 表格中的 oid 欄位,進一步取得表格的名稱。
xmin
這指的是資料列在插入資料的版本資訊。(每一個資料列的版本,都是一個獨立的資料狀態;每一次資料的更新,都會在邏輯層產生一個新的資料列版本。)
cmin
指令識別碼,會存在於新增資料的交易中。(從 0 開始)
xmax
刪除資料的交易版本資訊,如果是 0 的話,代表讓資料列不是刪除中的資料列版本。這通常是用來指出某個刪除的交易還未被完成,或某個刪除正在被回復。
cmax
指令識別碼,有數字的話表示一個刪除的交易指令,或是 0。
ctid
表示每一個資料列存在於該表格的實體位址。注意到的是,雖然 ctid 可以用來快速找到特定的資料列版本,但 ctid 是會改變的,如果有執行過 VACUUM FULL 的話。所以 ctid 如果要用於固定的資料定位的話,是不應該被考慮的選項。OID 或額外自訂序列數字,更適合用於分別邏輯上的資料列。
OID 是一個 32 位元的數字,以 cluster 為單位配發。在一個大型或長期使用的資料庫中,是有可能出現重覆的情況。所以,假設 OID 是唯一的識別是不正確的觀念,除非你還有搭配其他方法來確保唯一性。如果你需要識別表格中的資料列的話,使用序列數產生器是比較建議的作法。OID 也可以這樣用來得到一些額外的預防性功能:
唯一性的限制應該設定在 OID 欄位上,來確保每一個 OID 可以識別每一個資料列。當有唯一性限制存在的時候,對於已經存在的資料列就不會有重覆的 OID。(當然,這方法只能用於資料筆數在 40 億筆以下的表格。不過實務上的表格多數都少於這個數目,而且太多資料的話,效果也會變得很差。)
OID 在多個表格間就不能假設為是唯一,你應該搭配 tableoid 來識別資料庫層級的唯一性。
當然,在建立表格時必須要加入 WITH OIDS 語法。在 PostgreSQL 8.1 之前,WITHOUT OIDS 是預設值。
交易識別碼也是 32 位元的數字。在一個長期運行的資料庫中,交易識別碼也可能會重覆。只要有適當的管理機制的話,這並不會是什麼嚴重的問題,詳情請參閱第 24 章。然而,長期來說(超過 10 億個交易),假定交易識別碼的唯一性是不明智的作法。
指令識別碼也是 32 位元的數字,其絕對上限是約 40 億個指令在一個交易當中,實務上這個限制並不會是問題。注意到這個限制是 SQL 指令數量的限制,而不是處理資料的限制。只有真正有改變資料庫內容的指令才會有指令識別碼。
當你建立了一個複雜的資料庫結構,包含了許多資料表,也設計了許多外部索引鍵、檢視表、觸發事件、函數.....等等。也就是說,其實你建立了一堆物件之間的關連性。舉例來說,資料表的外部索引鍵就與另一個資料表有著參考的關連性。
要維護整個資料庫結構的完整性,PostgreSQL 得確保你不能在有關連性的情況下,隨意刪去物件。舉例來說,企圖刪去在 5.3.5 節中,我們所使用過的產品資料表,而訂單資料表與其有相依的關連性,那就會產生如下的錯誤訊息:
這個錯誤訊息包含了很有用的指引:如果你不想要一個個處理其相依關連性,那可以一次刪去他們:
如此所有相依的物件就會被刪除了,所有相互依存的物件都會,是遞迴式的處理流程。在這個例子中,它不會移除訂單資料表,只會移除外部索引鍵的限制條件,因為沒有其他物件與該外部索引鍵相依。(如果你要確認 DROP ... CASCADE 會處理哪些物件,你可以用 DETAIL 取代 CASCADE,就會輸出其相依的物件。)
幾乎所有 PostgreSQL 的 DROP 指令都支援 CASCADE 的用法。當然,有些自然的關連性是和物件型別有關。你也可以使用 RESTRICT 來取代 CASCADE 的位置,以強制以預設的行為來處理,也就是絕對不會刪去其他相關的物件。
根據 SQL 標準,不論是 RESTRICT 或 CASCADE,都必須要在 DROP 指令中明確表示,但沒有任何一套資料庫系統真的這樣設計。不過,都會內定預設行為是 RESTRICT 或 CASCADE,每個資料庫不同。
如果 DROP 指令列出了多個物件,CASCADE 只有在這些物件之外還有相依性時才會需要。舉個例子,當執行「DROP TABLE tab1, tab2」時,即使 tab1 與 tab2 之間有外部索引鍵的相依關係,而沒有指定 CASCADE,這個操作也會完成。
對於使用者自訂的函數來說,PostgreSQL 會引用函數的外顯屬性來判斷其相依性,例如函數的參數或輸出型態,但函數內部執行的相依關係就無法追蹤了。舉個列子:
(參閱 37.4 節,瞭解 SQL 語言的函數。)PostgreSQL 會知道 get_color_note 函數相依於 rainbow 資料型別:也就是刪去該資料型別時,也會強制要刪去該函數,因為它的參數將不再合法。但 PostgreSQL 就無法發現 get_color_note 和 my_colors 之間的關連性,當該資料表被移除時,此函數並不會跟著被移除。這種情況有好有壞,函數基本上還是合法的,即使內含的資料表不存在的話,頂多就是執行會出錯就是了,只要再建立該名稱的資料表就可以讓這個函數重新正常運作。
當一個資料庫物件被建立時,它會先指定存取權限給擁有者,而擁有者一般來說就是執行建立指令的使用者。對大多數的資料庫物件來說,其預設的狀態就是只有擁有者(或超級使用者)可以對該物件進行所有操作。要讓給其他使用者來操作的話,就必須進行授權的動作。
有很多不同種類的權限:SELECT、INSERT、UPDATE、DELETE、TRUNCATE、REFERENCES、TRIGGER、CREATE、CONNECT、TEMPORARY、EXECUTE、USAGE。這些權限對於不同物件的效果,會因為是哪一種物件而有所差別(表格、函式...等等)。要瞭解完整在 PostgreSQL 中所支援的各種物件權限,請參考 GRANT 語法頁面。這裡的內容主要說明如何使用。
修改和移除一個資料庫物件,是只有擁有者才具備的權力。
要把一個物件被指派給一個新的擁有者的話,使用該物件的 ALTER 指令,例如:ALTER TABLE。超級使用者也可以做指派的動作;原來的擁有者如果它仍是該物件的管理群組一員的話,當然也可以;再來就管理群組新的成員。
要進行授權行為的話,請使用 GRANT 指令。舉例來說,如果 joe 是一個使用者,而 accounts 是一個表格,要讓他可以獲得更新表格資料的權力:
使用 ALL 的權限,就代表授權所有可設定的權限。
有一個特別的使用者是 PUBLIC,代表的是系統內的所有使用者。當資料庫內有很多使用者時,可以制定「群組(group)」來簡化管理。這部份詳細的說明請參閱第 21 章。
要移除權限,請使用 REVOKE 指令:
物件擁有者的特殊權限(例如DROP、GRANT、REVOKE...等)都是和擁有者一起設定,而無法單獨授權。不過,擁有者可以選擇移除自己的權限,例如建立一個唯讀的表格,讓自己和其他人一樣。
回到前面所說的,只有物件的擁有者(或超級使用者)可以變更該物件的權限。然而,也可以使用「with grant option」讓另一個使用者可以代授權給其他使用者。不過如果這個「grant option」被移除時,所有被代授權的使用者都會同時喪失該權限。更詳細的說明請參閱 GRANT 及 REVOKE 說明頁面。