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 里,独立的元素值的写法和数组里没有元素 时的写法一样。