8.10. 數組

PostgreSQL 允許記錄的字段定義成 定長或不定長的多維數組.數組類型可以是任何基本類型或用戶定義類型.

8.10.1. 數組類型的聲明

為說明這些用法,我們先創建一個由基本類型數組構成的表:

CREATE TABLE sal_emp (
    name            text,
    pay_by_quarter  integer[],
    schedule        text[][]
);

如上所示,一個數組類型是通過在數組元素類型名後面附加方括弧 ([]) 來命名的. 上面的命令將創建一個叫 sal_emp 的表,它的字段中有一個 text 類型字符串(name), 一個一維 integer型數組 (pay_by_quarter), 代表雇員的季度薪水和一個兩維 text 類型數組(schedule), 表示雇員的週計劃.

CREATE TABLE 的語法允許聲明數組的確切大小,比如:

CREATE TABLE tictactoe (
    squares   integer[3][3]
);

不過,目前的實現並不強制數組尺寸限制--其行為和用于未聲明長度的 數組相同。

實際上,目前的聲明也不強制數組維數。特定元素類型的數組都被認為是 相同的類型,不管他們的大小或者維數。因此,在 CREATE TABLE 裡定義數字或者維數都只是簡單的文檔,它並不影響運行時的行為。

另外,我們還可以使用 SQL99 標準的語法來聲明一維的數組。 pay_by_quarter 可以定義為:

    pay_by_quarter  integer ARRAY[4],

這個語法要求一個整數常量表示數組尺寸。不過,和以前一樣,PostgreSQL 並不強制這個尺寸限制。

8.10.2. 數組值輸入

把一個數組數值寫成一個文本值的時候, 我們用花括弧把數值括起來並且用逗號將它們分開. (如果你懂 C,那麼這與初始化一個結構很像。) 你可以在任何數組值週圍放置雙引號,如果這個值包含逗號或者花括弧, 那麼你就必須加上雙引號。(下面有更多細節。)因此,一個數組常量 的常見格式如下:

'{ val1 delim val2 delim ... }'

這裡的 delim 是該類型的分隔符,就是那個 在它的 pg_type 記錄裡指定的那個。(對于所有內置類型, 它就是逗號分隔符","。)每個 val 要麼是一個數組元素類型的常量,要麼是一個子數組。一個數組常量的例子是

'{{1,2,3},{4,5,6},{7,8,9}}'

這個常量是一個兩維的,3乘3的數組,由三個整數子數組組成。

(這種數組常量實際上只是我們在 Section 4.1.2.4 裡討論過的一般類型常量的一種特例。常量最初是當作字串看待並且傳遞給數組 輸入轉換過程。可能需要我們用明確的類型聲明。)

現在我們可以顯示一些 INSERT 語句。

INSERT INTO sal_emp
    VALUES ('Bill',
    '{10000, 10000, 10000, 10000}',
    '{{"meeting", "lunch"}, {}}');

INSERT INTO sal_emp
    VALUES ('Carol',
    '{20000, 25000, 25000, 25000}',
    '{{"talk", "consult"}, {"meeting"}}');

目前的數組實現的一個局限是一個數組的獨立元素不能是 SQL 空值。 整個數組可以設置為空,但是你不能有這麼一個數組,裡面有些元素是空, 而有些不是。

這個性質可能導致奇怪的結果。比如,前面的兩個插入的結果看起來像這樣:

SELECT * FROM sal_emp;
 name  |      pay_by_quarter       |      schedule
-------+---------------------------+--------------------
 Bill  | {10000,10000,10000,10000} | {{meeting},{""}}
 Carol | {20000,25000,25000,25000} | {{talk},{meeting}}
(2 rows)

因為 schedule[2][2] 元素在每個 INSERT 語句裡都不見了,所以,[1][2] 元素就被拋棄了。

注意: 修補這個問題在 to-do 列表裡。

我們還可以使用 ARRAY 表達式語法:

INSERT INTO sal_emp
    VALUES ('Bill',
    ARRAY[10000, 10000, 10000, 10000],
    ARRAY[['meeting', 'lunch'], ['','']]);

