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 裡定義數字或者維數都只是簡單的文件,它並不影響執行時的行為。

另外還有一種語法,它遵循 SQL: 1999 標準,可以用於聲明一維數組。 pay_by_quarter 可以定義為:

    pay_by_quarter  integer ARRAY[4],

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

8.10.2. 數組值輸入

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

'{ val1 delim val2 delim ... }'

這裡的 delim 是該類型的分隔符, 就是那個在它的 pg_type 記錄裡指定的那個。 在 PostgreSQL 發佈提供的標準資料類型裡, 類型 box 使用分號(;),但是所有其它類型都用逗號(,)。 每個 val 要麼是一個數組元素類型的常量,要麼是一個子數組。一個數組常量的例子是

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

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

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

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

INSERT INTO sal_emp
    VALUES ('Bill',
    '{10000, 10000, 10000, 10000}',
    '{{"meeting", "lunch"}, {"meeting"}}');
ERROR:  multidimensional arrays must have array expressions with matching dimensions

請注意多維數組必須匹配每個維的元素數。如果不匹配則導致錯誤發生。

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

INSERT INTO sal_emp
    VALUES ('Carol',
    '{20000, 25000, 25000, 25000}',
    '{{"breakfast", "consulting"}, {"meeting", "lunch"}}');

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

前面的兩個插入的結果看起來像這樣:

SELECT * FROM sal_emp;
 name  |      pay_by_quarter       |                 schedule
-------+---------------------------+-------------------------------------------
 Bill  | {10000,10000,10000,10000} | {{meeting,lunch},{training,presentation}}
 Carol | {20000,25000,25000,25000} | {{breakfast,consulting},{meeting,lunch}}
(2 rows)

我們還可以使用 ARRAY 構造器語法:

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

INSERT INTO sal_emp
    VALUES ('Carol',
    ARRAY[20000, 25000, 25000, 25000],
    ARRAY[['breakfast', 'consulting'], ['meeting', 'lunch']]);

請注意數組元素是普通的 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},{training}}
(1 row)

我們還可以這樣寫

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

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

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

                 schedule
-------------------------------------------
 {{meeting,lunch},{training,presentation}}
(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 版本的行為的一個改變。)

預設時,一個數組的某維的下標索引是設置為一的。如果一個數組的某維的下標不等於一, 那麼就會在數組結構修飾域裡面放置一個實際的維數。這個修飾由方括弧([]) 圍繞在每個數組維的下界和上界索引,中間有一個冒號(:)分隔的字串組成。 數組維數修飾後面跟著一個等號操作符(=)。比如:

SELECT 1 || ARRAY[2,3] AS array;

     array
---------------
 [0:2]={1,2,3}
(1 row)

SELECT ARRAY[1,2] || ARRAY[[3,4]] AS array;

          array
--------------------------
 [0:1][1:2]={{1,2},{3,4}}
(1 row)

這個語法也可以用於在一個數組文本中聲明非預設數組腳標。比如:

SELECT f1[1][-2][3] AS e1, f1[1][-1][5] AS e2
 FROM (SELECT '[1:1][-2:-1][3:5]={{{1,2,3},{4,5,6}}}'::int[] AS f1) AS ss;

 e1 | e2
----+----
  1 |  6
(1 row)

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

您可以在左花括弧前面或者右花括弧後面寫空白。您還可以在任意獨立的項字串前面或者後面寫空白。 所有這些情況下,這些空白都會被忽略。不過,在雙引號包圍的元素裡面的空白,或者是元素裡被兩邊非空白字元包圍的空白,都不會被忽略。

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

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

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

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