支援的版本:目前版本 (17) / 16 / 15 / 14 / 13
開發版本:devel
不支援的版本:12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2 / 8.1 / 8.0

8.16. 複合類型 #

複合類型 代表一個列或記錄的結構;它本質上只是一個欄位名稱及其資料類型的列表。PostgreSQL 允許複合類型以與簡單類型相同的方式使用。例如,可以將表格的欄位宣告為複合類型。

8.16.1. 複合類型的宣告 #

以下是定義複合類型的兩個簡單範例

CREATE TYPE complex AS (
    r       double precision,
    i       double precision
);

CREATE TYPE inventory_item AS (
    name            text,
    supplier_id     integer,
    price           numeric
);

此語法與 CREATE TABLE 類似,但只能指定欄位名稱和類型;目前無法包含任何約束(例如 NOT NULL)。請注意,AS 關鍵字是必不可少的;如果沒有它,系統會認為是指另一種類型的 CREATE TYPE 命令,並且您會收到奇怪的語法錯誤。

定義類型後,我們可以使用它們來建立表格

CREATE TABLE on_hand (
    item      inventory_item,
    count     integer
);

INSERT INTO on_hand VALUES (ROW('fuzzy dice', 42, 1.99), 1000);

或函數

CREATE FUNCTION price_extension(inventory_item, integer) RETURNS numeric
AS 'SELECT $1.price * $2' LANGUAGE SQL;

SELECT price_extension(item, 10) FROM on_hand;

每當您建立表格時,也會自動建立一個複合類型,其名稱與表格相同,以代表表格的列類型。 例如,如果我們說

CREATE TABLE inventory_item (
    name            text,
    supplier_id     integer REFERENCES suppliers,
    price           numeric CHECK (price > 0)
);

那麼上面顯示的相同 inventory_item 複合類型將會作為副產品出現,並且可以像上面一樣使用。 但是請注意目前實作的一個重要限制:由於複合類型沒有與其關聯的約束,因此表格定義中顯示的約束不適用於表格外部的複合類型的值。(要解決此問題,請在複合類型上建立一個網域domain,並將所需的約束作為網域的 CHECK 約束應用。)

8.16.2. 建構複合值 #

要將複合值寫為文字常數,請將欄位值括在括號中,並用逗號分隔它們。 您可以在任何欄位值周圍加上雙引號,如果它包含逗號或括號,則必須這樣做。(更多詳細資訊請參閱下方。)因此,複合常數的一般格式如下

'( val1 , val2 , ... )'

一個例子是

'("fuzzy dice",42,1.99)'

這將是上面定義的 inventory_item 類型的有效值。 要使欄位為 NULL,請在其在列表中的位置寫入任何字元。 例如,此常數指定一個 NULL 的第三個欄位

'("fuzzy dice",42,)'

如果您想要一個空字串而不是 NULL,請寫入雙引號

'("",42,)'

這裡的第一個欄位是一個非 NULL 的空字串,第三個是 NULL。

(這些常數實際上只是在第 4.1.2.7 節中討論的通用類型常數的特殊情況。該常數最初被視為一個字串,並傳遞給複合類型輸入轉換例程。可能需要顯式類型規範來告訴將常數轉換為哪種類型。)

ROW 表達式語法也可以用於建構複合值。在大多數情況下,這比字串文字語法更容易使用,因為您不必擔心多個層次的引用。我們上面已經使用了這種方法

ROW('fuzzy dice', 42, 1.99)
ROW('', 42, NULL)

只要表達式中有一個以上的欄位,ROW 關鍵字實際上是可選的,因此這些可以簡化為

('fuzzy dice', 42, 1.99)
('', 42, NULL)

第 4.2.13 節中更詳細地討論了 ROW 表達式語法。

8.16.3. 存取複合類型 #

要存取複合欄位的欄位,需要寫一個點和欄位名稱,很像從表格名稱中選擇欄位。 事實上,它非常像從表格名稱中選擇,以至於您經常需要使用括號來避免混淆解析器。 例如,您可以嘗試從我們的 on_hand 範例表格中選擇一些子欄位,如下所示

SELECT item.name FROM on_hand WHERE item.price > 9.99;

這將不起作用,因為根據 SQL 語法規則,名稱 item 被視為表格名稱,而不是 on_hand 的欄位名稱。 您必須這樣寫

SELECT (item).name FROM on_hand WHERE (item).price > 9.99;

或者,如果您需要同時使用表格名稱(例如,在多表格查詢中),如下所示

SELECT (on_hand.item).name FROM on_hand WHERE (on_hand.item).price > 9.99;

現在,括號中的物件被正確地解釋為對 item 欄位的引用,然後可以從中選擇子欄位。

當您從複合值中選取欄位時,也會遇到類似的語法問題。例如,若要僅從傳回複合值的函式結果中選取一個欄位,您需要撰寫類似以下的內容:

SELECT (my_func(...)).field FROM ...

如果沒有額外的括號,將會產生語法錯誤。