INSERT INTO sal_emp
    VALUES ('Carol',
    ARRAY[20000, 25000, 25000, 25000],
    ARRAY[['talk', 'consult'], ['meeting', '']]);
SELECT * FROM sal_emp;
 name  |      pay_by_quarter       |           schedule
-------+---------------------------+-------------------------------
 Bill  | {10000,10000,10000,10000} | {{meeting,lunch},{"",""}}
 Carol | {20000,25000,25000,25000} | {{talk,consult},{meeting,""}}
(2 rows)

請注意在這個語法裡,多維數組必須匹配每個維的長度。錯誤的匹配導致一個錯誤, 而不是像前面那樣不聲不響地拋棄掉。比如:

INSERT INTO sal_emp
    VALUES ('Carol',
    ARRAY[20000, 25000, 25000, 25000],
    ARRAY[['talk', 'consult'], ['meeting']]);
ERROR:  multidimensional arrays must have array expressions with matching dimensions

還要注意數組元素是普通的 SQL 常量或者表達式;比如,字串文本是用單引號引起的, 而不是像數組文本那樣用雙引號。ARRAY 表達式在 Section 4.2.10 裡有更詳細的討論。

8.10.3. 訪問數組

現在我們可以在這個表上運行一些查詢。 首先,我們演示如何一次訪問數組的一個元素. 這個查詢檢索在第二季度薪水變化的雇員名:

SELECT name FROM sal_emp WHERE pay_by_quarter[1] <> pay_by_quarter[2];

 name
-------
 Carol
(1 row)

數組的腳標數字是寫在方括弧內的. PostgreSQL 缺省使用以一為基 的數組習慣, 也就是說,一個 n 元素的數組從array[1]開始, 到 array[n] 結束.

這個查詢檢索所有雇員第三季度的薪水:

SELECT pay_by_quarter[3] FROM sal_emp;

 pay_by_quarter
----------------
          10000
          25000
(2 rows)

我們還可以訪問一個數組的任意長方形片斷,或稱子數組. 對于一維或更多維數組,一個數組的某一部分是用 腳標下界 : 腳標上界 表示的。 比如,下面查詢檢索 Bill 該週頭兩天的第一件計劃.

SELECT schedule[1:2][1:1] FROM sal_emp WHERE name = 'Bill';

      schedule
--------------------
 {{meeting},{""}}
(1 row)

我們還可以這樣寫

SELECT schedule[1:2][1] FROM sal_emp WHERE name = 'Bill';

獲取同樣的結果。如果任何腳標寫成 lower: upper 的形式,那麼任何數組腳標操作 總是當做一個數組片斷對待. 如果只聲明了一個數值,那麼都是假設下界為 1,比如:

SELECT schedule[1:2][2] FROM sal_emp WHERE name = 'Bill';
         schedule
---------------------------
 {{meeting,lunch},{"",""}}
(1 row)

任何數組的當前維數都可以用 array_dims 函數檢索:

SELECT array_dims(schedule) FROM sal_emp WHERE name = 'Carol';

 array_dims
------------
 [1:2][1:1]
(1 row)

array_dims 生成一個 text 結果, 對于人類可能比較容易閱讀,但是對于程序可能就不那麼方便了。我們也 可以用 array_upperarray_lower 函數檢索,它們分別返回指定數組維的上界和下界。

SELECT array_upper(schedule, 1) FROM sal_emp WHERE name = 'Carol';

 array_upper
-------------
           2
(1 row)

8.10.4. 修改數組

一個數組值可以完全被代替:

UPDATE sal_emp SET pay_by_quarter = '{25000,25000,27000,27000}'
    WHERE name = 'Carol';

或者使用 ARRAY 表達式語法:

UPDATE sal_emp SET pay_by_quarter = ARRAY[25000,25000,27000,27000]
    WHERE name = 'Carol';

或者只是更新某一個元素:

UPDATE sal_emp SET pay_by_quarter[4] = 15000
    WHERE name = 'Bill';

