2025年9月25日: PostgreSQL 18 釋出!
支援的版本:當前 (18) / 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 複合型別將作為副產品產生,並且可以像上面一樣使用。但請注意當前實現的一個重要限制:由於複合型別不關聯任何約束,因此表定義中顯示的約束 不適用於 表外的複合型別值。(為解決此問題,請在複合型別上建立一個 ,並將所需的約束作為域的 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)

ROW 表示式語法將在 第 4.2.13 節 中更詳細地討論。

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);

如果我們沒有為列的所有子欄位提供值,則剩餘的子欄位將用 NULL 值填充。

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/MERGERETURNING 列表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 中,單個欄位值會按照它們不作為複合值成員時的寫法來編寫。

提交更正

如果您在文件中看到任何不正確、與您對特定功能的經驗不符或需要進一步澄清的內容,請使用 此表單 來報告文件問題。