特殊欄位名稱 * 代表所有欄位,如第 8.16.5 節中進一步說明。

8.16.4. 修改複合類型 #

以下是一些插入和更新複合欄位的正確語法範例。首先,插入或更新整個欄位:

INSERT INTO mytab (complex_col) VALUES((1.1,2.2));

UPDATE mytab SET complex_col = ROW(1.1,2.2) WHERE ...;

第一個範例省略了 ROW,第二個範例使用了它;我們可以用任何一種方式。

我們可以更新複合欄位的個別子欄位:

UPDATE mytab SET complex_col.r = (complex_col).r + 1 WHERE ...;

請注意,在 SET 之後直接出現的欄位名稱,我們不需要(而且實際上也不能)在其周圍加上括號,但當在等號右邊的表達式中引用同一個欄位時,我們需要加上括號。

我們也可以將子欄位指定為 INSERT 的目標:

INSERT INTO mytab (complex_col.r, complex_col.i) VALUES(1.1, 2.2);

如果我們沒有為該欄位的所有子欄位提供值,則剩餘的子欄位將會填入空值。

8.16.5. 在查詢中使用複合類型 #

在查詢中,複合類型具有各種特殊的語法規則和行為。這些規則提供了有用的快捷方式,但如果您不了解其背後的邏輯,可能會感到困惑。

PostgreSQL 中,在查詢中引用表格名稱(或別名)實際上是對該表格目前列的複合值的引用。例如,如果我們有一個如上方所示的 inventory_item 表格,我們可以撰寫:

SELECT c FROM inventory_item c;

這個查詢產生一個單一複合值的欄位,因此我們可能會得到類似以下的輸出:

           c
------------------------
 ("fuzzy dice",42,1.99)
(1 row)

但請注意,簡單的名稱會先與欄位名稱比對,然後才與表格名稱比對,因此只有當查詢的表格中沒有名為 c 的欄位時,此範例才有效。

一般的限定欄位名稱語法 table_name.column_name 可以理解為將欄位選取應用於表格目前列的複合值。(基於效率考量,實際上並非以這種方式實作。)

當我們撰寫:

SELECT c.* FROM inventory_item c;

那麼,根據 SQL 標準,我們應該將表格的內容擴展為個別的欄位:

    name    | supplier_id | price
------------+-------------+-------
 fuzzy dice |          42 |  1.99
(1 row)

如同查詢:

SELECT c.name, c.supplier_id, c.price FROM inventory_item c;

PostgreSQL 會將此擴展行為應用於任何複合值表達式,但如上方所示,當 .* 應用於的值不是簡單的表格名稱時,您需要將該值加上括號。例如,如果 myfunc() 是一個傳回具有欄位 abc 的複合類型的函式,則以下兩個查詢具有相同的結果:

SELECT (myfunc(x)).* FROM some_table;
SELECT (myfunc(x)).a, (myfunc(x)).b, (myfunc(x)).c FROM some_table;

提示

PostgreSQL 透過將第一種形式轉換為第二種形式來處理欄位擴展。因此,在此範例中,無論使用哪種語法,myfunc() 每列都會被呼叫三次。如果它是一個昂貴的函式,您可能希望避免這種情況,您可以使用類似以下的查詢來做到這一點:

SELECT m.* FROM some_table, LATERAL myfunc(x) AS m;

將函式放在 LATERAL FROM 項目中可以防止它每列被呼叫多次。m.* 仍然會擴展為 m.a, m.b, m.c,但現在這些變數只是對 FROM 項目的輸出的引用。(此處 LATERAL 關鍵字是可選的,但我們顯示它以闡明該函式正在從 some_table 取得 x。)

composite_value.* 語法出現在 SELECT 輸出清單INSERT/UPDATE/DELETE/MERGE 中的 RETURNING 清單VALUES 子句列建構子的最上層時,會導致這種欄位擴展。在所有其他情況下(包括巢狀於這些結構之一內部),將 .* 附加到複合值不會改變該值,因為它表示所有欄位,因此會再次產生相同的複合值。例如,如果 somefunc() 接受複合值的引數,則以下查詢是相同的:

SELECT somefunc(c.*) FROM inventory_item c;
SELECT somefunc(c) FROM inventory_item c;

在這兩種情況下,inventory_item 的目前列都會作為單一複合值的引數傳遞給函式。即使 .* 在這種情況下沒有任何作用,使用它仍然是一種良好的風格,因為它可以清楚地表明意圖使用複合值。特別是,剖析器會將 c.* 中的 c 視為表格名稱或別名,而不是欄位名稱,因此不會有歧義;而如果沒有 .*,則不清楚 c 是指表格名稱還是欄位名稱,而且如果有名為 c 的欄位,則實際上會優先選擇欄位名稱的解釋。

另一個演示這些概念的範例是,所有這些查詢都表示相同的含義:

SELECT * FROM inventory_item c ORDER BY c;
SELECT * FROM inventory_item c ORDER BY c.*;
SELECT * FROM inventory_item c ORDER BY ROW(c.*);