或者更新某個片斷:

UPDATE sal_emp SET pay_by_quarter[1:2] = '{27000,27000}'
    WHERE name = 'Carol';

我們可以通過給一個和已存在的元素相鄰元素賦值的方法, 或者是向已存在的數據相鄰或重疊的區域賦值的方法來擴大一個數組. 比如,如果一個數組 myarray 當前有 4 個元素,那麼如果我們給 myarray[5]賦值 後,它就有五個元素.目前,這樣的擴大只允許多一維數組進行, 不能對多維數組進行操作.

數組片段賦值允許創建不使用一為基的下標的數組. 比如,我們可以給 array[-2:7] 賦值, 創建一個腳標值在 -2 和 7 之間的數組.

新的數組值也可以用連接操作符 || 構造。

SELECT ARRAY[1,2] || ARRAY[3,4];
 ?column?
-----------
 {1,2,3,4}
(1 row)

SELECT ARRAY[5,6] || ARRAY[[1,2],[3,4]];
      ?column?
---------------------
 {{5,6},{1,2},{3,4}}
(1 row)

連接操作符允許把一個元素壓入一個一維數組的開頭或者結尾。它還接受兩個 N 維的數組,或者一個 N 維和一個 N+1 維的數組。

在向一個一維數組的開頭壓入一個元素後,結果是這樣的一個數組:它的低界下標 等于右手邊操作數的低界下標減一,如果向一個一維數組的結尾壓入一個元素, 結果數組就是一個保持左手邊操作數低界的數組。比如:

SELECT array_dims(1 || ARRAY[2,3]);
 array_dims
------------
 [0:2]
(1 row)

SELECT array_dims(ARRAY[1,2] || 3);
 array_dims
------------
 [1:3]
(1 row)

如果兩個相同維數的數組連接在一起,結果保持左手邊操作數的外層維數的低界下標。 結果是這樣一個數組:它包含左手邊操作數的每個元素,後面跟著右手邊操作數的每個 元素。比如:

SELECT array_dims(ARRAY[1,2] || ARRAY[3,4,5]);
 array_dims
------------
 [1:5]
(1 row)

SELECT array_dims(ARRAY[[1,2],[3,4]] || ARRAY[[5,6],[7,8],[9,0]]);
 array_dims
------------
 [1:5][1:2]
(1 row)

如果一個 N 維的數組壓到一個 N+1 維數組的 開頭或者結尾,結果和上面的數組元素的情況類似。每個 N 維 的子數組實際上都是 N+1 維數組的外層維數。比如:

SELECT array_dims(ARRAY[1,2] || ARRAY[[3,4],[5,6]]);
 array_dims
------------
 [0:2][1:2]
(1 row)

數組也可以用函數 array_prepend, 和 array_append, 以及 array_cat 構造。頭兩個只支持一維數組, 而 array_cat 支持多維數組。 請注意使用上面討論的連接操作符要比直接使用這些函數好。實際上, 這些函數主要用于實現連接操作符。不過,在用戶定義的創建函數裡 直接使用他們可能有必要。一些例子:

SELECT array_prepend(1, ARRAY[2,3]);
 array_prepend
---------------
 {1,2,3}
(1 row)

SELECT array_append(ARRAY[1,2], 3);
 array_append
--------------
 {1,2,3}
(1 row)

SELECT array_cat(ARRAY[1,2], ARRAY[3,4]);
 array_cat
-----------
 {1,2,3,4}
(1 row)

SELECT array_cat(ARRAY[[1,2],[3,4]], ARRAY[5,6]);
      array_cat
---------------------
 {{1,2},{3,4},{5,6}}
(1 row)

SELECT array_cat(ARRAY[5,6], ARRAY[[1,2],[3,4]]);
      array_cat
---------------------
 {{5,6},{1,2},{3,4}}

8.10.5. 在數組中檢索

要搜索一個數組中的數值,你必須檢查該數組的每一個值. 你可以手工處理(如果你知道數組尺寸)。比如:

