全部產品
Search
文件中心

PolarDB:組合類別型

更新時間:Jul 06, 2024

本文介紹了組合類別型的定義及相關文法。

一個組合類別型表示一行或一個記錄的結構,它本質上就是一個網域名稱和它們資料類型的列表。本資料庫允許把組合類別型用在很多能用簡單類型的地方。例如,一個表的一列可以被聲明為一種組合類別型。

組合類別型的聲明

這裡有兩個定義組合類別型的簡單例子:

    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約束)。

構造組合值

要把一個組合值寫作一個文字常量,將該域值封閉在圓括弧中並且用逗號分隔它們。你可以在任何域值周圍放上雙引號,並且如果該域值包含逗號或圓括弧則必須這樣做。這樣,一個組合常量的一般格式是下面這樣的:

    '( val1 , val2 , ... )'

一個例子是:

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

這將是上文定義的inventory_item類型的一個合法值。要讓一個域為 NULL,在列表中它的位置上根本不寫字元。例如,這個常量指定其第三個域為 NULL:

    '("fuzzy dice",42,)'

如果你寫一個Null 字元串而不是 NULL,寫上兩個引號:

    '("",42,)'

這裡第一個域是一個非 NULL Null 字元串,第三個是 NULL。

ROW運算式也能被用來構建組合值。在大部分情況下,比起使用字串文法,這相當簡單易用,因為你不必擔心多層引用。我們已經在上文用過這種方法:

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

只要在運算式中有多於一個域,ROW 關鍵詞實際上就是可選的,因此這些可以被簡化成:

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

訪問組合類別型

要訪問一個組合列的一個域,可以寫成一個點和域的名稱,更像從一個表名中選擇一個域。事實上,它太像從一個表名中選擇,這樣我們不得不使用圓括弧來避免讓解析器混淆。例如,你可能嘗試從例子表on_hand中選取一些子域:

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

這不會有用,因為名稱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 ...

如果沒有額外的圓括弧,這將產生一個語法錯誤。

特殊的網域名稱稱``表示“所有的域”。

修改組合類別型

這裡有一些插入和更新群組合列的正確文法的例子。首先,插入或者更新一整個列:

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

如果我們沒有為該列的所有子域提供值,剩下的子域將用空值填充。

在查詢中使用組合類別型

對於查詢中的組合類別型有各種特殊的文法規則和行為。這些規則提供了有用的捷徑,但是如果你不懂背後的邏輯就會被此困擾。

在本資料庫中,查詢中對一個表名(或別名)的引用實際上是對該表的當前行的組合值的引用。例如,如果我們有一個如上所示的表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;

儘管如上所示,本資料庫將對任何組合值運算式應用這種展開行為,但只要.所應用的值不是一個簡單的表名,你就需要把該值寫在圓括弧內。例如,如果myfunc()是一個返回組合類別型的函數,該組合類別型由列abc組成,那麼這兩個查詢有相同的結果:

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

本資料庫實際上通過將第一種形式轉換為第二種來處理列展開。因此,在這個例子中,用兩種文法時對每行都會調用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中的一個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子句指定該行的組合值對行進行排序。不過,如果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不是一個真實的表列。

說明

由於這種行為,讓一個接受單一組合類別型參數的函數與該組合類別型的任意欄位具有相同的名稱是不明智的。出現歧義時,如果使用了欄位名文法,則欄位名解釋將被選擇,而如果使用的是函數調用文法則會選擇函數解釋。不過,本資料庫在版本 11 之前總是選擇欄位名解釋,除非該調用的文法要求它是一個函數調用。在老的版本中強制函數解釋的一種方法是用方案限定函數名,也就是寫成schema``.``func``(``compositevalue``)

組合類別型輸入和輸出文法

一個組合值的外部文本表達由根據域類型的 I/O 轉換規則解釋的項,外加指示組合結構的裝飾組成。裝飾由整個值周圍的圓括弧(()),外加相鄰項之間的逗號(,)組成。圓括弧之外的空格會被忽略,但是在圓括弧之內空格會被當成域值的一部分,並且根據域資料類型的輸入轉換規則可能有意義,也可能沒有意義。例如,在 '( 42)'中,如果域類型是整數則空格會被忽略,而如果是文本則空格不會被忽略。

如前所示,在寫一個組合值時,你可以在任意域值周圍寫上雙引號。如果不這樣做會讓域值迷惑組合值解析器,你就必須這麼做。特別地,包含圓括弧、逗號、雙引號或反斜線的域必須用雙引號引用。要把一個雙引號或者反斜線放在一個被引用的組合域值中,需要在它前面放上一個反斜線(還有,一個雙引號引用的域值中的一對雙引號被認為是表示一個雙引號字元,這和 SQL 字串中單引號的規則類似)。另一種辦法是,你可以避免引用以及使用反斜線轉義來保護所有可能被當作組合文法的資料字元。

一個全空的域值(在逗號或圓括弧之間完全沒有字元)表示一個 NULL。要寫一個Null 字元串值而不是 NULL,可以寫成""

如果域值是空串或者包含圓括弧、逗號、雙引號、反斜線或空格,組合輸出常式將在域值周圍放上雙引號(對空格這樣處理並不是不可缺少的,但是可以提高可讀性)。嵌入在域值中的雙引號及反斜線將被雙寫。

記住你在一個 SQL 命令中寫的東西將首先被解釋為一個字串,然後才會被解釋為一個組合。這就讓你所需要的反斜線數量翻倍(假定使用了逸出字元串文法)。例如,要在組合值中插入一個含有一個雙引號和一個反斜線的text域,你需要寫成:

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

字串處理器會移除一層反斜線,這樣在組合值解析器那裡看到的就會是("\"\\")。接著,字串被交給text資料類型的輸入常式並且變成"\(如果我們使用的資料類型的輸入常式也會特別處理反斜線,例如bytea,在命令中我們可能需要八個反斜線用來在組合域中儲存一個反斜線)。美元引用可以被用來避免雙寫反斜線。

說明

當在 SQL 命令中書寫組合值時,ROW構造器文法通常比組合文字文法更容易使用。在ROW中,單個域值可以按照平時不是組合值成員的寫法來寫。