所有這些 ORDER BY 子句都指定了列的複合值,從而根據第 9.25.6 節中描述的規則對列進行排序。但是,如果 inventory_item 包含一個名為 c 的欄位,則第一種情況將與其他情況不同,因為它將表示僅按該欄位排序。給定先前顯示的欄位名稱,這些查詢也等同於上面的查詢:

SELECT * FROM inventory_item c ORDER BY ROW(c.name, c.supplier_id, c.price);
SELECT * FROM inventory_item c ORDER BY (c.name, c.supplier_id, c.price);

(最後一種情況使用省略關鍵字 ROW 的列建構子。)

與複合值相關的另一個特殊語法行為是,我們可以使用函式標記法來提取複合值的欄位。解釋這一點的簡單方法是,標記法 field(table)table.field 是可以互換的。例如,以下查詢是等效的:

SELECT c.name FROM inventory_item c WHERE c.price > 1000;
SELECT name(c) FROM inventory_item c WHERE price(c) > 1000;

此外,如果我們有一個接受複合類型單一引數的函式,我們可以使用任何一種標記法來呼叫它。這些查詢都是等效的:

SELECT somefunc(c) FROM inventory_item c;
SELECT somefunc(c.*) FROM inventory_item c;
SELECT c.somefunc FROM inventory_item c;

函式標記法和欄位標記法之間的這種等效性使得可以使用複合類型的函式來實作計算欄位 使用上述最後一個查詢的應用程式不需要直接知道 somefunc 不是表格的真實欄位。

提示

由於這種行為,給一個接受單一複合型別參數的函數,與該複合型別的任何欄位賦予相同的名稱是不明智的。如果存在歧義,當使用欄位名稱語法時,將選擇欄位名稱的解釋;而當使用函數呼叫語法時,將選擇函數。然而,在 PostgreSQL 11 之前的版本總是選擇欄位名稱的解釋,除非呼叫的語法要求它必須是函數呼叫。在舊版本中,強制使用函數解釋的一種方法是用綱要限定函數名稱,也就是寫成 schema.func(compositevalue)

8.16.6. 複合型別的輸入和輸出語法 #

複合值的外部文字表示由根據個別欄位型別的 I/O 轉換規則解釋的項目組成,再加上指示複合結構的裝飾。此裝飾包含包圍整個值的括號(()),以及相鄰項目之間的逗號(,)。括號外的空格會被忽略,但在括號內,它被視為欄位值的一部分,並且可能重要也可能不重要,這取決於欄位資料型別的輸入轉換規則。例如,在

'(  42)'

如果欄位型別是整數,則空格將被忽略,但如果欄位型別是文字,則不會被忽略。

如先前所示,在編寫複合值時,您可以在任何個別欄位值周圍加上雙引號。如果欄位值在其他情況下會混淆複合值剖析器,則您必須這樣做。特別是,包含括號、逗號、雙引號或反斜線的欄位必須用雙引號引起來。要在帶引號的複合欄位值中放入雙引號或反斜線,請在其前面加上反斜線。(此外,帶引號的欄位值中的一對雙引號被認為表示雙引號字符,類似於 SQL 字串文字中單引號的規則。)或者,您可以避免使用引號,並使用反斜線跳脫來保護所有原本會被視為複合語法的資料字符。

完全空的欄位值(逗號或括號之間根本沒有字符)表示 NULL。若要寫入一個空字串而不是 NULL 的值,請寫入 ""

如果欄位值是空字串或包含括號、逗號、雙引號、反斜線或空白,則複合輸出常式將在欄位值周圍加上雙引號。(這樣做對於空白來說不是必要的,但有助於提高可讀性。)嵌入在欄位值中的雙引號和反斜線將會加倍。

注意

請記住,您在 SQL 命令中寫入的內容會首先被解釋為字串文字,然後再被解釋為複合。這會使您需要的反斜線數量加倍(假設使用跳脫字串語法)。例如,若要在複合值中插入一個包含雙引號和反斜線的 text 欄位,您需要寫入

INSERT ... VALUES ('("\"\\")');

字串文字處理器會移除一層反斜線,因此到達複合值剖析器的內容看起來像 ("\"\\")。反過來,提供給 text 資料型別輸入常式的字串會變成 "\。(如果我們使用一個輸入常式也特別處理反斜線的資料型別,例如 bytea,我們可能需要在命令中使用多達八個反斜線,才能將一個反斜線放入儲存的複合欄位中。)美元符號引用(請參閱第 4.1.2.4 節)可用於避免需要加倍反斜線。

提示

在 SQL 命令中編寫複合值時,ROW 結構語法通常比複合文字語法更容易使用。在 ROW 中,個別欄位值的寫法與不是複合成員時的寫法相同。

提交更正

如果您在文件中看到任何不正確、與您對特定功能的體驗不符或需要進一步說明的地方,請使用此表單來回報文件問題。