SELECT * FROM sal_emp WHERE pay_by_quarter[1] = 10000 OR
                            pay_by_quarter[2] = 10000 OR
                            pay_by_quarter[3] = 10000 OR
                            pay_by_quarter[4] = 10000;

不過,對于大數組而言,這個方法很快就會讓人覺得無聊,並且如果你 不知道數組尺寸,那就沒什麼用了. 另外一個方法在 Section 9.17 裡描述。 上面的查詢可以用下面的代替:

SELECT * FROM sal_emp WHERE 10000 = ANY (pay_by_quarter);

另外,你可以用下面的語句找出所有數組有值等于 10000 的行:

SELECT * FROM sal_emp WHERE 10000 = ALL (pay_by_quarter);

提示: 數組不是集合;象我們前面那些段落裡描述的那樣使用數組 通常表明你的庫設計有問題.數組字段通常是可以分裂成獨立的表. 很明顯表要容易搜索得多.並且在元素數目非常龐大的時候也可以 更好地伸展。

8.10.6. 數組輸入和輸出語法

一個數組值的外部表現形式由一些根據該數組元素類型的 I/O 轉換 規則分析的項組成,再加上一些標明該數組結構的修飾. 這些修飾由圍繞在數組值週圍的花括弧({}), 加上相臨項之間的分隔字符組成.分隔字符通常是一個逗號(,), 但也可以是其它的東西︰它由該數組元素類型的 typdelim 設置 決定.(在 PostgreSQL 版本提供的標準 數據類型裡,類型 box 使用分號 (;),但所有 其它的類型使用逗號.)在多維數組裡,每個維(行,面,體,等)有自己級別的 花括弧,並且在同級相臨的花括弧項之間必須寫分隔符. 你可以在左花括弧,右花括弧,或者在任何獨立的項字串之前寫空白. 不過,在項後面的空白並不會被忽略︰在忽略前導空白之後,任何在 下一個右花括弧或者分隔符之前的東西都被當做項數值看待.

如上所述,在書寫一個數組的數值的時候你要用雙引號包圍任意獨立的 數組元素.如果元素數值可能令數組數值分析器產生歧意,那麼你 必須這麼做. 比如,那些包含花括弧,逗號(或者任何其它的分隔字符), 雙引號,反斜扛,或者前導的空白元素都必須加 雙引號.要把雙引號或者反斜扛放到數組元素值裡,給它們加一個反斜扛前綴. 另外,你可以用反斜扛逃逸的方法保護所有那些可能被當做數組語法的字符 或者可能被忽略的空白.

如果元素值是空字串或者包含花括弧,分隔符,雙引號,反斜扛,或者空白, 那麼數組輸出過程將在元素值週圍放上雙引號. 在元素值內嵌的雙引號和反斜扛將用反斜扛逃逸.對于數值類型, 我們可以安全地假設雙引號從不出現,但是對于文本型數據類型, 我們就得準備對付引號的出現或者缺席.(這個行為和 7.2 以前的 PostgreSQL 版本已經不同了.)

注意: 請記住你在 SQL 命令裡寫的任何東西都將首先解釋成一個字串文本, 然後才是一個數組.這樣就造成你所需要的反斜扛數量翻了翻. 比如,要插入一個包含反斜扛和雙引號的 text 數組, 你需要這麼寫

INSERT ... VALUES ('{"\\\\","\\""}');

字串文本處理器去掉第一層反斜扛,然後省下的東西到了數組數值分析器的 時候看起來象 {"\\","\""}.接著,該字串傳遞給 text 數據類型的輸入過程,分別變成 \". (如果我們用的數據類型對反斜扛也有特殊待遇,比如 bytea, 那麼我們可能需要在命令裡放多達八個反斜扛才能在存儲態的數組元素中 得到一個反斜扛.)

提示: ARRAY 構造起語法通常比數組文本語法好用些,尤其是在 SQL 命令裡 寫數組值的時候。在 ARRAY 裡,獨立的元素值的寫法和數組裡沒有元素 時的寫法一樣。